From: jenkins-bot Date: Thu, 26 Sep 2019 23:20:56 +0000 (+0000) Subject: Merge "Add .pipeline/ with dev image variant" X-Git-Tag: 1.34.0-rc.0~82 X-Git-Url: https://git.heureux-cyclage.org/?p=lhc%2Fweb%2Fwiklou.git;a=commitdiff_plain;h=fa0f6f34972c0e0f4aac24a03b3efdfc45f256f6;hp=315df62758acb54c52f42bd3067a9ae95bcb3aa6 Merge "Add .pipeline/ with dev image variant" --- diff --git a/.mailmap b/.mailmap index 1265bd2f86..82ebc2167a 100644 --- a/.mailmap +++ b/.mailmap @@ -59,7 +59,7 @@ Ariel Glenn Arlo Breault Arthur Richards Arthur Richards -Aryeh Gregor +Aryeh Gregor Asher Feldman Asher Feldman aude @@ -233,6 +233,7 @@ Joel Sahleen John Du Hart John Erling Blad Jon Harald Søby +Jon Harald Søby Jon Robson Jon Robson Juliusz Gonera @@ -286,6 +287,7 @@ Mark Holmquist Marko Obrovac Markus Glaser Markus Glaser +Martin Urbanec Matt Johnston Matthew Britton Matthew Bowker diff --git a/.phan/config.php b/.phan/config.php index 8746ada384..0fdefe054d 100644 --- a/.phan/config.php +++ b/.phan/config.php @@ -32,21 +32,36 @@ $cfg['file_list'] = array_merge( class_exists( PHPUnit_TextUI_Command::class ) ? [] : [ '.phan/stubs/phpunit4.php' ], class_exists( ProfilerExcimer::class ) ? [] : [ '.phan/stubs/excimer.php' ], [ - 'maintenance/cleanupTable.inc', - 'maintenance/CodeCleanerGlobalsPass.inc', - 'maintenance/commandLine.inc', - 'maintenance/sqlite.inc', - 'maintenance/userDupes.inc', - 'maintenance/language/checkLanguage.inc', - 'maintenance/language/languages.inc', + // This makes constants and globals known to Phan before processing all other files. + // You can check the parser order with --dump-parsed-file-list + 'includes/Defines.php', + // @todo This isn't working yet, see globals_type_map below + // 'includes/DefaultSettings.php', + // 'includes/Setup.php', ] ); +$cfg['exclude_file_list'] = array_merge( + $cfg['exclude_file_list'], + [ + // This file has invalid PHP syntax + 'vendor/squizlabs/php_codesniffer/src/Standards/PSR2/Tests/Methods/MethodDeclarationUnitTest.inc', + ] +); + +$cfg['analyzed_file_extensions'] = array_merge( + $cfg['analyzed_file_extensions'] ?? [ 'php' ], + [ 'inc' ] +); + $cfg['autoload_internal_extension_signatures'] = [ + 'dom' => '.phan/internal_stubs/dom.phan_php', 'imagick' => '.phan/internal_stubs/imagick.phan_php', + 'intl' => '.phan/internal_stubs/intl.phan_php', 'memcached' => '.phan/internal_stubs/memcached.phan_php', 'oci8' => '.phan/internal_stubs/oci8.phan_php', 'pcntl' => '.phan/internal_stubs/pcntl.phan_php', + 'pgsql' => '.phan/internal_stubs/pgsql.phan_php', 'redis' => '.phan/internal_stubs/redis.phan_php', 'sockets' => '.phan/internal_stubs/sockets.phan_php', 'sqlsrv' => '.phan/internal_stubs/sqlsrv.phan_php', @@ -65,7 +80,7 @@ $cfg['directory_list'] = [ $cfg['exclude_analysis_directory_list'] = [ 'vendor/', - '.phan/stubs/', + '.phan/', // The referenced classes are not available in vendor, only when // included from composer. 'includes/composer/', @@ -73,74 +88,55 @@ $cfg['exclude_analysis_directory_list'] = [ 'maintenance/language/', // External class 'includes/libs/jsminplus.php', + // External class + 'includes/libs/objectcache/utils/MemcachedClient.php', ]; +// NOTE: If you're facing an issue which you cannot easily fix, DO NOT add it here. Suppress it +// either in-line with @phan-suppress-next-line and similar, at block-level (via @suppress), or at +// file-level (with @phan-file-suppress), so that it stays enabled for the rest of the codebase. $cfg['suppress_issue_types'] = array_merge( $cfg['suppress_issue_types'], [ - // approximate error count: 18 - "PhanAccessMethodInternal", - // approximate error count: 17 - "PhanCommentParamOnEmptyParamList", - // approximate error count: 29 - "PhanCommentParamWithoutRealParam", - // approximate error count: 2 - "PhanCompatibleNegativeStringOffset", - // approximate error count: 21 - "PhanParamReqAfterOpt", - // approximate error count: 26 - "PhanParamSignatureMismatch", - // approximate error count: 4 - "PhanParamSignatureMismatchInternal", - // approximate error count: 127 - "PhanParamTooMany", - // approximate error count: 2 - "PhanTraitParentReference", - // approximate error count: 30 - "PhanTypeArraySuspicious", - // approximate error count: 27 - "PhanTypeArraySuspiciousNullable", - // approximate error count: 26 - "PhanTypeComparisonFromArray", - // approximate error count: 63 - "PhanTypeInvalidDimOffset", - // approximate error count: 7 - "PhanTypeInvalidLeftOperandOfIntegerOp", - // approximate error count: 2 - "PhanTypeInvalidRightOperandOfIntegerOp", - // approximate error count: 154 - "PhanTypeMismatchArgument", - // approximate error count: 27 - "PhanTypeMismatchArgumentInternal", - // approximate error count: 2 - "PhanTypeMismatchDimEmpty", - // approximate error count: 27 - "PhanTypeMismatchDimFetch", - // approximate error count: 10 - "PhanTypeMismatchForeach", - // approximate error count: 77 - "PhanTypeMismatchProperty", - // approximate error count: 84 - "PhanTypeMismatchReturn", - // approximate error count: 12 - "PhanTypeObjectUnsetDeclaredProperty", - // approximate error count: 9 - "PhanTypeSuspiciousNonTraversableForeach", - // approximate error count: 3 - "PhanTypeSuspiciousStringExpression", - // approximate error count: 22 - "PhanUndeclaredConstant", - // approximate error count: 3 - "PhanUndeclaredInvokeInCallable", - // approximate error count: 237 - "PhanUndeclaredMethod", - // approximate error count: 846 - "PhanUndeclaredProperty", - // approximate error count: 2 - "PhanUndeclaredVariableAssignOp", - // approximate error count: 55 - "PhanUndeclaredVariableDim", + // approximate error count: 19 + "PhanParamReqAfterOpt", // False positives with nullables (phan issue #3159). Use real nullables + //after dropping HHVM + // approximate error count: 110 + "PhanParamTooMany", // False positives with variargs. Unsuppress after dropping HHVM ] ); +// This helps a lot in discovering bad code, but unfortunately it will always fail for +// hooks + pass by reference, see phan issue #2943. +// @todo Enable when the issue above is resolved and we update our config! +$cfg['redundant_condition_detection'] = false; + +// Do not use aliases in core. +// Use the correct name, because we don't need backward compatibility +$cfg['enable_class_alias_support'] = false; + $cfg['ignore_undeclared_variables_in_global_scope'] = true; -$cfg['globals_type_map']['IP'] = 'string'; +// @todo It'd be great if we could just make phan read these from DefaultSettings, to avoid +// duplicating the types. +$cfg['globals_type_map'] = array_merge( $cfg['globals_type_map'], [ + 'IP' => 'string', + 'wgGalleryOptions' => 'array', + 'wgDummyLanguageCodes' => 'string[]', + 'wgNamespaceProtection' => 'array', + 'wgNamespaceAliases' => 'array', + 'wgLockManagers' => 'array[]', + 'wgForeignFileRepos' => 'array[]', + 'wgDefaultUserOptions' => 'array', + 'wgSkipSkins' => 'string[]', + 'wgLogTypes' => 'string[]', + 'wgLogNames' => 'array', + 'wgLogHeaders' => 'array', + 'wgLogActionsHandlers' => 'array', + 'wgPasswordPolicy' => 'array>', + 'wgVirtualRestConfig' => 'array', + 'wgWANObjectCaches' => 'array[]', + 'wgLocalInterwikis' => 'string[]', + 'wgDebugLogGroups' => 'string|false|array{destination:string,sample?:int,level:int}', + 'wgCookiePrefix' => 'string|false', + 'wgOut' => 'OutputPage', + 'wgExtraNamespaces' => 'string[]', +] ); return $cfg; diff --git a/.phan/internal_stubs/dom.phan_php b/.phan/internal_stubs/dom.phan_php new file mode 100644 index 0000000000..608e3a1549 --- /dev/null +++ b/.phan/internal_stubs/dom.phan_php @@ -0,0 +1,420 @@ + - */includes/specials/SpecialMostinterwikis\.php - */includes/specials/SpecialAncientpages\.php - */includes/specials/SpecialBrokenRedirects\.php - */includes/specials/SpecialConfirmemail\.php - */includes/specials/SpecialDeadendpages\.php - */includes/specials/SpecialDeletedContributions\.php - */includes/specials/SpecialDoubleRedirects\.php - */includes/specials/SpecialEmailInvalidate\.php - */includes/specials/SpecialFewestrevisions\.php - */includes/specials/SpecialFileDuplicateSearch\.php - */includes/specials/SpecialLinkSearch\.php - */includes/specials/SpecialListDuplicatedFiles\.php - */includes/specials/SpecialListredirects\.php - */includes/specials/SpecialLonelypages\.php - */includes/specials/SpecialLongpages\.php - */includes/specials/SpecialMIMEsearch\.php - */includes/specials/SpecialMediaStatistics\.php - */includes/specials/SpecialMostcategories\.php */includes/specials/SpecialMostimages\.php - */includes/specials/SpecialMostlinked\.php - */includes/specials/SpecialMostlinkedcategories\.php - */includes/specials/SpecialMostlinkedtemplates\.php - */includes/specials/SpecialMostrevisions\.php */includes/specials/SpecialMovepage\.php - */includes/specials/SpecialNewimages\.php */includes/specials/SpecialRandompage\.php - */includes/specials/SpecialShortpages\.php - */includes/specials/SpecialUncategorizedcategories\.php - */includes/specials/SpecialUncategorizedimages\.php - */includes/specials/SpecialUncategorizedpages\.php - */includes/specials/SpecialUncategorizedtemplates\.php - */includes/specials/SpecialUnusedcategories\.php - */includes/specials/SpecialUnusedimages\.php - */includes/specials/SpecialUnusedtemplates\.php - */includes/specials/SpecialUnwatchedpages\.php */includes/specials/SpecialUserrights\.php - */includes/specials/SpecialWantedcategories\.php */includes/specials/SpecialWantedfiles\.php */includes/specials/SpecialWantedpages\.php - */includes/specials/SpecialWantedtemplates\.php - */includes/specials/SpecialWithoutinterwiki\.php */maintenance/CodeCleanerGlobalsPass.inc */maintenance/archives/upgradeLogging\.php */maintenance/benchmarks/bench_HTTP_HTTPS\.php diff --git a/.travis.yml b/.travis.yml index d5607f1609..8dbc5f203c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -26,8 +26,6 @@ matrix: include: - php: 7.3 - php: 7.2 - - php: 7.1 - - php: 7 services: - mysql diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 498acf76f7..3d9f26ad44 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -1 +1,4 @@ +Code of Conduct +=============== + The development of this software is covered by a [Code of Conduct](https://www.mediawiki.org/wiki/Special:MyLanguage/Code_of_Conduct). diff --git a/CREDITS b/CREDITS index 319b56683f..140ada2b7a 100644 --- a/CREDITS +++ b/CREDITS @@ -314,7 +314,6 @@ The following list can be found parsed under Special:Version/Credits --> * Jaska Zedlik * Jason Richey * Jayprakash12345 -* jeblad * Jeff Hobson * Jeff Janes * jeff303 @@ -328,7 +327,6 @@ The following list can be found parsed under Special:Version/Credits --> * Jerome Jamnicky * Jesús Martínez Novo * jhobs -* jhsoby * Jiabao * Jidanni * Jimmy Collins diff --git a/Gruntfile.js b/Gruntfile.js index f3950f6f50..8115ea2aec 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -26,7 +26,7 @@ module.exports = function ( grunt ) { cache: true }, all: [ - '**/*.js{,on}', + '**/*.{js,json}', '!docs/**', '!node_modules/**', '!resources/lib/**', diff --git a/HISTORY b/HISTORY index ccdd2de0ee..4c5e344b3a 100644 --- a/HISTORY +++ b/HISTORY @@ -2234,7 +2234,7 @@ This is a security and maintenance release of the MediaWiki 1.29 branch. * (T194605, CVE-2018-0505) SECURITY: BotPasswords can bypass CentralAuth's account lock. * (T180551) Fix LanguageSrTest for language converter -* (T180552) Fix langauge converter parser test with self-close tags +* (T180552) Fix language converter parser test with self-close tags * (T180537) Remove $wgAuth usage from wrapOldPasswords.php * (T180485) InputBox: Have inputbox langconvert certain attributes * (T161732, T181547) Upgraded Moment.js from v2.15.0 to v2.19.3. diff --git a/INSTALL b/INSTALL index f31f753303..0359166d22 100644 --- a/INSTALL +++ b/INSTALL @@ -5,14 +5,20 @@ Installing MediaWiki Starting with MediaWiki 1.2.0, it's possible to install and configure the wiki "in-place", as long as you have the necessary prerequisites available. -Required software: -* Web server with PHP 7.0.0 or HHVM 3.18.5 or higher. +Required software as of MediaWiki 1.34.0: + +* Web server with PHP 7.2.0 or higher, plus the following extesnsions: +** ctype +** dom +** fileinfo +** iconv +** json +** mbstring +** xml * A SQL server, the following types are supported ** MySQL 5.5.8 or higher ** PostgreSQL 9.2 or higher ** SQLite 3.8.0 or higher -** Oracle 9.0.1 or higher -** Microsoft SQL Server 2005 (9.00.1399) MediaWiki is developed and tested mainly on Unix/Linux platforms, but should work on Windows as well. diff --git a/RELEASE-NOTES-1.34 b/RELEASE-NOTES-1.34 index 5e4f61d37a..0d6f1e11bd 100644 --- a/RELEASE-NOTES-1.34 +++ b/RELEASE-NOTES-1.34 @@ -26,6 +26,13 @@ For notes on 1.33.x and older releases, see HISTORY. === Configuration changes for system administrators in 1.34 === +In an effort to enforce best practices for passwords, MediaWiki will now warn +users, and suggest that they change their password, if it is in the list of +100,000 commonly used passwords that are considered bad passwords. If you want +to disable this for your users, please add the following to your local settings: + +$wgPasswordPolicy['policies']['default']['PasswordNotInLargeBlacklist'] = false; + ==== New configuration ==== * $wgAllowExternalReqID (T201409) - This configuration setting controls whether Mediawiki accepts the request ID set by the incoming request via the @@ -39,6 +46,7 @@ For notes on 1.33.x and older releases, see HISTORY. * editmyuserjsredirect user right – users without this right now cannot edit JS redirects in their userspace unless the target of the redirect is also in their userspace. By default, this right is given to everyone. +* (T226733) Add rate limiter to Special:ConfirmEmail. ==== Changed configuration ==== * $wgUseCdn, $wgCdnServers, $wgCdnServersNoPurge, and $wgCdnMaxAge – These four @@ -65,19 +73,26 @@ For notes on 1.33.x and older releases, see HISTORY. which was deprecated in 1.30, no longer works. Instead, $wgProxyList should be an array with IP addresses as the values, or a string path to a file containing one IP address per line. +* $wgCookieSetOnAutoblock and $wgCookieSetOnIpBlock are now enabled by default. * … ==== Removed configuration ==== * $wgWikiDiff2MovedParagraphDetectionCutoff — If you still want a custom change size threshold, please specify in php.ini, using the configuration variable wikidiff2.moved_paragraph_detection_cutoff. +* $wgUseESI - This experimental setting, deprecated in 1.33, is now removed. * $wgDebugPrintHttpHeaders - The default of including HTTP headers in the debug log channel is no longer configurable. The debug log itself remains configurable via $wgDebugLogFile. +* $wgMsgCacheExpiry - The MessageCache uses 24 hours as the expiry for values + stored in WANObjectCache. This is no longer configurable. * $wgPasswordSalt – This setting, used for migrating exceptionally old, insecure password setups and deprecated since 1.24, is now removed. * $wgDBOracleDRCP - If you must use persistent connections, set DBO_PERSISTENT in the 'flags' field for servers in $wgDBServers (or $wgLBFactoryConf). +* $wgMemCachedDebug - Set the cache "debug" field in $wgObjectCaches instead. +* $wgActorTableSchemaMigrationStage has been removed. Extension code for + MediaWiki 1.31+ finding it unset should treat it as being SCHEMA_COMPAT_NEW. === New user-facing features in 1.34 === * Special:Mute has been added as a quick way for users to block unwanted emails @@ -87,6 +102,10 @@ For notes on 1.33.x and older releases, see HISTORY. ([[Special:NewSection/Test]] redirects to creating a new section in "Test"). Otherwise, it displays a basic interface to allow the end user to specify the target manually. +* (T220447) Special:Contributions/newbies has been removed for performance and + usefulness reasons. Use Special:RecentChanges?userExpLevel=newcomer instead. +* Special:NewFiles/newbies has been removed for performance and usefulness + reasons. Use Special:RecentChanges?userExpLevel=newcomer&namespace=6 instead. === New developer features in 1.34 === * The ImgAuthModifyHeaders hook was added to img_auth.php to allow modification @@ -97,6 +116,19 @@ For notes on 1.33.x and older releases, see HISTORY. to add fields to Special:Mute. * (T100896) Skin authors can define custom OOUI themes using OOUIThemePaths. See for details. +* (T229035) The GetUserBlock hook was added. Use this instead of + GetBlockedStatus. +* ObjectFactory is available as a service. When used as a service, the object + specs can now specify needed DI services. +* (T222388) Special pages can now be specified as an ObjectFactory spec, + allowing the construction of special pages that require services to be + injected in their constructor. +* (T222388) API modules can now be specified as an ObjectFactory spec, + allowing the construction of modules that require services to be injected + in their constructor. +* (T117736) The function signature of SpecialContributions::getForm::filters + has changed. It now expects definitions of additional filter fields as array + rather than string. === External library changes in 1.34 === @@ -105,18 +137,20 @@ For notes on 1.33.x and older releases, see HISTORY. ==== Changed external libraries ==== * Updated Mustache from 1.0.0 to v3.0.1. -* Updated OOUI from v0.31.3 to v0.33.4. +* Updated OOUI from v0.31.3 to v0.34.0. +* Updated OOjs from v2.2.2 to v3.0.0. * Updated composer/semver from 1.4.2 to 1.5.0. * Updated composer/spdx-licenses from 1.4.0 to 1.5.1 (dev-only). * Updated mediawiki/codesniffer from 25.0.0 to 26.0.0 (dev-only). * Updated cssjanus/cssjanus from 1.2.1 to 1.3.0. * Updated wikimedia/at-ease from 1.2.0 to 2.0.0. -* Updated wikimedia/remex-html from 2.0.1 to 2.0.3. +* Updated wikimedia/remex-html from 2.0.1 to 2.1.0. * Updated monolog/monolog from 1.22.1 to 1.24.0 (dev-only). * Updated wikimedia/object-factory from 1.0.0 to 2.1.0. * Updated wikimedia/timestamp from 2.2.0 to 3.0.0. * Updated wikimedia/xmp-reader from 0.6.2 to 0.6.3. * Updated mediawiki/mediawiki-phan-config from 0.6.0 to 0.6.1 (dev-only). +* Updated wikimedia/avro from 1.8.0 to 1.9.0 (dev-only). * … ==== Removed external libraries ==== @@ -132,9 +166,31 @@ For notes on 1.33.x and older releases, see HISTORY. === Action API changes in 1.34 === * The 'recenteditcount' response property from action=query list=allusers, deprecated in 1.25, has been removed. +* (T60993) action=query list=filearchive, list=alldeletedrevisions and + prop=deletedrevisions no longer require the 'deletedhistory' user right. +* In the response to queries that use 'prop=imageinfo', entries for + non-existing files (indicated by the 'filemissing' field) now omit the + following fields, since they are meaningless in this context: + 'timestamp', 'userhidden', 'user', 'userid', 'anon', 'size', 'width', + 'height', 'pagecount', 'duration', 'commenthidden', 'parsedcomment', + 'comment', 'thumburl', 'thumbwidth', 'thumbheight', 'thumbmime', + 'thumberror', 'url', 'sha1', 'metadata', 'extmetadata', 'commonmetadata', + 'mime', 'mediadtype', 'bitdepth'. + Clients that process these fields should first check if 'filemissing' is + set. Fields that are supported even if the file is missing include: + 'canonicaltitle', ''archivename' (deleted files only), 'descriptionurl', + 'descriptionshorturl'. +* The 'blockexpiry' result property in list=users and list=allusers will now be + returned in the same format used by the rest of the API: ISO 8601 for + expiring blocks, and "infinite" for non-expiring blocks. === Action API internal changes in 1.34 === -* … +* The exception thrown in ApiModuleManager::getModule has been changed + from an MWException to an UnexpectedValueException, thrown by ObjectFactory. + ApiModuleManager::getModule now also throws InvalidArgumentExceptions when + ObjectFactory is presented with an invalid spec or incorrectly constructed + objects. +* Added ApiQueryBlockInfoTrait. === Languages updated in 1.34 === MediaWiki supports over 350 languages. Many localisations are updated regularly. @@ -153,6 +209,34 @@ because of Phabricator reports. * CryptRand class * CryptRand service * Functions of the MWCryptRand class: singleton(), wasStrong() and generate(). +* Various Special Page PHP Classes were renamed (mostly casing changes): + * SpecialAncientpages => SpecialAncientPages + * SpecialConfirmemail => SpecialConfirmEmail + * SpecialDeadendpages => SpecialDeadendPages + * SpecialFewestrevisions => SpecialFewestRevisions + * SpecialListredirects => SpecialListRedirects + * SpecialLonelypages => SpecialLonelyPages + * SpecialLongpages => SpecialLongPages + * SpecialMIMEsearch => SpecialMIMESearch + * SpecialMostcategories => SpecialMostCategories + * SpecialMostinterwikis => SpecialMostInterwikis + * SpecialMostlinked => SpecialMostLinked + * SpecialMostlinkedcategories => SpecialMostLinkedCategories + * SpecialMostlinkedtemplates => SpecialMostLinkedTemplates + * SpecialMostrevisions => SpecialMostRevisions + * SpecialNewimages => SpecialNewFiles + * SpecialShortpages => SpecialShortPages + * SpecialUncategorizedcategories => SpecialUncategorizedCategories + * SpecialUncategorizedimages => SpecialUncategorizedImages + * SpecialUncategorizedpages => SpecialUncategorizedPages + * SpecialUncategorizedtemplates => SpecialUncategorizedTemplates + * SpecialUnusedcategories => SpecialUnusedCategories + * SpecialUnusedimages => SpecialUnusedImages + * SpecialUnusedtemplates => SpecialUnusedTemplates + * SpecialUnwatchedpages => SpecialUnwatchedPages + * SpecialWantedcategories => SpecialWantedCategories + * SpecialWantedtemplates => SpecialWantedTemplates + * SpecialWithoutinterwiki => SpecialWithoutInterwiki * Language::setCode, deprecated in 1.32, was removed. Use Language::factory to create a new Language object with a different language code. * MWNamespace::clearCaches() has been removed. So has the $rebuild parameter @@ -237,6 +321,11 @@ because of Phabricator reports. Use the mediawiki.String module instead. * mw.language.specialCharacters, deprecated in 1.33, has been removed. Use require( 'mediawiki.language.specialCharacters' ) instead. +* The jquery.colorUtil module was removed. Use jquery.color instead. +* The jquery.checkboxShiftClick module was removed. The functionality + is provided by mediawiki.page.ready instead (T232688). +* The 'jquery.accessKeyLabel' module has been removed. This jQuery + plugin now ships as part of the 'mediawiki.util' module bundle. * EditPage::submit(), deprecated in 1.29, has been removed. Use $this->edit() directly. * HTMLForm::getErrors(), deprecated in 1.28, has been removed. Use @@ -340,7 +429,32 @@ because of Phabricator reports. * Database::getProperty(), deprecated in 1.28, has been removed. * IDatabase::getWikiId(), deprecated in 1.30, has been removed. Use IDatabase::getDomainID() instead. -* … +* (T191231) Support for using Oracle or MSSQL as database backends has been + dropped. +* MessageCache::destroyInstance() has been removed. Instead, call + MediaWikiTestCase::resetServices(). +* SearchResult protected field $searchEngine is removed and no longer + initialized after calling SearchResult::initFromTitle(). +* The UserIsBlockedFrom hook is only called if a block is found first, and + should only be used to unblock a blocked user. +* Parameters for index.php from PATH_INFO, such as the title, are no longer + written to $_GET. +* The selectFields() methods on classes LocalFile, ArchivedFile, OldLocalFile, + DatabaseBlock, and RecentChange, deprecated in 1.31, have been removed. Use + the corresponding getQueryInfo() methods instead. +* The following methods on Revision, deprecated since 1.31, have been removed. + Use RevisionStore::getQueryInfo() or RevisionStore::getArchiveQueryInfo() + instead. + * Revision::userJoinCond() + * Revision::pageJoinCond() + * Revision::selectFields() + * Revision::selectArchiveFields() + * Revision::selectTextFields() + * Revision::selectPageFields() + * Revision::selectUserFields() +* User::setNewpassword(), deprecated in 1.27 has been removed. +* The ObjectCache::getMainWANInstance and ObjectCache::getMainStashInstance + functions, deprecated since 1.28, have been removed. === Deprecations in 1.34 === * The MWNamespace class is deprecated. Use NamespaceInfo. @@ -396,6 +510,9 @@ because of Phabricator reports. * ResourceLoaderContext::getConfig and ResourceLoaderContext::getLogger have been deprecated. Inside ResourceLoaderModule subclasses, use the local methods instead. Elsewhere, use the methods from the ResourceLoader class. +* The Profiler::setTemplated and Profiler::getTemplated methods have been + deprecated. Use Profiler::setAllowOutput and Profiler::getAllowOutput + instead. * The Preprocessor_DOM implementation has been deprecated. It will be removed in a future release. Use the Preprocessor_Hash implementation instead. @@ -416,6 +533,8 @@ because of Phabricator reports. engines. * Skin::escapeSearchLink() is deprecated. Use Skin::getSearchLink() or the skin template option 'searchaction' instead. +* Skin::getRevisionId() and Skin::isRevisionCurrent() have been deprecated. + Use OutputPage::getRevisionId() and OutputPage::isRevisionCurrent() instead. * LoadBalancer::haveIndex() and LoadBalancer::isNonZeroLoad() have been deprecated. * FileBackend::getWikiId() has been deprecated. @@ -442,26 +561,68 @@ because of Phabricator reports. * SearchEngine::textAlreadyUpdatedForIndex() is deprecated, given the deprecation above this method is no longer needed/called and should not be implemented by SearchEngine implementation. +* IDatabase::bufferResults() has been deprecated. Use query batching instead. +* MessageCache::singleton() is deprecated. Use + MediaWikiServices::getMessageCache(). +* Constructing MovePage directly is deprecated. Use MovePageFactory. +* TempFSFile::factory() has been deprecated. Use TempFSFileFactory instead. +* wfIsBadImage() is deprecated. Use the BadFileLookup service instead. +* Building a new SearchResult is hard-deprecated, always call + SearchResult::newFromTitle(). This class is being refactored into an abstract + class. If you extend this class please be sure to override all its methods + or extend RevisionSearchResult. +* Skin::getSkinNameMessages() is deprecated and no longer used. +* The mediawiki.RegExp module is deprecated; use mw.util.escapeRegExp() instead. +* Specifying a SpecialPage object for the list of special pages (either through + the SpecialPage_initList hook or by adding to $wgSpecialPages) is now + deprecated. +* WebInstaller::getInfoBox(), getWarningBox() and getErrorBox() are deprecated. + Use Html::errorBox() or Html::warningBox() instead. +* Use of ActorMigration with 'ar_user', 'img_user', 'oi_user', 'fa_user', + 'rc_user', 'log_user', and 'ipb_by' is deprecated. Queries should be adjusted + to use the corresponding actor fields directly. Note that use with + 'rev_user' is *not* deprecated at this time. +* Specifying both the class and factory parameters for + ApiModuleManager::addModule is now deprecated. The ObjectFactory spec should + be used instead. +* The UserIsHidden hook is deprecated. Use GetUserBlock instead, and add a + system block that hides the user. +* The GetBlockedStatus hook is deprecated. Use GetUserBlock instead, to add or + remove a block. +* $wgContentHandlerUseDB is deprecated and should always be true. +* StreamFile::send404Message() and StreamFile::parseRange() are now deprecated. + Use HTTPFileStreamer::send404Message() and HTTPFileStreamer::parseRange() + respectively instead. +* Global variable $wgSysopEmailBans is deprecated; to allow sysops to ban + users from sending emails, use + $wgGroupPermissions['sysop']['blockemail'] = true; +* ApiQueryBase::showHiddenUsersAddBlockInfo() is deprecated. Use + ApiQueryBlockInfoTrait instead. === Other changes in 1.34 === * … == Compatibility == -MediaWiki 1.34 requires PHP 7.0.13 or later. Although HHVM 3.18.5 or later is -supported, it is generally advised to use PHP 7.0.13 or later for long term -support. +MediaWiki 1.34 requires PHP 7.2.0 or later. Although HHVM 3.18.5 or later is +supported, it is generally advised to use PHP 7.2.0 or later for long term +support. It also requires the following PHP extensions: + +* ctype +* dom +* fileinfo +* iconv +* json +* mbstring +* xml MySQL/MariaDB is the recommended DBMS. PostgreSQL or SQLite can also be used, -but support for them is somewhat less mature. There is experimental support for -Oracle and Microsoft SQL Server. +but support for them is somewhat less mature. The supported versions are: * MySQL 5.5.8 or later * PostgreSQL 9.2 or later * SQLite 3.8.0 or later -* Oracle 9.0.1 or later -* Microsoft SQL Server 2005 (9.00.1399) == Online documentation == Documentation for both end-users and site administrators is available on diff --git a/api.php b/api.php index 0fb674b9eb..6f4bac3901 100644 --- a/api.php +++ b/api.php @@ -31,6 +31,7 @@ use MediaWiki\Logger\LegacyLogger; // So extensions (and other code) can check whether they're running in API mode define( 'MW_API', true ); +define( 'MW_ENTRY_POINT', 'api' ); require __DIR__ . '/includes/WebStart.php'; @@ -44,7 +45,7 @@ if ( !$wgRequest->checkUrlExtension() ) { // PATH_INFO can be used for stupid things. We don't support it for api.php at // all, so error out if it's present. if ( isset( $_SERVER['PATH_INFO'] ) && $_SERVER['PATH_INFO'] != '' ) { - $correctUrl = wfAppendQuery( wfScript( 'api' ), $wgRequest->getQueryValues() ); + $correctUrl = wfAppendQuery( wfScript( 'api' ), $wgRequest->getQueryValuesOnly() ); $correctUrl = wfExpandUrl( $correctUrl, PROTO_CANONICAL ); header( "Location: $correctUrl", true, 301 ); echo 'This endpoint does not support "path info", i.e. extra text between "api.php"' diff --git a/autoload.php b/autoload.php index 9a555d0c67..55e5a7f089 100644 --- a/autoload.php +++ b/autoload.php @@ -20,7 +20,6 @@ $wgAutoloadLocalClasses = [ 'AllMessagesTablePager' => __DIR__ . '/includes/specials/pagers/AllMessagesTablePager.php', 'AllTrans' => __DIR__ . '/maintenance/language/alltrans.php', 'AlphabeticPager' => __DIR__ . '/includes/pager/AlphabeticPager.php', - 'AncientPagesPage' => __DIR__ . '/includes/specials/SpecialAncientpages.php', 'AnsiTermColorer' => __DIR__ . '/maintenance/term/MWTerm.php', 'ApiAMCreateAccount' => __DIR__ . '/includes/api/ApiAMCreateAccount.php', 'ApiAuthManagerHelper' => __DIR__ . '/includes/api/ApiAuthManagerHelper.php', @@ -90,6 +89,7 @@ $wgAutoloadLocalClasses = [ 'ApiQueryBacklinks' => __DIR__ . '/includes/api/ApiQueryBacklinks.php', 'ApiQueryBacklinksprop' => __DIR__ . '/includes/api/ApiQueryBacklinksprop.php', 'ApiQueryBase' => __DIR__ . '/includes/api/ApiQueryBase.php', + 'ApiQueryBlockInfoTrait' => __DIR__ . '/includes/api/ApiQueryBlockInfoTrait.php', 'ApiQueryBlocks' => __DIR__ . '/includes/api/ApiQueryBlocks.php', 'ApiQueryCategories' => __DIR__ . '/includes/api/ApiQueryCategories.php', 'ApiQueryCategoryInfo' => __DIR__ . '/includes/api/ApiQueryCategoryInfo.php', @@ -215,7 +215,6 @@ $wgAutoloadLocalClasses = [ 'BlockLogFormatter' => __DIR__ . '/includes/logging/BlockLogFormatter.php', 'BmpHandler' => __DIR__ . '/includes/media/BmpHandler.php', 'BotPassword' => __DIR__ . '/includes/user/BotPassword.php', - 'BrokenRedirectsPage' => __DIR__ . '/includes/specials/SpecialBrokenRedirects.php', 'BufferingStatsdDataFactory' => __DIR__ . '/includes/libs/stats/BufferingStatsdDataFactory.php', 'CLIParser' => __DIR__ . '/maintenance/parse.php', 'CSSMin' => __DIR__ . '/includes/libs/CSSMin.php', @@ -274,6 +273,7 @@ $wgAutoloadLocalClasses = [ 'CleanupInvalidDbKeys' => __DIR__ . '/maintenance/cleanupInvalidDbKeys.php', 'CleanupPreferences' => __DIR__ . '/maintenance/cleanupPreferences.php', 'CleanupRemovedModules' => __DIR__ . '/maintenance/cleanupRemovedModules.php', + 'CleanupRevActorPage' => __DIR__ . '/maintenance/cleanupRevActorPage.php', 'CleanupSpam' => __DIR__ . '/maintenance/cleanupSpam.php', 'CleanupUploadStash' => __DIR__ . '/maintenance/cleanupUploadStash.php', 'CleanupUsersWithNoId' => __DIR__ . '/maintenance/cleanupUsersWithNoId.php', @@ -285,6 +285,7 @@ $wgAutoloadLocalClasses = [ 'CloneDatabase' => __DIR__ . '/includes/db/CloneDatabase.php', 'CodeCleanerGlobalsPass' => __DIR__ . '/maintenance/CodeCleanerGlobalsPass.inc', 'CodeContentHandler' => __DIR__ . '/includes/content/CodeContentHandler.php', + 'CollapsibleFieldsetLayout' => __DIR__ . '/includes/htmlform/CollapsibleFieldsetLayout.php', 'Collation' => __DIR__ . '/includes/collation/Collation.php', 'CollationCkb' => __DIR__ . '/includes/collation/CollationCkb.php', 'CommandLineInc' => __DIR__ . '/maintenance/commandLine.inc', @@ -298,6 +299,7 @@ $wgAutoloadLocalClasses = [ 'ComposerJson' => __DIR__ . '/includes/libs/composer/ComposerJson.php', 'ComposerLock' => __DIR__ . '/includes/libs/composer/ComposerLock.php', 'ComposerPackageModifier' => __DIR__ . '/includes/composer/ComposerPackageModifier.php', + 'ComposerPhpunitXmlCoverageEdit' => __DIR__ . '/includes/composer/ComposerPhpunitXmlCoverageEdit.php', 'ComposerVendorHtaccessCreator' => __DIR__ . '/includes/composer/ComposerVendorHtaccessCreator.php', 'ComposerVersionNormalizer' => __DIR__ . '/includes/composer/ComposerVersionNormalizer.php', 'CompressOld' => __DIR__ . '/maintenance/storage/compressOld.php', @@ -316,7 +318,7 @@ $wgAutoloadLocalClasses = [ 'ConvertExtensionToRegistration' => __DIR__ . '/maintenance/convertExtensionToRegistration.php', 'ConvertLinks' => __DIR__ . '/maintenance/convertLinks.php', 'ConvertUserOptions' => __DIR__ . '/maintenance/convertUserOptions.php', - 'ConverterRule' => __DIR__ . '/languages/ConverterRule.php', + 'ConverterRule' => __DIR__ . '/includes/language/ConverterRule.php', 'Cookie' => __DIR__ . '/includes/libs/Cookie.php', 'CookieJar' => __DIR__ . '/includes/libs/CookieJar.php', 'CopyFileBackend' => __DIR__ . '/maintenance/copyFileBackend.php', @@ -365,7 +367,6 @@ $wgAutoloadLocalClasses = [ 'DateFormats' => __DIR__ . '/maintenance/language/date-formats.php', 'DateFormatter' => __DIR__ . '/includes/parser/DateFormatter.php', 'DateFormatterFactory' => __DIR__ . '/includes/parser/DateFormatterFactory.php', - 'DeadendPagesPage' => __DIR__ . '/includes/specials/SpecialDeadendpages.php', 'DeduplicateArchiveRevId' => __DIR__ . '/maintenance/deduplicateArchiveRevId.php', 'DeferrableCallback' => __DIR__ . '/includes/deferred/DeferrableCallback.php', 'DeferrableUpdate' => __DIR__ . '/includes/deferred/DeferrableUpdate.php', @@ -387,7 +388,6 @@ $wgAutoloadLocalClasses = [ 'DeletePageJob' => __DIR__ . '/includes/jobqueue/jobs/DeletePageJob.php', 'DeleteSelfExternals' => __DIR__ . '/maintenance/deleteSelfExternals.php', 'DeletedContribsPager' => __DIR__ . '/includes/specials/pagers/DeletedContribsPager.php', - 'DeletedContributionsPage' => __DIR__ . '/includes/specials/SpecialDeletedContributions.php', 'DependencyWrapper' => __DIR__ . '/includes/cache/dependency/DependencyWrapper.php', 'DeprecatedGlobal' => __DIR__ . '/includes/DeprecatedGlobal.php', 'DeprecatedInterfaceFinder' => __DIR__ . '/maintenance/findDeprecated.php', @@ -413,7 +413,6 @@ $wgAutoloadLocalClasses = [ 'DjVuImage' => __DIR__ . '/includes/media/DjVuImage.php', 'DnsSrvDiscoverer' => __DIR__ . '/includes/libs/DnsSrvDiscoverer.php', 'DoubleRedirectJob' => __DIR__ . '/includes/jobqueue/jobs/DoubleRedirectJob.php', - 'DoubleRedirectsPage' => __DIR__ . '/includes/specials/SpecialDoubleRedirects.php', 'DummyLinker' => __DIR__ . '/includes/DummyLinker.php', 'DummySearchIndexFieldDefinition' => __DIR__ . '/includes/search/DummySearchIndexFieldDefinition.php', 'DummyTermColorer' => __DIR__ . '/maintenance/term/MWTerm.php', @@ -446,8 +445,6 @@ $wgAutoloadLocalClasses = [ 'EditPage' => __DIR__ . '/includes/EditPage.php', 'EditWatchlistCheckboxSeriesField' => __DIR__ . '/includes/specials/formfields/EditWatchlistCheckboxSeriesField.php', 'EditWatchlistNormalHTMLForm' => __DIR__ . '/includes/specials/forms/EditWatchlistNormalHTMLForm.php', - 'EmailConfirmation' => __DIR__ . '/includes/specials/SpecialConfirmemail.php', - 'EmailInvalidation' => __DIR__ . '/includes/specials/SpecialEmailInvalidate.php', 'EmailNotification' => __DIR__ . '/includes/mail/EmailNotification.php', 'EmaillingJob' => __DIR__ . '/includes/jobqueue/jobs/EmaillingJob.php', 'EmptyBagOStuff' => __DIR__ . '/includes/libs/objectcache/EmptyBagOStuff.php', @@ -504,7 +501,6 @@ $wgAutoloadLocalClasses = [ 'FeedItem' => __DIR__ . '/includes/changes/FeedItem.php', 'FeedUtils' => __DIR__ . '/includes/FeedUtils.php', 'FetchText' => __DIR__ . '/maintenance/fetchText.php', - 'FewestrevisionsPage' => __DIR__ . '/includes/specials/SpecialFewestrevisions.php', 'Field' => __DIR__ . '/includes/libs/rdbms/field/Field.php', 'File' => __DIR__ . '/includes/filerepo/file/File.php', 'FileAwareNodeVisitor' => __DIR__ . '/maintenance/findDeprecated.php', @@ -524,7 +520,6 @@ $wgAutoloadLocalClasses = [ 'FileContentsHasher' => __DIR__ . '/includes/utils/FileContentsHasher.php', 'FileDeleteForm' => __DIR__ . '/includes/FileDeleteForm.php', 'FileDependency' => __DIR__ . '/includes/cache/dependency/FileDependency.php', - 'FileDuplicateSearchPage' => __DIR__ . '/includes/specials/SpecialFileDuplicateSearch.php', 'FileJournal' => __DIR__ . '/includes/libs/filebackend/filejournal/FileJournal.php', 'FileOp' => __DIR__ . '/includes/libs/filebackend/fileop/FileOp.php', 'FileOpBatch' => __DIR__ . '/includes/libs/filebackend/FileOpBatch.php', @@ -784,14 +779,11 @@ $wgAutoloadLocalClasses = [ 'LinkCache' => __DIR__ . '/includes/cache/LinkCache.php', 'LinkFilter' => __DIR__ . '/includes/LinkFilter.php', 'LinkHolderArray' => __DIR__ . '/includes/parser/LinkHolderArray.php', - 'LinkSearchPage' => __DIR__ . '/includes/specials/SpecialLinkSearch.php', 'Linker' => __DIR__ . '/includes/Linker.php', 'LinksDeletionUpdate' => __DIR__ . '/includes/deferred/LinksDeletionUpdate.php', 'LinksUpdate' => __DIR__ . '/includes/deferred/LinksUpdate.php', - 'ListDuplicatedFilesPage' => __DIR__ . '/includes/specials/SpecialListDuplicatedFiles.php', 'ListToggle' => __DIR__ . '/includes/ListToggle.php', 'ListVariants' => __DIR__ . '/maintenance/language/listVariants.php', - 'ListredirectsPage' => __DIR__ . '/includes/specials/SpecialListredirects.php', 'LoadBalancer' => __DIR__ . '/includes/libs/rdbms/loadbalancer/LoadBalancer.php', 'LoadBalancerSingle' => __DIR__ . '/includes/libs/rdbms/loadbalancer/LoadBalancerSingle.php', 'LocalFile' => __DIR__ . '/includes/filerepo/file/LocalFile.php', @@ -817,9 +809,6 @@ $wgAutoloadLocalClasses = [ 'LoggedUpdateMaintenance' => __DIR__ . '/maintenance/Maintenance.php', 'LoginHelper' => __DIR__ . '/includes/specials/helpers/LoginHelper.php', 'LoginSignupSpecialPage' => __DIR__ . '/includes/specialpage/LoginSignupSpecialPage.php', - 'LonelyPagesPage' => __DIR__ . '/includes/specials/SpecialLonelypages.php', - 'LongPagesPage' => __DIR__ . '/includes/specials/SpecialLongpages.php', - 'MIMEsearchPage' => __DIR__ . '/includes/specials/SpecialMIMEsearch.php', 'MSCompoundFileReader' => __DIR__ . '/includes/libs/mime/MSCompoundFileReader.php', 'MWCallableUpdate' => __DIR__ . '/includes/deferred/MWCallableUpdate.php', 'MWCallbackStream' => __DIR__ . '/includes/http/MWCallbackStream.php', @@ -829,6 +818,7 @@ $wgAutoloadLocalClasses = [ 'MWCryptRand' => __DIR__ . '/includes/utils/MWCryptRand.php', 'MWDebug' => __DIR__ . '/includes/debug/MWDebug.php', 'MWDocGen' => __DIR__ . '/maintenance/mwdocgen.php', + 'MWDoxygenFilter' => __DIR__ . '/maintenance/includes/MWDoxygenFilter.php', 'MWException' => __DIR__ . '/includes/exception/MWException.php', 'MWExceptionHandler' => __DIR__ . '/includes/exception/MWExceptionHandler.php', 'MWExceptionRenderer' => __DIR__ . '/includes/exception/MWExceptionRenderer.php', @@ -865,7 +855,6 @@ $wgAutoloadLocalClasses = [ 'McrUndoAction' => __DIR__ . '/includes/actions/McrUndoAction.php', 'MediaHandler' => __DIR__ . '/includes/media/MediaHandler.php', 'MediaHandlerFactory' => __DIR__ . '/includes/media/MediaHandlerFactory.php', - 'MediaStatisticsPage' => __DIR__ . '/includes/specials/SpecialMediaStatistics.php', 'MediaTransformError' => __DIR__ . '/includes/media/MediaTransformError.php', 'MediaTransformInvalidParametersException' => __DIR__ . '/includes/media/MediaTransformInvalidParametersException.php', 'MediaTransformOutput' => __DIR__ . '/includes/media/MediaTransformOutput.php', @@ -874,12 +863,15 @@ $wgAutoloadLocalClasses = [ 'MediaWikiSite' => __DIR__ . '/includes/site/MediaWikiSite.php', 'MediaWikiTitleCodec' => __DIR__ . '/includes/title/MediaWikiTitleCodec.php', 'MediaWikiVersionFetcher' => __DIR__ . '/includes/MediaWikiVersionFetcher.php', + 'MediaWiki\\BadFileLookup' => __DIR__ . '/includes/BadFileLookup.php', 'MediaWiki\\ChangeTags\\Taggable' => __DIR__ . '/includes/changetags/Taggable.php', 'MediaWiki\\Config\\ConfigRepository' => __DIR__ . '/includes/config/ConfigRepository.php', 'MediaWiki\\Config\\ServiceOptions' => __DIR__ . '/includes/config/ServiceOptions.php', 'MediaWiki\\DB\\PatchFileLocation' => __DIR__ . '/includes/db/PatchFileLocation.php', 'MediaWiki\\Diff\\ComplexityException' => __DIR__ . '/includes/diff/ComplexityException.php', 'MediaWiki\\Diff\\WordAccumulator' => __DIR__ . '/includes/diff/WordAccumulator.php', + 'MediaWiki\\FileBackend\\FSFile\\TempFSFileFactory' => __DIR__ . '/includes/libs/filebackend/fsfile/TempFSFileFactory.php', + 'MediaWiki\\FileBackend\\LockManager\\LockManagerGroupFactory' => __DIR__ . '/includes/filebackend/lockmanager/LockManagerGroupFactory.php', 'MediaWiki\\HeaderCallback' => __DIR__ . '/includes/HeaderCallback.php', 'MediaWiki\\Http\\HttpRequestFactory' => __DIR__ . '/includes/http/HttpRequestFactory.php', 'MediaWiki\\Installer\\InstallException' => __DIR__ . '/includes/installer/InstallException.php', @@ -913,6 +905,7 @@ $wgAutoloadLocalClasses = [ 'MediaWiki\\MediaWikiServices' => __DIR__ . '/includes/MediaWikiServices.php', 'MediaWiki\\Navigation\\PrevNextNavigationRenderer' => __DIR__ . '/includes/Navigation/PrevNextNavigationRenderer.php', 'MediaWiki\\OutputHandler' => __DIR__ . '/includes/OutputHandler.php', + 'MediaWiki\\Page\\MovePageFactory' => __DIR__ . '/includes/page/MovePageFactory.php', 'MediaWiki\\ProcOpenError' => __DIR__ . '/includes/exception/ProcOpenError.php', 'MediaWiki\\Search\\ParserOutputSearchDataExtractor' => __DIR__ . '/includes/search/ParserOutputSearchDataExtractor.php', 'MediaWiki\\Services\\CannotReplaceActiveServiceException' => __DIR__ . '/includes/libs/services/CannotReplaceActiveServiceException.php', @@ -971,7 +964,7 @@ $wgAutoloadLocalClasses = [ 'MediumSpecificBagOStuff' => __DIR__ . '/includes/libs/objectcache/MediumSpecificBagOStuff.php', 'MemcLockManager' => __DIR__ . '/includes/libs/lockmanager/MemcLockManager.php', 'MemcachedBagOStuff' => __DIR__ . '/includes/libs/objectcache/MemcachedBagOStuff.php', - 'MemcachedClient' => __DIR__ . '/includes/libs/objectcache/MemcachedClient.php', + 'MemcachedClient' => __DIR__ . '/includes/libs/objectcache/utils/MemcachedClient.php', 'MemcachedPeclBagOStuff' => __DIR__ . '/includes/libs/objectcache/MemcachedPeclBagOStuff.php', 'MemcachedPhpBagOStuff' => __DIR__ . '/includes/libs/objectcache/MemcachedPhpBagOStuff.php', 'MemoizedCallable' => __DIR__ . '/includes/libs/MemoizedCallable.php', @@ -996,13 +989,7 @@ $wgAutoloadLocalClasses = [ 'MigrateUserGroup' => __DIR__ . '/maintenance/migrateUserGroup.php', 'MimeAnalyzer' => __DIR__ . '/includes/libs/mime/MimeAnalyzer.php', 'MinifyScript' => __DIR__ . '/maintenance/minify.php', - 'MostcategoriesPage' => __DIR__ . '/includes/specials/SpecialMostcategories.php', 'MostimagesPage' => __DIR__ . '/includes/specials/SpecialMostimages.php', - 'MostinterwikisPage' => __DIR__ . '/includes/specials/SpecialMostinterwikis.php', - 'MostlinkedCategoriesPage' => __DIR__ . '/includes/specials/SpecialMostlinkedcategories.php', - 'MostlinkedPage' => __DIR__ . '/includes/specials/SpecialMostlinked.php', - 'MostlinkedTemplatesPage' => __DIR__ . '/includes/specials/SpecialMostlinkedtemplates.php', - 'MostrevisionsPage' => __DIR__ . '/includes/specials/SpecialMostrevisions.php', 'MoveBatch' => __DIR__ . '/maintenance/moveBatch.php', 'MoveFileOp' => __DIR__ . '/includes/libs/filebackend/fileop/MoveFileOp.php', 'MoveLogFormatter' => __DIR__ . '/includes/logging/MoveLogFormatter.php', @@ -1041,8 +1028,6 @@ $wgAutoloadLocalClasses = [ 'NullStatsdDataFactory' => __DIR__ . '/includes/libs/stats/NullStatsdDataFactory.php', 'NumericUppercaseCollation' => __DIR__ . '/includes/collation/NumericUppercaseCollation.php', 'OOUIHTMLForm' => __DIR__ . '/includes/htmlform/OOUIHTMLForm.php', - 'ORAField' => __DIR__ . '/includes/db/ORAField.php', - 'ORAResult' => __DIR__ . '/includes/db/ORAResult.php', 'ObjectCache' => __DIR__ . '/includes/objectcache/ObjectCache.php', 'OldChangesList' => __DIR__ . '/includes/changes/OldChangesList.php', 'OldLocalFile' => __DIR__ . '/includes/filerepo/file/OldLocalFile.php', @@ -1284,6 +1269,8 @@ $wgAutoloadLocalClasses = [ 'RevisionItemBase' => __DIR__ . '/includes/revisionlist/RevisionItemBase.php', 'RevisionList' => __DIR__ . '/includes/revisionlist/RevisionList.php', 'RevisionListBase' => __DIR__ . '/includes/revisionlist/RevisionListBase.php', + 'RevisionSearchResult' => __DIR__ . '/includes/search/RevisionSearchResult.php', + 'RevisionSearchResultTrait' => __DIR__ . '/includes/search/RevisionSearchResultTrait.php', 'RiffExtractor' => __DIR__ . '/includes/libs/RiffExtractor.php', 'RightsLogFormatter' => __DIR__ . '/includes/logging/RightsLogFormatter.php', 'RollbackAction' => __DIR__ . '/includes/actions/RollbackAction.php', @@ -1308,15 +1295,14 @@ $wgAutoloadLocalClasses = [ 'SearchHighlighter' => __DIR__ . '/includes/search/SearchHighlighter.php', 'SearchIndexField' => __DIR__ . '/includes/search/SearchIndexField.php', 'SearchIndexFieldDefinition' => __DIR__ . '/includes/search/SearchIndexFieldDefinition.php', - 'SearchMssql' => __DIR__ . '/includes/search/SearchMssql.php', 'SearchMySQL' => __DIR__ . '/includes/search/SearchMySQL.php', 'SearchNearMatchResultSet' => __DIR__ . '/includes/search/SearchNearMatchResultSet.php', 'SearchNearMatcher' => __DIR__ . '/includes/search/SearchNearMatcher.php', - 'SearchOracle' => __DIR__ . '/includes/search/SearchOracle.php', 'SearchPostgres' => __DIR__ . '/includes/search/SearchPostgres.php', 'SearchResult' => __DIR__ . '/includes/search/SearchResult.php', 'SearchResultSet' => __DIR__ . '/includes/search/SearchResultSet.php', 'SearchResultSetTrait' => __DIR__ . '/includes/search/SearchResultSetTrait.php', + 'SearchResultTrait' => __DIR__ . '/includes/search/SearchResultTrait.php', 'SearchSqlite' => __DIR__ . '/includes/search/SearchSqlite.php', 'SearchSuggestion' => __DIR__ . '/includes/search/SearchSuggestion.php', 'SearchSuggestionSet' => __DIR__ . '/includes/search/SearchSuggestionSet.php', @@ -1326,7 +1312,6 @@ $wgAutoloadLocalClasses = [ 'SerializedValueContainer' => __DIR__ . '/includes/libs/objectcache/serialized/SerializedValueContainer.php', 'SevenZipStream' => __DIR__ . '/maintenance/includes/SevenZipStream.php', 'ShiConverter' => __DIR__ . '/languages/classes/LanguageShi.php', - 'ShortPagesPage' => __DIR__ . '/includes/specials/SpecialShortpages.php', 'ShowJobs' => __DIR__ . '/maintenance/showJobs.php', 'ShowSiteStats' => __DIR__ . '/maintenance/showSiteStats.php', 'Site' => __DIR__ . '/includes/site/Site.php', @@ -1353,6 +1338,7 @@ $wgAutoloadLocalClasses = [ 'SpecialAllMessages' => __DIR__ . '/includes/specials/SpecialAllMessages.php', 'SpecialAllMyUploads' => __DIR__ . '/includes/specials/redirects/SpecialAllMyUploads.php', 'SpecialAllPages' => __DIR__ . '/includes/specials/SpecialAllPages.php', + 'SpecialAncientPages' => __DIR__ . '/includes/specials/SpecialAncientPages.php', 'SpecialApiHelp' => __DIR__ . '/includes/specials/SpecialApiHelp.php', 'SpecialApiSandbox' => __DIR__ . '/includes/specials/SpecialApiSandbox.php', 'SpecialAutoblockList' => __DIR__ . '/includes/specials/SpecialAutoblockList.php', @@ -1361,6 +1347,7 @@ $wgAutoloadLocalClasses = [ 'SpecialBlockList' => __DIR__ . '/includes/specials/SpecialBlockList.php', 'SpecialBookSources' => __DIR__ . '/includes/specials/SpecialBookSources.php', 'SpecialBotPasswords' => __DIR__ . '/includes/specials/SpecialBotPasswords.php', + 'SpecialBrokenRedirects' => __DIR__ . '/includes/specials/SpecialBrokenRedirects.php', 'SpecialCachedPage' => __DIR__ . '/includes/specials/SpecialCachedPage.php', 'SpecialCategories' => __DIR__ . '/includes/specials/SpecialCategories.php', 'SpecialChangeContentModel' => __DIR__ . '/includes/specials/SpecialChangeContentModel.php', @@ -1368,35 +1355,55 @@ $wgAutoloadLocalClasses = [ 'SpecialChangeEmail' => __DIR__ . '/includes/specials/SpecialChangeEmail.php', 'SpecialChangePassword' => __DIR__ . '/includes/specials/SpecialChangePassword.php', 'SpecialComparePages' => __DIR__ . '/includes/specials/SpecialComparePages.php', + 'SpecialConfirmEmail' => __DIR__ . '/includes/specials/SpecialConfirmEmail.php', 'SpecialContributions' => __DIR__ . '/includes/specials/SpecialContributions.php', 'SpecialCreateAccount' => __DIR__ . '/includes/specials/SpecialCreateAccount.php', + 'SpecialDeadendPages' => __DIR__ . '/includes/specials/SpecialDeadendPages.php', + 'SpecialDeletedContributions' => __DIR__ . '/includes/specials/SpecialDeletedContributions.php', 'SpecialDiff' => __DIR__ . '/includes/specials/SpecialDiff.php', + 'SpecialDoubleRedirects' => __DIR__ . '/includes/specials/SpecialDoubleRedirects.php', 'SpecialEditTags' => __DIR__ . '/includes/specials/SpecialEditTags.php', 'SpecialEditWatchlist' => __DIR__ . '/includes/specials/SpecialEditWatchlist.php', + 'SpecialEmailInvalidate' => __DIR__ . '/includes/specials/SpecialEmailInvalidate.php', 'SpecialEmailUser' => __DIR__ . '/includes/specials/SpecialEmailUser.php', 'SpecialExpandTemplates' => __DIR__ . '/includes/specials/SpecialExpandTemplates.php', 'SpecialExport' => __DIR__ . '/includes/specials/SpecialExport.php', + 'SpecialFewestRevisions' => __DIR__ . '/includes/specials/SpecialFewestRevisions.php', + 'SpecialFileDuplicateSearch' => __DIR__ . '/includes/specials/SpecialFileDuplicateSearch.php', 'SpecialFilepath' => __DIR__ . '/includes/specials/SpecialFilepath.php', 'SpecialGoToInterwiki' => __DIR__ . '/includes/specials/SpecialGoToInterwiki.php', 'SpecialImport' => __DIR__ . '/includes/specials/SpecialImport.php', 'SpecialJavaScriptTest' => __DIR__ . '/includes/specials/SpecialJavaScriptTest.php', 'SpecialLinkAccounts' => __DIR__ . '/includes/specials/SpecialLinkAccounts.php', + 'SpecialLinkSearch' => __DIR__ . '/includes/specials/SpecialLinkSearch.php', 'SpecialListAdmins' => __DIR__ . '/includes/specials/redirects/SpecialListAdmins.php', 'SpecialListBots' => __DIR__ . '/includes/specials/redirects/SpecialListBots.php', + 'SpecialListDuplicatedFiles' => __DIR__ . '/includes/specials/SpecialListDuplicatedFiles.php', 'SpecialListFiles' => __DIR__ . '/includes/specials/SpecialListFiles.php', 'SpecialListGrants' => __DIR__ . '/includes/specials/SpecialListGrants.php', 'SpecialListGroupRights' => __DIR__ . '/includes/specials/SpecialListGroupRights.php', + 'SpecialListRedirects' => __DIR__ . '/includes/specials/SpecialListRedirects.php', 'SpecialListUsers' => __DIR__ . '/includes/specials/SpecialListUsers.php', 'SpecialLockdb' => __DIR__ . '/includes/specials/SpecialLockdb.php', 'SpecialLog' => __DIR__ . '/includes/specials/SpecialLog.php', + 'SpecialLonelyPages' => __DIR__ . '/includes/specials/SpecialLonelyPages.php', + 'SpecialLongPages' => __DIR__ . '/includes/specials/SpecialLongPages.php', + 'SpecialMIMESearch' => __DIR__ . '/includes/specials/SpecialMIMESearch.php', + 'SpecialMediaStatistics' => __DIR__ . '/includes/specials/SpecialMediaStatistics.php', 'SpecialMergeHistory' => __DIR__ . '/includes/specials/SpecialMergeHistory.php', + 'SpecialMostCategories' => __DIR__ . '/includes/specials/SpecialMostCategories.php', + 'SpecialMostInterwikis' => __DIR__ . '/includes/specials/SpecialMostInterwikis.php', + 'SpecialMostLinked' => __DIR__ . '/includes/specials/SpecialMostLinked.php', + 'SpecialMostLinkedCategories' => __DIR__ . '/includes/specials/SpecialMostLinkedCategories.php', + 'SpecialMostLinkedTemplates' => __DIR__ . '/includes/specials/SpecialMostLinkedTemplates.php', + 'SpecialMostRevisions' => __DIR__ . '/includes/specials/SpecialMostRevisions.php', 'SpecialMute' => __DIR__ . '/includes/specials/SpecialMute.php', 'SpecialMyLanguage' => __DIR__ . '/includes/specials/SpecialMyLanguage.php', 'SpecialMycontributions' => __DIR__ . '/includes/specials/redirects/SpecialMycontributions.php', 'SpecialMypage' => __DIR__ . '/includes/specials/redirects/SpecialMypage.php', 'SpecialMytalk' => __DIR__ . '/includes/specials/redirects/SpecialMytalk.php', 'SpecialMyuploads' => __DIR__ . '/includes/specials/redirects/SpecialMyuploads.php', - 'SpecialNewFiles' => __DIR__ . '/includes/specials/SpecialNewimages.php', + 'SpecialNewFiles' => __DIR__ . '/includes/specials/SpecialNewFiles.php', 'SpecialNewSection' => __DIR__ . '/includes/specials/SpecialNewSection.php', 'SpecialNewpages' => __DIR__ . '/includes/specials/SpecialNewpages.php', 'SpecialPage' => __DIR__ . '/includes/specialpage/SpecialPage.php', @@ -1424,22 +1431,34 @@ $wgAutoloadLocalClasses = [ 'SpecialRevisionDelete' => __DIR__ . '/includes/specials/SpecialRevisionDelete.php', 'SpecialRunJobs' => __DIR__ . '/includes/specials/SpecialRunJobs.php', 'SpecialSearch' => __DIR__ . '/includes/specials/SpecialSearch.php', + 'SpecialShortPages' => __DIR__ . '/includes/specials/SpecialShortPages.php', 'SpecialSpecialpages' => __DIR__ . '/includes/specials/SpecialSpecialpages.php', 'SpecialStatistics' => __DIR__ . '/includes/specials/SpecialStatistics.php', 'SpecialTags' => __DIR__ . '/includes/specials/SpecialTags.php', 'SpecialTrackingCategories' => __DIR__ . '/includes/specials/SpecialTrackingCategories.php', 'SpecialUnblock' => __DIR__ . '/includes/specials/SpecialUnblock.php', + 'SpecialUncategorizedCategories' => __DIR__ . '/includes/specials/SpecialUncategorizedCategories.php', + 'SpecialUncategorizedImages' => __DIR__ . '/includes/specials/SpecialUncategorizedImages.php', + 'SpecialUncategorizedPages' => __DIR__ . '/includes/specials/SpecialUncategorizedPages.php', + 'SpecialUncategorizedTemplates' => __DIR__ . '/includes/specials/SpecialUncategorizedTemplates.php', 'SpecialUndelete' => __DIR__ . '/includes/specials/SpecialUndelete.php', 'SpecialUnlinkAccounts' => __DIR__ . '/includes/specials/SpecialUnlinkAccounts.php', 'SpecialUnlockdb' => __DIR__ . '/includes/specials/SpecialUnlockdb.php', + 'SpecialUnusedCategories' => __DIR__ . '/includes/specials/SpecialUnusedCategories.php', + 'SpecialUnusedImages' => __DIR__ . '/includes/specials/SpecialUnusedImages.php', + 'SpecialUnusedTemplates' => __DIR__ . '/includes/specials/SpecialUnusedTemplates.php', + 'SpecialUnwatchedPages' => __DIR__ . '/includes/specials/SpecialUnwatchedPages.php', 'SpecialUpload' => __DIR__ . '/includes/specials/SpecialUpload.php', 'SpecialUploadStash' => __DIR__ . '/includes/specials/SpecialUploadStash.php', 'SpecialUploadStashTooLargeException' => __DIR__ . '/includes/specials/exception/SpecialUploadStashTooLargeException.php', 'SpecialUserLogin' => __DIR__ . '/includes/specials/SpecialUserLogin.php', 'SpecialUserLogout' => __DIR__ . '/includes/specials/SpecialUserLogout.php', 'SpecialVersion' => __DIR__ . '/includes/specials/SpecialVersion.php', + 'SpecialWantedCategories' => __DIR__ . '/includes/specials/SpecialWantedCategories.php', + 'SpecialWantedTemplates' => __DIR__ . '/includes/specials/SpecialWantedTemplates.php', 'SpecialWatchlist' => __DIR__ . '/includes/specials/SpecialWatchlist.php', 'SpecialWhatLinksHere' => __DIR__ . '/includes/specials/SpecialWhatLinksHere.php', + 'SpecialWithoutInterwiki' => __DIR__ . '/includes/specials/SpecialWithoutInterwiki.php', 'SqlBagOStuff' => __DIR__ . '/includes/objectcache/SqlBagOStuff.php', 'SqlSearchResult' => __DIR__ . '/includes/search/SqlSearchResult.php', 'SqlSearchResultSet' => __DIR__ . '/includes/search/SqlSearchResultSet.php', @@ -1512,20 +1531,15 @@ $wgAutoloadLocalClasses = [ 'UDPTransport' => __DIR__ . '/includes/libs/UDPTransport.php', 'UIDGenerator' => __DIR__ . '/includes/utils/UIDGenerator.php', 'UcdXmlReader' => __DIR__ . '/maintenance/language/generateCollationData.php', - 'UncategorizedCategoriesPage' => __DIR__ . '/includes/specials/SpecialUncategorizedcategories.php', - 'UncategorizedImagesPage' => __DIR__ . '/includes/specials/SpecialUncategorizedimages.php', - 'UncategorizedPagesPage' => __DIR__ . '/includes/specials/SpecialUncategorizedpages.php', - 'UncategorizedTemplatesPage' => __DIR__ . '/includes/specials/SpecialUncategorizedtemplates.php', 'Undelete' => __DIR__ . '/maintenance/undelete.php', 'UnifiedDiffFormatter' => __DIR__ . '/includes/diff/UnifiedDiffFormatter.php', + 'UnknownContent' => __DIR__ . '/includes/content/UnknownContent.php', + 'UnknownContentHandler' => __DIR__ . '/includes/content/UnknownContentHandler.php', 'UnlistedSpecialPage' => __DIR__ . '/includes/specialpage/UnlistedSpecialPage.php', 'UnprotectAction' => __DIR__ . '/includes/actions/UnprotectAction.php', 'UnregisteredLocalFile' => __DIR__ . '/includes/filerepo/file/UnregisteredLocalFile.php', - 'UnusedCategoriesPage' => __DIR__ . '/includes/specials/SpecialUnusedcategories.php', - 'UnusedimagesPage' => __DIR__ . '/includes/specials/SpecialUnusedimages.php', - 'UnusedtemplatesPage' => __DIR__ . '/includes/specials/SpecialUnusedtemplates.php', + 'UnsupportedSlotDiffRenderer' => __DIR__ . '/includes/diff/UnsupportedSlotDiffRenderer.php', 'UnwatchAction' => __DIR__ . '/includes/actions/UnwatchAction.php', - 'UnwatchedpagesPage' => __DIR__ . '/includes/specials/SpecialUnwatchedpages.php', 'UpdateArticleCount' => __DIR__ . '/maintenance/updateArticleCount.php', 'UpdateCollation' => __DIR__ . '/maintenance/updateCollation.php', 'UpdateDoubleWidthSearch' => __DIR__ . '/maintenance/updateDoubleWidthSearch.php', @@ -1589,11 +1603,9 @@ $wgAutoloadLocalClasses = [ 'WANCacheReapUpdate' => __DIR__ . '/includes/deferred/WANCacheReapUpdate.php', 'WANObjectCache' => __DIR__ . '/includes/libs/objectcache/wancache/WANObjectCache.php', 'WANObjectCacheReaper' => __DIR__ . '/includes/libs/objectcache/wancache/WANObjectCacheReaper.php', - 'WantedCategoriesPage' => __DIR__ . '/includes/specials/SpecialWantedcategories.php', 'WantedFilesPage' => __DIR__ . '/includes/specials/SpecialWantedfiles.php', 'WantedPagesPage' => __DIR__ . '/includes/specials/SpecialWantedpages.php', 'WantedQueryPage' => __DIR__ . '/includes/specialpage/WantedQueryPage.php', - 'WantedTemplatesPage' => __DIR__ . '/includes/specials/SpecialWantedtemplates.php', 'WatchAction' => __DIR__ . '/includes/actions/WatchAction.php', 'WatchedItem' => __DIR__ . '/includes/watcheditem/WatchedItem.php', 'WatchedItemQueryService' => __DIR__ . '/includes/watcheditem/WatchedItemQueryService.php', @@ -1682,9 +1694,6 @@ $wgAutoloadLocalClasses = [ 'Wikimedia\\Rdbms\\LoadMonitorMySQL' => __DIR__ . '/includes/libs/rdbms/loadmonitor/LoadMonitorMySQL.php', 'Wikimedia\\Rdbms\\LoadMonitorNull' => __DIR__ . '/includes/libs/rdbms/loadmonitor/LoadMonitorNull.php', 'Wikimedia\\Rdbms\\MaintainableDBConnRef' => __DIR__ . '/includes/libs/rdbms/database/MaintainableDBConnRef.php', - 'Wikimedia\\Rdbms\\MssqlBlob' => __DIR__ . '/includes/libs/rdbms/encasing/MssqlBlob.php', - 'Wikimedia\\Rdbms\\MssqlField' => __DIR__ . '/includes/libs/rdbms/field/MssqlField.php', - 'Wikimedia\\Rdbms\\MssqlResultWrapper' => __DIR__ . '/includes/libs/rdbms/database/resultwrapper/MssqlResultWrapper.php', 'Wikimedia\\Rdbms\\MySQLField' => __DIR__ . '/includes/libs/rdbms/field/MySQLField.php', 'Wikimedia\\Rdbms\\MySQLMasterPos' => __DIR__ . '/includes/libs/rdbms/database/position/MySQLMasterPos.php', 'Wikimedia\\Rdbms\\NextSequenceValue' => __DIR__ . '/includes/libs/rdbms/database/utils/NextSequenceValue.php', @@ -1700,7 +1709,6 @@ $wgAutoloadLocalClasses = [ 'WikitextContentHandler' => __DIR__ . '/includes/content/WikitextContentHandler.php', 'WikitextLogFormatter' => __DIR__ . '/includes/logging/WikitextLogFormatter.php', 'WinCacheBagOStuff' => __DIR__ . '/includes/libs/objectcache/WinCacheBagOStuff.php', - 'WithoutInterwikiPage' => __DIR__ . '/includes/specials/SpecialWithoutinterwiki.php', 'WordLevelDiff' => __DIR__ . '/includes/diff/WordLevelDiff.php', 'WrapOldPasswords' => __DIR__ . '/maintenance/wrapOldPasswords.php', 'XCFHandler' => __DIR__ . '/includes/media/XCFHandler.php', diff --git a/composer.json b/composer.json index 98e7ebf710..bd34b922df 100644 --- a/composer.json +++ b/composer.json @@ -20,6 +20,7 @@ "composer/semver": "1.5.0", "cssjanus/cssjanus": "1.3.0", "ext-ctype": "*", + "ext-dom": "*", "ext-fileinfo": "*", "ext-iconv": "*", "ext-json": "*", @@ -27,7 +28,7 @@ "ext-xml": "*", "guzzlehttp/guzzle": "6.3.3", "liuggio/statsd-php-client": "1.0.18", - "oojs/oojs-ui": "0.33.4", + "oojs/oojs-ui": "0.34.1", "pear/mail": "1.4.1", "pear/mail_mime": "1.10.2", "pear/net_smtp": "1.8.1", @@ -41,14 +42,14 @@ "wikimedia/cldr-plural-rule-parser": "1.0.0", "wikimedia/composer-merge-plugin": "1.4.1", "wikimedia/html-formatter": "1.0.2", - "wikimedia/ip-set": "2.0.1", + "wikimedia/ip-set": "2.1.0", "wikimedia/less.php": "1.8.0", "wikimedia/object-factory": "2.1.0", "wikimedia/password-blacklist": "0.1.4", "wikimedia/php-session-serializer": "1.0.7", "wikimedia/purtle": "1.0.7", "wikimedia/relpath": "2.1.1", - "wikimedia/remex-html": "2.0.3", + "wikimedia/remex-html": "2.1.0", "wikimedia/running-stat": "1.2.1", "wikimedia/scoped-callback": "3.0.0", "wikimedia/utfnormal": "2.0.0", @@ -73,10 +74,10 @@ "nmred/kafka-php": "0.1.5", "phpunit/phpunit": "4.8.36 || ^6.5", "psy/psysh": "0.9.9", - "wikimedia/avro": "1.8.0", + "wikimedia/avro": "1.9.0", "wikimedia/testing-access-wrapper": "~1.0", "wmde/hamcrest-html-matchers": "^0.1.0", - "mediawiki/mediawiki-phan-config": "0.6.1", + "mediawiki/mediawiki-phan-config": "0.7.1", "symfony/yaml": "3.4.28", "johnkary/phpunit-speedtrap": "^1.0 | ^2.0" }, @@ -96,7 +97,8 @@ "autoload": { "psr-0": { "ComposerHookHandler": "includes/composer", - "ComposerVendorHtaccessCreator": "includes/composer" + "ComposerVendorHtaccessCreator": "includes/composer", + "ComposerPhpunitXmlCoverageEdit":"includes/composer" } }, "autoload-dev": { @@ -120,7 +122,8 @@ "phpunit": "phpunit", "phpunit:unit": "phpunit --colors=always --testsuite=core:unit,extensions:unit,skins:unit", "phpunit:integration": "phpunit --colors=always --testsuite=core:integration,extensions:integration,skins:integration", - "phpunit:coverage": "phpunit --testsuite=core:unit --exclude-group Dump,Broken" + "phpunit:coverage": "phpunit --testsuite=core:unit --exclude-group Dump,Broken", + "phpunit:coverage-edit": "ComposerPhpunitXmlCoverageEdit::onEvent" }, "config": { "optimize-autoloader": true, diff --git a/docs/Introduction.md b/docs/Introduction.md new file mode 100644 index 0000000000..4814599bc0 --- /dev/null +++ b/docs/Introduction.md @@ -0,0 +1,7 @@ +Introduction {#mainpage} +======= + +Welcome on MediaWiki autogenerated documentation system. + +If you are looking to use, install or configure your wiki, you probably +want to look at the main site: . diff --git a/docs/database.txt b/docs/database.txt index 6e88d681f4..c09dd38900 100644 --- a/docs/database.txt +++ b/docs/database.txt @@ -176,8 +176,6 @@ MediaWiki does support the following other DBMSs to varying degrees. * PostgreSQL * SQLite -* Oracle -* MSSQL More information can be found about each of these databases (known issues, level of support, extra configuration) in the "databases" subdirectory in diff --git a/docs/doxygen_first_page.php b/docs/doxygen_first_page.php deleted file mode 100644 index 77ae1dcf36..0000000000 --- a/docs/doxygen_first_page.php +++ /dev/null @@ -1,19 +0,0 @@ - value ] $skin: Skin +$config: Config object (since 1.34) 'ResourceLoaderJqueryMsgModuleMagicWords': Called in ResourceLoaderJqueryMsgModule to allow adding magic words for jQueryMsg. @@ -2908,7 +2921,7 @@ result augmentors. Note that lists should be in the format name => object and the names in both lists should be distinct. -'SecondaryDataUpdates': DEPRECATED! Use RevisionDataUpdates or override +'SecondaryDataUpdates': DEPRECATED since 1.32! Use RevisionDataUpdates or override ContentHandler::getSecondaryDataUpdates instead. Allows modification of the list of DataUpdates to perform when page content is modified. $title: Title of the page that is being edited. @@ -3164,7 +3177,7 @@ $row: Revision information from the database 'SpecialContributions::getForm::filters': Called with a list of filters to render on Special:Contributions. $sp: SpecialContributions object, for context -&$filters: List of filters rendered as HTML +&$filters: List of filter object definitions (compatible with OOUI form) 'SpecialListusersDefaultQuery': Called right before the end of UsersPager::getDefaultQuery(). @@ -3205,6 +3218,9 @@ $request: WebRequest object for getting the value provided by the current user $sp: SpecialPage object, for context &$fields: Current HTMLForm fields descriptors +'SpecialMuteSubmit': DEPRECATED since 1.34! Used only for instrumentation on SpecialMute +$data: Array containing information about submitted options on SpecialMute form + 'SpecialNewpagesConditions': Called when building sql query for Special:NewPages. &$special: NewPagesPager object (subclass of ReverseChronologicalPager) @@ -3490,6 +3506,12 @@ processing. &$archive: PageArchive object $title: Title object of the page that we're about to undelete +'UndeletePageToolLinks': Add one or more links to edit page subtitle when a page +has been previously deleted. +$context: IContextSource (object) +$linkRenderer: LinkRenderer instance +&$links: Array of HTML strings + 'UndeleteShowRevision': Called when showing a revision in Special:Undelete. $title: title object related to the revision $rev: revision (object) that will be viewed @@ -3691,7 +3713,7 @@ $newUGMs: An associative array (group name => UserGroupMembership object) of the user's current group memberships. 'UserIsBlockedFrom': Check if a user is blocked from a specific page (for -specific block exemptions). +specific block exemptions if a user is already blocked). $user: User in question $title: Title of the page in question &$blocked: Out-param, whether or not the user is blocked from that page. @@ -3708,7 +3730,9 @@ $ip: User's IP address false if a UserGetRights hook might remove the named right. $right: The user right being checked -'UserIsHidden': Check if the user's name should be hidden. See User::isHidden(). +'UserIsHidden': DEPRECATED since 1.34 - use GetUserBlock instead, and add a +system block that hides the user. Check if the user's name should be hidden. +See User::isHidden(). $user: User in question. &$hidden: Set true if the user's name should be hidden. @@ -3945,7 +3969,7 @@ dumps. One, and only one hook should set this, and return false. &$opts: Options to use for the query &$join: Join conditions -'WikiPageDeletionUpdates': DEPRECATED! Use PageDeletionDataUpdates or +'WikiPageDeletionUpdates': DEPRECATED since 1.32! Use PageDeletionDataUpdates or override ContentHandler::getDeletionDataUpdates instead. Manipulates the list of DeferrableUpdates to be applied when a page is deleted. $page: the WikiPage diff --git a/docs/memcached.txt b/docs/memcached.txt index ba325fe672..d0a6e3dedd 100644 --- a/docs/memcached.txt +++ b/docs/memcached.txt @@ -131,13 +131,7 @@ Localisation: cleared by: Language::loadLocalisation() Message Cache: - backend: $wgMessageCacheType - key: $wgDBname:messages, $wgDBname:messages-hash, $wgDBname:messages-status - ex: wikidb:messages, wikidb:messages-hash, wikidb:messages-status - stores: an array where the keys are DB keys and the values are messages - set in: wfMessage(), Article::editUpdates() and Title::moveTo() - expiry: $wgMsgCacheExpiry - cleared by: nothing + See MessageCache.php. Newtalk: key: $wgDBname:newtalk:ip:$ip diff --git a/docs/pageupdater.txt b/docs/pageupdater.txt index fd084c0587..3d113f6569 100644 --- a/docs/pageupdater.txt +++ b/docs/pageupdater.txt @@ -148,7 +148,7 @@ parent of $revision parameter passed to prepareUpdate(). transformation (PST) and allow subsequent access to the canonical ParserOutput of the revision. getSlots() and getCanonicalParserOutput() as well as getSecondaryDataUpdates() may be used after prepareContent() was called. Calling prepareContent() with the same -parameters again has no effect. Calling it again with mismatching paramters, or calling +parameters again has no effect. Calling it again with mismatching parameters, or calling it after prepareUpdate() was called, triggers a LogicException. - prepareUpdate() is called after the new revision has been created. This may happen diff --git a/img_auth.php b/img_auth.php index 914014d85f..f23de4f470 100644 --- a/img_auth.php +++ b/img_auth.php @@ -39,6 +39,7 @@ */ define( 'MW_NO_OUTPUT_COMPRESSION', 1 ); +define( 'MW_ENTRY_POINT', 'img_auth' ); require __DIR__ . '/includes/WebStart.php'; # Set action base paths so that WebRequest::getPathInfo() @@ -53,9 +54,10 @@ $mediawiki->doPostOutputShutdown( 'fast' ); function wfImageAuthMain() { global $wgImgAuthUrlPathMap; + $permissionManager = \MediaWiki\MediaWikiServices::getInstance()->getPermissionManager(); $request = RequestContext::getMain()->getRequest(); - $publicWiki = in_array( 'read', User::getGroupPermissions( [ '*' ] ), true ); + $publicWiki = in_array( 'read', $permissionManager->getGroupPermissions( [ '*' ] ), true ); // Get the requested file path (source file or thumbnail) $matches = WebRequest::getPathInfo(); @@ -160,7 +162,6 @@ function wfImageAuthMain() { // Check user authorization for this title // Checks Whitelist too - $permissionManager = \MediaWiki\MediaWikiServices::getInstance()->getPermissionManager(); if ( !$permissionManager->userCan( 'read', $user, $title ) ) { wfForbidden( 'img-auth-accessdenied', 'img-auth-noread', $name ); diff --git a/includes/ActorMigration.php b/includes/ActorMigration.php index 5dde8a04d4..c79074df75 100644 --- a/includes/ActorMigration.php +++ b/includes/ActorMigration.php @@ -28,15 +28,18 @@ use Wikimedia\Rdbms\IDatabase; * This class handles the logic for the actor table migration. * * This is not intended to be a long-term part of MediaWiki; it will be - * deprecated and removed along with $wgActorTableSchemaMigrationStage. + * deprecated and removed once actor migration is complete. * * @since 1.31 + * @since 1.34 Use with 'ar_user', 'img_user', 'oi_user', 'fa_user', + * 'rc_user', 'log_user', and 'ipb_by' is deprecated. Callers should + * reference the corresponding actor fields directly. */ class ActorMigration { /** * Constant for extensions to feature-test whether $wgActorTableSchemaMigrationStage - * expects MIGRATION_* or SCHEMA_COMPAT_* + * (in MW <1.34) expects MIGRATION_* or SCHEMA_COMPAT_* */ const MIGRATION_STAGE_SCHEMA_COMPAT = 1; @@ -68,6 +71,28 @@ class ActorMigration { */ private static $formerTempTables = []; + /** + * Define fields that are deprecated for use with this class. + * @var (string|null)[] Keys are '$key', value is null for soft deprecation + * or a string naming the deprecated version for hard deprecation. + */ + private static $deprecated = [ + 'ar_user' => null, // 1.34 + 'img_user' => null, // 1.34 + 'oi_user' => null, // 1.34 + 'fa_user' => null, // 1.34 + 'rc_user' => null, // 1.34 + 'log_user' => null, // 1.34 + 'ipb_by' => null, // 1.34 + ]; + + /** + * Define fields that are removed for use with this class. + * @var string[] Keys are '$key', value is the MediaWiki version in which + * use was removed. + */ + private static $removed = []; + /** * Define fields that use non-standard mapping * @var array Keys are the user id column name, values are arrays with two @@ -112,6 +137,21 @@ class ActorMigration { return MediaWikiServices::getInstance()->getActorMigration(); } + /** + * Issue deprecation warning/error as appropriate. + * @param string $key + */ + private static function checkDeprecation( $key ) { + if ( isset( self::$removed[$key] ) ) { + throw new InvalidArgumentException( + "Use of " . static::class . " for '$key' was removed in MediaWiki " . self::$removed[$key] + ); + } + if ( !empty( self::$deprecated[$key] ) ) { + wfDeprecated( static::class . " for '$key'", self::$deprecated[$key], false, 3 ); + } + } + /** * Return an SQL condition to test if a user field is anonymous * @param string $field Field name or SQL fragment @@ -152,6 +192,8 @@ class ActorMigration { * @phan-return array{tables:string[],fields:string[],joins:array} */ public function getJoin( $key ) { + self::checkDeprecation( $key ); + if ( !isset( $this->joinCache[$key] ) ) { $tables = []; $fields = []; @@ -203,6 +245,8 @@ class ActorMigration { * @return array to merge into `$values` to `IDatabase->update()` or `$a` to `IDatabase->insert()` */ public function getInsertValues( IDatabase $dbw, $key, UserIdentity $user ) { + self::checkDeprecation( $key ); + if ( isset( self::$tempTables[$key] ) ) { throw new InvalidArgumentException( "Must use getInsertValuesWithTempTable() for $key" ); } @@ -236,6 +280,8 @@ class ActorMigration { * and extra fields needed for the temp table. */ public function getInsertValuesWithTempTable( IDatabase $dbw, $key, UserIdentity $user ) { + self::checkDeprecation( $key ); + if ( isset( self::$formerTempTables[$key] ) ) { wfDeprecated( __METHOD__ . " for $key", self::$formerTempTables[$key] ); } elseif ( !isset( self::$tempTables[$key] ) ) { @@ -319,6 +365,8 @@ class ActorMigration { * All tables and joins are aliased, so `+` is safe to use. */ public function getWhere( IDatabase $db, $key, $users, $useId = true ) { + self::checkDeprecation( $key ); + $tables = []; $conds = []; $joins = []; diff --git a/includes/AjaxDispatcher.php b/includes/AjaxDispatcher.php index f6c9075136..ea10a2e863 100644 --- a/includes/AjaxDispatcher.php +++ b/includes/AjaxDispatcher.php @@ -114,6 +114,7 @@ class AjaxDispatcher { return; } + $permissionManager = MediaWikiServices::getInstance()->getPermissionManager(); if ( !in_array( $this->func_name, $this->config->get( 'AjaxExportList' ) ) ) { wfDebug( __METHOD__ . ' Bad Request for unknown function ' . $this->func_name . "\n" ); wfHttpError( @@ -121,7 +122,8 @@ class AjaxDispatcher { 'Bad Request', "unknown function " . $this->func_name ); - } elseif ( !User::isEveryoneAllowed( 'read' ) && !$user->isAllowed( 'read' ) ) { + } elseif ( !$permissionManager->isEveryoneAllowed( 'read' ) && + !$permissionManager->userHasRight( $user, 'read' ) ) { wfHttpError( 403, 'Forbidden', diff --git a/includes/AjaxResponse.php b/includes/AjaxResponse.php index 323c5d30ac..faff021859 100644 --- a/includes/AjaxResponse.php +++ b/includes/AjaxResponse.php @@ -55,7 +55,7 @@ class AjaxResponse { /** * HTTP response code - * @var string $mResponseCode + * @var int|string $mResponseCode */ private $mResponseCode; @@ -114,7 +114,7 @@ class AjaxResponse { /** * Set the HTTP response code - * @param string $code + * @param int|string $code */ function setResponseCode( $code ) { $this->mResponseCode = $code; @@ -182,16 +182,7 @@ class AjaxResponse { if ( $this->mConfig->get( 'UseCdn' ) ) { # Expect explicit purge of the proxy cache, but require end user agents # to revalidate against the proxy on each visit. - # Surrogate-Control controls our CDN, Cache-Control downstream caches - - if ( $this->mConfig->get( 'UseESI' ) ) { - wfDeprecated( '$wgUseESI = true', '1.33' ); - header( 'Surrogate-Control: max-age=' . $this->mCacheDuration . ', content="ESI/1.0"' ); - header( 'Cache-Control: s-maxage=0, must-revalidate, max-age=0' ); - } else { - header( 'Cache-Control: s-maxage=' . $this->mCacheDuration . ', must-revalidate, max-age=0' ); - } - + header( 'Cache-Control: s-maxage=' . $this->mCacheDuration . ', must-revalidate, max-age=0' ); } else { # Let the client do the caching. Cache is not purged. header( "Expires: " . gmdate( "D, d M Y H:i:s", time() + $this->mCacheDuration ) . " GMT" ); diff --git a/includes/AutoLoader.php b/includes/AutoLoader.php index b893bc9e14..abbc62c7f6 100644 --- a/includes/AutoLoader.php +++ b/includes/AutoLoader.php @@ -134,6 +134,7 @@ class AutoLoader { 'MediaWiki\\Edit\\' => __DIR__ . '/edit/', 'MediaWiki\\EditPage\\' => __DIR__ . '/editpage/', 'MediaWiki\\Linker\\' => __DIR__ . '/linker/', + 'MediaWiki\\Message\\' => __DIR__ . '/Message', 'MediaWiki\\Permissions\\' => __DIR__ . '/Permissions/', 'MediaWiki\\Preferences\\' => __DIR__ . '/preferences/', 'MediaWiki\\Rest\\' => __DIR__ . '/Rest/', @@ -143,6 +144,7 @@ class AutoLoader { 'MediaWiki\\Sparql\\' => __DIR__ . '/sparql/', 'MediaWiki\\Storage\\' => __DIR__ . '/Storage/', 'MediaWiki\\Tidy\\' => __DIR__ . '/tidy/', + 'Wikimedia\\Message\\' => __DIR__ . '/libs/Message/', 'Wikimedia\\ParamValidator\\' => __DIR__ . '/libs/ParamValidator/', 'Wikimedia\\Services\\' => __DIR__ . '/libs/services/', ]; diff --git a/includes/Autopromote.php b/includes/Autopromote.php index b17f1ab1c6..f8f3c24a6a 100644 --- a/includes/Autopromote.php +++ b/includes/Autopromote.php @@ -21,6 +21,8 @@ * @file */ +use MediaWiki\MediaWikiServices; + /** * This class checks if user can get extra rights * because of conditions specified in $wgAutopromote @@ -185,10 +187,10 @@ class Autopromote { } return $user->getEditCount() >= $reqEditCount; case APCOND_AGE: - $age = time() - wfTimestampOrNull( TS_UNIX, $user->getRegistration() ); + $age = time() - (int)wfTimestampOrNull( TS_UNIX, $user->getRegistration() ); return $age >= $cond[1]; case APCOND_AGE_FROM_EDIT: - $age = time() - wfTimestampOrNull( TS_UNIX, $user->getFirstEditTimestamp() ); + $age = time() - (int)wfTimestampOrNull( TS_UNIX, $user->getFirstEditTimestamp() ); return $age >= $cond[1]; case APCOND_INGROUPS: $groups = array_slice( $cond, 1 ); @@ -200,7 +202,9 @@ class Autopromote { case APCOND_BLOCKED: return $user->getBlock() && $user->getBlock()->isSitewide(); case APCOND_ISBOT: - return in_array( 'bot', User::getGroupPermissions( $user->getGroups() ) ); + return in_array( 'bot', MediaWikiServices::getInstance() + ->getPermissionManager() + ->getGroupPermissions( $user->getGroups() ) ); default: $result = null; Hooks::run( 'AutopromoteCondition', [ $cond[0], diff --git a/includes/BadFileLookup.php b/includes/BadFileLookup.php new file mode 100644 index 0000000000..2f7c0eade3 --- /dev/null +++ b/includes/BadFileLookup.php @@ -0,0 +1,127 @@ +blacklistCallback = $blacklistCallback; + $this->cache = $cache; + $this->repoGroup = $repoGroup; + $this->titleParser = $titleParser; + } + + /** + * Determine if a file exists on the 'bad image list'. + * + * The format of MediaWiki:Bad_image_list is as follows: + * * Only list items (lines starting with "*") are considered + * * The first link on a line must be a link to a bad file + * * Any subsequent links on the same line are considered to be exceptions, + * i.e. articles where the file may occur inline. + * + * @param string $name The file name to check + * @param LinkTarget|null $contextTitle The page on which the file occurs, if known + * @return bool + */ + public function isBadFile( $name, LinkTarget $contextTitle = null ) { + // Handle redirects; callers almost always hit wfFindFile() anyway, so just use that method + // because it has a fast process cache. + $file = $this->repoGroup->findFile( $name ); + // XXX If we don't find the file we also don't replace spaces by underscores or otherwise + // validate or normalize the title, is this right? + if ( $file ) { + $name = $file->getTitle()->getDBkey(); + } + + // Run the extension hook + $bad = false; + if ( !Hooks::run( 'BadImage', [ $name, &$bad ] ) ) { + return (bool)$bad; + } + + if ( $this->badFiles === null ) { + // Not used before in this request, try the cache + $blacklist = ( $this->blacklistCallback )(); + $key = $this->cache->makeKey( 'bad-image-list', sha1( $blacklist ) ); + $this->badFiles = $this->cache->get( $key ) ?: null; + } + + if ( $this->badFiles === null ) { + // Cache miss, build the list now + $this->badFiles = []; + $lines = explode( "\n", $blacklist ); + foreach ( $lines as $line ) { + // List items only + if ( substr( $line, 0, 1 ) !== '*' ) { + continue; + } + + // Find all links + $m = []; + // XXX What is the ':?' doing in the regex? Why not let the TitleParser strip it? + if ( !preg_match_all( '/\[\[:?(.*?)\]\]/', $line, $m ) ) { + continue; + } + + $fileDBkey = null; + $exceptions = []; + foreach ( $m[1] as $i => $titleText ) { + try { + $title = $this->titleParser->parseTitle( $titleText ); + } catch ( MalformedTitleException $e ) { + continue; + } + if ( $i == 0 ) { + $fileDBkey = $title->getDBkey(); + } else { + $exceptions[$title->getNamespace()][$title->getDBkey()] = true; + } + } + + if ( $fileDBkey !== null ) { + $this->badFiles[$fileDBkey] = $exceptions; + } + } + $this->cache->set( $key, $this->badFiles, 24 * 60 * 60 ); + } + + return isset( $this->badFiles[$name] ) && ( !$contextTitle || + !isset( $this->badFiles[$name][$contextTitle->getNamespace()] + [$contextTitle->getDBkey()] ) ); + } +} diff --git a/includes/Category.php b/includes/Category.php index 34ac0e1936..229958a9d2 100644 --- a/includes/Category.php +++ b/includes/Category.php @@ -340,7 +340,7 @@ class Category { $dbw->lockForUpdate( 'category', [ 'cat_title' => $this->mName ], __METHOD__ ); // Lock all the `categorylinks` records and gaps for this category; - // this is a separate query due to postgres/oracle limitations + // this is a separate query due to postgres limitations $dbw->selectRowCount( [ 'categorylinks', 'page' ], '*', diff --git a/includes/CommentStore.php b/includes/CommentStore.php index 994a064f2c..9054e7afe8 100644 --- a/includes/CommentStore.php +++ b/includes/CommentStore.php @@ -83,7 +83,8 @@ class CommentStore { protected $key = null; /** - * @var int One of the MIGRATION_* constants + * @var int One of the MIGRATION_* constants, or an appropriate combination + * of SCHEMA_COMPAT_* constants. * @todo Deprecate and remove once extensions seem unlikely to need to use * it for migration anymore. */ @@ -98,11 +99,19 @@ class CommentStore { /** * @param Language $lang Language to use for comment truncation. Defaults * to content language. - * @param int $migrationStage One of the MIGRATION_* constants. Always - * MIGRATION_NEW for MediaWiki core since 1.33. + * @param int $stage One of the MIGRATION_* constants, or an appropriate + * combination of SCHEMA_COMPAT_* constants. Always MIGRATION_NEW for + * MediaWiki core since 1.33. */ - public function __construct( Language $lang, $migrationStage ) { - $this->stage = $migrationStage; + public function __construct( Language $lang, $stage ) { + if ( ( $stage & SCHEMA_COMPAT_WRITE_BOTH ) === 0 ) { + throw new InvalidArgumentException( '$stage must include a write mode' ); + } + if ( ( $stage & SCHEMA_COMPAT_READ_BOTH ) === 0 ) { + throw new InvalidArgumentException( '$stage must include a read mode' ); + } + + $this->stage = $stage; $this->lang = $lang; } @@ -166,21 +175,21 @@ class CommentStore { public function getFields( $key = null ) { $key = $this->getKey( $key ); $fields = []; - if ( $this->stage === MIGRATION_OLD ) { + if ( ( $this->stage & SCHEMA_COMPAT_READ_BOTH ) === SCHEMA_COMPAT_READ_OLD ) { $fields["{$key}_text"] = $key; $fields["{$key}_data"] = 'NULL'; $fields["{$key}_cid"] = 'NULL'; - } else { - if ( $this->stage < MIGRATION_NEW ) { + } else { // READ_BOTH or READ_NEW + if ( $this->stage & SCHEMA_COMPAT_READ_OLD ) { $fields["{$key}_old"] = $key; } $tempTableStage = isset( $this->tempTables[$key] ) ? $this->tempTables[$key]['stage'] : MIGRATION_NEW; - if ( $tempTableStage < MIGRATION_NEW ) { + if ( $tempTableStage & SCHEMA_COMPAT_READ_OLD ) { $fields["{$key}_pk"] = $this->tempTables[$key]['joinPK']; } - if ( $tempTableStage > MIGRATION_OLD ) { + if ( $tempTableStage & SCHEMA_COMPAT_READ_NEW ) { $fields["{$key}_id"] = "{$key}_id"; } } @@ -211,21 +220,21 @@ class CommentStore { $fields = []; $joins = []; - if ( $this->stage === MIGRATION_OLD ) { + if ( ( $this->stage & SCHEMA_COMPAT_READ_BOTH ) === SCHEMA_COMPAT_READ_OLD ) { $fields["{$key}_text"] = $key; $fields["{$key}_data"] = 'NULL'; $fields["{$key}_cid"] = 'NULL'; - } else { - $join = $this->stage === MIGRATION_NEW ? 'JOIN' : 'LEFT JOIN'; + } else { // READ_BOTH or READ_NEW + $join = ( $this->stage & SCHEMA_COMPAT_READ_OLD ) ? 'LEFT JOIN' : 'JOIN'; $tempTableStage = isset( $this->tempTables[$key] ) ? $this->tempTables[$key]['stage'] : MIGRATION_NEW; - if ( $tempTableStage < MIGRATION_NEW ) { + if ( $tempTableStage & SCHEMA_COMPAT_READ_OLD ) { $t = $this->tempTables[$key]; $alias = "temp_$key"; $tables[$alias] = $t['table']; $joins[$alias] = [ $join, "{$alias}.{$t['pk']} = {$t['joinPK']}" ]; - if ( $tempTableStage === MIGRATION_OLD ) { + if ( ( $tempTableStage & SCHEMA_COMPAT_READ_BOTH ) === SCHEMA_COMPAT_READ_OLD ) { $joinField = "{$alias}.{$t['field']}"; } else { // Nothing hits this code path for now, but will in the future when we set @@ -245,7 +254,7 @@ class CommentStore { $tables[$alias] = 'comment'; $joins[$alias] = [ $join, "{$alias}.comment_id = {$joinField}" ]; - if ( $this->stage === MIGRATION_NEW ) { + if ( ( $this->stage & SCHEMA_COMPAT_READ_BOTH ) === SCHEMA_COMPAT_READ_NEW ) { $fields["{$key}_text"] = "{$alias}.comment_text"; } else { $fields["{$key}_text"] = "COALESCE( {$alias}.comment_text, $key )"; @@ -282,13 +291,15 @@ class CommentStore { $cid = $row["{$key}_cid"] ?? null; $text = $row["{$key}_text"]; $data = $row["{$key}_data"]; - } elseif ( $this->stage === MIGRATION_OLD ) { + } elseif ( ( $this->stage & SCHEMA_COMPAT_READ_BOTH ) === SCHEMA_COMPAT_READ_OLD ) { $cid = null; if ( $fallback && isset( $row[$key] ) ) { wfLogWarning( "Using deprecated fallback handling for comment $key" ); $text = $row[$key]; } else { - wfLogWarning( "Missing {$key}_text and {$key}_data fields in row with MIGRATION_OLD" ); + wfLogWarning( + "Missing {$key}_text and {$key}_data fields in row with MIGRATION_OLD / READ_OLD" + ); $text = ''; } $data = null; @@ -296,7 +307,7 @@ class CommentStore { $tempTableStage = isset( $this->tempTables[$key] ) ? $this->tempTables[$key]['stage'] : MIGRATION_NEW; $row2 = null; - if ( $tempTableStage > MIGRATION_OLD && array_key_exists( "{$key}_id", $row ) ) { + if ( ( $tempTableStage & SCHEMA_COMPAT_READ_NEW ) && array_key_exists( "{$key}_id", $row ) ) { if ( !$db ) { throw new InvalidArgumentException( "\$row does not contain fields needed for comment $key and getComment(), but " @@ -311,7 +322,9 @@ class CommentStore { __METHOD__ ); } - if ( !$row2 && $tempTableStage < MIGRATION_NEW && array_key_exists( "{$key}_pk", $row ) ) { + if ( !$row2 && ( $tempTableStage & SCHEMA_COMPAT_READ_OLD ) && + array_key_exists( "{$key}_pk", $row ) + ) { if ( !$db ) { throw new InvalidArgumentException( "\$row does not contain fields needed for comment $key and getComment(), but " @@ -341,7 +354,9 @@ class CommentStore { $cid = $row2->comment_id; $text = $row2->comment_text; $data = $row2->comment_data; - } elseif ( $this->stage < MIGRATION_NEW && array_key_exists( "{$key}_old", $row ) ) { + } elseif ( ( $this->stage & SCHEMA_COMPAT_READ_OLD ) && + array_key_exists( "{$key}_old", $row ) + ) { $cid = null; $text = $row["{$key}_old"]; $data = null; @@ -474,7 +489,7 @@ class CommentStore { # Truncate comment in a Unicode-sensitive manner $comment->text = $this->lang->truncateForVisual( $comment->text, self::COMMENT_CHARACTER_LIMIT ); - if ( $this->stage > MIGRATION_OLD && !$comment->id ) { + if ( ( $this->stage & SCHEMA_COMPAT_WRITE_NEW ) && !$comment->id ) { $dbData = $comment->data; if ( !$comment->message instanceof RawMessage ) { if ( $dbData === null ) { @@ -534,14 +549,14 @@ class CommentStore { $comment = $this->createComment( $dbw, $comment, $data ); - if ( $this->stage <= MIGRATION_WRITE_BOTH ) { + if ( $this->stage & SCHEMA_COMPAT_WRITE_OLD ) { $fields[$key] = $this->lang->truncateForDatabase( $comment->text, 255 ); } - if ( $this->stage >= MIGRATION_WRITE_BOTH ) { + if ( $this->stage & SCHEMA_COMPAT_WRITE_NEW ) { $tempTableStage = isset( $this->tempTables[$key] ) ? $this->tempTables[$key]['stage'] : MIGRATION_NEW; - if ( $tempTableStage <= MIGRATION_WRITE_BOTH ) { + if ( $tempTableStage & SCHEMA_COMPAT_WRITE_OLD ) { $t = $this->tempTables[$key]; $func = __METHOD__; $commentId = $comment->id; @@ -556,7 +571,7 @@ class CommentStore { ); }; } - if ( $tempTableStage >= MIGRATION_WRITE_BOTH ) { + if ( $tempTableStage & SCHEMA_COMPAT_WRITE_NEW ) { $fields["{$key}_id"] = $comment->id; } } @@ -594,7 +609,7 @@ class CommentStore { $tempTableStage = isset( $this->tempTables[$key] ) ? $this->tempTables[$key]['stage'] : MIGRATION_NEW; - if ( $tempTableStage < MIGRATION_WRITE_NEW ) { + if ( $tempTableStage & SCHEMA_COMPAT_WRITE_OLD ) { throw new InvalidArgumentException( "Must use insertWithTempTable() for $key" ); } diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php index 50a2057855..31cb7ae37c 100644 --- a/includes/DefaultSettings.php +++ b/includes/DefaultSettings.php @@ -788,10 +788,6 @@ $wgFileBackends = []; * See LockManager::__construct() for more details. * Additional parameters are specific to the lock manager class used. * These settings should be global to all wikis. - * - * When using DBLockManager, the 'dbsByBucket' map can reference 'localDBMaster' as - * a peer database in each bucket. This will result in an extra connection to the domain - * that the LockManager services, which must also be a valid wiki ID. */ $wgLockManagers = []; @@ -1282,7 +1278,7 @@ $wgMaxAnimatedGifArea = 1.25e7; * $wgTiffThumbnailType = [ 'jpg', 'image/jpeg' ]; * @endcode */ -$wgTiffThumbnailType = false; +$wgTiffThumbnailType = []; /** * If rendered thumbnail files are older than this timestamp, they @@ -1887,6 +1883,36 @@ $wgUsersNotifiedOnAllChanges = []; * @{ */ +/** + * Current wiki database name + * + * Should be alphanumeric, without spaces nor hyphens. + * This is used to determine the current/local wiki ID (WikiMap::getCurrentWikiDbDomain). + * + * This should still be set even if $wgLBFactoryConf is configured. + */ +$wgDBname = 'my_wiki'; + +/** + * Current wiki database schema name + * + * Should be alphanumeric, without spaces nor hyphens. + * This is used to determine the current/local wiki ID (WikiMap::getCurrentWikiDbDomain). + * + * This should still be set even if $wgLBFactoryConf is configured. + */ +$wgDBmwschema = null; + +/** + * Current wiki database table name prefix + * + * Should be alphanumeric, without spaces nor hyphens, preferably ending in an underscore. + * This is used to determine the current/local wiki ID (WikiMap::getCurrentWikiDbDomain). + * + * This should still be set even if $wgLBFactoryConf is configured. + */ +$wgDBprefix = ''; + /** * Database host name or IP address */ @@ -1897,11 +1923,6 @@ $wgDBserver = 'localhost'; */ $wgDBport = 5432; -/** - * Name of the database; this should be alphanumeric and not contain spaces nor hyphens - */ -$wgDBname = 'my_wiki'; - /** * Database username */ @@ -1964,13 +1985,6 @@ $wgSearchType = null; */ $wgSearchTypeAlternatives = null; -/** - * Table name prefix. - * Should be alphanumeric plus underscores, and not contain spaces nor hyphens. - * Suggested format ends with an underscore. - */ -$wgDBprefix = ''; - /** * MySQL table options to use during installation or update */ @@ -1984,11 +1998,6 @@ $wgDBTableOptions = 'ENGINE=InnoDB, DEFAULT CHARSET=binary'; */ $wgSQLMode = ''; -/** - * Mediawiki schema; this should be alphanumeric and not contain spaces nor hyphens - */ -$wgDBmwschema = null; - /** * Default group to use when getting database connections. * Will be used as default query group in ILoadBalancer::getConnection. @@ -2545,11 +2554,6 @@ $wgPHPSessionHandling = 'enable'; */ $wgSessionPbkdf2Iterations = 10001; -/** - * If enabled, will send MemCached debugging information to $wgDebugLogFile - */ -$wgMemCachedDebug = false; - /** * The list of MemCached servers and port numbers */ @@ -2729,27 +2733,22 @@ $wgExtensionInfoMTime = false; * although they are sometimes still referred to as Squid settings for * historical reasons. * - * Achieving a high hit ratio with an HTTP proxy requires special - * configuration. See https://www.mediawiki.org/wiki/Manual:Squid_caching for - * more details. + * Achieving a high hit ratio with an HTTP proxy requires special configuration. + * See https://www.mediawiki.org/wiki/Manual:Performance_tuning#Page_view_caching + * for more details. * * @{ */ /** * Enable/disable CDN. - * See https://www.mediawiki.org/wiki/Manual:Squid_caching + * + * See https://www.mediawiki.org/wiki/Manual:Performance_tuning#Page_view_caching * * @since 1.34 Renamed from $wgUseSquid. */ $wgUseCdn = false; -/** - * If you run Squid3 with ESI support, enable this (default:false): - * @deprecated in 1.33. This was a now-defunct experimental feature. - */ -$wgUseESI = false; - /** * Add X-Forwarded-Proto to the Vary and Key headers for API requests and * RSS/Atom feeds. Use this if you have an SSL termination setup @@ -3105,11 +3104,6 @@ $wgTranslateNumerals = true; */ $wgUseDatabaseMessages = true; -/** - * Expiry time for the message cache key - */ -$wgMsgCacheExpiry = 86400; - /** * Maximum entry size in the message cache, in bytes */ @@ -3761,19 +3755,16 @@ $wgIncludeLegacyJavaScript = false; $wgLegacyJavaScriptGlobals = true; /** - * If set to a positive number, ResourceLoader will not generate URLs whose - * query string is more than this many characters long, and will instead use - * multiple requests with shorter query strings. This degrades performance, - * but may be needed if your web server has a low (less than, say 1024) - * query string length limit or a low value for suhosin.get.max_value_length - * that you can't increase. - * - * If set to a negative number, ResourceLoader will assume there is no query - * string length limit. + * ResourceLoader will not generate URLs whose query string is more than + * this many characters long, and will instead use multiple requests with + * shorter query strings. This degrades performance, but may be needed based + * on the query string limit supported by your web server and/or your user's + * web browsers. * - * Defaults to a value based on php configuration. + * @since 1.17 + * @var int */ -$wgResourceLoaderMaxQueryLength = false; +$wgResourceLoaderMaxQueryLength = 2000; /** * If set to true, JavaScript modules loaded from wiki pages will be parsed @@ -4454,7 +4445,8 @@ $wgCentralIdLookupProvider = 'local'; * The checks supported by core are: * - MinimalPasswordLength - Minimum length a user can set. * - MinimumPasswordLengthToLogin - Passwords shorter than this will - * not be allowed to login, regardless if it is correct. + * not be allowed to login, or offered a chance to reset their password + * as part of the login workflow, regardless if it is correct. * - MaximalPasswordLength - maximum length password a user is allowed * to attempt. Prevents DoS attacks with pbkdf2. * - PasswordCannotMatchUsername - Password cannot match the username. @@ -4467,7 +4459,7 @@ $wgCentralIdLookupProvider = 'local'; * Deprecated since 1.33. Use PasswordNotInLargeBlacklist instead. * - PasswordNotInLargeBlacklist - Password not in best practices list of * 100,000 commonly used passwords. Due to the size of the list this - * is a probabilistic test. + * is a probabilistic test. * * If you add custom checks, for Special:PasswordPolicies to display them correctly, * every check should have a corresponding passwordpolicies-policy- message, @@ -4485,28 +4477,25 @@ $wgPasswordPolicy = [ 'bureaucrat' => [ 'MinimalPasswordLength' => 10, 'MinimumPasswordLengthToLogin' => 1, - 'PasswordNotInLargeBlacklist' => true, ], 'sysop' => [ 'MinimalPasswordLength' => 10, 'MinimumPasswordLengthToLogin' => 1, - 'PasswordNotInLargeBlacklist' => true, ], 'interface-admin' => [ 'MinimalPasswordLength' => 10, 'MinimumPasswordLengthToLogin' => 1, - 'PasswordNotInLargeBlacklist' => true, ], 'bot' => [ 'MinimalPasswordLength' => 10, 'MinimumPasswordLengthToLogin' => 1, - 'PasswordNotInLargeBlacklist' => true, ], 'default' => [ 'MinimalPasswordLength' => [ 'value' => 1, 'suggestChangeOnLogin' => true ], 'PasswordCannotMatchUsername' => [ 'value' => true, 'suggestChangeOnLogin' => true ], 'PasswordCannotMatchBlacklist' => [ 'value' => true, 'suggestChangeOnLogin' => true ], 'MaximalPasswordLength' => [ 'value' => 4096, 'suggestChangeOnLogin' => true ], + 'PasswordNotInLargeBlacklist' => [ 'value' => true, 'suggestChangeOnLogin' => true ], ], ], 'checks' => [ @@ -4901,6 +4890,7 @@ $wgDefaultUserOptions = [ 'wllimit' => 250, 'useeditwarning' => 1, 'prefershttps' => 1, + 'requireemail' => 0, ]; /** @@ -4970,6 +4960,15 @@ $wgSessionProviders = [ ], ]; +/** + * Temporary feature flag that controls whether users will see a checkbox allowing them to + * require providing email during password resets. + * + * @deprecated This feature is under development, don't assume this flag's existence or function + * outside of MediaWiki. + */ +$wgAllowRequiringEmailForResets = false; + /** @} */ # end user accounts } /************************************************************************//** @@ -4993,6 +4992,8 @@ $wgBlockAllowsUTEdit = true; /** * Allow sysops to ban users from accessing Emailuser + * @deprecated since 1.34; `$wgGroupPermissions['sysop']['blockemail'] = true;` + * should be used instead */ $wgSysopEmailBans = true; @@ -5232,13 +5233,16 @@ $wgGroupPermissions['bureaucrat']['noratelimit'] = true; # $wgGroupPermissions['sysop']['deletelogentry'] = true; # $wgGroupPermissions['sysop']['deleterevision'] = true; // To hide usernames from users and Sysops -# $wgGroupPermissions['suppress']['hideuser'] = true; +$wgGroupPermissions['suppress']['hideuser'] = true; // To hide revisions/log items from users and Sysops -# $wgGroupPermissions['suppress']['suppressrevision'] = true; +$wgGroupPermissions['suppress']['suppressrevision'] = true; // To view revisions/log items hidden from users and Sysops -# $wgGroupPermissions['suppress']['viewsuppressed'] = true; +$wgGroupPermissions['suppress']['viewsuppressed'] = true; // For private suppression log access -# $wgGroupPermissions['suppress']['suppressionlog'] = true; +$wgGroupPermissions['suppress']['suppressionlog'] = true; +// Basic rights for revision delete +$wgGroupPermissions['suppress']['deleterevision'] = true; +$wgGroupPermissions['suppress']['deletelogentry'] = true; /** * The developer group is deprecated, but can be activated if need be @@ -5711,6 +5715,11 @@ $wgRateLimits = [ 'ip-all' => [ 10, 3600 ], 'user' => [ 4, 86400 ] ], + // since 1.33 - rate limit email confirmations + 'confirmemail' => [ + 'ip-all' => [ 10, 3600 ], + 'user' => [ 4, 86400 ] + ], // Purging pages 'purge' => [ 'ip' => [ 30, 60 ], @@ -5803,6 +5812,7 @@ $wgGrantPermissions = []; // @TODO: clean up grants // @TODO: auto-include read/editsemiprotected rights? +$wgGrantPermissions['basic']['autocreateaccount'] = true; $wgGrantPermissions['basic']['autoconfirmed'] = true; $wgGrantPermissions['basic']['autopatrol'] = true; $wgGrantPermissions['basic']['editsemiprotected'] = true; @@ -5854,6 +5864,7 @@ $wgGrantPermissions['createeditmovepage']['move'] = true; $wgGrantPermissions['createeditmovepage']['move-rootuserpages'] = true; $wgGrantPermissions['createeditmovepage']['move-subpages'] = true; $wgGrantPermissions['createeditmovepage']['move-categorypages'] = true; +$wgGrantPermissions['createeditmovepage']['suppressredirect'] = true; $wgGrantPermissions['uploadfile']['upload'] = true; $wgGrantPermissions['uploadfile']['reupload-own'] = true; @@ -6065,7 +6076,7 @@ $wgSessionName = false; * which case there is a possibility of an attacker discovering the names of revdeleted users, so * it is best to use this in conjunction with $wgSecretKey being set). */ -$wgCookieSetOnAutoblock = false; +$wgCookieSetOnAutoblock = true; /** * Whether to set a cookie when a logged-out user is blocked. Doing so means that a blocked user, @@ -6074,7 +6085,7 @@ $wgCookieSetOnAutoblock = false; * case there is a possibility of an attacker discovering the names of revdeleted users, so it * is best to use this in conjunction with $wgSecretKey being set). */ -$wgCookieSetOnIpBlock = false; +$wgCookieSetOnIpBlock = true; /** @} */ # end of cookie settings } @@ -6822,6 +6833,8 @@ $wgRCLinkLimits = [ 50, 100, 250, 500 ]; /** * List of Days options to list in the Special:Recentchanges and * Special:Recentchangeslinked pages. + * + * @see ChangesListSpecialPage::getLinkDays */ $wgRCLinkDays = [ 1, 3, 7, 14, 30 ]; @@ -7924,6 +7937,7 @@ $wgAllowSpecialInclusion = true; /** * Set this to an array of special page names to prevent * maintenance/updateSpecialPages.php from updating those pages. + * Mapping each special page name to an run mode like 'periodical' if a cronjob is set up. */ $wgDisableQueryPageUpdate = false; @@ -8610,6 +8624,7 @@ $wgContentHandlerTextFallback = 'ignore'; * handling is less robust and less flexible. * * @since 1.21 + * @deprecated since 1.34, and should always be set true. */ $wgContentHandlerUseDB = true; @@ -8980,24 +8995,6 @@ $wgMultiContentRevisionSchemaMigrationStage = SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_ */ $wgXmlDumpSchemaVersion = XML_DUMP_SCHEMA_VERSION_10; -/** - * Actor table schema migration stage. - * - * Use the SCHEMA_COMPAT_XXX flags. Supported values: - * - SCHEMA_COMPAT_OLD - * - SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD - * - SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW - * - SCHEMA_COMPAT_NEW - * - * Note that reading the old and new schema at the same time is not supported - * in 1.32, but was (with significant query performance issues) in 1.31. - * - * @since 1.31 - * @since 1.32 changed allowed flags - * @var int An appropriate combination of SCHEMA_COMPAT_XXX flags. - */ -$wgActorTableSchemaMigrationStage = SCHEMA_COMPAT_NEW; - /** * Flag to enable Partial Blocks. This allows an admin to prevent a user from editing specific pages * or namespaces. @@ -9088,6 +9085,26 @@ $wgFeaturePolicyReportOnly = []; */ $wgSpecialSearchFormOptions = []; +/** + * Toggles native image lazy loading, via the "loading" attribute. + * + * @warning EXPERIMENTAL! + * + * @since 1.34 + * @var array + */ +$wgNativeImageLazyLoading = false; + +/** + * Option to whether serve the main page as the domain root + * + * @warning EXPERIMENTAL! + * + * @since 1.34 + * @var bool + */ +$wgMainPageIsDomainRoot = false; + /** * For really cool vim folding this needs to be at the end: * vim: foldmarker=@{,@} foldmethod=marker diff --git a/includes/DevelopmentSettings.php b/includes/DevelopmentSettings.php index d93caa7dea..668de3975d 100644 --- a/includes/DevelopmentSettings.php +++ b/includes/DevelopmentSettings.php @@ -27,7 +27,7 @@ ini_set( 'display_errors', 1 ); global $wgDevelopmentWarnings, $wgShowExceptionDetails, $wgShowHostnames, $wgDebugRawPage, $wgCommandLineMode, $wgDebugLogFile, - $wgDBerrorLog, $wgDebugLogGroups; + $wgDBerrorLog, $wgDebugLogGroups, $wgLocalisationCacheConf; // Use of wfWarn() should cause tests to fail $wgDevelopmentWarnings = true; @@ -74,3 +74,6 @@ $wgSQLMode = 'TRADITIONAL'; // Disable legacy javascript globals in CI and for devs (T72470) $wgLegacyJavaScriptGlobals = false; + +// Localisation Cache to StaticArray (T218207) +$wgLocalisationCacheConf['store'] = 'array'; diff --git a/includes/EditPage.php b/includes/EditPage.php index 74ec883a06..fe00149920 100644 --- a/includes/EditPage.php +++ b/includes/EditPage.php @@ -25,7 +25,7 @@ use MediaWiki\EditPage\TextboxBuilder; use MediaWiki\EditPage\TextConflictHelper; use MediaWiki\Logger\LoggerFactory; use MediaWiki\MediaWikiServices; -use MediaWiki\Storage\RevisionRecord; +use MediaWiki\Revision\RevisionRecord; use Wikimedia\ScopedCallback; /** @@ -689,10 +689,6 @@ class EditPage { # checking, etc. if ( $this->formtype == 'initial' || $this->firsttime ) { if ( $this->initialiseForm() === false ) { - $out = $this->context->getOutput(); - if ( $out->getRedirect() === '' ) { // mcrundo hack redirects, don't override it - $this->noSuchSectionPage(); - } return; } @@ -1131,7 +1127,7 @@ class EditPage { * @return string|null */ protected function importContentFormData( &$request ) { - return; // Don't do anything, EditPage already extracted wpTextbox1 + return null; // Don't do anything, EditPage already extracted wpTextbox1 } /** @@ -1145,8 +1141,26 @@ class EditPage { $content = $this->getContentObject( false ); # TODO: track content object?! if ( $content === false ) { + $out = $this->context->getOutput(); + if ( $out->getRedirect() === '' ) { // mcrundo hack redirects, don't override it + $this->noSuchSectionPage(); + } + return false; + } + + if ( !$this->isSupportedContentModel( $content->getModel() ) ) { + $modelMsg = $this->getContext()->msg( 'content-model-' . $content->getModel() ); + $modelName = $modelMsg->exists() ? $modelMsg->text() : $content->getModel(); + + $out = $this->context->getOutput(); + $out->showErrorPage( + 'modeleditnotsupported-title', + 'modeleditnotsupported-text', + [ $modelName ] + ); return false; } + $this->textbox1 = $this->toEditText( $content ); $user = $this->context->getUser(); @@ -1174,11 +1188,13 @@ class EditPage { /** * @param Content|null $def_content The default value to return * - * @return Content|null Content on success, $def_content for invalid sections + * @return Content|false|null Content on success, $def_content for invalid sections * * @since 1.21 */ protected function getContentObject( $def_content = null ) { + global $wgDisableAnonTalk; + $content = false; $user = $this->context->getUser(); @@ -1278,8 +1294,11 @@ class EditPage { $undo )->inContentLanguage()->text(); } else { + $undoMessage = ( $undorev->getUser() === 0 && $wgDisableAnonTalk ) ? + 'undo-summary-anon' : + 'undo-summary'; $undoSummary = $this->context->msg( - 'undo-summary', + $undoMessage, $undo, $userText )->inContentLanguage()->text(); @@ -1593,7 +1612,8 @@ class EditPage { // This is needed since PageUpdater no longer checks these rights! // Allow bots to exempt some edits from bot flagging - $bot = $this->context->getUser()->isAllowed( 'bot' ) && $this->bot; + $permissionManager = MediaWikiServices::getInstance()->getPermissionManager(); + $bot = $permissionManager->userHasRight( $this->context->getUser(), 'bot' ) && $this->bot; $status = $this->internalAttemptSave( $resultDetails, $bot ); Hooks::run( 'EditPage::attemptSave:after', [ $this, $status, $resultDetails ] ); @@ -1663,7 +1683,9 @@ class EditPage { case self::AS_CANNOT_USE_CUSTOM_MODEL: case self::AS_PARSE_ERROR: case self::AS_UNICODE_NOT_SUPPORTED: - $out->wrapWikiTextAsInterface( 'error', $status->getWikiText() ); + $out->wrapWikiTextAsInterface( 'error', + $status->getWikiText( false, false, $this->context->getLanguage() ) + ); return true; case self::AS_SUCCESS_NEW_ARTICLE: @@ -1737,7 +1759,8 @@ class EditPage { // is if an extension hook aborted from inside ArticleSave. // Render the status object into $this->hookError // FIXME this sucks, we should just use the Status object throughout - $this->hookError = '
' . "\n" . $status->getWikiText() . + $this->hookError = '
' . "\n" . + $status->getWikiText( false, false, $this->context->getLanguage() ) . '
'; return true; } @@ -1784,8 +1807,11 @@ class EditPage { } elseif ( !$status->isOK() ) { # ...or the hook could be expecting us to produce an error // FIXME this sucks, we should just use the Status object throughout + if ( !$status->getErrors() ) { + // Provide a fallback error message if none was set + $status->fatal( 'hookaborted' ); + } $this->hookError = $this->formatStatusErrors( $status ); - $status->fatal( 'hookaborted' ); $status->value = self::AS_HOOK_ERROR_EXPECTED; return false; } @@ -1870,6 +1896,7 @@ ERROR; public function internalAttemptSave( &$result, $bot = false ) { $status = Status::newGood(); $user = $this->context->getUser(); + $permissionManager = MediaWikiServices::getInstance()->getPermissionManager(); if ( !Hooks::run( 'EditPage::attemptSave', [ $this ] ) ) { wfDebug( "Hook 'EditPage::attemptSave' aborted article saving\n" ); @@ -1918,7 +1945,7 @@ ERROR; # Check image redirect if ( $this->mTitle->getNamespace() == NS_FILE && $textbox_content->isRedirect() && - !$user->isAllowed( 'upload' ) + !$permissionManager->userHasRight( $user, 'upload' ) ) { $code = $user->isAnon() ? self::AS_IMAGE_REDIRECT_ANON : self::AS_IMAGE_REDIRECT_LOGGED; $status->setResult( false, $code ); @@ -1968,7 +1995,7 @@ ERROR; return $status; } - if ( $user->isBlockedFrom( $this->mTitle ) ) { + if ( $permissionManager->isBlockedFrom( $user, $this->mTitle ) ) { // Auto-block user's IP if the account was "hard" blocked if ( !wfReadOnly() ) { $user->spreadAnyEditBlock(); @@ -1988,7 +2015,7 @@ ERROR; return $status; } - if ( !$user->isAllowed( 'edit' ) ) { + if ( !$permissionManager->userHasRight( $user, 'edit' ) ) { if ( $user->isAnon() ) { $status->setResult( false, self::AS_READ_ONLY_PAGE_ANON ); return $status; @@ -1999,15 +2026,13 @@ ERROR; } } - $permissionManager = MediaWikiServices::getInstance()->getPermissionManager(); - $changingContentModel = false; if ( $this->contentModel !== $this->mTitle->getContentModel() ) { if ( !$config->get( 'ContentHandlerUseDB' ) ) { $status->fatal( 'editpage-cannot-use-custom-model' ); $status->value = self::AS_CANNOT_USE_CUSTOM_MODEL; return $status; - } elseif ( !$user->isAllowed( 'editcontentmodel' ) ) { + } elseif ( !$permissionManager->userHasRight( $user, 'editcontentmodel' ) ) { $status->setResult( false, self::AS_NO_CHANGE_CONTENT_MODEL ); return $status; } @@ -2720,7 +2745,7 @@ ERROR; * content. * * @param Content|null|bool|string $content - * @return string The editable text form of the content. + * @return string|false|null The editable text form of the content. * * @throws MWException If $content is not an instance of TextContent and * $this->allowNonTextContent is not true. @@ -4023,11 +4048,11 @@ ERROR; if ( $this->isConflict ) { $conflict = Html::rawElement( - 'h2', [ 'id' => 'mw-previewconflict' ], + 'div', [ 'id' => 'mw-previewconflict', 'class' => 'warningbox' ], $this->context->msg( 'previewconflict' )->escaped() ); } else { - $conflict = '
'; + $conflict = ''; } $previewhead = Html::rawElement( @@ -4036,7 +4061,9 @@ ERROR; 'h2', [ 'id' => 'mw-previewheader' ], $this->context->msg( 'preview' )->escaped() ) . - $out->parseAsInterface( $note ) . $conflict + Html::rawElement( 'div', [ 'class' => 'warningbox' ], + $out->parseAsInterface( $note ) + ) . $conflict ); $pageViewLang = $this->mTitle->getPageViewLanguage(); @@ -4152,14 +4179,15 @@ ERROR; * - 'legacy-name' (optional): short name for backwards-compatibility * @param array $checked Array of checkbox name (matching the 'legacy-name') => bool, * where bool indicates the checked status of the checkbox - * @return array + * @return array[] */ public function getCheckboxesDefinition( $checked ) { $checkboxes = []; $user = $this->context->getUser(); // don't show the minor edit checkbox if it's a new page or section - if ( !$this->isNew && $user->isAllowed( 'minoredit' ) ) { + $permissionManager = MediaWikiServices::getInstance()->getPermissionManager(); + if ( !$this->isNew && $permissionManager->userHasRight( $user, 'minoredit' ) ) { $checkboxes['wpMinoredit'] = [ 'id' => 'wpMinoredit', 'label-message' => 'minoredit', @@ -4446,8 +4474,8 @@ ERROR; protected function addPageProtectionWarningHeaders() { $out = $this->context->getOutput(); if ( $this->mTitle->isProtected( 'edit' ) && - MediaWikiServices::getInstance()->getNamespaceInfo()->getRestrictionLevels( - $this->mTitle->getNamespace() + MediaWikiServices::getInstance()->getPermissionManager()->getNamespaceRestrictionLevels( + $this->getTitle()->getNamespace() ) !== [ '' ] ) { # Is the title semi-protected? diff --git a/includes/FauxRequest.php b/includes/FauxRequest.php index ecbc6e3373..9337270303 100644 --- a/includes/FauxRequest.php +++ b/includes/FauxRequest.php @@ -86,13 +86,6 @@ class FauxRequest extends WebRequest { return (string)$this->getVal( $name, $default ); } - /** - * @return array - */ - public function getValues() { - return $this->data; - } - /** * @return array */ diff --git a/includes/FeedUtils.php b/includes/FeedUtils.php index 8efae4f7d6..bfd1e2a6cd 100644 --- a/includes/FeedUtils.php +++ b/includes/FeedUtils.php @@ -21,7 +21,7 @@ * @ingroup Feed */ -use MediaWiki\Storage\RevisionRecord; +use MediaWiki\Revision\RevisionRecord; /** * Helper functions for feeds diff --git a/includes/FileDeleteForm.php b/includes/FileDeleteForm.php index 5aa6edf879..e31f9d218d 100644 --- a/includes/FileDeleteForm.php +++ b/includes/FileDeleteForm.php @@ -36,18 +36,18 @@ class FileDeleteForm { private $title = null; /** - * @var File + * @var LocalFile */ private $file = null; /** - * @var File + * @var LocalFile */ private $oldfile = null; private $oldimage = ''; /** - * @param File $file File object we're deleting + * @param LocalFile $file File object we're deleting */ public function __construct( $file ) { $this->title = $file->getTitle(); @@ -79,7 +79,9 @@ class FileDeleteForm { $this->oldimage = $wgRequest->getText( 'oldimage', false ); $token = $wgRequest->getText( 'wpEditToken' ); # Flag to hide all contents of the archived revisions - $suppress = $wgRequest->getCheck( 'wpSuppress' ) && $wgUser->isAllowed( 'suppressrevision' ); + $permissionManager = MediaWikiServices::getInstance()->getPermissionManager(); + $suppress = $wgRequest->getCheck( 'wpSuppress' ) && + $permissionManager->userHasRight( $wgUser, 'suppressrevision' ); if ( $this->oldimage ) { $this->oldfile = RepoGroup::singleton()->getLocalRepo()->newFromArchiveName( @@ -145,7 +147,7 @@ class FileDeleteForm { * Really delete the file * * @param Title &$title - * @param File &$file + * @param LocalFile &$file * @param string &$oldimage Archive name * @param string $reason Reason of the deletion * @param bool $suppress Whether to mark all deleted versions as restricted @@ -165,7 +167,7 @@ class FileDeleteForm { if ( $oldimage ) { $page = null; $status = $file->deleteOld( $oldimage, $reason, $suppress, $user ); - if ( $status->ok ) { + if ( $status->isOK() ) { // Need to do a log item $logComment = wfMessage( 'deletedrevision', $oldimage )->inContentLanguage()->text(); if ( trim( $reason ) != '' ) { @@ -179,7 +181,7 @@ class FileDeleteForm { $logEntry->setPerformer( $user ); $logEntry->setTarget( $title ); $logEntry->setComment( $logComment ); - $logEntry->setTags( $tags ); + $logEntry->addTags( $tags ); $logid = $logEntry->insert(); $logEntry->publish( $logid ); @@ -210,7 +212,7 @@ class FileDeleteForm { $logEntry->setPerformer( $user ); $logEntry->setTarget( clone $title ); $logEntry->setComment( $reason ); - $logEntry->setTags( $tags ); + $logEntry->addTags( $tags ); $logid = $logEntry->insert(); $dbw->onTransactionPreCommitOrIdle( function () use ( $logEntry, $logid ) { @@ -245,6 +247,7 @@ class FileDeleteForm { */ private function showForm() { global $wgOut, $wgUser, $wgRequest; + $permissionManager = MediaWikiServices::getInstance()->getPermissionManager(); $wgOut->addModules( 'mediawiki.action.delete.file' ); @@ -252,16 +255,18 @@ class FileDeleteForm { $wgOut->enableOOUI(); + $fields = []; + + $fields[] = new OOUI\LabelWidget( [ 'label' => new OOUI\HtmlSnippet( + $this->prepareMessage( 'filedelete-intro' ) ) ] + ); + $options = Xml::listDropDownOptions( $wgOut->msg( 'filedelete-reason-dropdown' )->inContentLanguage()->text(), [ 'other' => $wgOut->msg( 'filedelete-reason-otherlist' )->inContentLanguage()->text() ] ); $options = Xml::listDropDownOptionsOoui( $options ); - $fields[] = new OOUI\LabelWidget( [ 'label' => new OOUI\HtmlSnippet( - $this->prepareMessage( 'filedelete-intro' ) ) ] - ); - $fields[] = new OOUI\FieldLayout( new OOUI\DropdownInputWidget( [ 'name' => 'wpDeleteReasonList', @@ -296,7 +301,7 @@ class FileDeleteForm { ] ); - if ( $wgUser->isAllowed( 'suppressrevision' ) ) { + if ( $permissionManager->userHasRight( $wgUser, 'suppressrevision' ) ) { $fields[] = new OOUI\FieldLayout( new OOUI\CheckboxInputWidget( [ 'name' => 'wpSuppress', @@ -370,7 +375,7 @@ class FileDeleteForm { ] ) ); - if ( $wgUser->isAllowed( 'editinterface' ) ) { + if ( $permissionManager->userHasRight( $wgUser, 'editinterface' ) ) { $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer(); $link = $linkRenderer->makeKnownLink( $wgOut->msg( 'filedelete-reason-dropdown' )->inContentLanguage()->getTitle(), @@ -446,9 +451,9 @@ class FileDeleteForm { * value was provided, does it correspond to an * existing, local, old version of this file? * - * @param File &$file - * @param File &$oldfile - * @param File $oldimage + * @param LocalFile &$file + * @param LocalFile &$oldfile + * @param LocalFile $oldimage * @return bool */ public static function haveDeletableFile( &$file, &$oldfile, $oldimage ) { diff --git a/includes/ForkController.php b/includes/ForkController.php index 85f3a7dc63..af06a88982 100644 --- a/includes/ForkController.php +++ b/includes/ForkController.php @@ -154,7 +154,6 @@ class ForkController { // Don't share DB, storage, or memcached connections MediaWikiServices::resetChildProcessServices(); FileBackendGroup::destroySingleton(); - LockManagerGroup::destroySingletons(); JobQueueGroup::destroySingletons(); ObjectCache::clear(); RedisConnectionPool::destroySingletons(); diff --git a/includes/GlobalFunctions.php b/includes/GlobalFunctions.php index 1741958681..125b917caa 100644 --- a/includes/GlobalFunctions.php +++ b/includes/GlobalFunctions.php @@ -24,14 +24,15 @@ if ( !defined( 'MEDIAWIKI' ) ) { die( "This file is part of MediaWiki, it is not a valid entry point" ); } +use MediaWiki\BadFileLookup; use MediaWiki\Linker\LinkTarget; use MediaWiki\Logger\LoggerFactory; use MediaWiki\MediaWikiServices; use MediaWiki\ProcOpenError; use MediaWiki\Session\SessionManager; use MediaWiki\Shell\Shell; -use Wikimedia\WrappedString; use Wikimedia\AtEase\AtEase; +use Wikimedia\WrappedString; /** * Load an extension @@ -517,7 +518,7 @@ function wfExpandUrl( $url, $defaultProto = PROTO_CURRENT ) { } } - $defaultProtoWithoutSlashes = substr( $defaultProto, 0, -2 ); + $defaultProtoWithoutSlashes = $defaultProto !== null ? substr( $defaultProto, 0, -2 ) : ''; if ( substr( $url, 0, 2 ) == '//' ) { $url = $defaultProtoWithoutSlashes . $url; @@ -1126,6 +1127,7 @@ function wfLogProfilingData() { if ( isset( $ctx['forwarded_for'] ) || isset( $ctx['client_ip'] ) || isset( $ctx['from'] ) ) { + // @phan-suppress-next-line PhanTypeArraySuspiciousNullable $ctx['proxy'] = $_SERVER['REMOTE_ADDR']; } @@ -1814,10 +1816,11 @@ function mimeTypeMatch( $type, $avail ) { * * @param array $cprefs Client's acceptable type list * @param array $sprefs Server's offered types - * @return string + * @return string|null * * @todo FIXME: Doesn't handle params like 'text/plain; charset=UTF-8' * XXX: generalize to negotiate other stuff + * @todo The function appears unused. Is it worth to keep? */ function wfNegotiateType( $cprefs, $sprefs ) { $combine = []; @@ -2028,7 +2031,7 @@ function wfRecursiveRemoveDir( $dir ) { */ function wfPercent( $nr, $acc = 2, $round = true ) { $ret = sprintf( "%.${acc}f", $nr ); - return $round ? round( $ret, $acc ) . '%' : "$ret%"; + return $round ? round( (float)$ret, $acc ) . '%' : "$ret%"; } /** @@ -2114,6 +2117,7 @@ function wfEscapeShellArg( ...$args ) { * including errors from limit.sh * - profileMethod: By default this function will profile based on the calling * method. Set this to a string for an alternative method to profile from + * @phan-param array{duplicateStderr?:bool,profileMethod?:string} $options * * @return string Collected stdout as a string * @deprecated since 1.30 use class MediaWiki\Shell\Shell @@ -2188,6 +2192,7 @@ function wfShellExecWithStderr( $cmd, &$retval = null, $environ = [], $limits = * @param array $options Associative array of options: * 'php': The path to the php executable * 'wrapper': Path to a PHP wrapper to handle the maintenance script + * @phan-param array{php?:string,wrapper?:string} $options * @return string */ function wfShellWikiCmd( $script, array $parameters = [], array $options = [] ) { @@ -2789,7 +2794,7 @@ function wfMemoryLimit( $newLimit ) { function wfTransactionalTimeLimit() { global $wgTransactionalTimeLimit; - $timeLimit = ini_get( 'max_execution_time' ); + $timeLimit = (int)ini_get( 'max_execution_time' ); // Note that CLI scripts use 0 if ( $timeLimit > 0 && $wgTransactionalTimeLimit > $timeLimit ) { set_time_limit( $wgTransactionalTimeLimit ); @@ -2907,72 +2912,27 @@ function wfUnpack( $format, $data, $length = false ) { * * Any subsequent links on the same line are considered to be exceptions, * i.e. articles where the image may occur inline. * + * @deprecated since 1.34, use the BadFileLookup service directly instead + * * @param string $name The image name to check * @param Title|bool $contextTitle The page on which the image occurs, if known * @param string|null $blacklist Wikitext of a file blacklist * @return bool */ function wfIsBadImage( $name, $contextTitle = false, $blacklist = null ) { - # Handle redirects; callers almost always hit wfFindFile() anyway, - # so just use that method because it has a fast process cache. - $file = MediaWikiServices::getInstance()->getRepoGroup()->findFile( $name ); // get the final name - $name = $file ? $file->getTitle()->getDBkey() : $name; - - # Run the extension hook - $bad = false; - if ( !Hooks::run( 'BadImage', [ $name, &$bad ] ) ) { - return (bool)$bad; - } - - $cache = ObjectCache::getLocalServerInstance( 'hash' ); - $key = $cache->makeKey( - 'bad-image-list', ( $blacklist === null ) ? 'default' : md5( $blacklist ) - ); - $badImages = $cache->get( $key ); - - if ( $badImages === false ) { // cache miss - if ( $blacklist === null ) { - $blacklist = wfMessage( 'bad_image_list' )->inContentLanguage()->plain(); // site list - } - # Build the list now - $badImages = []; - $lines = explode( "\n", $blacklist ); - foreach ( $lines as $line ) { - # List items only - if ( substr( $line, 0, 1 ) !== '*' ) { - continue; - } - - # Find all links - $m = []; - if ( !preg_match_all( '/\[\[:?(.*?)\]\]/', $line, $m ) ) { - continue; - } - - $exceptions = []; - $imageDBkey = false; - foreach ( $m[1] as $i => $titleText ) { - $title = Title::newFromText( $titleText ); - if ( !is_null( $title ) ) { - if ( $i == 0 ) { - $imageDBkey = $title->getDBkey(); - } else { - $exceptions[$title->getPrefixedDBkey()] = true; - } - } - } - - if ( $imageDBkey !== false ) { - $badImages[$imageDBkey] = $exceptions; - } - } - $cache->set( $key, $badImages, 60 ); - } - - $contextKey = $contextTitle ? $contextTitle->getPrefixedDBkey() : false; - $bad = isset( $badImages[$name] ) && !isset( $badImages[$name][$contextKey] ); - - return $bad; + $services = MediaWikiServices::getInstance(); + if ( $blacklist !== null ) { + wfDeprecated( __METHOD__ . ' with $blacklist parameter', '1.34' ); + return ( new BadFileLookup( + function () use ( $blacklist ) { + return $blacklist; + }, + $services->getLocalServerObjectCache(), + $services->getRepoGroup(), + $services->getTitleParser() + ) )->isBadFile( $name, $contextTitle ?: null ); + } + return $services->getBadFileLookup()->isBadFile( $name, $contextTitle ?: null ); } /** diff --git a/includes/Html.php b/includes/Html.php index c4b57af978..ea2ce07a32 100644 --- a/includes/Html.php +++ b/includes/Html.php @@ -704,7 +704,7 @@ class Html { * Return the HTML for a message box. * @since 1.31 * @param string $html of contents of box - * @param string $className corresponding to box + * @param string|array $className corresponding to box * @param string $heading (optional) * @return string of HTML representing a box. */ @@ -718,32 +718,38 @@ class Html { /** * Return a warning box. * @since 1.31 + * @since 1.34 $className optional parameter added * @param string $html of contents of box + * @param string $className (optional) corresponding to box * @return string of HTML representing a warning box. */ - public static function warningBox( $html ) { - return self::messageBox( $html, 'warningbox' ); + public static function warningBox( $html, $className = '' ) { + return self::messageBox( $html, [ 'warningbox', $className ] ); } /** * Return an error box. * @since 1.31 + * @since 1.34 $className optional parameter added * @param string $html of contents of error box * @param string $heading (optional) + * @param string $className (optional) corresponding to box * @return string of HTML representing an error box. */ - public static function errorBox( $html, $heading = '' ) { - return self::messageBox( $html, 'errorbox', $heading ); + public static function errorBox( $html, $heading = '', $className = '' ) { + return self::messageBox( $html, [ 'errorbox', $className ], $heading ); } /** * Return a success box. * @since 1.31 + * @since 1.34 $className optional parameter added * @param string $html of contents of box + * @param string $className (optional) corresponding to box * @return string of HTML representing a success box. */ - public static function successBox( $html ) { - return self::messageBox( $html, 'successbox' ); + public static function successBox( $html, $className = '' ) { + return self::messageBox( $html, [ 'successbox', $className ] ); } /** @@ -1005,7 +1011,7 @@ class Html { /** * Get HTML for an information message box with an icon. * - * @internal For use by the WebInstaller class. + * @internal For use by the WebInstaller class only. * @param string $rawHtml HTML * @param string $icon Path to icon file (used as 'src' attribute) * @param string $alt Alternate text for the icon diff --git a/includes/LinkFilter.php b/includes/LinkFilter.php index 6ad9b31ab9..e4a5f962d1 100644 --- a/includes/LinkFilter.php +++ b/includes/LinkFilter.php @@ -292,7 +292,7 @@ class LinkFilter { // The constant prefix is smaller than el_index_60, so we use a LIKE // for a prefix search. return [ - "{$p}_index_60" . $db->buildLike( [ $index, $db->anyString() ] ), + "{$p}_index_60" . $db->buildLike( $index, $db->anyString() ), "{$p}_index" . $db->buildLike( $like ), ]; } @@ -311,6 +311,7 @@ class LinkFilter { */ public static function makeLikeArray( $filterEntry, $protocol = 'http://' ) { $db = wfGetDB( DB_REPLICA ); + $like = []; $target = $protocol . $filterEntry; $bits = wfParseUrl( $target ); diff --git a/includes/Linker.php b/includes/Linker.php index db3e2f5f03..864019dcc7 100644 --- a/includes/Linker.php +++ b/includes/Linker.php @@ -21,7 +21,7 @@ */ use MediaWiki\Linker\LinkTarget; use MediaWiki\MediaWikiServices; -use MediaWiki\Storage\RevisionRecord; +use MediaWiki\Revision\RevisionRecord; /** * Some internal bits split of from Skin.php. These functions are used @@ -688,35 +688,38 @@ class Linker { if ( $label == '' ) { $label = $title->getPrefixedText(); } - $encLabel = htmlspecialchars( $label ); + $repoGroup = MediaWikiServices::getInstance()->getRepoGroup(); $currentExists = $time - && MediaWikiServices::getInstance()->getRepoGroup()->findFile( $title ) !== false; + && $repoGroup->findFile( $title ) !== false; if ( ( $wgUploadMissingFileUrl || $wgUploadNavigationUrl || $wgEnableUploads ) && !$currentExists ) { - $redir = RepoGroup::singleton()->getLocalRepo()->checkRedirect( $title ); - - if ( $redir ) { - // We already know it's a redirect, so mark it - // accordingly + if ( $repoGroup->getLocalRepo()->checkRedirect( $title ) ) { + // We already know it's a redirect, so mark it accordingly return self::link( $title, - $encLabel, + htmlspecialchars( $label ), [ 'class' => 'mw-redirect' ], wfCgiToArray( $query ), [ 'known', 'noclasses' ] ); } - $href = self::getUploadUrl( $title, $query ); - - return '' . - $encLabel . ''; + return Html::element( 'a', [ + 'href' => self::getUploadUrl( $title, $query ), + 'class' => 'new', + 'title' => $title->getPrefixedText() + ], $label ); } - return self::link( $title, $encLabel, [], wfCgiToArray( $query ), [ 'known', 'noclasses' ] ); + return self::link( + $title, + htmlspecialchars( $label ), + [], + wfCgiToArray( $query ), + [ 'known', 'noclasses' ] + ); } /** @@ -888,7 +891,7 @@ class Linker { * Make user link (or user contributions for unregistered users) * @param int $userId User id in database. * @param string $userName User name in database. - * @param string $altUserName Text to display instead of the user name (optional) + * @param string|false $altUserName Text to display instead of the user name (optional) * @return string HTML fragment * @since 1.16.3. $altUserName was added in 1.19. */ @@ -978,7 +981,9 @@ class Linker { $items[] = self::link( $contribsPage, wfMessage( 'contribslink' )->escaped(), $attribs ); } - if ( $blockable && $wgUser->isAllowed( 'block' ) ) { + $userCanBlock = MediaWikiServices::getInstance()->getPermissionManager() + ->userHasRight( $wgUser, 'block' ); + if ( $blockable && $userCanBlock ) { $items[] = self::blockLink( $userId, $userText ); } @@ -1036,7 +1041,7 @@ class Linker { } $userTalkPage = new TitleValue( NS_USER_TALK, strtr( $userText, ' ', '_' ) ); - $moreLinkAttribs['class'] = 'mw-usertoollinks-talk'; + $moreLinkAttribs = [ 'class' => 'mw-usertoollinks-talk' ]; return self::link( $userTalkPage, wfMessage( 'talkpagelinktext' )->escaped(), @@ -1058,7 +1063,7 @@ class Linker { } $blockPage = SpecialPage::getTitleFor( 'Block', $userText ); - $moreLinkAttribs['class'] = 'mw-usertoollinks-block'; + $moreLinkAttribs = [ 'class' => 'mw-usertoollinks-block' ]; return self::link( $blockPage, wfMessage( 'blocklink' )->escaped(), @@ -1079,7 +1084,7 @@ class Linker { } $emailPage = SpecialPage::getTitleFor( 'Emailuser', $userText ); - $moreLinkAttribs['class'] = 'mw-usertoollinks-mail'; + $moreLinkAttribs = [ 'class' => 'mw-usertoollinks-mail' ]; return self::link( $emailPage, wfMessage( 'emaillink' )->escaped(), $moreLinkAttribs @@ -1320,7 +1325,7 @@ class Linker { $services->getNamespaceInfo()->getCanonicalName( NS_MEDIA ), '/' ); $medians .= '|'; $medians .= preg_quote( - MediaWikiServices::getInstance()->getContentLanguage()->getNsText( NS_MEDIA ), + $services->getContentLanguage()->getNsText( NS_MEDIA ), '/' ) . '):'; @@ -1357,7 +1362,7 @@ class Linker { } if ( $match[1] !== false && $match[1] !== '' ) { if ( preg_match( - MediaWikiServices::getInstance()->getContentLanguage()->linkTrail(), + $services->getContentLanguage()->linkTrail(), $match[3], $submatch ) ) { @@ -1373,7 +1378,7 @@ class Linker { Title::newFromText( $linkTarget ); try { - $target = MediaWikiServices::getInstance()->getTitleParser()-> + $target = $services->getTitleParser()-> parseTitle( $linkTarget ); if ( $target->getText() == '' && !$target->isExternal() @@ -1907,7 +1912,7 @@ class Linker { * @since 1.16.3. $context added in 1.20. $editCount added in 1.21 * @param Revision $rev * @param IContextSource|null $context Context to use or null for the main context. - * @param int $editCount Number of edits that would be reverted + * @param int|false $editCount Number of edits that would be reverted * @return string HTML fragment */ public static function buildRollbackLink( $rev, IContextSource $context = null, @@ -2103,8 +2108,10 @@ class Linker { * @return string HTML fragment */ public static function getRevDeleteLink( User $user, Revision $rev, LinkTarget $title ) { - $canHide = $user->isAllowed( 'deleterevision' ); - if ( !$canHide && !( $rev->getVisibility() && $user->isAllowed( 'deletedhistory' ) ) ) { + $permissionManager = MediaWikiServices::getInstance()->getPermissionManager(); + $canHide = $permissionManager->userHasRight( $user, 'deleterevision' ); + $canHideHistory = $permissionManager->userHasRight( $user, 'deletedhistory' ); + if ( !$canHide && !( $rev->getVisibility() && $canHideHistory ) ) { return ''; } diff --git a/includes/MWNamespace.php b/includes/MWNamespace.php index 0121bd589c..4a911b0819 100644 --- a/includes/MWNamespace.php +++ b/includes/MWNamespace.php @@ -318,8 +318,9 @@ class MWNamespace { * @return array */ public static function getRestrictionLevels( $index, User $user = null ) { - return MediaWikiServices::getInstance()->getNamespaceInfo()-> - getRestrictionLevels( $index, $user ); + return MediaWikiServices::getInstance() + ->getPermissionManager() + ->getNamespaceRestrictionLevels( $index, $user ); } /** diff --git a/includes/MediaWiki.php b/includes/MediaWiki.php index 7a6987e20f..1a75714fbc 100644 --- a/includes/MediaWiki.php +++ b/includes/MediaWiki.php @@ -346,6 +346,10 @@ class MediaWiki { return false; } + if ( $this->config->get( 'MainPageIsDomainRoot' ) && $request->getRequestURL() === '/' ) { + return false; + } + if ( $title->isSpecialPage() ) { list( $name, $subpage ) = MediaWikiServices::getInstance()->getSpecialPageFactory()-> resolveAlias( $title->getDBkey() ); @@ -522,11 +526,15 @@ class MediaWiki { try { $this->main(); } catch ( ErrorPageError $e ) { + $out = $this->context->getOutput(); + // TODO: Should ErrorPageError::report accept a OutputPage parameter? + $e->report( ErrorPageError::STAGE_OUTPUT ); + // T64091: while exceptions are convenient to bubble up GUI errors, // they are not internal application faults. As with normal requests, this // should commit, print the output, do deferred updates, jobs, and profiling. $this->doPreOutputCommit(); - $e->report(); // display the GUI error + $out->output(); // display the GUI error } } catch ( Exception $e ) { $context = $this->context; @@ -745,7 +753,7 @@ class MediaWiki { Profiler::instance()->logDataPageOutputOnly(); } catch ( Exception $e ) { // An error may already have been shown in run(), so just log it to be safe - MWExceptionHandler::rollbackMasterChangesAndLog( $e ); + MWExceptionHandler::logException( $e ); } // Disable WebResponse setters for post-send processing (T191537). diff --git a/includes/MediaWikiServices.php b/includes/MediaWikiServices.php index 7fda45280a..8f4ddf60dc 100644 --- a/includes/MediaWikiServices.php +++ b/includes/MediaWikiServices.php @@ -16,12 +16,16 @@ use IBufferingStatsdDataFactory; use Liuggio\StatsdClient\Factory\StatsdDataFactoryInterface; use MediaWiki\Block\BlockManager; use MediaWiki\Block\BlockRestrictionStore; +use MediaWiki\FileBackend\FSFile\TempFSFileFactory; +use MediaWiki\FileBackend\LockManager\LockManagerGroupFactory; use MediaWiki\Http\HttpRequestFactory; +use Wikimedia\Message\IMessageFormatterFactory; +use MediaWiki\Page\MovePageFactory; use MediaWiki\Permissions\PermissionManager; use MediaWiki\Preferences\PreferencesFactory; -use MediaWiki\Shell\CommandFactory; use MediaWiki\Revision\RevisionRenderer; use MediaWiki\Revision\SlotRoleRegistry; +use MediaWiki\Shell\CommandFactory; use MediaWiki\Special\SpecialPageFactory; use MediaWiki\Storage\BlobStore; use MediaWiki\Storage\BlobStoreFactory; @@ -40,6 +44,7 @@ use MediaWiki\Config\ConfigRepository; use MediaWiki\Linker\LinkRenderer; use MediaWiki\Linker\LinkRendererFactory; use MWException; +use MessageCache; use MimeAnalyzer; use NamespaceInfo; use ObjectCache; @@ -61,6 +66,7 @@ use SkinFactory; use TitleFormatter; use TitleParser; use VirtualRESTServiceClient; +use Wikimedia\ObjectFactory; use Wikimedia\Rdbms\LBFactory; use Wikimedia\Services\SalvageableService; use Wikimedia\Services\ServiceContainer; @@ -426,6 +432,14 @@ class MediaWikiServices extends ServiceContainer { return $this->getService( 'ActorMigration' ); } + /** + * @since 1.34 + * @return BadFileLookup + */ + public function getBadFileLookup() : BadFileLookup { + return $this->getService( 'BadFileLookup' ); + } + /** * @since 1.31 * @return BlobStore @@ -646,6 +660,14 @@ class MediaWikiServices extends ServiceContainer { return $this->getService( 'LocalServerObjectCache' ); } + /** + * @since 1.34 + * @return LockManagerGroupFactory + */ + public function getLockManagerGroupFactory() : LockManagerGroupFactory { + return $this->getService( 'LockManagerGroupFactory' ); + } + /** * @since 1.32 * @return MagicWordFactory @@ -689,6 +711,22 @@ class MediaWikiServices extends ServiceContainer { return $this->getService( 'MediaHandlerFactory' ); } + /** + * @since 1.34 + * @return MessageCache + */ + public function getMessageCache() : MessageCache { + return $this->getService( 'MessageCache' ); + } + + /** + * @since 1.34 + * @return IMessageFormatterFactory + */ + public function getMessageFormatterFactory() { + return $this->getService( 'MessageFormatterFactory' ); + } + /** * @since 1.28 * @return MimeAnalyzer @@ -697,6 +735,14 @@ class MediaWikiServices extends ServiceContainer { return $this->getService( 'MimeAnalyzer' ); } + /** + * @since 1.34 + * @return MovePageFactory + */ + public function getMovePageFactory() : MovePageFactory { + return $this->getService( 'MovePageFactory' ); + } + /** * @since 1.34 * @return NamespaceInfo @@ -713,6 +759,17 @@ class MediaWikiServices extends ServiceContainer { return $this->getService( 'NameTableStoreFactory' ); } + /** + * ObjectFactory is intended for instantiating "handlers" from declarative definitions, + * such as Action API modules, special pages, or REST API handlers. + * + * @since 1.34 + * @return ObjectFactory + */ + public function getObjectFactory() { + return $this->getService( 'ObjectFactory' ); + } + /** * @since 1.32 * @return OldRevisionImporter @@ -946,6 +1003,14 @@ class MediaWikiServices extends ServiceContainer { return $this->getService( 'StatsdDataFactory' ); } + /** + * @since 1.34 + * @return TempFSFileFactory + */ + public function getTempFSFileFactory() : TempFSFileFactory { + return $this->getService( 'TempFSFileFactory' ); + } + /** * @since 1.28 * @return TitleFormatter diff --git a/includes/MergeHistory.php b/includes/MergeHistory.php index 6bd4471723..1a950c5f5e 100644 --- a/includes/MergeHistory.php +++ b/includes/MergeHistory.php @@ -178,7 +178,8 @@ class MergeHistory { } // Check mergehistory permission - if ( !$user->isAllowed( 'mergehistory' ) ) { + $permissionManager = MediaWikiServices::getInstance()->getPermissionManager(); + if ( !$permissionManager->userHasRight( $user, 'mergehistory' ) ) { // User doesn't have the right to merge histories $status->fatal( 'mergehistory-fail-permission' ); } @@ -272,6 +273,18 @@ class MergeHistory { return $status; } + // Update denormalized revactor_page too + $this->dbw->update( + 'revision_actor_temp', + [ 'revactor_page' => $this->dest->getArticleID() ], + [ + 'revactor_page' => $this->source->getArticleID(), + // Slightly hacky, but should work given the values assigned in this class + str_replace( 'rev_timestamp', 'revactor_timestamp', $this->timeWhere ) + ], + __METHOD__ + ); + // Make the source page a redirect if no revisions are left $haveRevisions = $this->dbw->lockForUpdate( 'revision', diff --git a/includes/Message/MessageFormatterFactory.php b/includes/Message/MessageFormatterFactory.php new file mode 100644 index 0000000000..101224a6a8 --- /dev/null +++ b/includes/Message/MessageFormatterFactory.php @@ -0,0 +1,29 @@ +textFormatters[$langCode] ) ) { + $this->textFormatters[$langCode] = new TextFormatter( $langCode ); + } + return $this->textFormatters[$langCode]; + } +} diff --git a/includes/Message/TextFormatter.php b/includes/Message/TextFormatter.php new file mode 100644 index 0000000000..783dd4302e --- /dev/null +++ b/includes/Message/TextFormatter.php @@ -0,0 +1,83 @@ +langCode = $langCode; + } + + /** + * Allow the Message class to be mocked in tests by constructing objects in + * a protected method. + * + * @internal + * @param string $key + * @return Message + */ + protected function createMessage( $key ) { + return new Message( $key ); + } + + public function getLangCode() { + return $this->langCode; + } + + private function convertParam( MessageParam $param ) { + if ( $param instanceof ListParam ) { + $convertedElements = []; + foreach ( $param->getValue() as $element ) { + $convertedElements[] = $this->convertParam( $element ); + } + return Message::listParam( $convertedElements, $param->getListType() ); + } elseif ( $param instanceof MessageParam ) { + $value = $param->getValue(); + if ( $value instanceof MessageValue ) { + $mv = $value; + $value = $this->createMessage( $mv->getKey() ); + foreach ( $mv->getParams() as $mvParam ) { + $value->params( $this->convertParam( $mvParam ) ); + } + } + + if ( $param->getType() === ParamType::TEXT ) { + return $value; + } else { + return [ $param->getType() => $value ]; + } + } else { + throw new \InvalidArgumentException( 'Invalid message parameter type' ); + } + } + + public function format( MessageValue $mv ) { + $message = $this->createMessage( $mv->getKey() ); + foreach ( $mv->getParams() as $param ) { + $message->params( $this->convertParam( $param ) ); + } + $message->inLanguage( $this->langCode ); + return $message->text(); + } +} diff --git a/includes/MovePage.php b/includes/MovePage.php index 832e24af81..59baae78e2 100644 --- a/includes/MovePage.php +++ b/includes/MovePage.php @@ -19,9 +19,13 @@ * @file */ +use MediaWiki\Config\ServiceOptions; use MediaWiki\MediaWikiServices; +use MediaWiki\Page\MovePageFactory; +use MediaWiki\Permissions\PermissionManager; use MediaWiki\Revision\SlotRecord; use Wikimedia\Rdbms\IDatabase; +use Wikimedia\Rdbms\ILoadBalancer; /** * Handles the backend logic of moving a page from one title @@ -41,9 +45,69 @@ class MovePage { */ protected $newTitle; - public function __construct( Title $oldTitle, Title $newTitle ) { + /** + * @var ServiceOptions + */ + protected $options; + + /** + * @var ILoadBalancer + */ + protected $loadBalancer; + + /** + * @var NamespaceInfo + */ + protected $nsInfo; + + /** + * @var WatchedItemStoreInterface + */ + protected $watchedItems; + + /** + * @var PermissionManager + */ + protected $permMgr; + + /** + * @var RepoGroup + */ + protected $repoGroup; + + /** + * Calling this directly is deprecated in 1.34. Use MovePageFactory instead. + * + * @param Title $oldTitle + * @param Title $newTitle + * @param ServiceOptions|null $options + * @param ILoadBalancer|null $loadBalancer + * @param NamespaceInfo|null $nsInfo + * @param WatchedItemStoreInterface|null $watchedItems + * @param PermissionManager|null $permMgr + */ + public function __construct( + Title $oldTitle, + Title $newTitle, + ServiceOptions $options = null, + ILoadBalancer $loadBalancer = null, + NamespaceInfo $nsInfo = null, + WatchedItemStoreInterface $watchedItems = null, + PermissionManager $permMgr = null, + RepoGroup $repoGroup = null + ) { $this->oldTitle = $oldTitle; $this->newTitle = $newTitle; + $this->options = $options ?? + new ServiceOptions( MovePageFactory::$constructorOptions, + MediaWikiServices::getInstance()->getMainConfig() ); + $this->loadBalancer = + $loadBalancer ?? MediaWikiServices::getInstance()->getDBLoadBalancer(); + $this->nsInfo = $nsInfo ?? MediaWikiServices::getInstance()->getNamespaceInfo(); + $this->watchedItems = + $watchedItems ?? MediaWikiServices::getInstance()->getWatchedItemStore(); + $this->permMgr = $permMgr ?? MediaWikiServices::getInstance()->getPermissionManager(); + $this->repoGroup = $repoGroup ?? MediaWikiServices::getInstance()->getRepoGroup(); } /** @@ -58,10 +122,10 @@ class MovePage { $status = new Status(); $errors = wfMergeErrorArrays( - $this->oldTitle->getUserPermissionsErrors( 'move', $user ), - $this->oldTitle->getUserPermissionsErrors( 'edit', $user ), - $this->newTitle->getUserPermissionsErrors( 'move-target', $user ), - $this->newTitle->getUserPermissionsErrors( 'edit', $user ) + $this->permMgr->getPermissionErrors( 'move', $user, $this->oldTitle ), + $this->permMgr->getPermissionErrors( 'edit', $user, $this->oldTitle ), + $this->permMgr->getPermissionErrors( 'move-target', $user, $this->newTitle ), + $this->permMgr->getPermissionErrors( 'edit', $user, $this->newTitle ) ); // Convert into a Status object @@ -77,8 +141,9 @@ class MovePage { } $tp = $this->newTitle->getTitleProtection(); - if ( $tp !== false && !$user->isAllowed( $tp['permission'] ) ) { - $status->fatal( 'cantmove-titleprotected' ); + $permissionManager = MediaWikiServices::getInstance()->getPermissionManager(); + if ( $tp !== false && !$permissionManager->userHasRight( $user, $tp['permission'] ) ) { + $status->fatal( 'cantmove-titleprotected' ); } Hooks::run( 'MovePageCheckPermissions', @@ -96,44 +161,41 @@ class MovePage { * @return Status */ public function isValidMove() { - global $wgContentHandlerUseDB; $status = new Status(); if ( $this->oldTitle->equals( $this->newTitle ) ) { $status->fatal( 'selfmove' ); + } elseif ( $this->newTitle->getArticleID() && !$this->isValidMoveTarget() ) { + // The move is allowed only if (1) the target doesn't exist, or (2) the target is a + // redirect to the source, and has no history (so we can undo bad moves right after + // they're done). + $status->fatal( 'articleexists' ); } - if ( !$this->oldTitle->isMovable() ) { + + // @todo If the old title is invalid, maybe we should check if it somehow exists in the + // database and allow moving it to a valid name? Why prohibit the move from an empty name + // without checking in the database? + if ( $this->oldTitle->getDBkey() == '' ) { + $status->fatal( 'badarticleerror' ); + } elseif ( $this->oldTitle->isExternal() ) { + $status->fatal( 'immobile-source-namespace-iw' ); + } elseif ( !$this->oldTitle->isMovable() ) { $status->fatal( 'immobile-source-namespace', $this->oldTitle->getNsText() ); + } elseif ( !$this->oldTitle->exists() ) { + $status->fatal( 'movepage-source-doesnt-exist' ); } + if ( $this->newTitle->isExternal() ) { $status->fatal( 'immobile-target-namespace-iw' ); - } - if ( !$this->newTitle->isMovable() ) { + } elseif ( !$this->newTitle->isMovable() ) { $status->fatal( 'immobile-target-namespace', $this->newTitle->getNsText() ); } - - $oldid = $this->oldTitle->getArticleID(); - - if ( $this->newTitle->getDBkey() === '' ) { - $status->fatal( 'articleexists' ); - } - if ( - ( $this->oldTitle->getDBkey() == '' ) || - ( !$oldid ) || - ( $this->newTitle->getDBkey() == '' ) - ) { - $status->fatal( 'badarticleerror' ); - } - - # The move is allowed only if (1) the target doesn't exist, or - # (2) the target is a redirect to the source, and has no history - # (so we can undo bad moves right after they're done). - if ( $this->newTitle->getArticleID() && !$this->isValidMoveTarget() ) { - $status->fatal( 'articleexists' ); + if ( !$this->newTitle->isValid() ) { + $status->fatal( 'movepage-invalid-target-title' ); } // Content model checks - if ( !$wgContentHandlerUseDB && + if ( !$this->options->get( 'ContentHandlerUseDB' ) && $this->oldTitle->getContentModel() !== $this->newTitle->getContentModel() ) { // can't move a page if that would change the page's content model $status->fatal( @@ -174,7 +236,14 @@ class MovePage { */ protected function isValidFileMove() { $status = new Status(); - $file = wfLocalFile( $this->oldTitle ); + + if ( !$this->newTitle->inNamespace( NS_FILE ) ) { + $status->fatal( 'imagenocrossnamespace' ); + // No need for further errors about the target filename being wrong + return $status; + } + + $file = $this->repoGroup->getLocalRepo()->newFile( $this->oldTitle ); $file->load( File::READ_LATEST ); if ( $file->exists() ) { if ( $this->newTitle->getText() != wfStripIllegalFilenameChars( $this->newTitle->getText() ) ) { @@ -185,10 +254,6 @@ class MovePage { } } - if ( !$this->newTitle->inNamespace( NS_FILE ) ) { - $status->fatal( 'imagenocrossnamespace' ); - } - return $status; } @@ -202,7 +267,7 @@ class MovePage { protected function isValidMoveTarget() { # Is it an existing file? if ( $this->newTitle->inNamespace( NS_FILE ) ) { - $file = wfLocalFile( $this->newTitle ); + $file = $this->repoGroup->getLocalRepo()->newFile( $this->newTitle ); $file->load( File::READ_LATEST ); if ( $file->exists() ) { wfDebug( __METHOD__ . ": file exists\n" ); @@ -287,7 +352,8 @@ class MovePage { } // Check suppressredirect permission - if ( !$user->isAllowed( 'suppressredirect' ) ) { + $permissionManager = MediaWikiServices::getInstance()->getPermissionManager(); + if ( !$permissionManager->userHasRight( $user, 'suppressredirect' ) ) { $createRedirect = true; } @@ -380,7 +446,7 @@ class MovePage { $status = Status::newFatal( 'movepage-max-pages', $wgMaximumMovedPages ); $perTitleStatus[$oldSubpage->getPrefixedText()] = $status; $topStatus->merge( $status ); - $topStatus->setOk( true ); + $topStatus->setOK( true ); break; } @@ -407,13 +473,14 @@ class MovePage { $mp = new MovePage( $oldSubpage, $newSubpage ); $method = $checkPermissions ? 'moveIfAllowed' : 'move'; + /** @var Status $status */ $status = $mp->$method( $user, $reason, $createRedirect, $changeTags ); if ( $status->isOK() ) { $status->setResult( true, $newSubpage->getPrefixedText() ); } $perTitleStatus[$oldSubpage->getPrefixedText()] = $status; $topStatus->merge( $status ); - $topStatus->setOk( true ); + $topStatus->setOK( true ); } $topStatus->value = $perTitleStatus; @@ -430,8 +497,6 @@ class MovePage { * @return Status */ private function moveUnsafe( User $user, $reason, $createRedirect, array $changeTags ) { - global $wgCategoryCollation; - $status = Status::newGood(); Hooks::run( 'TitleMove', [ $this->oldTitle, $this->newTitle, $user, $reason, &$status ] ); if ( !$status->isOK() ) { @@ -439,12 +504,12 @@ class MovePage { return $status; } - $dbw = wfGetDB( DB_MASTER ); + $dbw = $this->loadBalancer->getConnection( DB_MASTER ); $dbw->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE ); Hooks::run( 'TitleMoveStarting', [ $this->oldTitle, $this->newTitle, $user ] ); - $pageid = $this->oldTitle->getArticleID( Title::GAID_FOR_UPDATE ); + $pageid = $this->oldTitle->getArticleID( Title::READ_LATEST ); $protected = $this->oldTitle->isProtected(); // Do the actual move; if this fails, it will throw an MWException(!) @@ -461,9 +526,7 @@ class MovePage { [ 'cl_from' => $pageid ], __METHOD__ ); - $services = MediaWikiServices::getInstance(); - $type = $services->getNamespaceInfo()-> - getCategoryLinkType( $this->newTitle->getNamespace() ); + $type = $this->nsInfo->getCategoryLinkType( $this->newTitle->getNamespace() ); foreach ( $prefixes as $prefixRow ) { $prefix = $prefixRow->cl_sortkey_prefix; $catTo = $prefixRow->cl_to; @@ -471,7 +534,7 @@ class MovePage { [ 'cl_sortkey' => Collation::singleton()->getSortKey( $this->newTitle->getCategorySortkey( $prefix ) ), - 'cl_collation' => $wgCategoryCollation, + 'cl_collation' => $this->options->get( 'CategoryCollation' ), 'cl_type' => $type, 'cl_timestamp=cl_timestamp' ], [ @@ -536,7 +599,7 @@ class MovePage { '4::oldtitle' => $this->oldTitle->getPrefixedText(), ] ); $logEntry->setRelations( [ 'pr_id' => $logRelationsValues ] ); - $logEntry->setTags( $changeTags ); + $logEntry->addTags( $changeTags ); $logId = $logEntry->insert(); $logEntry->publish( $logId ); } @@ -563,13 +626,10 @@ class MovePage { # Update watchlists $oldtitle = $this->oldTitle->getDBkey(); $newtitle = $this->newTitle->getDBkey(); - $oldsnamespace = $services->getNamespaceInfo()-> - getSubject( $this->oldTitle->getNamespace() ); - $newsnamespace = $services->getNamespaceInfo()-> - getSubject( $this->newTitle->getNamespace() ); + $oldsnamespace = $this->nsInfo->getSubject( $this->oldTitle->getNamespace() ); + $newsnamespace = $this->nsInfo->getSubject( $this->newTitle->getNamespace() ); if ( $oldsnamespace != $newsnamespace || $oldtitle != $newtitle ) { - $services->getWatchedItemStore()->duplicateAllAssociatedEntries( - $this->oldTitle, $this->newTitle ); + $this->watchedItems->duplicateAllAssociatedEntries( $this->oldTitle, $this->newTitle ); } // If it is a file then move it last. @@ -630,15 +690,15 @@ class MovePage { $oldTitle->getPrefixedText() ); - $file = wfLocalFile( $oldTitle ); + $file = $this->repoGroup->getLocalRepo()->newFile( $oldTitle ); $file->load( File::READ_LATEST ); if ( $file->exists() ) { $status = $file->move( $newTitle ); } // Clear RepoGroup process cache - RepoGroup::singleton()->clearCache( $oldTitle ); - RepoGroup::singleton()->clearCache( $newTitle ); # clear false negative cache + $this->repoGroup->clearCache( $oldTitle ); + $this->repoGroup->clearCache( $newTitle ); # clear false negative cache return $status; } @@ -739,7 +799,7 @@ class MovePage { $comment .= wfMessage( 'colon-separator' )->inContentLanguage()->text() . $reason; } - $dbw = wfGetDB( DB_MASTER ); + $dbw = $this->loadBalancer->getConnection( DB_MASTER ); $oldpage = WikiPage::factory( $this->oldTitle ); $oldcountable = $oldpage->isCountable(); @@ -836,7 +896,7 @@ class MovePage { # Log the move $logid = $logEntry->insert(); - $logEntry->setTags( $changeTags ); + $logEntry->addTags( $changeTags ); $logEntry->publish( $logid ); return $nullRevision; diff --git a/includes/Navigation/PrevNextNavigationRenderer.php b/includes/Navigation/PrevNextNavigationRenderer.php index c60b8c6130..539758f28c 100644 --- a/includes/Navigation/PrevNextNavigationRenderer.php +++ b/includes/Navigation/PrevNextNavigationRenderer.php @@ -20,12 +20,13 @@ namespace MediaWiki\Navigation; -use MediaWiki\Linker\LinkTarget; -use MessageLocalizer; use Html; +use MessageLocalizer; +use Title; /** * Helper class for generating prev/next links for paging. + * @todo Use LinkTarget instead of Title * * @since 1.34 */ @@ -36,6 +37,9 @@ class PrevNextNavigationRenderer { */ private $messageLocalizer; + /** + * @param MessageLocalizer $messageLocalizer + */ public function __construct( MessageLocalizer $messageLocalizer ) { $this->messageLocalizer = $messageLocalizer; } @@ -43,15 +47,19 @@ class PrevNextNavigationRenderer { /** * Generate (prev x| next x) (20|50|100...) type links for paging * - * @param LinkTarget $title LinkTarget object to link + * @param Title $title Title object to link * @param int $offset * @param int $limit * @param array $query Optional URL query parameter string * @param bool $atend Optional param for specified if this is the last page * @return string */ - public function buildPrevNextNavigation( LinkTarget $title, $offset, $limit, - array $query = [], $atend = false + public function buildPrevNextNavigation( + Title $title, + $offset, + $limit, + array $query = [], + $atend = false ) { # Make 'previous' link $prev = $this->messageLocalizer->msg( 'prevn' )->title( $title ) @@ -76,6 +84,8 @@ class PrevNextNavigationRenderer { # Make links to set number of items per page $numLinks = []; + // @phan-suppress-next-next-line PhanUndeclaredMethod + // @fixme MessageLocalizer doesn't have a getLanguage() method! $lang = $this->messageLocalizer->getLanguage(); foreach ( [ 20, 50, 100, 250, 500 ] as $num ) { $numLinks[] = $this->numLink( $title, $offset, $num, $query, @@ -89,7 +99,7 @@ class PrevNextNavigationRenderer { /** * Helper function for buildPrevNextNavigation() that generates links * - * @param LinkTarget $title LinkTarget object to link + * @param Title $title Title object to link * @param int $offset * @param int $limit * @param array $query Extra query parameters @@ -98,7 +108,7 @@ class PrevNextNavigationRenderer { * @param string $class Value of the "class" attribute of the link * @return string HTML fragment */ - private function numLink( LinkTarget $title, $offset, $limit, array $query, $link, + private function numLink( Title $title, $offset, $limit, array $query, $link, $tooltipMsg, $class ) { $query = [ 'limit' => $limit, 'offset' => $offset ] + $query; diff --git a/includes/OutputPage.php b/includes/OutputPage.php index b7341e3b7a..acf2d25532 100644 --- a/includes/OutputPage.php +++ b/includes/OutputPage.php @@ -44,13 +44,13 @@ use Wikimedia\WrappedStringList; * @todo document */ class OutputPage extends ContextSource { - /** @var array Should be private. Used with addMeta() which adds "" */ + /** @var string[][] Should be private. Used with addMeta() which adds "" */ protected $mMetatags = []; /** @var array */ protected $mLinktags = []; - /** @var bool */ + /** @var string|bool */ protected $mCanonicalUrl = false; /** @@ -332,7 +332,7 @@ class OutputPage extends ContextSource { * a OutputPage tied to that context. * @param IContextSource $context */ - function __construct( IContextSource $context ) { + public function __construct( IContextSource $context ) { $this->setContext( $context ); } @@ -385,8 +385,8 @@ class OutputPage extends ContextSource { * @param string $name Name of the meta tag * @param string $val Value of the meta tag */ - function addMeta( $name, $val ) { - array_push( $this->mMetatags, [ $name, $val ] ); + public function addMeta( $name, $val ) { + $this->mMetatags[] = [ $name, $val ]; } /** @@ -406,8 +406,8 @@ class OutputPage extends ContextSource { * * @param array $linkarr Associative array of attributes. */ - function addLink( array $linkarr ) { - array_push( $this->mLinktags, $linkarr ); + public function addLink( array $linkarr ) { + $this->mLinktags[] = $linkarr; } /** @@ -425,7 +425,7 @@ class OutputPage extends ContextSource { * in preference to addLink(), to avoid duplicate link tags. * @param string $url */ - function setCanonicalUrl( $url ) { + public function setCanonicalUrl( $url ) { $this->mCanonicalUrl = $url; } @@ -447,7 +447,7 @@ class OutputPage extends ContextSource { * * @param string $script Raw HTML */ - function addScript( $script ) { + public function addScript( $script ) { $this->mScripts .= $script; } @@ -621,7 +621,7 @@ class OutputPage extends ContextSource { * * @return array */ - function getHeadItemsArray() { + public function getHeadItemsArray() { return $this->mHeadItems; } @@ -995,6 +995,8 @@ class OutputPage extends ContextSource { * @param Title $t */ public function setTitle( Title $t ) { + // @phan-suppress-next-next-line PhanUndeclaredMethod + // @fixme Not all implementations of IContextSource have this method! $this->getContext()->setTitle( $t ); } @@ -1596,6 +1598,7 @@ class OutputPage extends ContextSource { * @param ParserOptions|null $options Either the ParserOption to use or null to only get the * current ParserOption object. This parameter is deprecated since 1.31. * @return ParserOptions + * @suppress PhanUndeclaredProperty For isBogus */ public function parserOptions( $options = null ) { if ( $options !== null ) { @@ -1661,6 +1664,16 @@ class OutputPage extends ContextSource { return $this->mRevisionId; } + /** + * Whether the revision displayed is the latest revision of the page + * + * @since 1.34 + * @return bool + */ + public function isRevisionCurrent() { + return $this->mRevisionId == 0 || $this->mRevisionId == $this->getTitle()->getLatestRevID(); + } + /** * Set the timestamp of the revision which will be displayed. This is used * to avoid a extra DB call in Skin::lastModified(). @@ -1810,14 +1823,10 @@ class OutputPage extends ContextSource { * @param string $text Wikitext * @param Title $title * @param bool $linestart Is this the start of a line? - * @param bool $tidy Whether to use tidy. - * Setting this to false (or omitting it) is deprecated - * since 1.32; all wikitext should be tidied. * @param bool $interface Whether it is an interface message * (for example disables conversion) * @param string $wrapperClass if not empty, wraps the output in * a `
` - * @private */ private function addWikiTextTitleInternal( $text, Title $title, $linestart, $interface, $wrapperClass = null @@ -1946,7 +1955,7 @@ class OutputPage extends ContextSource { * @param ParserOutput $parserOutput * @param array $poOptions Options to ParserOutput::getText() */ - function addParserOutput( ParserOutput $parserOutput, $poOptions = [] ) { + public function addParserOutput( ParserOutput $parserOutput, $poOptions = [] ) { $this->addParserOutputMetadata( $parserOutput ); $this->addParserOutputText( $parserOutput, $poOptions ); } @@ -2138,7 +2147,7 @@ class OutputPage extends ContextSource { } /** - * Get TTL in [$minTTL,$maxTTL] in pass it to lowerCdnMaxage() + * Get TTL in [$minTTL,$maxTTL] and pass it to lowerCdnMaxage() * * This sets and returns $minTTL if $mtime is false or null. Otherwise, * the TTL is higher the older the $mtime timestamp is. Essentially, the @@ -2154,10 +2163,10 @@ class OutputPage extends ContextSource { $maxTTL = $maxTTL ?: $this->getConfig()->get( 'CdnMaxAge' ); if ( $mtime === null || $mtime === false ) { - return $minTTL; // entity does not exist + return; // entity does not exist } - $age = MWTimestamp::time() - wfTimestamp( TS_UNIX, $mtime ); + $age = MWTimestamp::time() - (int)wfTimestamp( TS_UNIX, $mtime ); $adaptiveTTL = max( 0.9 * $age, $minTTL ); $adaptiveTTL = min( $adaptiveTTL, $maxTTL ); @@ -2180,7 +2189,7 @@ class OutputPage extends ContextSource { * * @return array */ - function getCacheVaryCookies() { + public function getCacheVaryCookies() { if ( self::$cacheVaryCookies === null ) { $config = $this->getConfig(); self::$cacheVaryCookies = array_values( array_unique( array_merge( @@ -2201,7 +2210,7 @@ class OutputPage extends ContextSource { * * @return bool */ - function haveCacheVaryCookies() { + public function haveCacheVaryCookies() { $request = $this->getRequest(); foreach ( $this->getCacheVaryCookies() as $cookieName ) { if ( $request->getCookie( $cookieName, '', '' ) !== '' ) { @@ -2261,7 +2270,7 @@ class OutputPage extends ContextSource { /** * Return a Link: header. Based on the values of $mLinkHeader. * - * @return string + * @return string|false */ public function getLinkHeader() { if ( !$this->mLinkHeader ) { @@ -2406,32 +2415,16 @@ class OutputPage extends ContextSource { $this->mCdnMaxage != 0 && !$this->haveCacheVaryCookies() ) { - if ( $config->get( 'UseESI' ) ) { - wfDeprecated( '$wgUseESI = true', '1.33' ); - # We'll purge the proxy cache explicitly, but require end user agents - # to revalidate against the proxy on each visit. - # Surrogate-Control controls our CDN, Cache-Control downstream caches - wfDebug( __METHOD__ . - ": proxy caching with ESI; {$this->mLastModified} **", 'private' ); - # start with a shorter timeout for initial testing - # header( 'Surrogate-Control: max-age=2678400+2678400, content="ESI/1.0"'); - $response->header( - "Surrogate-Control: max-age={$config->get( 'CdnMaxAge' )}" . - "+{$this->mCdnMaxage}, content=\"ESI/1.0\"" - ); - $response->header( 'Cache-Control: s-maxage=0, must-revalidate, max-age=0' ); - } else { - # We'll purge the proxy cache for anons explicitly, but require end user agents - # to revalidate against the proxy on each visit. - # IMPORTANT! The CDN needs to replace the Cache-Control header with - # Cache-Control: s-maxage=0, must-revalidate, max-age=0 - wfDebug( __METHOD__ . - ": local proxy caching; {$this->mLastModified} **", 'private' ); - # start with a shorter timeout for initial testing - # header( "Cache-Control: s-maxage=2678400, must-revalidate, max-age=0" ); - $response->header( "Cache-Control: " . - "s-maxage={$this->mCdnMaxage}, must-revalidate, max-age=0" ); - } + # We'll purge the proxy cache for anons explicitly, but require end user agents + # to revalidate against the proxy on each visit. + # IMPORTANT! The CDN needs to replace the Cache-Control header with + # Cache-Control: s-maxage=0, must-revalidate, max-age=0 + wfDebug( __METHOD__ . + ": local proxy caching; {$this->mLastModified} **", 'private' ); + # start with a shorter timeout for initial testing + # header( "Cache-Control: s-maxage=2678400, must-revalidate, max-age=0" ); + $response->header( "Cache-Control: " . + "s-maxage={$this->mCdnMaxage}, must-revalidate, max-age=0" ); } else { # We do want clients to cache if they can, but they *must* check for updates # on revisiting the page. @@ -2609,7 +2602,7 @@ class OutputPage extends ContextSource { * and optionally an custom HTML title (content of the "" tag). * * @param string|Message $pageTitle Will be passed directly to setPageTitle() - * @param string|Message $htmlTitle Will be passed directly to setHTMLTitle(); + * @param string|Message|false $htmlTitle Will be passed directly to setHTMLTitle(); * optional, if not passed the "<title>" attribute will be * based on $pageTitle */ @@ -2666,6 +2659,8 @@ class OutputPage extends ContextSource { * @param string|null $action Action that was denied or null if unknown */ public function showPermissionsErrorPage( array $errors, $action = null ) { + $services = MediaWikiServices::getInstance(); + $permissionManager = $services->getPermissionManager(); foreach ( $errors as $key => $error ) { $errors[$key] = (array)$error; } @@ -2675,11 +2670,12 @@ class OutputPage extends ContextSource { // 1. the user is not logged in // 2. the only error is insufficient permissions (i.e. no block or something else) // 3. the error can be avoided simply by logging in + if ( in_array( $action, [ 'read', 'edit', 'createpage', 'createtalk', 'upload' ] ) && $this->getUser()->isAnon() && count( $errors ) == 1 && isset( $errors[0][0] ) && ( $errors[0][0] == 'badaccess-groups' || $errors[0][0] == 'badaccess-group0' ) - && ( User::groupHasPermission( 'user', $action ) - || User::groupHasPermission( 'autoconfirmed', $action ) ) + && ( $permissionManager->groupHasPermission( 'user', $action ) + || $permissionManager->groupHasPermission( 'autoconfirmed', $action ) ) ) { $displayReturnto = null; @@ -2715,8 +2711,6 @@ class OutputPage extends ContextSource { } } - $services = MediaWikiServices::getInstance(); - $title = SpecialPage::getTitleFor( 'Userlogin' ); $linkRenderer = $services->getLinkRenderer(); $loginUrl = $title->getLinkURL( $query, false, PROTO_RELATIVE ); @@ -2730,8 +2724,6 @@ class OutputPage extends ContextSource { $this->prepareErrorPage( $this->msg( 'loginreqtitle' ) ); $this->addHTML( $this->msg( $msg )->rawParams( $loginLink )->params( $loginUrl )->parse() ); - $permissionManager = $services->getPermissionManager(); - # Don't return to a page the user can't read otherwise # we'll end up in a pointless loop if ( $displayReturnto && $permissionManager->userCan( @@ -2831,7 +2823,7 @@ class OutputPage extends ContextSource { /** * Add a "return to" link pointing to a specified title * - * @param Title $title Title to link + * @param LinkTarget $title Title to link * @param array $query Query string parameters * @param string|null $text Text of the link (input is not escaped) * @param array $options Options array to pass to Linker @@ -3018,10 +3010,11 @@ class OutputPage extends ContextSource { $sitedir = MediaWikiServices::getInstance()->getContentLanguage()->getDir(); $pieces = []; - $pieces[] = Html::htmlHeader( Sanitizer::mergeAttributes( + $htmlAttribs = Sanitizer::mergeAttributes( $this->getRlClient()->getDocumentAttributes(), $sk->getHtmlElementAttributes() - ) ); + ); + $pieces[] = Html::htmlHeader( $htmlAttribs ); $pieces[] = Html::openElement( 'head' ); if ( $this->getHTMLTitle() == '' ) { @@ -3041,7 +3034,7 @@ class OutputPage extends ContextSource { } $pieces[] = Html::element( 'title', null, $this->getHTMLTitle() ); - $pieces[] = $this->getRlClient()->getHeadHtml(); + $pieces[] = $this->getRlClient()->getHeadHtml( $htmlAttribs['class'] ?? null ); $pieces[] = $this->buildExemptModules(); $pieces = array_merge( $pieces, array_values( $this->getHeadLinksArray() ) ); $pieces = array_merge( $pieces, array_values( $this->mHeadItems ) ); @@ -3218,7 +3211,7 @@ class OutputPage extends ContextSource { $title = $this->getTitle(); $ns = $title->getNamespace(); - $nsInfo = MediaWikiServices::getInstance()->getNamespaceInfo(); + $nsInfo = $services->getNamespaceInfo(); $canonicalNamespace = $nsInfo->exists( $ns ) ? $nsInfo->getCanonicalName( $ns ) : $title->getNsText(); @@ -3290,7 +3283,7 @@ class OutputPage extends ContextSource { $vars['wgUserId'] = $user->getId(); $vars['wgUserEditCount'] = $user->getEditCount(); $userReg = $user->getRegistration(); - $vars['wgUserRegistration'] = $userReg ? wfTimestamp( TS_UNIX, $userReg ) * 1000 : null; + $vars['wgUserRegistration'] = $userReg ? (int)wfTimestamp( TS_UNIX, $userReg ) * 1000 : null; // Get the revision ID of the oldest new message on the user's talk // page. This can be used for constructing new message alerts on // the client side. @@ -3302,12 +3295,10 @@ class OutputPage extends ContextSource { $vars['wgUserVariant'] = $contLang->getPreferredVariant(); } // Same test as SkinTemplate - $vars['wgIsProbablyEditable'] = $title->quickUserCan( 'edit', $user ) - && ( $title->exists() || $title->quickUserCan( 'create', $user ) ); + $vars['wgIsProbablyEditable'] = $this->userCanEditOrCreate( $user, $title ); - $vars['wgRelevantPageIsProbablyEditable'] = $relevantTitle - && $relevantTitle->quickUserCan( 'edit', $user ) - && ( $relevantTitle->exists() || $relevantTitle->quickUserCan( 'create', $user ) ); + $vars['wgRelevantPageIsProbablyEditable'] = $relevantTitle && + $this->userCanEditOrCreate( $user, $relevantTitle ); foreach ( $title->getRestrictionTypes() as $type ) { // Following keys are set in $vars: @@ -3374,6 +3365,21 @@ class OutputPage extends ContextSource { return true; } + /** + * @param User $user + * @param LinkTarget $title + * @return bool + */ + private function userCanEditOrCreate( + User $user, + LinkTarget $title + ) { + $pm = MediaWikiServices::getInstance()->getPermissionManager(); + return $pm->quickUserCan( 'edit', $user, $title ) + && ( $this->getTitle()->exists() || + $pm->quickUserCan( 'create', $user, $title ) ); + } + /** * @return array Array in format "link name or number => 'link html'". */ @@ -3438,11 +3444,7 @@ class OutputPage extends ContextSource { # Universal edit button if ( $config->get( 'UniversalEditButton' ) && $this->isArticleRelated() ) { - $user = $this->getUser(); - if ( $this->getTitle()->quickUserCan( 'edit', $user ) - && ( $this->getTitle()->exists() || - $this->getTitle()->quickUserCan( 'create', $user ) ) - ) { + if ( $this->userCanEditOrCreate( $this->getUser(), $this->getTitle() ) ) { // Original UniversalEditButton $msg = $this->msg( 'edit' )->text(); $tags['universal-edit-button'] = Html::element( 'link', [ diff --git a/includes/PHPVersionCheck.php b/includes/PHPVersionCheck.php index b63a84d807..8d642e1785 100644 --- a/includes/PHPVersionCheck.php +++ b/includes/PHPVersionCheck.php @@ -31,8 +31,6 @@ * * @note This class uses setter methods instead of a constructor so that * it can be compatible with PHP 4, PHP 5 and PHP 7 (without warnings). - * - * @class */ class PHPVersionCheck { /* @var string The number of the MediaWiki version used. */ @@ -79,8 +77,8 @@ class PHPVersionCheck { /** * Return the version of the installed PHP implementation. * - * @param string $impl By default, the function returns the info of the currently installed PHP - * implementation. Using this parameter the caller can decide, what version info will be + * @param string|false $impl By default, the function returns the info of the currently installed + * PHP implementation. Using this parameter the caller can decide, what version info will be * returned. Valid values: HHVM, PHP * @return array An array of information about the PHP implementation, containing: * - 'version': The version of the PHP implementation (specific to the implementation, not @@ -110,8 +108,8 @@ class PHPVersionCheck { 'implementation' => 'PHP', 'version' => PHP_VERSION, 'vendor' => 'the PHP Group', - 'upstreamSupported' => '5.6.0', - 'minSupported' => '7.0.13', + 'upstreamSupported' => '7.1.0', + 'minSupported' => '7.2.0', 'upgradeURL' => 'https://www.php.net/downloads.php', ); } @@ -260,7 +258,7 @@ HTML; <head> <meta charset="UTF-8" /> <title>MediaWiki {$this->mwVersion} - - The MediaWiki logo + The MediaWiki logo

MediaWiki {$this->mwVersion} internal error

-
+

{$shortHtml}

diff --git a/includes/PathRouter.php b/includes/PathRouter.php index 2882e6632e..4d7bd38149 100644 --- a/includes/PathRouter.php +++ b/includes/PathRouter.php @@ -401,4 +401,22 @@ class PathRouter { return $value; } + + /** + * @internal For use by Title and WebRequest only. + * @param array $actionPaths + * @param string $articlePath + * @return string[]|false + */ + public static function getActionPaths( array $actionPaths, $articlePath ) { + if ( !$actionPaths ) { + return false; + } + // Processing of urls for this feature requires that 'view' is set. + // By default, set it to the pretty article path. + if ( !isset( $actionPaths['view'] ) ) { + $actionPaths['view'] = $articlePath; + } + return $actionPaths; + } } diff --git a/includes/Permissions/PermissionManager.php b/includes/Permissions/PermissionManager.php index a04b29cb34..ef6b8ac5a9 100644 --- a/includes/Permissions/PermissionManager.php +++ b/includes/Permissions/PermissionManager.php @@ -22,6 +22,7 @@ namespace MediaWiki\Permissions; use Action; use Exception; use Hooks; +use MediaWiki\Config\ServiceOptions; use MediaWiki\Linker\LinkTarget; use MediaWiki\Revision\RevisionLookup; use MediaWiki\Revision\RevisionRecord; @@ -54,38 +55,38 @@ class PermissionManager { /** @var string Does cheap and expensive checks, using the master as needed */ const RIGOR_SECURE = 'secure'; + /** + * TODO Make this const when HHVM support is dropped (T192166) + * + * @since 1.34 + * @var array + */ + public static $constructorOptions = [ + 'WhitelistRead', + 'WhitelistReadRegexp', + 'EmailConfirmToEdit', + 'BlockDisablesLogin', + 'GroupPermissions', + 'RevokePermissions', + 'AvailableRights', + 'NamespaceProtection', + 'RestrictionLevels' + ]; + + /** @var ServiceOptions */ + private $options; + /** @var SpecialPageFactory */ private $specialPageFactory; /** @var RevisionLookup */ private $revisionLookup; - /** @var string[] List of pages names anonymous user may see */ - private $whitelistRead; - - /** @var string[] Whitelists publicly readable titles with regular expressions */ - private $whitelistReadRegexp; - - /** @var bool Require users to confirm email address before they can edit */ - private $emailConfirmToEdit; - - /** @var bool If set to true, blocked users will no longer be allowed to log in */ - private $blockDisablesLogin; - /** @var NamespaceInfo */ private $nsInfo; - /** @var string[][] Access rights for groups and users in these groups */ - private $groupPermissions; - - /** @var string[][] Permission keys revoked from users in each group */ - private $revokePermissions; - - /** @var string[] A list of available rights, in addition to the ones defined by the core */ - private $availableRights; - - /** @var string[] Cached results of getAllRights() */ - private $allRights = false; + /** @var string[]|null Cached results of getAllRights() */ + private $allRights; /** @var string[][] Cached user rights */ private $usersRights = null; @@ -96,7 +97,7 @@ class PermissionManager { */ private $temporaryUserRights = []; - /** @var string[] Cached rights for isEveryoneAllowed */ + /** @var bool[] Cached rights for isEveryoneAllowed, [ right => allowed ] */ private $cachedRights = []; /** @@ -189,38 +190,21 @@ class PermissionManager { ]; /** + * @param ServiceOptions $options * @param SpecialPageFactory $specialPageFactory * @param RevisionLookup $revisionLookup - * @param string[] $whitelistRead - * @param string[] $whitelistReadRegexp - * @param bool $emailConfirmToEdit - * @param bool $blockDisablesLogin - * @param string[][] $groupPermissions - * @param string[][] $revokePermissions - * @param string[] $availableRights * @param NamespaceInfo $nsInfo */ public function __construct( + ServiceOptions $options, SpecialPageFactory $specialPageFactory, RevisionLookup $revisionLookup, - $whitelistRead, - $whitelistReadRegexp, - $emailConfirmToEdit, - $blockDisablesLogin, - $groupPermissions, - $revokePermissions, - $availableRights, NamespaceInfo $nsInfo ) { + $options->assertRequiredOptions( self::$constructorOptions ); + $this->options = $options; $this->specialPageFactory = $specialPageFactory; $this->revisionLookup = $revisionLookup; - $this->whitelistRead = $whitelistRead; - $this->whitelistReadRegexp = $whitelistReadRegexp; - $this->emailConfirmToEdit = $emailConfirmToEdit; - $this->blockDisablesLogin = $blockDisablesLogin; - $this->groupPermissions = $groupPermissions; - $this->revokePermissions = $revokePermissions; - $this->availableRights = $availableRights; $this->nsInfo = $nsInfo; } @@ -247,6 +231,25 @@ class PermissionManager { return !count( $this->getPermissionErrorsInternal( $action, $user, $page, $rigor, true ) ); } + /** + * A convenience method for calling PermissionManager::userCan + * with PermissionManager::RIGOR_QUICK + * + * Suitable for use for nonessential UI controls in common cases, but + * _not_ for functional access control. + * May provide false positives, but should never provide a false negative. + * + * @see PermissionManager::userCan() + * + * @param string $action + * @param User $user + * @param LinkTarget $page + * @return bool + */ + public function quickUserCan( $action, User $user, LinkTarget $page ) { + return $this->userCan( $action, $user, $page, self::RIGOR_QUICK ); + } + /** * Can $user perform $action on a page? * @@ -289,7 +292,8 @@ class PermissionManager { } /** - * Check if user is blocked from editing a particular article + * Check if user is blocked from editing a particular article. If the user does not + * have a block, this will return false. * * @param User $user * @param LinkTarget $page Title to check @@ -298,28 +302,30 @@ class PermissionManager { * @return bool */ public function isBlockedFrom( User $user, LinkTarget $page, $fromReplica = false ) { - $blocked = $user->isHidden(); + $block = $user->getBlock( $fromReplica ); + if ( !$block ) { + return false; + } // TODO: remove upon further migration to LinkTarget - $page = Title::newFromLinkTarget( $page ); + $title = Title::newFromLinkTarget( $page ); + $blocked = $user->isHidden(); if ( !$blocked ) { - $block = $user->getBlock( $fromReplica ); - if ( $block ) { - // Special handling for a user's own talk page. The block is not aware - // of the user, so this must be done here. - if ( $page->equals( $user->getTalkPage() ) ) { - $blocked = $block->appliesToUsertalk( $page ); - } else { - $blocked = $block->appliesToTitle( $page ); - } + // Special handling for a user's own talk page. The block is not aware + // of the user, so this must be done here. + if ( $title->equals( $user->getTalkPage() ) ) { + $blocked = $block->appliesToUsertalk( $title ); + } else { + $blocked = $block->appliesToTitle( $title ); } } // only for the purpose of the hook. We really don't need this here. $allowUsertalk = $user->isAllowUsertalk(); - Hooks::run( 'UserIsBlockedFrom', [ $user, $page, &$blocked, &$allowUsertalk ] ); + // Allow extensions to let a blocked user access a particular page + Hooks::run( 'UserIsBlockedFrom', [ $user, $title, &$blocked, &$allowUsertalk ] ); return $blocked; } @@ -423,21 +429,21 @@ class PermissionManager { LinkTarget $page ) { // TODO: remove when LinkTarget usage will expand further - $page = Title::newFromLinkTarget( $page ); + $title = Title::newFromLinkTarget( $page ); // Use getUserPermissionsErrors instead $result = ''; - if ( !Hooks::run( 'userCan', [ &$page, &$user, $action, &$result ] ) ) { + if ( !Hooks::run( 'userCan', [ &$title, &$user, $action, &$result ] ) ) { return $result ? [] : [ [ 'badaccess-group0' ] ]; } // Check getUserPermissionsErrors hook - if ( !Hooks::run( 'getUserPermissionsErrors', [ &$page, &$user, $action, &$result ] ) ) { + if ( !Hooks::run( 'getUserPermissionsErrors', [ &$title, &$user, $action, &$result ] ) ) { $errors = $this->resultToError( $errors, $result ); } // Check getUserPermissionsErrorsExpensive hook if ( $rigor !== self::RIGOR_QUICK && !( $short && count( $errors ) > 0 ) - && !Hooks::run( 'getUserPermissionsErrorsExpensive', [ &$page, &$user, $action, &$result ] ) + && !Hooks::run( 'getUserPermissionsErrorsExpensive', [ &$title, &$user, $action, &$result ] ) ) { $errors = $this->resultToError( $errors, $result ); } @@ -498,57 +504,59 @@ class PermissionManager { LinkTarget $page ) { // TODO: remove when LinkTarget usage will expand further - $page = Title::newFromLinkTarget( $page ); + $title = Title::newFromLinkTarget( $page ); + $whiteListRead = $this->options->get( 'WhitelistRead' ); $whitelisted = false; - if ( User::isEveryoneAllowed( 'read' ) ) { + if ( $this->isEveryoneAllowed( 'read' ) ) { # Shortcut for public wikis, allows skipping quite a bit of code $whitelisted = true; - } elseif ( $user->isAllowed( 'read' ) ) { + } elseif ( $this->userHasRight( $user, 'read' ) ) { # If the user is allowed to read pages, he is allowed to read all pages $whitelisted = true; - } elseif ( $this->isSameSpecialPage( 'Userlogin', $page ) - || $this->isSameSpecialPage( 'PasswordReset', $page ) - || $this->isSameSpecialPage( 'Userlogout', $page ) + } elseif ( $this->isSameSpecialPage( 'Userlogin', $title ) + || $this->isSameSpecialPage( 'PasswordReset', $title ) + || $this->isSameSpecialPage( 'Userlogout', $title ) ) { # Always grant access to the login page. # Even anons need to be able to log in. $whitelisted = true; - } elseif ( is_array( $this->whitelistRead ) && count( $this->whitelistRead ) ) { + } elseif ( is_array( $whiteListRead ) && count( $whiteListRead ) ) { # Time to check the whitelist # Only do these checks is there's something to check against - $name = $page->getPrefixedText(); - $dbName = $page->getPrefixedDBkey(); + $name = $title->getPrefixedText(); + $dbName = $title->getPrefixedDBkey(); // Check for explicit whitelisting with and without underscores - if ( in_array( $name, $this->whitelistRead, true ) - || in_array( $dbName, $this->whitelistRead, true ) ) { + if ( in_array( $name, $whiteListRead, true ) + || in_array( $dbName, $whiteListRead, true ) ) { $whitelisted = true; - } elseif ( $page->getNamespace() == NS_MAIN ) { + } elseif ( $title->getNamespace() == NS_MAIN ) { # Old settings might have the title prefixed with # a colon for main-namespace pages - if ( in_array( ':' . $name, $this->whitelistRead ) ) { + if ( in_array( ':' . $name, $whiteListRead ) ) { $whitelisted = true; } - } elseif ( $page->isSpecialPage() ) { + } elseif ( $title->isSpecialPage() ) { # If it's a special page, ditch the subpage bit and check again - $name = $page->getDBkey(); + $name = $title->getDBkey(); list( $name, /* $subpage */ ) = $this->specialPageFactory->resolveAlias( $name ); if ( $name ) { $pure = SpecialPage::getTitleFor( $name )->getPrefixedText(); - if ( in_array( $pure, $this->whitelistRead, true ) ) { + if ( in_array( $pure, $whiteListRead, true ) ) { $whitelisted = true; } } } } - if ( !$whitelisted && is_array( $this->whitelistReadRegexp ) - && !empty( $this->whitelistReadRegexp ) ) { - $name = $page->getPrefixedText(); + $whitelistReadRegexp = $this->options->get( 'WhitelistReadRegexp' ); + if ( !$whitelisted && is_array( $whitelistReadRegexp ) + && !empty( $whitelistReadRegexp ) ) { + $name = $title->getPrefixedText(); // Check for regex whitelisting - foreach ( $this->whitelistReadRegexp as $listItem ) { + foreach ( $whitelistReadRegexp as $listItem ) { if ( preg_match( $listItem, $name ) ) { $whitelisted = true; break; @@ -558,7 +566,7 @@ class PermissionManager { if ( !$whitelisted ) { # If the title is not whitelisted, give extensions a chance to do so... - Hooks::run( 'TitleReadWhitelist', [ $page, $user, &$whitelisted ] ); + Hooks::run( 'TitleReadWhitelist', [ $title, $user, &$whitelisted ] ); if ( !$whitelisted ) { $errors[] = $this->missingPermissionError( $action, $short ); } @@ -636,11 +644,11 @@ class PermissionManager { } // Optimize for a very common case - if ( $action === 'read' && !$this->blockDisablesLogin ) { + if ( $action === 'read' && !$this->options->get( 'BlockDisablesLogin' ) ) { return $errors; } - if ( $this->emailConfirmToEdit + if ( $this->options->get( 'EmailConfirmToEdit' ) && !$user->isEmailConfirmed() && $action === 'edit' ) { @@ -715,47 +723,49 @@ class PermissionManager { LinkTarget $page ) { // TODO: remove when LinkTarget usage will expand further - $page = Title::newFromLinkTarget( $page ); + $title = Title::newFromLinkTarget( $page ); if ( !Hooks::run( 'TitleQuickPermissions', - [ $page, $user, $action, &$errors, ( $rigor !== self::RIGOR_QUICK ), $short ] ) + [ $title, $user, $action, &$errors, ( $rigor !== self::RIGOR_QUICK ), $short ] ) ) { return $errors; } - $isSubPage = $this->nsInfo->hasSubpages( $page->getNamespace() ) ? - strpos( $page->getText(), '/' ) !== false : false; + $isSubPage = $this->nsInfo->hasSubpages( $title->getNamespace() ) ? + strpos( $title->getText(), '/' ) !== false : false; if ( $action == 'create' ) { if ( - ( $this->nsInfo->isTalk( $page->getNamespace() ) && - !$user->isAllowed( 'createtalk' ) ) || - ( !$this->nsInfo->isTalk( $page->getNamespace() ) && - !$user->isAllowed( 'createpage' ) ) + ( $this->nsInfo->isTalk( $title->getNamespace() ) && + !$this->userHasRight( $user, 'createtalk' ) ) || + ( !$this->nsInfo->isTalk( $title->getNamespace() ) && + !$this->userHasRight( $user, 'createpage' ) ) ) { $errors[] = $user->isAnon() ? [ 'nocreatetext' ] : [ 'nocreate-loggedin' ]; } } elseif ( $action == 'move' ) { - if ( !$user->isAllowed( 'move-rootuserpages' ) - && $page->getNamespace() == NS_USER && !$isSubPage ) { + if ( !$this->userHasRight( $user, 'move-rootuserpages' ) + && $title->getNamespace() == NS_USER && !$isSubPage ) { // Show user page-specific message only if the user can move other pages $errors[] = [ 'cant-move-user-page' ]; } // Check if user is allowed to move files if it's a file - if ( $page->getNamespace() == NS_FILE && !$user->isAllowed( 'movefile' ) ) { + if ( $title->getNamespace() == NS_FILE && + !$this->userHasRight( $user, 'movefile' ) ) { $errors[] = [ 'movenotallowedfile' ]; } // Check if user is allowed to move category pages if it's a category page - if ( $page->getNamespace() == NS_CATEGORY && !$user->isAllowed( 'move-categorypages' ) ) { + if ( $title->getNamespace() == NS_CATEGORY && + !$this->userHasRight( $user, 'move-categorypages' ) ) { $errors[] = [ 'cant-move-category-page' ]; } - if ( !$user->isAllowed( 'move' ) ) { + if ( !$this->userHasRight( $user, 'move' ) ) { // User can't move anything - $userCanMove = User::groupHasPermission( 'user', 'move' ); - $autoconfirmedCanMove = User::groupHasPermission( 'autoconfirmed', 'move' ); + $userCanMove = $this->groupHasPermission( 'user', 'move' ); + $autoconfirmedCanMove = $this->groupHasPermission( 'autoconfirmed', 'move' ); if ( $user->isAnon() && ( $userCanMove || $autoconfirmedCanMove ) ) { // custom message if logged-in users without any special rights can move $errors[] = [ 'movenologintext' ]; @@ -764,19 +774,19 @@ class PermissionManager { } } } elseif ( $action == 'move-target' ) { - if ( !$user->isAllowed( 'move' ) ) { + if ( !$this->userHasRight( $user, 'move' ) ) { // User can't move anything $errors[] = [ 'movenotallowed' ]; - } elseif ( !$user->isAllowed( 'move-rootuserpages' ) - && $page->getNamespace() == NS_USER && !$isSubPage ) { + } elseif ( !$this->userHasRight( $user, 'move-rootuserpages' ) + && $title->getNamespace() == NS_USER && !$isSubPage ) { // Show user page-specific message only if the user can move other pages $errors[] = [ 'cant-move-to-user-page' ]; - } elseif ( !$user->isAllowed( 'move-categorypages' ) - && $page->getNamespace() == NS_CATEGORY ) { + } elseif ( !$this->userHasRight( $user, 'move-categorypages' ) + && $title->getNamespace() == NS_CATEGORY ) { // Show category page-specific message only if the user can move other pages $errors[] = [ 'cant-move-to-category-page' ]; } - } elseif ( !$user->isAllowed( $action ) ) { + } elseif ( !$this->userHasRight( $user, $action ) ) { $errors[] = $this->missingPermissionError( $action, $short ); } @@ -810,8 +820,8 @@ class PermissionManager { LinkTarget $page ) { // TODO: remove & rework upon further use of LinkTarget - $page = Title::newFromLinkTarget( $page ); - foreach ( $page->getRestrictions( $action ) as $right ) { + $title = Title::newFromLinkTarget( $page ); + foreach ( $title->getRestrictions( $action ) as $right ) { // Backwards compatibility, rewrite sysop -> editprotected if ( $right == 'sysop' ) { $right = 'editprotected'; @@ -823,9 +833,10 @@ class PermissionManager { if ( $right == '' ) { continue; } - if ( !$user->isAllowed( $right ) ) { + if ( !$this->userHasRight( $user, $right ) ) { $errors[] = [ 'protectedpagetext', $right, $action ]; - } elseif ( $page->areRestrictionsCascading() && !$user->isAllowed( 'protect' ) ) { + } elseif ( $title->areRestrictionsCascading() && + !$this->userHasRight( $user, 'protect' ) ) { $errors[] = [ 'protectedpagetext', 'protect', $action ]; } } @@ -837,7 +848,7 @@ class PermissionManager { * Check restrictions on cascading pages. * * @param string $action The action to check - * @param User $user User to check + * @param UserIdentity $user User to check * @param array $errors List of current errors * @param string $rigor One of PermissionManager::RIGOR_ constants * - RIGOR_QUICK : does cheap permission checks from replica DBs (usable for GUI creation) @@ -851,21 +862,21 @@ class PermissionManager { */ private function checkCascadingSourcesRestrictions( $action, - User $user, + UserIdentity $user, $errors, $rigor, $short, LinkTarget $page ) { // TODO: remove & rework upon further use of LinkTarget - $page = Title::newFromLinkTarget( $page ); - if ( $rigor !== self::RIGOR_QUICK && !$page->isUserConfigPage() ) { + $title = Title::newFromLinkTarget( $page ); + if ( $rigor !== self::RIGOR_QUICK && !$title->isUserConfigPage() ) { # We /could/ use the protection level on the source page, but it's # fairly ugly as we have to establish a precedence hierarchy for pages # included by multiple cascade-protected pages. So just restrict # it to people with 'protect' permission, as they could remove the # protection anyway. - list( $cascadingSources, $restrictions ) = $page->getCascadeProtectionSources(); + list( $cascadingSources, $restrictions ) = $title->getCascadeProtectionSources(); # Cascading protection depends on more than this page... # Several cascading protected pages may include this page... # Check each cascading level @@ -880,7 +891,7 @@ class PermissionManager { if ( $right == 'autoconfirmed' ) { $right = 'editsemiprotected'; } - if ( $right != '' && !$user->isAllowedAll( 'protect', $right ) ) { + if ( $right != '' && !$this->userHasAllRights( $user, 'protect', $right ) ) { $wikiPages = ''; /** @var Title $wikiPage */ foreach ( $cascadingSources as $wikiPage ) { @@ -922,18 +933,18 @@ class PermissionManager { global $wgDeleteRevisionsLimit, $wgLang; // TODO: remove & rework upon further use of LinkTarget - $page = Title::newFromLinkTarget( $page ); + $title = Title::newFromLinkTarget( $page ); if ( $action == 'protect' ) { - if ( count( $this->getPermissionErrorsInternal( 'edit', $user, $page, $rigor, true ) ) ) { + if ( count( $this->getPermissionErrorsInternal( 'edit', $user, $title, $rigor, true ) ) ) { // If they can't edit, they shouldn't protect. $errors[] = [ 'protect-cantedit' ]; } } elseif ( $action == 'create' ) { - $title_protection = $page->getTitleProtection(); + $title_protection = $title->getTitleProtection(); if ( $title_protection ) { if ( $title_protection['permission'] == '' - || !$user->isAllowed( $title_protection['permission'] ) + || !$this->userHasRight( $user, $title_protection['permission'] ) ) { $errors[] = [ 'titleprotected', @@ -945,41 +956,41 @@ class PermissionManager { } } elseif ( $action == 'move' ) { // Check for immobile pages - if ( !$this->nsInfo->isMovable( $page->getNamespace() ) ) { + if ( !$this->nsInfo->isMovable( $title->getNamespace() ) ) { // Specific message for this case - $errors[] = [ 'immobile-source-namespace', $page->getNsText() ]; - } elseif ( !$page->isMovable() ) { + $errors[] = [ 'immobile-source-namespace', $title->getNsText() ]; + } elseif ( !$title->isMovable() ) { // Less specific message for rarer cases $errors[] = [ 'immobile-source-page' ]; } } elseif ( $action == 'move-target' ) { - if ( !$this->nsInfo->isMovable( $page->getNamespace() ) ) { - $errors[] = [ 'immobile-target-namespace', $page->getNsText() ]; - } elseif ( !$page->isMovable() ) { + if ( !$this->nsInfo->isMovable( $title->getNamespace() ) ) { + $errors[] = [ 'immobile-target-namespace', $title->getNsText() ]; + } elseif ( !$title->isMovable() ) { $errors[] = [ 'immobile-target-page' ]; } } elseif ( $action == 'delete' ) { - $tempErrors = $this->checkPageRestrictions( 'edit', $user, [], $rigor, true, $page ); + $tempErrors = $this->checkPageRestrictions( 'edit', $user, [], $rigor, true, $title ); if ( !$tempErrors ) { $tempErrors = $this->checkCascadingSourcesRestrictions( 'edit', - $user, $tempErrors, $rigor, true, $page ); + $user, $tempErrors, $rigor, true, $title ); } if ( $tempErrors ) { // If protection keeps them from editing, they shouldn't be able to delete. $errors[] = [ 'deleteprotected' ]; } if ( $rigor !== self::RIGOR_QUICK && $wgDeleteRevisionsLimit - && !$this->userCan( 'bigdelete', $user, $page ) && $page->isBigDeletion() + && !$this->userCan( 'bigdelete', $user, $title ) && $title->isBigDeletion() ) { $errors[] = [ 'delete-toobig', $wgLang->formatNum( $wgDeleteRevisionsLimit ) ]; } } elseif ( $action === 'undelete' ) { - if ( count( $this->getPermissionErrorsInternal( 'edit', $user, $page, $rigor, true ) ) ) { + if ( count( $this->getPermissionErrorsInternal( 'edit', $user, $title, $rigor, true ) ) ) { // Undeleting implies editing $errors[] = [ 'undelete-cantedit' ]; } - if ( !$page->exists() - && count( $this->getPermissionErrorsInternal( 'create', $user, $page, $rigor, true ) ) + if ( !$title->exists() + && count( $this->getPermissionErrorsInternal( 'create', $user, $title, $rigor, true ) ) ) { // Undeleting where nothing currently exists implies creating $errors[] = [ 'undelete-cantcreate' ]; @@ -992,7 +1003,7 @@ class PermissionManager { * Check permissions on special pages & namespaces * * @param string $action The action to check - * @param User $user User to check + * @param UserIdentity $user User to check * @param array $errors List of current errors * @param string $rigor One of PermissionManager::RIGOR_ constants * - RIGOR_QUICK : does cheap permission checks from replica DBs (usable for GUI creation) @@ -1006,26 +1017,26 @@ class PermissionManager { */ private function checkSpecialsAndNSPermissions( $action, - User $user, + UserIdentity $user, $errors, $rigor, $short, LinkTarget $page ) { // TODO: remove & rework upon further use of LinkTarget - $page = Title::newFromLinkTarget( $page ); + $title = Title::newFromLinkTarget( $page ); # Only 'createaccount' can be performed on special pages, # which don't actually exist in the DB. - if ( $page->getNamespace() == NS_SPECIAL && $action !== 'createaccount' ) { + if ( $title->getNamespace() == NS_SPECIAL && $action !== 'createaccount' ) { $errors[] = [ 'ns-specialprotected' ]; } # Check $wgNamespaceProtection for restricted namespaces - if ( $page->isNamespaceProtected( $user ) ) { - $ns = $page->getNamespace() == NS_MAIN ? - wfMessage( 'nstab-main' )->text() : $page->getNsText(); - $errors[] = $page->getNamespace() == NS_MEDIAWIKI ? + if ( $this->isNamespaceProtected( $title->getNamespace(), $user ) ) { + $ns = $title->getNamespace() == NS_MAIN ? + wfMessage( 'nstab-main' )->text() : $title->getNsText(); + $errors[] = $title->getNamespace() == NS_MEDIAWIKI ? [ 'protectedinterface', $action ] : [ 'namespaceprotected', $ns, $action ]; } @@ -1057,29 +1068,29 @@ class PermissionManager { LinkTarget $page ) { // TODO: remove & rework upon further use of LinkTarget - $page = Title::newFromLinkTarget( $page ); + $title = Title::newFromLinkTarget( $page ); if ( $action != 'patrol' ) { $error = null; // Sitewide CSS/JSON/JS changes, like all NS_MEDIAWIKI changes, also require the // editinterface right. That's implemented as a restriction so no check needed here. - if ( $page->isSiteCssConfigPage() && !$user->isAllowed( 'editsitecss' ) ) { + if ( $title->isSiteCssConfigPage() && !$this->userHasRight( $user, 'editsitecss' ) ) { $error = [ 'sitecssprotected', $action ]; - } elseif ( $page->isSiteJsonConfigPage() && !$user->isAllowed( 'editsitejson' ) ) { + } elseif ( $title->isSiteJsonConfigPage() && !$this->userHasRight( $user, 'editsitejson' ) ) { $error = [ 'sitejsonprotected', $action ]; - } elseif ( $page->isSiteJsConfigPage() && !$user->isAllowed( 'editsitejs' ) ) { + } elseif ( $title->isSiteJsConfigPage() && !$this->userHasRight( $user, 'editsitejs' ) ) { $error = [ 'sitejsprotected', $action ]; - } elseif ( $page->isRawHtmlMessage() ) { + } elseif ( $title->isRawHtmlMessage() ) { // Raw HTML can be used to deploy CSS or JS so require rights for both. - if ( !$user->isAllowed( 'editsitejs' ) ) { + if ( !$this->userHasRight( $user, 'editsitejs' ) ) { $error = [ 'sitejsprotected', $action ]; - } elseif ( !$user->isAllowed( 'editsitecss' ) ) { + } elseif ( !$this->userHasRight( $user, 'editsitecss' ) ) { $error = [ 'sitecssprotected', $action ]; } } if ( $error ) { - if ( $user->isAllowed( 'editinterface' ) ) { + if ( $this->userHasRight( $user, 'editinterface' ) ) { // Most users / site admins will probably find out about the new, more restrictive // permissions by failing to edit something. Give them more info. // TODO remove this a few release cycles after 1.32 @@ -1096,7 +1107,7 @@ class PermissionManager { * Check CSS/JSON/JS sub-page permissions * * @param string $action The action to check - * @param User $user User to check + * @param UserIdentity $user User to check * @param array $errors List of current errors * @param string $rigor One of PermissionManager::RIGOR_ constants * - RIGOR_QUICK : does cheap permission checks from replica DBs (usable for GUI creation) @@ -1110,14 +1121,14 @@ class PermissionManager { */ private function checkUserConfigPermissions( $action, - User $user, + UserIdentity $user, $errors, $rigor, $short, LinkTarget $page ) { // TODO: remove & rework upon further use of LinkTarget - $page = Title::newFromLinkTarget( $page ); + $title = Title::newFromLinkTarget( $page ); # Protect css/json/js subpages of user pages # XXX: this might be better using restrictions @@ -1126,29 +1137,29 @@ class PermissionManager { return $errors; } - if ( preg_match( '/^' . preg_quote( $user->getName(), '/' ) . '\//', $page->getText() ) ) { + if ( preg_match( '/^' . preg_quote( $user->getName(), '/' ) . '\//', $title->getText() ) ) { // Users need editmyuser* to edit their own CSS/JSON/JS subpages. if ( - $page->isUserCssConfigPage() - && !$user->isAllowedAny( 'editmyusercss', 'editusercss' ) + $title->isUserCssConfigPage() + && !$this->userHasAnyRight( $user, 'editmyusercss', 'editusercss' ) ) { $errors[] = [ 'mycustomcssprotected', $action ]; } elseif ( - $page->isUserJsonConfigPage() - && !$user->isAllowedAny( 'editmyuserjson', 'edituserjson' ) + $title->isUserJsonConfigPage() + && !$this->userHasAnyRight( $user, 'editmyuserjson', 'edituserjson' ) ) { $errors[] = [ 'mycustomjsonprotected', $action ]; } elseif ( - $page->isUserJsConfigPage() - && !$user->isAllowedAny( 'editmyuserjs', 'edituserjs' ) + $title->isUserJsConfigPage() + && !$this->userHasAnyRight( $user, 'editmyuserjs', 'edituserjs' ) ) { $errors[] = [ 'mycustomjsprotected', $action ]; } elseif ( - $page->isUserJsConfigPage() - && !$user->isAllowedAny( 'edituserjs', 'editmyuserjsredirect' ) + $title->isUserJsConfigPage() + && !$this->userHasAnyRight( $user, 'edituserjs', 'editmyuserjsredirect' ) ) { // T207750 - do not allow users to edit a redirect if they couldn't edit the target - $rev = $this->revisionLookup->getRevisionByTitle( $page ); + $rev = $this->revisionLookup->getRevisionByTitle( $title ); $content = $rev ? $rev->getContent( 'main', RevisionRecord::RAW ) : null; $target = $content ? $content->getUltimateRedirectTarget() : null; if ( $target && ( @@ -1165,18 +1176,18 @@ class PermissionManager { // and only very highly privileged users could remove it. if ( !in_array( $action, [ 'delete', 'deleterevision', 'suppressrevision' ], true ) ) { if ( - $page->isUserCssConfigPage() - && !$user->isAllowed( 'editusercss' ) + $title->isUserCssConfigPage() + && !$this->userHasRight( $user, 'editusercss' ) ) { $errors[] = [ 'customcssprotected', $action ]; } elseif ( - $page->isUserJsonConfigPage() - && !$user->isAllowed( 'edituserjson' ) + $title->isUserJsonConfigPage() + && !$this->userHasRight( $user, 'edituserjson' ) ) { $errors[] = [ 'customjsonprotected', $action ]; } elseif ( - $page->isUserJsConfigPage() - && !$user->isAllowed( 'edituserjs' ) + $title->isUserJsConfigPage() + && !$this->userHasRight( $user, 'edituserjs' ) ) { $errors[] = [ 'customjsprotected', $action ]; } @@ -1205,6 +1216,44 @@ class PermissionManager { return in_array( $action, $this->getUserPermissions( $user ), true ); } + /** + * Check if user is allowed to make any action + * + * @param UserIdentity $user + * // TODO: HHVM bug T228695#5450847 @param string ...$actions + * @suppress PhanCommentParamWithoutRealParam + * @return bool True if user is allowed to perform *any* of the given actions + * @since 1.34 + */ + public function userHasAnyRight( UserIdentity $user ) { + $actions = array_slice( func_get_args(), 1 ); + foreach ( $actions as $action ) { + if ( $this->userHasRight( $user, $action ) ) { + return true; + } + } + return false; + } + + /** + * Check if user is allowed to make all actions + * + * @param UserIdentity $user + * // TODO: HHVM bug T228695#5450847 @param string ...$actions + * @suppress PhanCommentParamWithoutRealParam + * @return bool True if user is allowed to perform *all* of the given actions + * @since 1.34 + */ + public function userHasAllRights( UserIdentity $user ) { + $actions = array_slice( func_get_args(), 1 ); + foreach ( $actions as $action ) { + if ( !$this->userHasRight( $user, $action ) ) { + return false; + } + } + return true; + } + /** * Get the permissions this user has. * @@ -1216,11 +1265,12 @@ class PermissionManager { */ public function getUserPermissions( UserIdentity $user ) { $user = User::newFromIdentity( $user ); - if ( !isset( $this->usersRights[ $user->getId() ] ) ) { - $this->usersRights[ $user->getId() ] = $this->getGroupPermissions( + $rightsCacheKey = $this->getRightsCacheKey( $user ); + if ( !isset( $this->usersRights[ $rightsCacheKey ] ) ) { + $this->usersRights[ $rightsCacheKey ] = $this->getGroupPermissions( $user->getEffectiveGroups() ); - Hooks::run( 'UserGetRights', [ $user, &$this->usersRights[ $user->getId() ] ] ); + Hooks::run( 'UserGetRights', [ $user, &$this->usersRights[ $rightsCacheKey ] ] ); // Deny any rights denied by the user's session, unless this // endpoint has no sessions. @@ -1228,32 +1278,32 @@ class PermissionManager { // FIXME: $user->getRequest().. need to be replaced with something else $allowedRights = $user->getRequest()->getSession()->getAllowedUserRights(); if ( $allowedRights !== null ) { - $this->usersRights[ $user->getId() ] = array_intersect( - $this->usersRights[ $user->getId() ], + $this->usersRights[ $rightsCacheKey ] = array_intersect( + $this->usersRights[ $rightsCacheKey ], $allowedRights ); } } - Hooks::run( 'UserGetRightsRemove', [ $user, &$this->usersRights[ $user->getId() ] ] ); + Hooks::run( 'UserGetRightsRemove', [ $user, &$this->usersRights[ $rightsCacheKey ] ] ); // Force reindexation of rights when a hook has unset one of them - $this->usersRights[ $user->getId() ] = array_values( - array_unique( $this->usersRights[ $user->getId() ] ) + $this->usersRights[ $rightsCacheKey ] = array_values( + array_unique( $this->usersRights[ $rightsCacheKey ] ) ); if ( $user->isLoggedIn() && - $this->blockDisablesLogin && + $this->options->get( 'BlockDisablesLogin' ) && $user->getBlock() ) { $anon = new User; - $this->usersRights[ $user->getId() ] = array_intersect( - $this->usersRights[ $user->getId() ], + $this->usersRights[ $rightsCacheKey ] = array_intersect( + $this->usersRights[ $rightsCacheKey ], $this->getUserPermissions( $anon ) ); } } - $rights = $this->usersRights[ $user->getId() ]; + $rights = $this->usersRights[ $rightsCacheKey ]; foreach ( $this->temporaryUserRights[ $user->getId() ] ?? [] as $overrides ) { $rights = array_values( array_unique( array_merge( $rights, $overrides ) ) ); } @@ -1270,14 +1320,24 @@ class PermissionManager { */ public function invalidateUsersRightsCache( $user = null ) { if ( $user !== null ) { - if ( isset( $this->usersRights[ $user->getId() ] ) ) { - unset( $this->usersRights[$user->getId()] ); + $rightsCacheKey = $this->getRightsCacheKey( $user ); + if ( isset( $this->usersRights[ $rightsCacheKey ] ) ) { + unset( $this->usersRights[ $rightsCacheKey ] ); } } else { $this->usersRights = null; } } + /** + * Gets a unique key for user rights cache. + * @param UserIdentity $user + * @return string + */ + private function getRightsCacheKey( UserIdentity $user ) { + return $user->isRegistered() ? "u:{$user->getId()}" : "anon:{$user->getName()}"; + } + /** * Check, if the given group has the given permission * @@ -1293,10 +1353,10 @@ class PermissionManager { * @return bool */ public function groupHasPermission( $group, $role ) { - return isset( $this->groupPermissions[$group][$role] ) && - $this->groupPermissions[$group][$role] && - !( isset( $this->revokePermissions[$group][$role] ) && - $this->revokePermissions[$group][$role] ); + $groupPermissions = $this->options->get( 'GroupPermissions' ); + $revokePermissions = $this->options->get( 'RevokePermissions' ); + return isset( $groupPermissions[$group][$role] ) && $groupPermissions[$group][$role] && + !( isset( $revokePermissions[$group][$role] ) && $revokePermissions[$group][$role] ); } /** @@ -1311,17 +1371,17 @@ class PermissionManager { $rights = []; // grant every granted permission first foreach ( $groups as $group ) { - if ( isset( $this->groupPermissions[$group] ) ) { + if ( isset( $this->options->get( 'GroupPermissions' )[$group] ) ) { $rights = array_merge( $rights, // array_filter removes empty items - array_keys( array_filter( $this->groupPermissions[$group] ) ) ); + array_keys( array_filter( $this->options->get( 'GroupPermissions' )[$group] ) ) ); } } // now revoke the revoked permissions foreach ( $groups as $group ) { - if ( isset( $this->revokePermissions[$group] ) ) { + if ( isset( $this->options->get( 'RevokePermissions' )[$group] ) ) { $rights = array_diff( $rights, - array_keys( array_filter( $this->revokePermissions[$group] ) ) ); + array_keys( array_filter( $this->options->get( 'RevokePermissions' )[$group] ) ) ); } } return array_unique( $rights ); @@ -1337,7 +1397,7 @@ class PermissionManager { */ public function getGroupsWithPermission( $role ) { $allowedGroups = []; - foreach ( array_keys( $this->groupPermissions ) as $group ) { + foreach ( array_keys( $this->options->get( 'GroupPermissions' ) ) as $group ) { if ( $this->groupHasPermission( $group, $role ) ) { $allowedGroups[] = $group; } @@ -1367,14 +1427,14 @@ class PermissionManager { return $this->cachedRights[$right]; } - if ( !isset( $this->groupPermissions['*'][$right] ) - || !$this->groupPermissions['*'][$right] ) { + if ( !isset( $this->options->get( 'GroupPermissions' )['*'][$right] ) + || !$this->options->get( 'GroupPermissions' )['*'][$right] ) { $this->cachedRights[$right] = false; return false; } // If it's revoked anywhere, then everyone doesn't have it - foreach ( $this->revokePermissions as $rights ) { + foreach ( $this->options->get( 'RevokePermissions' ) as $rights ) { if ( isset( $rights[$right] ) && $rights[$right] ) { $this->cachedRights[$right] = false; return false; @@ -1411,11 +1471,11 @@ class PermissionManager { * @return string[] Array of permission names */ public function getAllPermissions() { - if ( $this->allRights === false ) { - if ( count( $this->availableRights ) ) { + if ( $this->allRights === null ) { + if ( count( $this->options->get( 'AvailableRights' ) ) ) { $this->allRights = array_unique( array_merge( $this->coreRights, - $this->availableRights + $this->options->get( 'AvailableRights' ) ) ); } else { $this->allRights = $this->coreRights; @@ -1425,6 +1485,99 @@ class PermissionManager { return $this->allRights; } + /** + * Determines if $user is unable to edit pages in namespace because it has been protected. + * @param $index + * @param UserIdentity $user + * @return bool + */ + private function isNamespaceProtected( $index, UserIdentity $user ) { + $namespaceProtection = $this->options->get( 'NamespaceProtection' ); + if ( isset( $namespaceProtection[$index] ) ) { + return !$this->userHasAllRights( $user, ...(array)$namespaceProtection[$index] ); + } + return false; + } + + /** + * Determine which restriction levels it makes sense to use in a namespace, + * optionally filtered by a user's rights. + * + * @param int $index Index to check + * @param UserIdentity|null $user User to check + * @return array + */ + public function getNamespaceRestrictionLevels( $index, UserIdentity $user = null ) { + if ( !isset( $this->options->get( 'NamespaceProtection' )[$index] ) ) { + // All levels are valid if there's no namespace restriction. + // But still filter by user, if necessary + $levels = $this->options->get( 'RestrictionLevels' ); + if ( $user ) { + $levels = array_values( array_filter( $levels, function ( $level ) use ( $user ) { + $right = $level; + if ( $right == 'sysop' ) { + $right = 'editprotected'; // BC + } + if ( $right == 'autoconfirmed' ) { + $right = 'editsemiprotected'; // BC + } + return $this->userHasRight( $user, $right ); + } ) ); + } + return $levels; + } + + // $wgNamespaceProtection can require one or more rights to edit the namespace, which + // may be satisfied by membership in multiple groups each giving a subset of those rights. + // A restriction level is redundant if, for any one of the namespace rights, all groups + // giving that right also give the restriction level's right. Or, conversely, a + // restriction level is not redundant if, for every namespace right, there's at least one + // group giving that right without the restriction level's right. + // + // First, for each right, get a list of groups with that right. + $namespaceRightGroups = []; + foreach ( (array)$this->options->get( 'NamespaceProtection' )[$index] as $right ) { + if ( $right == 'sysop' ) { + $right = 'editprotected'; // BC + } + if ( $right == 'autoconfirmed' ) { + $right = 'editsemiprotected'; // BC + } + if ( $right != '' ) { + $namespaceRightGroups[$right] = $this->getGroupsWithPermission( $right ); + } + } + + // Now, go through the protection levels one by one. + $usableLevels = [ '' ]; + foreach ( $this->options->get( 'RestrictionLevels' ) as $level ) { + $right = $level; + if ( $right == 'sysop' ) { + $right = 'editprotected'; // BC + } + if ( $right == 'autoconfirmed' ) { + $right = 'editsemiprotected'; // BC + } + + if ( $right != '' && + !isset( $namespaceRightGroups[$right] ) && + ( !$user || $this->userHasRight( $user, $right ) ) + ) { + // Do any of the namespace rights imply the restriction right? (see explanation above) + foreach ( $namespaceRightGroups as $groups ) { + if ( !array_diff( $groups, $this->getGroupsWithPermission( $right ) ) ) { + // Yes, this one does. + continue 2; + } + } + // No, keep the restriction level + $usableLevels[] = $level; + } + } + + return $usableLevels; + } + /** * Add temporary user rights, only valid for the current scope. * This is meant for making it possible to programatically trigger certain actions that @@ -1462,7 +1615,8 @@ class PermissionManager { if ( !defined( 'MW_PHPUNIT_TEST' ) ) { throw new Exception( __METHOD__ . ' can not be called outside of tests' ); } - $this->usersRights[ $user->getId() ] = is_array( $rights ) ? $rights : [ $rights ]; + $this->usersRights[ $this->getRightsCacheKey( $user ) ] = + is_array( $rights ) ? $rights : [ $rights ]; } } diff --git a/includes/ProtectionForm.php b/includes/ProtectionForm.php index 4bead3464c..3e639b92a2 100644 --- a/includes/ProtectionForm.php +++ b/includes/ProtectionForm.php @@ -58,6 +58,18 @@ class ProtectionForm { /** @var array Map of action to the expiry time of the existing protection */ protected $mExistingExpiry = []; + /** @var Article */ + protected $mArticle; + + /** @var Title */ + protected $mTitle; + + /** @var bool */ + protected $disabled; + + /** @var array */ + protected $disabledAttrib; + /** @var IContextSource */ private $mContext; @@ -78,7 +90,7 @@ class ProtectionForm { if ( wfReadOnly() ) { $this->mPermErrors[] = [ 'readonlytext', wfReadOnlyReason() ]; } - $this->disabled = $this->mPermErrors != []; + $this->disabled = $this->mPermErrors !== []; $this->disabledAttrib = $this->disabled ? [ 'disabled' => 'disabled' ] : []; @@ -90,7 +102,7 @@ class ProtectionForm { * Loads the current state of protection into the object. */ function loadData() { - $levels = MediaWikiServices::getInstance()->getNamespaceInfo()->getRestrictionLevels( + $levels = MediaWikiServices::getInstance()->getPermissionManager()->getNamespaceRestrictionLevels( $this->mTitle->getNamespace(), $this->mContext->getUser() ); $this->mCascade = $this->mTitle->areRestrictionsCascading(); @@ -180,7 +192,7 @@ class ProtectionForm { */ function execute() { if ( - MediaWikiServices::getInstance()->getNamespaceInfo()->getRestrictionLevels( + MediaWikiServices::getInstance()->getPermissionManager()->getNamespaceRestrictionLevels( $this->mTitle->getNamespace() ) === [ '' ] ) { @@ -321,7 +333,9 @@ class ProtectionForm { ); if ( !$status->isOK() ) { - $this->show( $out->parseInlineAsInterface( $status->getWikiText() ) ); + $this->show( $out->parseInlineAsInterface( + $status->getWikiText( false, false, $this->mContext->getLanguage() ) + ) ); return false; } @@ -553,7 +567,8 @@ class ProtectionForm { } $out .= Xml::closeElement( 'fieldset' ); - if ( $user->isAllowed( 'editinterface' ) ) { + if ( MediaWikiServices::getInstance()->getPermissionManager() + ->userHasRight( $user, 'editinterface' ) ) { $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer(); $link = $linkRenderer->makeKnownLink( $context->msg( 'protect-dropdown' )->inContentLanguage()->getTitle(), @@ -585,10 +600,12 @@ class ProtectionForm { function buildSelector( $action, $selected ) { // If the form is disabled, display all relevant levels. Otherwise, // just show the ones this user can use. - $levels = MediaWikiServices::getInstance()->getNamespaceInfo()->getRestrictionLevels( - $this->mTitle->getNamespace(), - $this->disabled ? null : $this->mContext->getUser() - ); + $levels = MediaWikiServices::getInstance() + ->getPermissionManager() + ->getNamespaceRestrictionLevels( + $this->mTitle->getNamespace(), + $this->disabled ? null : $this->mContext->getUser() + ); $id = 'mwProtect-level-' . $action; diff --git a/includes/ProxyLookup.php b/includes/ProxyLookup.php index 246ae95ae9..7450bb91dc 100644 --- a/includes/ProxyLookup.php +++ b/includes/ProxyLookup.php @@ -58,7 +58,7 @@ class ProxyLookup { */ public function isConfiguredProxy( $ip ) { // Quick check of known singular proxy servers - if ( in_array( $ip, $this->proxyServers ) ) { + if ( in_array( $ip, $this->proxyServers, true ) ) { return true; } diff --git a/includes/Rest/BasicAccess/MWBasicAuthorizer.php b/includes/Rest/BasicAccess/MWBasicAuthorizer.php index 43014f1379..92529b3679 100644 --- a/includes/Rest/BasicAccess/MWBasicAuthorizer.php +++ b/includes/Rest/BasicAccess/MWBasicAuthorizer.php @@ -2,24 +2,24 @@ namespace MediaWiki\Rest\BasicAccess; -use User; use MediaWiki\Permissions\PermissionManager; use MediaWiki\Rest\Handler; use MediaWiki\Rest\RequestInterface; +use MediaWiki\User\UserIdentity; /** - * A factory for MWBasicRequestAuthorizer which passes through a User object + * A factory for MWBasicRequestAuthorizer which passes through a UserIdentity. * * @internal */ class MWBasicAuthorizer extends BasicAuthorizerBase { - /** @var User */ + /** @var UserIdentity */ private $user; /** @var PermissionManager */ private $permissionManager; - public function __construct( User $user, PermissionManager $permissionManager ) { + public function __construct( UserIdentity $user, PermissionManager $permissionManager ) { $this->user = $user; $this->permissionManager = $permissionManager; } diff --git a/includes/Rest/BasicAccess/MWBasicRequestAuthorizer.php b/includes/Rest/BasicAccess/MWBasicRequestAuthorizer.php index 8c459c63f4..671488aaf2 100644 --- a/includes/Rest/BasicAccess/MWBasicRequestAuthorizer.php +++ b/includes/Rest/BasicAccess/MWBasicRequestAuthorizer.php @@ -2,7 +2,7 @@ namespace MediaWiki\Rest\BasicAccess; -use User; +use MediaWiki\User\UserIdentity; use MediaWiki\Permissions\PermissionManager; use MediaWiki\Rest\Handler; use MediaWiki\Rest\RequestInterface; @@ -13,14 +13,14 @@ use MediaWiki\Rest\RequestInterface; * @internal */ class MWBasicRequestAuthorizer extends BasicRequestAuthorizer { - /** @var User */ + /** @var UserIdentity */ private $user; /** @var PermissionManager */ private $permissionManager; public function __construct( RequestInterface $request, Handler $handler, - User $user, PermissionManager $permissionManager + UserIdentity $user, PermissionManager $permissionManager ) { parent::__construct( $request, $handler ); $this->user = $user; diff --git a/includes/Rest/EntryPoint.php b/includes/Rest/EntryPoint.php index a14c1a1294..070451d9ce 100644 --- a/includes/Rest/EntryPoint.php +++ b/includes/Rest/EntryPoint.php @@ -3,11 +3,14 @@ namespace MediaWiki\Rest; use ExtensionRegistry; +use MediaWiki; use MediaWiki\MediaWikiServices; use MediaWiki\Rest\BasicAccess\MWBasicAuthorizer; +use MediaWiki\Rest\Validator\Validator; use RequestContext; use Title; use WebResponse; +use Wikimedia\Message\ITextFormatter; class EntryPoint { /** @var RequestInterface */ @@ -16,6 +19,8 @@ class EntryPoint { private $webResponse; /** @var Router */ private $router; + /** @var RequestContext */ + private $context; public static function main() { // URL safety checks @@ -24,13 +29,16 @@ class EntryPoint { return; } + $context = RequestContext::getMain(); + // Set $wgTitle and the title in RequestContext, as in api.php global $wgTitle; $wgTitle = Title::makeTitle( NS_SPECIAL, 'Badtitle/rest.php' ); - RequestContext::getMain()->setTitle( $wgTitle ); + $context->setTitle( $wgTitle ); $services = MediaWikiServices::getInstance(); $conf = $services->getMainConfig(); + $objectFactory = $services->getObjectFactory(); if ( !$conf->get( 'EnableRestAPI' ) ) { wfHttpError( 403, 'Access Denied', @@ -42,35 +50,69 @@ class EntryPoint { 'cookiePrefix' => $conf->get( 'CookiePrefix' ) ] ); - $authorizer = new MWBasicAuthorizer( RequestContext::getMain()->getUser(), + $responseFactory = new ResponseFactory( self::getTextFormatters( $services ) ); + + // @phan-suppress-next-line PhanAccessMethodInternal + $authorizer = new MWBasicAuthorizer( $context->getUser(), $services->getPermissionManager() ); + // @phan-suppress-next-line PhanAccessMethodInternal + $restValidator = new Validator( $objectFactory, + $services->getPermissionManager(), + $request, + RequestContext::getMain()->getUser() + ); + global $IP; $router = new Router( [ "$IP/includes/Rest/coreRoutes.json" ], ExtensionRegistry::getInstance()->getAttribute( 'RestRoutes' ), $conf->get( 'RestPath' ), $services->getLocalServerObjectCache(), - new ResponseFactory, - $authorizer + $responseFactory, + $authorizer, + $objectFactory, + $restValidator ); $entryPoint = new self( + $context, $request, $wgRequest->response(), $router ); $entryPoint->execute(); } - public function __construct( RequestInterface $request, WebResponse $webResponse, - Router $router + /** + * Get a TextFormatter array from MediaWikiServices + * + * @param MediaWikiServices $services + * @return ITextFormatter[] + */ + public static function getTextFormatters( MediaWikiServices $services ) { + $langs = array_unique( [ + $services->getMainConfig()->get( 'ContLang' )->getCode(), + 'en' + ] ); + $textFormatters = []; + $factory = $services->getMessageFormatterFactory(); + foreach ( $langs as $lang ) { + $textFormatters[] = $factory->getTextFormatter( $lang ); + } + return $textFormatters; + } + + public function __construct( RequestContext $context, RequestInterface $request, + WebResponse $webResponse, Router $router ) { + $this->context = $context; $this->request = $request; $this->webResponse = $webResponse; $this->router = $router; } public function execute() { + ob_start(); $response = $this->router->execute( $this->request ); $this->webResponse->header( @@ -90,8 +132,14 @@ class EntryPoint { $cookie['options'] ); } + // Clear all errors that might have been displayed if display_errors=On + ob_end_clean(); + $stream = $response->getBody(); $stream->rewind(); + + MediaWiki::preOutputCommit( $this->context ); + if ( $stream instanceof CopyableStreamInterface ) { $stream->copyToStream( fopen( 'php://output', 'w' ) ); } else { @@ -103,5 +151,8 @@ class EntryPoint { echo $buffer; } } + + $mw = new MediaWiki; + $mw->doPostOutputShutdown( 'fast' ); } } diff --git a/includes/Rest/Handler.php b/includes/Rest/Handler.php index c05d8e774a..efe2b7e9e2 100644 --- a/includes/Rest/Handler.php +++ b/includes/Rest/Handler.php @@ -2,7 +2,18 @@ namespace MediaWiki\Rest; +use MediaWiki\Rest\Validator\BodyValidator; +use MediaWiki\Rest\Validator\NullBodyValidator; +use MediaWiki\Rest\Validator\Validator; + abstract class Handler { + + /** + * (string) ParamValidator constant to specify the source of the parameter. + * Value must be 'path', 'query', or 'post'. + */ + const PARAM_SOURCE = 'rest-param-source'; + /** @var Router */ private $router; @@ -15,6 +26,12 @@ abstract class Handler { /** @var ResponseFactory */ private $responseFactory; + /** @var array|null */ + private $validatedParams; + + /** @var mixed */ + private $validatedBody; + /** * Initialise with dependencies from the Router. This is called after construction. * @internal @@ -68,6 +85,62 @@ abstract class Handler { return $this->responseFactory; } + /** + * Validate the request parameters/attributes and body. If there is a validation + * failure, a response with an error message should be returned or an + * HttpException should be thrown. + * + * @param Validator $restValidator + * @throws HttpException On validation failure. + */ + public function validate( Validator $restValidator ) { + $validatedParams = $restValidator->validateParams( $this->getParamSettings() ); + $validatedBody = $restValidator->validateBody( $this->request, $this ); + $this->validatedParams = $validatedParams; + $this->validatedBody = $validatedBody; + } + + /** + * Fetch ParamValidator settings for parameters + * + * Every setting must include self::PARAM_SOURCE to specify which part of + * the request is to contain the parameter. + * + * @return array[] Associative array mapping parameter names to + * ParamValidator settings arrays + */ + public function getParamSettings() { + return []; + } + + /** + * Fetch the BodyValidator + * @param string $contentType Content type of the request. + * @return BodyValidator + */ + public function getBodyValidator( $contentType ) { + return new NullBodyValidator(); + } + + /** + * Fetch the validated parameters + * + * @return array|null Array mapping parameter names to validated values, + * or null if validateParams() was not called yet or validation failed. + */ + public function getValidatedParams() { + return $this->validatedParams; + } + + /** + * Fetch the validated body + * @return mixed Value returned by the body validator, or null if validateParams() was + * not called yet, validation failed, there was no body, or the body was form data. + */ + public function getValidatedBody() { + return $this->validatedBody; + } + /** * The subclass should override this to provide the maximum last modified * timestamp for the current request. This is called before execute() in diff --git a/includes/Rest/Handler/HelloHandler.php b/includes/Rest/Handler/HelloHandler.php index 34faee26d3..495b10139a 100644 --- a/includes/Rest/Handler/HelloHandler.php +++ b/includes/Rest/Handler/HelloHandler.php @@ -2,6 +2,7 @@ namespace MediaWiki\Rest\Handler; +use Wikimedia\ParamValidator\ParamValidator; use MediaWiki\Rest\SimpleHandler; /** @@ -16,4 +17,14 @@ class HelloHandler extends SimpleHandler { public function needsWriteAccess() { return false; } + + public function getParamSettings() { + return [ + 'name' => [ + self::PARAM_SOURCE => 'path', + ParamValidator::PARAM_TYPE => 'string', + ParamValidator::PARAM_REQUIRED => true, + ], + ]; + } } diff --git a/includes/Rest/HeaderContainer.php b/includes/Rest/HeaderContainer.php index a71f6a6ce1..528bac1ad0 100644 --- a/includes/Rest/HeaderContainer.php +++ b/includes/Rest/HeaderContainer.php @@ -51,7 +51,6 @@ class HeaderContainer { * better served by an HTTP header parsing library which provides the full * parse tree. * - * @param string $name The header name * @param string|string[] $value The input header value * @return array */ diff --git a/includes/Rest/HttpException.php b/includes/Rest/HttpException.php index ae6dde2b3f..bcc414fdf1 100644 --- a/includes/Rest/HttpException.php +++ b/includes/Rest/HttpException.php @@ -8,7 +8,19 @@ namespace MediaWiki\Rest; * error response. */ class HttpException extends \Exception { - public function __construct( $message, $code = 500 ) { + + /** @var array|null */ + private $errorData = null; + + public function __construct( $message, $code = 500, $errorData = null ) { parent::__construct( $message, $code ); + $this->errorData = $errorData; + } + + /** + * @return array|null + */ + public function getErrorData() { + return $this->errorData; } } diff --git a/includes/Rest/LocalizedHttpException.php b/includes/Rest/LocalizedHttpException.php new file mode 100644 index 0000000000..184fe164b7 --- /dev/null +++ b/includes/Rest/LocalizedHttpException.php @@ -0,0 +1,18 @@ +getKey(), $code ); + $this->messageValue = $messageValue; + } + + public function getMessageValue() { + return $this->messageValue; + } +} diff --git a/includes/Rest/ResponseFactory.php b/includes/Rest/ResponseFactory.php index d18cdb5d6b..fd0f3c7975 100644 --- a/includes/Rest/ResponseFactory.php +++ b/includes/Rest/ResponseFactory.php @@ -5,19 +5,31 @@ namespace MediaWiki\Rest; use Exception; use HttpStatus; use InvalidArgumentException; +use LanguageCode; use MWExceptionHandler; use stdClass; use Throwable; +use Wikimedia\Message\ITextFormatter; +use Wikimedia\Message\MessageValue; /** * Generates standardized response objects. */ class ResponseFactory { - const CT_PLAIN = 'text/plain; charset=utf-8'; const CT_HTML = 'text/html; charset=utf-8'; const CT_JSON = 'application/json'; + /** @var ITextFormatter[] */ + private $textFormatters; + + /** + * @param ITextFormatter[] $textFormatters + */ + public function __construct( $textFormatters ) { + $this->textFormatters = $textFormatters; + } + /** * Encode a stdClass object or array to a JSON string * @@ -167,16 +179,31 @@ class ResponseFactory { return $response; } + /** + * Create an HTTP 4xx or 5xx response with error message localisation + */ + public function createLocalizedHttpError( $errorCode, MessageValue $messageValue ) { + return $this->createHttpError( $errorCode, $this->formatMessage( $messageValue ) ); + } + /** * Turn an exception into a JSON error response. * @param Exception|Throwable $exception * @return Response */ public function createFromException( $exception ) { - if ( $exception instanceof HttpException ) { + if ( $exception instanceof LocalizedHttpException ) { + $response = $this->createLocalizedHttpError( $exception->getCode(), + $exception->getMessageValue() ); + } elseif ( $exception instanceof HttpException ) { // FIXME can HttpException represent 2xx or 3xx responses? - $response = $this->createHttpError( $exception->getCode(), - [ 'message' => $exception->getMessage() ] ); + $response = $this->createHttpError( + $exception->getCode(), + array_merge( + [ 'message' => $exception->getMessage() ], + (array)$exception->getErrorData() + ) + ); } else { $response = $this->createHttpError( 500, [ 'message' => 'Error: exception of type ' . get_class( $exception ), @@ -235,4 +262,18 @@ class ResponseFactory { return "Redirect$url"; } + public function formatMessage( MessageValue $messageValue ) { + if ( !$this->textFormatters ) { + // For unit tests + return []; + } + $translations = []; + foreach ( $this->textFormatters as $formatter ) { + $lang = LanguageCode::bcp47( $formatter->getLangCode() ); + $messageText = $formatter->format( $messageValue ); + $translations[$lang] = $messageText; + } + return [ 'messageTranslations' => $translations ]; + } + } diff --git a/includes/Rest/Router.php b/includes/Rest/Router.php index 14b4c9cb89..6dfcf3c2d7 100644 --- a/includes/Rest/Router.php +++ b/includes/Rest/Router.php @@ -4,8 +4,10 @@ namespace MediaWiki\Rest; use AppendIterator; use BagOStuff; +use Wikimedia\Message\MessageValue; use MediaWiki\Rest\BasicAccess\BasicAuthorizerInterface; use MediaWiki\Rest\PathTemplateMatcher\PathMatcher; +use MediaWiki\Rest\Validator\Validator; use Wikimedia\ObjectFactory; /** @@ -44,6 +46,12 @@ class Router { /** @var BasicAuthorizerInterface */ private $basicAuth; + /** @var ObjectFactory */ + private $objectFactory; + + /** @var Validator */ + private $restValidator; + /** * @param string[] $routeFiles List of names of JSON files containing routes * @param array $extraRoutes Extension route array @@ -51,10 +59,13 @@ class Router { * @param BagOStuff $cacheBag A cache in which to store the matcher trees * @param ResponseFactory $responseFactory * @param BasicAuthorizerInterface $basicAuth + * @param ObjectFactory $objectFactory + * @param Validator $restValidator */ public function __construct( $routeFiles, $extraRoutes, $rootPath, BagOStuff $cacheBag, ResponseFactory $responseFactory, - BasicAuthorizerInterface $basicAuth + BasicAuthorizerInterface $basicAuth, ObjectFactory $objectFactory, + Validator $restValidator ) { $this->routeFiles = $routeFiles; $this->extraRoutes = $extraRoutes; @@ -62,6 +73,8 @@ class Router { $this->cacheBag = $cacheBag; $this->responseFactory = $responseFactory; $this->basicAuth = $basicAuth; + $this->objectFactory = $objectFactory; + $this->restValidator = $restValidator; } /** @@ -214,18 +227,28 @@ class Router { $path = $request->getUri()->getPath(); $relPath = $this->getRelativePath( $path ); if ( $relPath === false ) { - return $this->responseFactory->createHttpError( 404 ); + return $this->responseFactory->createLocalizedHttpError( 404, + ( new MessageValue( 'rest-prefix-mismatch' ) ) + ->plaintextParams( $path, $this->rootPath ) + ); } + $requestMethod = $request->getMethod(); $matchers = $this->getMatchers(); - $matcher = $matchers[$request->getMethod()] ?? null; + $matcher = $matchers[$requestMethod] ?? null; $match = $matcher ? $matcher->match( $relPath ) : null; + // For a HEAD request, execute the GET handler instead if one exists. + // The webserver will discard the body. + if ( !$match && $requestMethod === 'HEAD' && isset( $matchers['GET'] ) ) { + $match = $matchers['GET']->match( $relPath ); + } + if ( !$match ) { // Check for 405 wrong method $allowed = []; foreach ( $matchers as $allowedMethod => $allowedMatcher ) { - if ( $allowedMethod === $request->getMethod() ) { + if ( $allowedMethod === $requestMethod ) { continue; } if ( $allowedMatcher->match( $relPath ) ) { @@ -233,21 +256,29 @@ class Router { } } if ( $allowed ) { - $response = $this->responseFactory->createHttpError( 405 ); + $response = $this->responseFactory->createLocalizedHttpError( 405, + ( new MessageValue( 'rest-wrong-method' ) ) + ->textParams( $requestMethod ) + ->commaListParams( $allowed ) + ->numParams( count( $allowed ) ) + ); $response->setHeader( 'Allow', $allowed ); return $response; } else { // Did not match with any other method, must be 404 - return $this->responseFactory->createHttpError( 404 ); + return $this->responseFactory->createLocalizedHttpError( 404, + ( new MessageValue( 'rest-no-match' ) ) + ->plaintextParams( $relPath ) + ); } } $request->setPathParams( array_map( 'rawurldecode', $match['params'] ) ); $spec = $match['userData']; $objectFactorySpec = array_intersect_key( $spec, - [ 'factory' => true, 'class' => true, 'args' => true ] ); + [ 'factory' => true, 'class' => true, 'args' => true, 'services' => true ] ); /** @var $handler Handler (annotation for PHPStorm) */ - $handler = ObjectFactory::getObjectFromSpec( $objectFactorySpec ); + $handler = $this->objectFactory->createObject( $objectFactorySpec ); $handler->init( $this, $request, $spec, $this->responseFactory ); try { @@ -259,14 +290,19 @@ class Router { /** * Execute a fully-constructed handler + * * @param Handler $handler * @return ResponseInterface */ private function executeHandler( $handler ): ResponseInterface { + // @phan-suppress-next-line PhanAccessMethodInternal $authResult = $this->basicAuth->authorize( $handler->getRequest(), $handler ); if ( $authResult ) { return $this->responseFactory->createHttpError( 403, [ 'error' => $authResult ] ); } + + $handler->validate( $this->restValidator ); + $response = $handler->execute(); if ( !( $response instanceof ResponseInterface ) ) { $response = $this->responseFactory->createFromReturnValue( $response ); diff --git a/includes/Rest/SimpleHandler.php b/includes/Rest/SimpleHandler.php index 85749c6229..3c19e48e87 100644 --- a/includes/Rest/SimpleHandler.php +++ b/includes/Rest/SimpleHandler.php @@ -8,12 +8,33 @@ namespace MediaWiki\Rest; * * run() must be declared in the subclass. It cannot be declared as abstract * here because it has a variable parameter list. + * @todo Declare it as abstract after dropping HHVM * * @package MediaWiki\Rest */ class SimpleHandler extends Handler { public function execute() { - $params = array_values( $this->getRequest()->getPathParams() ); + $paramSettings = $this->getParamSettings(); + $validatedParams = $this->getValidatedParams(); + $unvalidatedParams = []; + $params = []; + foreach ( $this->getRequest()->getPathParams() as $name => $value ) { + $source = $paramSettings[$name][self::PARAM_SOURCE] ?? 'unknown'; + if ( $source !== 'path' ) { + $unvalidatedParams[] = $name; + $params[] = $value; + } else { + $params[] = $validatedParams[$name]; + } + } + + if ( $unvalidatedParams ) { + throw new \LogicException( + 'Path parameters were not validated: ' . implode( ', ', $unvalidatedParams ) + ); + } + + // @phan-suppress-next-line PhanUndeclaredMethod return $this->run( ...$params ); } } diff --git a/includes/Rest/Validator/BodyValidator.php b/includes/Rest/Validator/BodyValidator.php new file mode 100644 index 0000000000..0147fa880c --- /dev/null +++ b/includes/Rest/Validator/BodyValidator.php @@ -0,0 +1,26 @@ +permissionManager = $permissionManager; + $this->request = $request; + $this->user = $user; + } + + /** + * Get the raw parameters from a source in the request + * @param string $source 'path', 'query', or 'post' + * @return array + */ + private function getParamsFromSource( $source ) { + switch ( $source ) { + case 'path': + return $this->request->getPathParams(); + + case 'query': + return $this->request->getQueryParams(); + + case 'post': + return $this->request->getPostParams(); + + default: + throw new InvalidArgumentException( __METHOD__ . ": Invalid source '$source'" ); + } + } + + public function hasParam( $name, array $options ) { + $params = $this->getParamsFromSource( $options['source'] ); + return isset( $params[$name] ); + } + + public function getValue( $name, $default, array $options ) { + $params = $this->getParamsFromSource( $options['source'] ); + return $params[$name] ?? $default; + // @todo Should normalization to NFC UTF-8 be done here (much like in the + // action API and the rest of MW), or should it be left to handlers to + // do whatever normalization they need? + } + + public function hasUpload( $name, array $options ) { + if ( $options['source'] !== 'post' ) { + return false; + } + return $this->getUploadedFile( $name, $options ) !== null; + } + + public function getUploadedFile( $name, array $options ) { + if ( $options['source'] !== 'post' ) { + return null; + } + $upload = $this->request->getUploadedFiles()[$name] ?? null; + return $upload instanceof UploadedFileInterface ? $upload : null; + } + + public function recordCondition( ValidationException $condition, array $options ) { + // @todo Figure out how to handle warnings + } + + public function useHighLimits( array $options ) { + return $this->permissionManager->userHasRight( $this->user, 'apihighlimits' ); + } + +} diff --git a/includes/Rest/Validator/Validator.php b/includes/Rest/Validator/Validator.php new file mode 100644 index 0000000000..be8d7a4e66 --- /dev/null +++ b/includes/Rest/Validator/Validator.php @@ -0,0 +1,168 @@ + [ 'class' => BooleanDef::class ], + 'enum' => [ 'class' => EnumDef::class ], + 'integer' => [ 'class' => IntegerDef::class ], + 'float' => [ 'class' => FloatDef::class ], + 'double' => [ 'class' => FloatDef::class ], + 'NULL' => [ + 'class' => StringDef::class, + 'args' => [ [ + 'allowEmptyWhenRequired' => true, + ] ], + ], + 'password' => [ 'class' => PasswordDef::class ], + 'string' => [ 'class' => StringDef::class ], + 'timestamp' => [ 'class' => TimestampDef::class ], + 'upload' => [ 'class' => UploadDef::class ], + ]; + + /** @var string[] HTTP request methods that we expect never to have a payload */ + private static $noBodyMethods = [ 'GET', 'HEAD', 'DELETE' ]; + + /** @var string[] HTTP request methods that we expect always to have a payload */ + private static $bodyMethods = [ 'POST', 'PUT' ]; + + /** @var string[] Content types handled via $_POST */ + private static $formDataContentTypes = [ + 'application/x-www-form-urlencoded', + 'multipart/form-data', + ]; + + /** @var ParamValidator */ + private $paramValidator; + + /** + * @param ObjectFactory $objectFactory + * @param PermissionManager $permissionManager + * @param RequestInterface $request + * @param UserIdentity $user + * @internal + */ + public function __construct( + ObjectFactory $objectFactory, + PermissionManager $permissionManager, + RequestInterface $request, + UserIdentity $user + ) { + $this->paramValidator = new ParamValidator( + new ParamValidatorCallbacks( $permissionManager, $request, $user ), + $objectFactory, + [ + 'typeDefs' => self::$typeDefs, + ] + ); + } + + /** + * Validate parameters + * @param array[] $paramSettings Parameter settings + * @return array Validated parameters + * @throws HttpException on validaton failure + */ + public function validateParams( array $paramSettings ) { + $validatedParams = []; + foreach ( $paramSettings as $name => $settings ) { + try { + $validatedParams[$name] = $this->paramValidator->getValue( $name, $settings, [ + 'source' => $settings[Handler::PARAM_SOURCE] ?? 'unspecified', + ] ); + } catch ( ValidationException $e ) { + throw new HttpException( 'Parameter validation failed', 400, [ + 'error' => 'parameter-validation-failed', + 'name' => $e->getParamName(), + 'value' => $e->getParamValue(), + 'failureCode' => $e->getFailureCode(), + 'failureData' => $e->getFailureData(), + ] ); + } + } + return $validatedParams; + } + + /** + * Validate the body of a request. + * + * This may return a data structure representing the parsed body. When used + * in the context of Handler::validateParams(), the returned value will be + * available to the handler via Handler::getValidatedBody(). + * + * @param RequestInterface $request + * @param Handler $handler Used to call getBodyValidator() + * @return mixed May be null + * @throws HttpException on validation failure + */ + public function validateBody( RequestInterface $request, Handler $handler ) { + $method = strtoupper( trim( $request->getMethod() ) ); + + // If the method should never have a body, don't bother validating. + if ( in_array( $method, self::$noBodyMethods, true ) ) { + return null; + } + + // Get the content type + list( $ct ) = explode( ';', $request->getHeaderLine( 'Content-Type' ), 2 ); + $ct = strtolower( trim( $ct ) ); + if ( $ct === '' ) { + // No Content-Type was supplied. RFC 7231 § 3.1.1.5 allows this, but since it's probably a + // client error let's return a 415. But don't 415 for unknown methods and an empty body. + if ( !in_array( $method, self::$bodyMethods, true ) ) { + $body = $request->getBody(); + $size = $body->getSize(); + if ( $size === null ) { + // No size available. Try reading 1 byte. + if ( $body->isSeekable() ) { + $body->rewind(); + } + $size = $body->read( 1 ) === '' ? 0 : 1; + } + if ( $size === 0 ) { + return null; + } + } + throw new HttpException( "A Content-Type header must be supplied with a request payload.", 415, [ + 'error' => 'no-content-type', + ] ); + } + + // Form data is parsed into $_POST and $_FILES by PHP and from there is accessed as parameters, + // don't bother trying to handle these via BodyValidator too. + if ( in_array( $ct, self::$formDataContentTypes, true ) ) { + return null; + } + + // Validate the body. BodyValidator throws an HttpException on failure. + return $handler->getBodyValidator( $ct )->validateBody( $request ); + } + +} diff --git a/includes/Revision.php b/includes/Revision.php index de3c2998b5..828f647552 100644 --- a/includes/Revision.php +++ b/includes/Revision.php @@ -89,6 +89,7 @@ class Revision implements IDBAccessObject { * @return SqlBlobStore */ protected static function getBlobStore( $wiki = false ) { + // @phan-suppress-next-line PhanAccessMethodInternal $store = MediaWikiServices::getInstance() ->getBlobStoreFactory() ->newSqlBlobStore( $wiki ); @@ -297,203 +298,6 @@ class Revision implements IDBAccessObject { return $rec ? new Revision( $rec ) : null; } - /** - * Return the value of a select() JOIN conds array for the user table. - * This will get user table rows for logged-in users. - * @since 1.19 - * @deprecated since 1.31, use RevisionStore::getQueryInfo( [ 'user' ] ) instead. - * @return array - */ - public static function userJoinCond() { - global $wgActorTableSchemaMigrationStage; - - wfDeprecated( __METHOD__, '1.31' ); - if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_READ_NEW ) { - // If code is using this instead of self::getQueryInfo(), there's - // no way the join it's trying to do can work once the old fields - // aren't being used anymore. - throw new BadMethodCallException( - 'Cannot use ' . __METHOD__ - . ' when $wgActorTableSchemaMigrationStage has SCHEMA_COMPAT_READ_NEW' - ); - } - - return [ 'LEFT JOIN', [ 'rev_user != 0', 'user_id = rev_user' ] ]; - } - - /** - * Return the value of a select() page conds array for the page table. - * This will assure that the revision(s) are not orphaned from live pages. - * @since 1.19 - * @deprecated since 1.31, use RevisionStore::getQueryInfo( [ 'page' ] ) instead. - * @return array - */ - public static function pageJoinCond() { - wfDeprecated( __METHOD__, '1.31' ); - return [ 'JOIN', [ 'page_id = rev_page' ] ]; - } - - /** - * Return the list of revision fields that should be selected to create - * a new revision. - * @deprecated since 1.31, use RevisionStore::getQueryInfo() instead. - * @return array - */ - public static function selectFields() { - global $wgContentHandlerUseDB, $wgActorTableSchemaMigrationStage; - global $wgMultiContentRevisionSchemaMigrationStage; - - if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_READ_NEW ) { - // If code is using this instead of self::getQueryInfo(), there's a - // decent chance it's going to try to directly access - // $row->rev_user or $row->rev_user_text and we can't give it - // useful values here once those aren't being used anymore. - throw new BadMethodCallException( - 'Cannot use ' . __METHOD__ - . ' when $wgActorTableSchemaMigrationStage has SCHEMA_COMPAT_READ_NEW' - ); - } - - if ( !( $wgMultiContentRevisionSchemaMigrationStage & SCHEMA_COMPAT_WRITE_OLD ) ) { - // If code is using this instead of self::getQueryInfo(), there's a - // decent chance it's going to try to directly access - // $row->rev_text_id or $row->rev_content_model and we can't give it - // useful values here once those aren't being written anymore, - // and may not exist at all. - throw new BadMethodCallException( - 'Cannot use ' . __METHOD__ . ' when $wgMultiContentRevisionSchemaMigrationStage ' - . 'does not have SCHEMA_COMPAT_WRITE_OLD set.' - ); - } - - wfDeprecated( __METHOD__, '1.31' ); - - $fields = [ - 'rev_id', - 'rev_page', - 'rev_text_id', - 'rev_timestamp', - 'rev_user_text', - 'rev_user', - 'rev_actor' => 'NULL', - 'rev_minor_edit', - 'rev_deleted', - 'rev_len', - 'rev_parent_id', - 'rev_sha1', - ]; - - $fields += CommentStore::getStore()->getFields( 'rev_comment' ); - - if ( $wgContentHandlerUseDB ) { - $fields[] = 'rev_content_format'; - $fields[] = 'rev_content_model'; - } - - return $fields; - } - - /** - * Return the list of revision fields that should be selected to create - * a new revision from an archive row. - * @deprecated since 1.31, use RevisionStore::getArchiveQueryInfo() instead. - * @return array - */ - public static function selectArchiveFields() { - global $wgContentHandlerUseDB, $wgActorTableSchemaMigrationStage; - global $wgMultiContentRevisionSchemaMigrationStage; - - if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_READ_NEW ) { - // If code is using this instead of self::getQueryInfo(), there's a - // decent chance it's going to try to directly access - // $row->ar_user or $row->ar_user_text and we can't give it - // useful values here once those aren't being used anymore. - throw new BadMethodCallException( - 'Cannot use ' . __METHOD__ - . ' when $wgActorTableSchemaMigrationStage has SCHEMA_COMPAT_READ_NEW' - ); - } - - if ( !( $wgMultiContentRevisionSchemaMigrationStage & SCHEMA_COMPAT_WRITE_OLD ) ) { - // If code is using this instead of self::getQueryInfo(), there's a - // decent chance it's going to try to directly access - // $row->ar_text_id or $row->ar_content_model and we can't give it - // useful values here once those aren't being written anymore, - // and may not exist at all. - throw new BadMethodCallException( - 'Cannot use ' . __METHOD__ . ' when $wgMultiContentRevisionSchemaMigrationStage ' - . 'does not have SCHEMA_COMPAT_WRITE_OLD set.' - ); - } - - wfDeprecated( __METHOD__, '1.31' ); - - $fields = [ - 'ar_id', - 'ar_page_id', - 'ar_rev_id', - 'ar_text_id', - 'ar_timestamp', - 'ar_user_text', - 'ar_user', - 'ar_actor' => 'NULL', - 'ar_minor_edit', - 'ar_deleted', - 'ar_len', - 'ar_parent_id', - 'ar_sha1', - ]; - - $fields += CommentStore::getStore()->getFields( 'ar_comment' ); - - if ( $wgContentHandlerUseDB ) { - $fields[] = 'ar_content_format'; - $fields[] = 'ar_content_model'; - } - return $fields; - } - - /** - * Return the list of text fields that should be selected to read the - * revision text - * @deprecated since 1.31, use RevisionStore::getQueryInfo( [ 'text' ] ) instead. - * @return array - */ - public static function selectTextFields() { - wfDeprecated( __METHOD__, '1.31' ); - return [ - 'old_text', - 'old_flags' - ]; - } - - /** - * Return the list of page fields that should be selected from page table - * @deprecated since 1.31, use RevisionStore::getQueryInfo( [ 'page' ] ) instead. - * @return array - */ - public static function selectPageFields() { - wfDeprecated( __METHOD__, '1.31' ); - return [ - 'page_namespace', - 'page_title', - 'page_id', - 'page_latest', - 'page_is_redirect', - 'page_len', - ]; - } - - /** - * Return the list of user fields that should be selected from user table - * @deprecated since 1.31, use RevisionStore::getQueryInfo( [ 'user' ] ) instead. - * @return array - */ - public static function selectUserFields() { - wfDeprecated( __METHOD__, '1.31' ); - return [ 'user_name' ]; - } - /** * Return the tables, fields, and join conditions to be selected to create * a new revision object. @@ -1201,7 +1005,7 @@ class Revision implements IDBAccessObject { $comment = CommentStoreComment::newUnsavedComment( $summary, null ); - $title = Title::newFromID( $pageId, Title::GAID_FOR_UPDATE ); + $title = Title::newFromID( $pageId, Title::READ_LATEST ); if ( $title === null ) { return null; } diff --git a/includes/Revision/MutableRevisionRecord.php b/includes/Revision/MutableRevisionRecord.php index e9136cbb5d..8bb2c89893 100644 --- a/includes/Revision/MutableRevisionRecord.php +++ b/includes/Revision/MutableRevisionRecord.php @@ -37,6 +37,7 @@ use Wikimedia\Assert\Assert; * * @since 1.31 * @since 1.32 Renamed from MediaWiki\Storage\MutableRevisionRecord + * @property MutableRevisionSlots $mSlots */ class MutableRevisionRecord extends RevisionRecord { @@ -78,8 +79,6 @@ class MutableRevisionRecord extends RevisionRecord { $slots = new MutableRevisionSlots(); parent::__construct( $title, $slots, $dbDomain ); - - $this->mSlots = $slots; // redundant, but nice for static analysis } /** diff --git a/includes/Revision/RenderedRevision.php b/includes/Revision/RenderedRevision.php index 3bc8dda578..ba229d1e8a 100644 --- a/includes/Revision/RenderedRevision.php +++ b/includes/Revision/RenderedRevision.php @@ -185,6 +185,7 @@ class RenderedRevision implements SlotRenderingProvider { * @param array $hints Hints given as an associative array. Known keys: * - 'generate-html' => bool: Whether the caller is interested in output HTML (as opposed * to just meta-data). Default is to generate HTML. + * @phan-param array{generate-html?:bool} $hints * * @return ParserOutput */ @@ -212,6 +213,7 @@ class RenderedRevision implements SlotRenderingProvider { * @param array $hints Hints given as an associative array. Known keys: * - 'generate-html' => bool: Whether the caller is interested in output HTML (as opposed * to just meta-data). Default is to generate HTML. + * @phan-param array{generate-html?:bool} $hints * * @throws SuppressedDataException if the content is not accessible for the audience * specified in the constructor. diff --git a/includes/Revision/RevisionRecord.php b/includes/Revision/RevisionRecord.php index ff9ac579e8..cf353718f8 100644 --- a/includes/Revision/RevisionRecord.php +++ b/includes/Revision/RevisionRecord.php @@ -59,7 +59,7 @@ abstract class RevisionRecord { const FOR_THIS_USER = 2; const RAW = 3; - /** @var string Wiki ID; false means the current wiki */ + /** @var string|false Wiki ID; false means the current wiki */ protected $mWiki = false; /** @var int|null */ protected $mId; @@ -531,8 +531,6 @@ abstract class RevisionRecord { $text = $title->getPrefixedText(); wfDebug( "Checking for $permissionlist on $text due to $field match on $bitfield\n" ); - $permissionManager = MediaWikiServices::getInstance()->getPermissionManager(); - foreach ( $permissions as $perm ) { if ( $permissionManager->userCan( $perm, $user, $title ) ) { return true; diff --git a/includes/Revision/RevisionRenderer.php b/includes/Revision/RevisionRenderer.php index ca4bb73bb0..5d09e011cd 100644 --- a/includes/Revision/RevisionRenderer.php +++ b/includes/Revision/RevisionRenderer.php @@ -95,6 +95,7 @@ class RevisionRenderer { * matched the $rev and $options. This mechanism is intended as a temporary stop-gap, * for the time until caches have been changed to store RenderedRevision states instead * of ParserOutput objects. + * @phan-param array{use-master?:bool,audience?:int,known-revision-output?:ParserOutput} $hints * * @return RenderedRevision|null The rendered revision, or null if the audience checks fails. */ @@ -164,13 +165,9 @@ class RevisionRenderer { } private function getSpeculativeRevId( $dbIndex ) { - // Use a fresh master connection in order to see the latest data, by avoiding + // Use a separate master connection in order to see the latest data, by avoiding // stale data from REPEATABLE-READ snapshots. - // HACK: But don't use a fresh connection in unit tests, since it would not have - // the fake tables. This should be handled by the LoadBalancer! - $flags = defined( 'MW_PHPUNIT_TEST' ) || $dbIndex === DB_REPLICA - ? 0 - : ILoadBalancer::CONN_TRX_AUTOCOMMIT; + $flags = ILoadBalancer::CONN_TRX_AUTOCOMMIT; $db = $this->loadBalancer->getConnectionRef( $dbIndex, [], $this->dbDomain, $flags ); @@ -183,13 +180,9 @@ class RevisionRenderer { } private function getSpeculativePageId( $dbIndex ) { - // Use a fresh master connection in order to see the latest data, by avoiding + // Use a separate master connection in order to see the latest data, by avoiding // stale data from REPEATABLE-READ snapshots. - // HACK: But don't use a fresh connection in unit tests, since it would not have - // the fake tables. This should be handled by the LoadBalancer! - $flags = defined( 'MW_PHPUNIT_TEST' ) || $dbIndex === DB_REPLICA - ? 0 - : ILoadBalancer::CONN_TRX_AUTOCOMMIT; + $flags = ILoadBalancer::CONN_TRX_AUTOCOMMIT; $db = $this->loadBalancer->getConnectionRef( $dbIndex, [], $this->dbDomain, $flags ); diff --git a/includes/Revision/RevisionStore.php b/includes/Revision/RevisionStore.php index 9e8dfe7e40..a1aeccb297 100644 --- a/includes/Revision/RevisionStore.php +++ b/includes/Revision/RevisionStore.php @@ -54,8 +54,10 @@ use Psr\Log\NullLogger; use RecentChange; use Revision; use RuntimeException; +use StatusValue; use stdClass; use Title; +use Traversable; use User; use WANObjectCache; use Wikimedia\Assert\Assert; @@ -324,10 +326,10 @@ class RevisionStore $canUseTitleNewFromId = ( $pageId !== null && $pageId > 0 && $this->dbDomain === false ); list( $dbMode, $dbOptions ) = DBAccessObjectUtils::getDBOptions( $queryFlags ); - $titleFlags = ( $dbMode == DB_MASTER ? Title::GAID_FOR_UPDATE : 0 ); // Loading by ID is best, but Title::newFromID does not support that for foreign IDs. if ( $canUseTitleNewFromId ) { + $titleFlags = ( $dbMode == DB_MASTER ? Title::READ_LATEST : 0 ); // TODO: better foreign title handling (introduce TitleFactory) $title = Title::newFromID( $pageId, $titleFlags ); if ( $title ) { @@ -1621,10 +1623,18 @@ class RevisionStore * @param object[]|IResultWrapper $slotRows * @param int $queryFlags * @param Title $title + * @param array|null $slotContents a map from blobAddress to slot + * content blob or Content object. * * @return SlotRecord[] */ - private function constructSlotRecords( $revId, $slotRows, $queryFlags, Title $title ) { + private function constructSlotRecords( + $revId, + $slotRows, + $queryFlags, + Title $title, + $slotContents = null + ) { $slots = []; foreach ( $slotRows as $row ) { @@ -1649,8 +1659,15 @@ class RevisionStore = $this->emulateContentId( intval( $row->rev_text_id ) ); } - $contentCallback = function ( SlotRecord $slot ) use ( $queryFlags ) { - return $this->loadSlotContent( $slot, null, null, null, $queryFlags ); + $contentCallback = function ( SlotRecord $slot ) use ( $slotContents, $queryFlags ) { + $blob = null; + if ( isset( $slotContents[$slot->getAddress()] ) ) { + $blob = $slotContents[$slot->getAddress()]; + if ( $blob instanceof Content ) { + return $blob; + } + } + return $this->loadSlotContent( $slot, $blob, null, null, $queryFlags ); }; $slots[$row->role_name] = new SlotRecord( $row, $contentCallback ); @@ -1802,8 +1819,10 @@ class RevisionStore /** * @param object $row A database row generated from a query based on getQueryInfo() - * @param null|object[] $slotRows Database rows generated from a query based on - * getSlotsQueryInfo with the 'content' flag set. + * @param null|object[]|RevisionSlots $slots + * - Database rows generated from a query based on getSlotsQueryInfo + * with the 'content' flag set. Or + * - RevisionSlots instance * @param int $queryFlags * @param Title|null $title * @param bool $fromCache if true, the returned RevisionRecord will ensure that no stale @@ -1814,11 +1833,10 @@ class RevisionStore * @see RevisionFactory::newRevisionFromRow * * MCR migration note: this replaces Revision::newFromRow - * */ public function newRevisionFromRowAndSlots( $row, - $slotRows, + $slots, $queryFlags = 0, Title $title = null, $fromCache = false @@ -1855,7 +1873,9 @@ class RevisionStore // Legacy because $row may have come from self::selectFields() $comment = $this->commentStore->getCommentLegacy( $db, 'rev_comment', $row, true ); - $slots = $this->newRevisionSlots( $row->rev_id, $row, $slotRows, $queryFlags, $title ); + if ( !( $slots instanceof RevisionSlots ) ) { + $slots = $this->newRevisionSlots( $row->rev_id, $row, $slots, $queryFlags, $title ); + } // If this is a cached row, instantiate a cache-aware revision class to avoid stale data. if ( $fromCache ) { @@ -1876,6 +1896,156 @@ class RevisionStore return $rev; } + /** + * Construct a RevisionRecord instance for each row in $rows, + * and return them as an associative array indexed by revision ID. + * @param Traversable|array $rows the rows to construct revision records from + * @param array $options Supports the following options: + * 'slots' - whether metadata about revision slots should be + * loaded immediately. Supports falsy or truthy value as well + * as an explicit list of slot role names. + * 'content'- whether the actual content of the slots should be + * preloaded. + * @param int $queryFlags + * @param Title|null $title The title to which all the revision rows belong, if there + * is such a title and the caller has it handy, so we don't have to look it up again. + * If this parameter is given and any of the rows has a rev_page_id that is different + * from $title->getArticleID(), an InvalidArgumentException is thrown. + * + * @return StatusValue a status with a RevisionRecord[] of successfully fetched revisions + * and an array of errors for the revisions failed to fetch. + */ + public function newRevisionsFromBatch( + $rows, + array $options = [], + $queryFlags = 0, + Title $title = null + ) { + $result = new StatusValue(); + + $rowsByRevId = []; + $pageIds = []; + $titlesByPageId = []; + foreach ( $rows as $row ) { + if ( isset( $rowsByRevId[$row->rev_id] ) ) { + $result->warning( + 'internalerror', + "Duplicate rows in newRevisionsFromBatch, rev_id {$row->rev_id}" + ); + } + if ( $title && $row->rev_page != $title->getArticleID() ) { + throw new InvalidArgumentException( + "Revision {$row->rev_id} doesn't belong to page {$title->getArticleID()}" + ); + } + $pageIds[] = $row->rev_page; + $rowsByRevId[$row->rev_id] = $row; + } + + if ( empty( $rowsByRevId ) ) { + $result->setResult( true, [] ); + return $result; + } + + // If the title is not supplied, batch-fetch Title objects. + if ( $title ) { + $titlesByPageId[$title->getArticleID()] = $title; + } else { + $pageIds = array_unique( $pageIds ); + foreach ( Title::newFromIDs( $pageIds ) as $t ) { + $titlesByPageId[$t->getArticleID()] = $t; + } + } + + if ( !isset( $options['slots'] ) || $this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_OLD ) ) { + $result->setResult( true, + array_map( function ( $row ) use ( $queryFlags, $titlesByPageId, $result ) { + try { + return $this->newRevisionFromRow( + $row, + $queryFlags, + $titlesByPageId[$row->rev_page] + ); + } catch ( MWException $e ) { + $result->warning( 'internalerror', $e->getMessage() ); + return null; + } + }, $rowsByRevId ) + ); + return $result; + } + + $slotQueryConds = [ 'slot_revision_id' => array_keys( $rowsByRevId ) ]; + if ( is_array( $options['slots'] ) ) { + $slotQueryConds['slot_role_id'] = array_map( function ( $slot_name ) { + return $this->slotRoleStore->getId( $slot_name ); + }, $options['slots'] ); + } + + // We need to set the `content` flag because newRevisionFromRowAndSlots requires content + // metadata to be loaded. + $slotQueryInfo = self::getSlotsQueryInfo( [ 'content' ] ); + $db = $this->getDBConnectionRefForQueryFlags( $queryFlags ); + $slotRows = $db->select( + $slotQueryInfo['tables'], + $slotQueryInfo['fields'], + $slotQueryConds, + __METHOD__, + [], + $slotQueryInfo['joins'] + ); + + $slotRowsByRevId = []; + foreach ( $slotRows as $slotRow ) { + $slotRowsByRevId[$slotRow->slot_revision_id][] = $slotRow; + } + + $slotContents = null; + if ( $options['content'] ?? false ) { + $blobAddresses = []; + foreach ( $slotRows as $slotRow ) { + $blobAddresses[] = $slotRow->content_address; + } + $slotContentFetchStatus = $this->blobStore + ->getBlobBatch( $blobAddresses, $queryFlags ); + foreach ( $slotContentFetchStatus->getErrors() as $error ) { + $result->warning( $error['message'], ...$error['params'] ); + } + $slotContents = $slotContentFetchStatus->getValue(); + } + + $result->setResult( true, array_map( function ( $row ) use + ( $slotRowsByRevId, $queryFlags, $titlesByPageId, $slotContents, $result ) { + if ( !isset( $slotRowsByRevId[$row->rev_id] ) ) { + $result->warning( + 'internalerror', + "Couldn't find slots for rev {$row->rev_id}" + ); + return null; + } + try { + return $this->newRevisionFromRowAndSlots( + $row, + new RevisionSlots( + $this->constructSlotRecords( + $row->rev_id, + $slotRowsByRevId[$row->rev_id], + $queryFlags, + $titlesByPageId[$row->rev_page], + $slotContents + ) + ), + $queryFlags, + $titlesByPageId[$row->rev_page] + ); + } catch ( MWException $e ) { + $result->warning( 'internalerror', $e->getMessage() ); + return null; + } + }, $rowsByRevId ) ); + return $result; + } + /** * Constructs a new MutableRevisionRecord based on the given associative array following * the MW1.29 convention for the Revision constructor. @@ -2324,6 +2494,7 @@ class RevisionStore * - tables: (string[]) to include in the `$table` to `IDatabase->select()` * - fields: (string[]) to include in the `$vars` to `IDatabase->select()` * - joins: (array) to include in the `$join_conds` to `IDatabase->select()` + * @phan-return array{tables:string[],fields:string[],joins:array} */ public function getQueryInfo( $options = [] ) { $ret = [ diff --git a/includes/Revision/RevisionStoreFactory.php b/includes/Revision/RevisionStoreFactory.php index 0475557387..1de4d7fdd0 100644 --- a/includes/Revision/RevisionStoreFactory.php +++ b/includes/Revision/RevisionStoreFactory.php @@ -27,7 +27,7 @@ namespace MediaWiki\Revision; use ActorMigration; use CommentStore; -use MediaWiki\Logger\Spi as LoggerSpi; +use Psr\Log\LoggerInterface; use MediaWiki\Storage\BlobStoreFactory; use MediaWiki\Storage\NameTableStoreFactory; use WANObjectCache; @@ -54,8 +54,8 @@ class RevisionStoreFactory { private $dbLoadBalancerFactory; /** @var WANObjectCache */ private $cache; - /** @var LoggerSpi */ - private $loggerProvider; + /** @var LoggerInterface */ + private $logger; /** @var CommentStore */ private $commentStore; @@ -84,7 +84,7 @@ class RevisionStoreFactory { * @param CommentStore $commentStore * @param ActorMigration $actorMigration * @param int $migrationStage - * @param LoggerSpi $loggerProvider + * @param LoggerInterface $logger * @param bool $contentHandlerUseDB see {@link $wgContentHandlerUseDB}. Must be the same * for all wikis in the cluster. Will go away after MCR migration. */ @@ -97,7 +97,7 @@ class RevisionStoreFactory { CommentStore $commentStore, ActorMigration $actorMigration, $migrationStage, - LoggerSpi $loggerProvider, + LoggerInterface $logger, $contentHandlerUseDB ) { Assert::parameterType( 'integer', $migrationStage, '$migrationStage' ); @@ -109,7 +109,7 @@ class RevisionStoreFactory { $this->commentStore = $commentStore; $this->actorMigration = $actorMigration; $this->mcrMigrationStage = $migrationStage; - $this->loggerProvider = $loggerProvider; + $this->logger = $logger; $this->contentHandlerUseDB = $contentHandlerUseDB; } @@ -118,13 +118,14 @@ class RevisionStoreFactory { * * @param bool|string $dbDomain DB domain of the relevant wiki or false for the current one * - * @return RevisionStore for the given wikiId with all necessary services and a logger + * @return RevisionStore for the given wikiId with all necessary services */ public function getRevisionStore( $dbDomain = false ) { Assert::parameterType( 'string|boolean', $dbDomain, '$dbDomain' ); $store = new RevisionStore( $this->dbLoadBalancerFactory->getMainLB( $dbDomain ), + // @phan-suppress-next-line PhanAccessMethodInternal $this->blobStoreFactory->newSqlBlobStore( $dbDomain ), $this->cache, // Pass local cache instance; Leave cache sharing to RevisionStore. $this->commentStore, @@ -136,7 +137,7 @@ class RevisionStoreFactory { $dbDomain ); - $store->setLogger( $this->loggerProvider->getLogger( 'RevisionStore' ) ); + $store->setLogger( $this->logger ); $store->setContentHandlerUseDB( $this->contentHandlerUseDB ); return $store; diff --git a/includes/Revision/RevisionStoreRecord.php b/includes/Revision/RevisionStoreRecord.php index 469e494a3d..dabf62bb0b 100644 --- a/includes/Revision/RevisionStoreRecord.php +++ b/includes/Revision/RevisionStoreRecord.php @@ -151,7 +151,7 @@ class RevisionStoreRecord extends RevisionRecord { /** * @throws RevisionAccessException if the size was unknown and could not be calculated. - * @return string The nominal revision size, never null. May be computed on the fly. + * @return int The nominal revision size, never null. May be computed on the fly. */ public function getSize() { // If length is null, calculate and remember it (potentially SLOW!). diff --git a/includes/Revision/SlotRoleHandler.php b/includes/Revision/SlotRoleHandler.php index 85b4c5ab34..7c2623bebd 100644 --- a/includes/Revision/SlotRoleHandler.php +++ b/includes/Revision/SlotRoleHandler.php @@ -150,7 +150,7 @@ class SlotRoleHandler { * * The default implementation always returns false. * - * @return string + * @return bool */ public function supportsArticleCount() { return false; diff --git a/includes/ServiceWiring.php b/includes/ServiceWiring.php index c192b5a266..ab51eabd47 100644 --- a/includes/ServiceWiring.php +++ b/includes/ServiceWiring.php @@ -27,6 +27,13 @@ * For every service that MediaWiki core requires, an instantiator must be defined in * this file. * + * Note that, ideally, all information used to instantiate service objects should come + * from configuration. Information derived from the current request is acceptable, but + * only where there is no feasible alternative. It is preferred that such information + * (like the client IP, the acting user's identity, requested title, etc) be passed to + * the service object's methods as parameters. This makes the flow of information more + * obvious, and makes it easier to understand the behavior of services. + * * @note As of version 1.27, MediaWiki is only beginning to use dependency injection. * The services defined here do not yet fully represent all services used by core, * much of the code still relies on global state for this accessing services. @@ -39,10 +46,13 @@ use Liuggio\StatsdClient\Factory\StatsdDataFactoryInterface; use MediaWiki\Auth\AuthManager; +use MediaWiki\BadFileLookup; use MediaWiki\Block\BlockManager; use MediaWiki\Block\BlockRestrictionStore; use MediaWiki\Config\ConfigRepository; use MediaWiki\Config\ServiceOptions; +use MediaWiki\FileBackend\FSFile\TempFSFileFactory; +use MediaWiki\FileBackend\LockManager\LockManagerGroupFactory; use MediaWiki\Http\HttpRequestFactory; use MediaWiki\Interwiki\ClassicInterwikiLookup; use MediaWiki\Interwiki\InterwikiLookup; @@ -50,6 +60,9 @@ use MediaWiki\Linker\LinkRenderer; use MediaWiki\Linker\LinkRendererFactory; use MediaWiki\Logger\LoggerFactory; use MediaWiki\MediaWikiServices; +use Wikimedia\Message\IMessageFormatterFactory; +use MediaWiki\Message\MessageFormatterFactory; +use MediaWiki\Page\MovePageFactory; use MediaWiki\Permissions\PermissionManager; use MediaWiki\Preferences\PreferencesFactory; use MediaWiki\Preferences\DefaultPreferencesFactory; @@ -67,11 +80,21 @@ use MediaWiki\Storage\BlobStoreFactory; use MediaWiki\Storage\NameTableStoreFactory; use MediaWiki\Storage\SqlBlobStore; use MediaWiki\Storage\PageEditStash; +use Wikimedia\ObjectFactory; return [ 'ActorMigration' => function ( MediaWikiServices $services ) : ActorMigration { - return new ActorMigration( - $services->getMainConfig()->get( 'ActorTableSchemaMigrationStage' ) + return new ActorMigration( SCHEMA_COMPAT_NEW ); + }, + + 'BadFileLookup' => function ( MediaWikiServices $services ) : BadFileLookup { + return new BadFileLookup( + function () { + return wfMessage( 'bad_image_list' )->inContentLanguage()->plain(); + }, + $services->getLocalServerObjectCache(), + $services->getRepoGroup(), + $services->getTitleParser() ); }, @@ -91,13 +114,12 @@ return [ }, 'BlockManager' => function ( MediaWikiServices $services ) : BlockManager { - $context = RequestContext::getMain(); return new BlockManager( new ServiceOptions( BlockManager::$constructorOptions, $services->getMainConfig() ), - $context->getUser(), - $context->getRequest() + $services->getPermissionManager(), + LoggerFactory::getInstance( 'BlockManager' ) ); }, @@ -252,6 +274,8 @@ return [ if ( defined( 'MW_NO_SESSION' ) ) { return $services->getLinkRendererFactory()->create(); } else { + // Normally information from the current request would not be passed in here; + // this is an exception. (See also the class documentation.) return $services->getLinkRendererFactory()->createForUser( RequestContext::getMain()->getUser() ); @@ -267,9 +291,18 @@ return [ }, 'LocalServerObjectCache' => function ( MediaWikiServices $services ) : BagOStuff { - $cacheId = \ObjectCache::detectLocalServerCache(); + $config = $services->getMainConfig(); + $cacheId = ObjectCache::detectLocalServerCache(); + + return ObjectCache::newFromParams( $config->get( 'ObjectCaches' )[$cacheId] ); + }, - return \ObjectCache::newFromId( $cacheId ); + 'LockManagerGroupFactory' => function ( MediaWikiServices $services ) : LockManagerGroupFactory { + return new LockManagerGroupFactory( + WikiMap::getCurrentWikiDbDomain()->getId(), + $services->getMainConfig()->get( 'LockManagers' ), + $services->getDBLoadBalancerFactory() + ); }, 'MagicWordFactory' => function ( MediaWikiServices $services ) : MagicWordFactory { @@ -290,7 +323,7 @@ return [ "Cache type \"$id\" is not present in \$wgObjectCaches." ); } - return \ObjectCache::newFromParams( $mainConfig->get( 'ObjectCaches' )[$id] ); + return ObjectCache::newFromParams( $mainConfig->get( 'ObjectCaches' )[$id] ); }, 'MainWANObjectCache' => function ( MediaWikiServices $services ) : WANObjectCache { @@ -310,7 +343,7 @@ return [ } $params['store'] = $mainConfig->get( 'ObjectCaches' )[$objectCacheId]; - return \ObjectCache::newWANCacheFromParams( $params ); + return ObjectCache::newWANCacheFromParams( $params ); }, 'MediaHandlerFactory' => function ( MediaWikiServices $services ) : MediaHandlerFactory { @@ -319,6 +352,25 @@ return [ ); }, + 'MessageCache' => function ( MediaWikiServices $services ) : MessageCache { + $mainConfig = $services->getMainConfig(); + return new MessageCache( + $services->getMainWANObjectCache(), + ObjectCache::getInstance( $mainConfig->get( 'MessageCacheType' ) ), + $mainConfig->get( 'UseLocalMessageCache' ) + ? $services->getLocalServerObjectCache() + : new EmptyBagOStuff(), + $mainConfig->get( 'UseDatabaseMessages' ), + $services->getContentLanguage() + ); + }, + + 'MessageFormatterFactory' => + function ( MediaWikiServices $services ) : IMessageFormatterFactory { + // @phan-suppress-next-line PhanAccessMethodInternal + return new MessageFormatterFactory(); + }, + 'MimeAnalyzer' => function ( MediaWikiServices $services ) : MimeAnalyzer { $logger = LoggerFactory::getInstance( 'Mime' ); $mainConfig = $services->getMainConfig(); @@ -377,6 +429,17 @@ return [ return new MimeAnalyzer( $params ); }, + 'MovePageFactory' => function ( MediaWikiServices $services ) : MovePageFactory { + return new MovePageFactory( + new ServiceOptions( MovePageFactory::$constructorOptions, $services->getMainConfig() ), + $services->getDBLoadBalancer(), + $services->getNamespaceInfo(), + $services->getWatchedItemStore(), + $services->getPermissionManager(), + $services->getRepoGroup() + ); + }, + 'NamespaceInfo' => function ( MediaWikiServices $services ) : NamespaceInfo { return new NamespaceInfo( new ServiceOptions( NamespaceInfo::$constructorOptions, $services->getMainConfig() ) ); @@ -390,6 +453,10 @@ return [ ); }, + 'ObjectFactory' => function ( MediaWikiServices $services ) : ObjectFactory { + return new ObjectFactory( $services ); + }, + 'OldRevisionImporter' => function ( MediaWikiServices $services ) : OldRevisionImporter { return new ImportableOldRevisionImporter( true, @@ -432,8 +499,7 @@ return [ // 'class' and 'preprocessorClass' $services->getMainConfig()->get( 'ParserConf' ), // Make sure to have defaults in case someone overrode ParserConf with something silly - [ 'class' => Parser::class, - 'preprocessorClass' => Parser::getDefaultPreprocessorClass() ], + [ 'class' => Parser::class, 'preprocessorClass' => Preprocessor_Hash::class ], // Plus a buch of actual config options $services->getMainConfig() ); @@ -469,17 +535,12 @@ return [ }, 'PermissionManager' => function ( MediaWikiServices $services ) : PermissionManager { - $config = $services->getMainConfig(); return new PermissionManager( + new ServiceOptions( + PermissionManager::$constructorOptions, $services->getMainConfig() + ), $services->getSpecialPageFactory(), $services->getRevisionLookup(), - $config->get( 'WhitelistRead' ), - $config->get( 'WhitelistReadRegexp' ), - $config->get( 'EmailConfirmToEdit' ), - $config->get( 'BlockDisablesLogin' ), - $config->get( 'GroupPermissions' ), - $config->get( 'RevokePermissions' ), - $config->get( 'AvailableRights' ), $services->getNamespaceInfo() ); }, @@ -491,7 +552,8 @@ return [ $services->getContentLanguage(), AuthManager::singleton(), $services->getLinkRendererFactory()->create(), - $services->getNamespaceInfo() + $services->getNamespaceInfo(), + $services->getPermissionManager() ); $factory->setLogger( LoggerFactory::getInstance( 'preferences' ) ); @@ -580,7 +642,7 @@ return [ $services->getCommentStore(), $services->getActorMigration(), $config->get( 'MultiContentRevisionSchemaMigrationStage' ), - LoggerFactory::getProvider(), + LoggerFactory::getInstance( 'RevisionStore' ), $config->get( 'ContentHandlerUseDB' ) ); @@ -678,7 +740,8 @@ return [ return new SpecialPageFactory( new ServiceOptions( SpecialPageFactory::$constructorOptions, $services->getMainConfig() ), - $services->getContentLanguage() + $services->getContentLanguage(), + $services->getObjectFactory() ); }, @@ -688,6 +751,10 @@ return [ ); }, + 'TempFSFileFactory' => function ( MediaWikiServices $services ) : TempFSFileFactory { + return new TempFSFileFactory( $services->getMainConfig()->get( 'TmpDirectory' ) ); + }, + 'TitleFormatter' => function ( MediaWikiServices $services ) : TitleFormatter { return $services->getService( '_MediaWikiTitleCodec' ); }, @@ -726,7 +793,8 @@ return [ $services->getDBLoadBalancer(), $services->getCommentStore(), $services->getActorMigration(), - $services->getWatchedItemStore() + $services->getWatchedItemStore(), + $services->getPermissionManager() ); }, @@ -770,6 +838,7 @@ return [ }, '_SqlBlobStore' => function ( MediaWikiServices $services ) : SqlBlobStore { + // @phan-suppress-next-line PhanAccessMethodInternal return $services->getBlobStoreFactory()->newSqlBlobStore(); }, diff --git a/includes/Setup.php b/includes/Setup.php index 201e1a9d42..d450bdd33c 100644 --- a/includes/Setup.php +++ b/includes/Setup.php @@ -52,6 +52,17 @@ if ( ini_get( 'mbstring.func_overload' ) ) { die( 'MediaWiki does not support installations where mbstring.func_overload is non-zero.' ); } +// Define MW_ENTRY_POINT if it's not already, so that config code can check the +// value without using defined() +if ( !defined( 'MW_ENTRY_POINT' ) ) { + /** + * The entry point, which may be either the script filename without the + * file extension, or "cli" for maintenance scripts, or "unknown" for any + * entry point that does not set the constant. + */ + define( 'MW_ENTRY_POINT', 'unknown' ); +} + // Start the autoloader, so that extensions can derive classes from core files require_once "$IP/includes/AutoLoader.php"; @@ -96,7 +107,7 @@ if ( !interface_exists( 'Psr\Log\LoggerInterface' ) ) { // Install a header callback MediaWiki\HeaderCallback::register(); -// Set the encoding used by reading HTTP input, writing HTTP output. +// Set the encoding used by PHP for reading HTTP input, and writing output. // This is also the default for mbstring functions. mb_internal_encoding( 'UTF-8' ); @@ -128,9 +139,6 @@ if ( defined( 'MW_SETUP_CALLBACK' ) ) { * Main setup */ -$fname = 'Setup.php'; -$ps_setup = Profiler::instance()->scopedProfileIn( $fname ); - // Load queued extensions ExtensionRegistry::getInstance()->loadFromQueue(); // Don't let any other extensions load @@ -141,8 +149,6 @@ putenv( "LC_ALL=$wgShellLocale" ); setlocale( LC_ALL, $wgShellLocale ); // Set various default paths sensibly... -$ps_default = Profiler::instance()->scopedProfileIn( $fname . '-defaults' ); - if ( $wgScript === false ) { $wgScript = "$wgScriptPath/index.php"; } @@ -161,12 +167,6 @@ if ( $wgArticlePath === false ) { } } -if ( !empty( $wgActionPaths ) && !isset( $wgActionPaths['view'] ) ) { - // 'view' is assumed the default action path everywhere in the code - // but is rarely filled in $wgActionPaths - $wgActionPaths['view'] = $wgArticlePath; -} - if ( $wgResourceBasePath === null ) { $wgResourceBasePath = $wgScriptPath; } @@ -368,19 +368,6 @@ foreach ( $wgForeignFileRepos as &$repo ) { unset( $repo ); // no global pollution; destroy reference $rcMaxAgeDays = $wgRCMaxAge / ( 3600 * 24 ); -if ( $wgRCFilterByAge ) { - // Trim down $wgRCLinkDays so that it only lists links which are valid - // as determined by $wgRCMaxAge. - // Note that we allow 1 link higher than the max for things like 56 days but a 60 day link. - sort( $wgRCLinkDays ); - - foreach ( $wgRCLinkDays as $i => $days ) { - if ( $days >= $rcMaxAgeDays ) { - array_splice( $wgRCLinkDays, $i + 1 ); - break; - } - } -} // Ensure that default user options are not invalid, since that breaks Special:Preferences $wgDefaultUserOptions['rcdays'] = min( $wgDefaultUserOptions['rcdays'], @@ -404,6 +391,7 @@ $wgSkipSkins[] = 'apioutput'; if ( $wgLocalInterwiki ) { // Hard deprecated in 1.34. wfDeprecated( '$wgLocalInterwiki – use $wgLocalInterwikis instead', '1.23' ); + // @phan-suppress-next-line PhanUndeclaredVariableDim array_unshift( $wgLocalInterwikis, $wgLocalInterwiki ); } @@ -452,19 +440,15 @@ if ( $wgEnableEmail ) { $wgUsersNotifiedOnAllChanges = []; } -if ( $wgMetaNamespace === false ) { - $wgMetaNamespace = str_replace( ' ', '_', $wgSitename ); +// $wgSysopEmailBans deprecated in 1.34 +if ( isset( $wgSysopEmailBans ) && $wgSysopEmailBans === false ) { + foreach ( $wgGroupPermissions as $group => $_ ) { + unset( $wgGroupPermissions[$group]['blockemail'] ); + } } -// Default value is 2000 or the suhosin limit if it is between 1 and 2000 -if ( $wgResourceLoaderMaxQueryLength === false ) { - $suhosinMaxValueLength = (int)ini_get( 'suhosin.get.max_value_length' ); - if ( $suhosinMaxValueLength > 0 && $suhosinMaxValueLength < 2000 ) { - $wgResourceLoaderMaxQueryLength = $suhosinMaxValueLength; - } else { - $wgResourceLoaderMaxQueryLength = 2000; - } - unset( $suhosinMaxValueLength ); +if ( $wgMetaNamespace === false ) { + $wgMetaNamespace = str_replace( ' ', '_', $wgSitename ); } // Ensure the minimum chunk size is less than PHP upload limits or the maximum @@ -565,12 +549,6 @@ if ( isset( $wgSquidMaxage ) ) { $wgSquidMaxage = $wgCdnMaxAge; } -// Easy to forget to falsify $wgDebugToolbar for static caches. -// If file cache or CDN cache is on, just disable this (DWIMD). -if ( $wgUseFileCache || $wgUseCdn ) { - $wgDebugToolbar = false; -} - // Blacklisted file extensions shouldn't appear on the "allowed" list $wgFileExtensions = array_values( array_diff( $wgFileExtensions, $wgFileBlacklist ) ); @@ -635,17 +613,11 @@ if ( $wgPHPSessionHandling !== 'enable' && if ( defined( 'MW_NO_SESSION' ) ) { // If the entry point wants no session, force 'disable' here unless they // specifically set it to the (undocumented) 'warn'. + // @phan-suppress-next-line PhanUndeclaredConstant $wgPHPSessionHandling = MW_NO_SESSION === 'warn' ? 'warn' : 'disable'; } -Profiler::instance()->scopedProfileOut( $ps_default ); - -// Disable MWDebug for command line mode, this prevents MWDebug from eating up -// all the memory from logging SQL queries on maintenance scripts -global $wgCommandLineMode; -if ( $wgDebugToolbar && !$wgCommandLineMode ) { - MWDebug::init(); -} +MWDebug::setup(); // Reset the global service locator, so any services that have already been created will be // re-created while taking into account any custom settings and extensions. @@ -669,8 +641,6 @@ foreach ( [ 'wgArticlePath', 'wgVariantArticlePath' ] as $varName ) { } } -$ps_default2 = Profiler::instance()->scopedProfileIn( $fname . '-defaults2' ); - if ( $wgCanonicalServer === false ) { $wgCanonicalServer = wfExpandUrl( $wgServer, PROTO_HTTP ); } @@ -740,10 +710,6 @@ if ( $wgSharedDB && $wgSharedTables ) { ); } -Profiler::instance()->scopedProfileOut( $ps_default2 ); - -$ps_misc = Profiler::instance()->scopedProfileIn( $fname . '-misc' ); - // Raise the memory limit if it's too low // Note, this makes use of wfDebug, and thus should not be before // MWDebug::init() is called. @@ -762,7 +728,7 @@ if ( is_null( $wgLocaltimezone ) ) { date_default_timezone_set( $wgLocaltimezone ); if ( is_null( $wgLocalTZoffset ) ) { - $wgLocalTZoffset = date( 'Z' ) / 60; + $wgLocalTZoffset = (int)date( 'Z' ) / 60; } // The part after the System| is ignored, but rest of MW fills it // out as the local offset. @@ -815,21 +781,9 @@ if ( $wgCommandLineMode ) { $wgMemc = ObjectCache::getLocalClusterInstance(); $messageMemc = wfGetMessageCacheStorage(); -wfDebugLog( 'caches', - 'cluster: ' . get_class( $wgMemc ) . - ', WAN: ' . ( $wgMainWANCache === CACHE_NONE ? 'CACHE_NONE' : $wgMainWANCache ) . - ', stash: ' . $wgMainStash . - ', message: ' . get_class( $messageMemc ) . - ', session: ' . get_class( ObjectCache::getInstance( $wgSessionCacheType ) ) -); - -Profiler::instance()->scopedProfileOut( $ps_misc ); - // Most of the config is out, some might want to run hooks here. Hooks::run( 'SetupAfterCache' ); -$ps_globals = Profiler::instance()->scopedProfileIn( $fname . '-globals' ); - /** * @var Language $wgContLang * @deprecated since 1.32, use the ContentLanguage service directly @@ -861,22 +815,17 @@ if ( !defined( 'MW_NO_SESSION' ) && !$wgCommandLineMode ) { // Initialize the session try { $session = MediaWiki\Session\SessionManager::getGlobalSession(); - } catch ( OverflowException $ex ) { - if ( isset( $ex->sessionInfos ) && count( $ex->sessionInfos ) >= 2 ) { - // The exception is because the request had multiple possible - // sessions tied for top priority. Report this to the user. - $list = []; - foreach ( $ex->sessionInfos as $info ) { - $list[] = $info->getProvider()->describe( $wgContLang ); - } - $list = $wgContLang->listToText( $list ); - throw new HttpError( 400, - Message::newFromKey( 'sessionmanager-tie', $list )->inLanguage( $wgContLang )->plain() - ); + } catch ( MediaWiki\Session\SessionOverflowException $ex ) { + // The exception is because the request had multiple possible + // sessions tied for top priority. Report this to the user. + $list = []; + foreach ( $ex->getSessionInfos() as $info ) { + $list[] = $info->getProvider()->describe( $wgContLang ); } - - // Not the one we want, rethrow - throw $ex; + $list = $wgContLang->listToText( $list ); + throw new HttpError( 400, + Message::newFromKey( 'sessionmanager-tie', $list )->inLanguage( $wgContLang )->plain() + ); } if ( $session->isPersistent() ) { @@ -939,9 +888,6 @@ $wgParser = new StubObject( 'wgParser', function () { */ $wgTitle = null; -Profiler::instance()->scopedProfileOut( $ps_globals ); -$ps_extensions = Profiler::instance()->scopedProfileIn( $fname . '-extensions' ); - // Extension setup functions // Entries should be added to this variable during the inclusion // of the extension file. This allows the extension to perform @@ -974,6 +920,3 @@ if ( !$wgCommandLineMode ) { } $wgFullyInitialised = true; - -Profiler::instance()->scopedProfileOut( $ps_extensions ); -Profiler::instance()->scopedProfileOut( $ps_setup ); diff --git a/includes/Status.php b/includes/Status.php index 76b905eca4..932fd2a650 100644 --- a/includes/Status.php +++ b/includes/Status.php @@ -115,6 +115,7 @@ class Status extends StatusValue { * ] * * @return Status[] + * @suppress PhanUndeclaredProperty Status vs StatusValue */ public function splitByErrorType() { list( $errorsOnlyStatus, $warningsOnlyStatus ) = parent::splitByErrorType(); diff --git a/includes/Storage/BlobStore.php b/includes/Storage/BlobStore.php index 8b1112b277..78885db5fe 100644 --- a/includes/Storage/BlobStore.php +++ b/includes/Storage/BlobStore.php @@ -22,6 +22,8 @@ namespace MediaWiki\Storage; +use StatusValue; + /** * Service for loading and storing data blobs. * @@ -95,6 +97,19 @@ interface BlobStore { */ public function getBlob( $blobAddress, $queryFlags = 0 ); + /** + * A batched version of BlobStore::getBlob. + * + * @param string[] $blobAddresses An array of blob addresses. + * @param int $queryFlags See IDBAccessObject. + * @throws BlobAccessException + * @return StatusValue A status with a map of blobAddress => binary blob data or null + * if fetching the blob has failed. Fetch failures errors are the + * warnings in the status object. + * @since 1.34 + */ + public function getBlobBatch( $blobAddresses, $queryFlags = 0 ); + /** * Stores an arbitrary blob of data and returns an address that can be used with * getBlob() to retrieve the same blob of data, diff --git a/includes/Storage/DerivedPageDataUpdater.php b/includes/Storage/DerivedPageDataUpdater.php index 68814ef3aa..b2c003ab4c 100644 --- a/includes/Storage/DerivedPageDataUpdater.php +++ b/includes/Storage/DerivedPageDataUpdater.php @@ -659,7 +659,7 @@ class DerivedPageDataUpdater implements IDBAccessObject, LoggerAwareInterface { $hasLinks = (bool)count( $this->getCanonicalParserOutput()->getLinks() ); } - foreach ( $this->getModifiedSlotRoles() as $role ) { + foreach ( $this->getSlots()->getSlotRoles() as $role ) { $roleHandler = $this->slotRoleRegistry->getRoleHandler( $role ); if ( $roleHandler->supportsArticleCount() ) { $content = $this->getRawContent( $role ); @@ -1208,7 +1208,8 @@ class DerivedPageDataUpdater implements IDBAccessObject, LoggerAwareInterface { } // "created" is forced here - $this->options['created'] = ( $this->pageState['oldId'] === 0 ); + $this->options['created'] = ( $this->options['created'] || + ( $this->pageState['oldId'] === 0 ) ); $this->revision = $revision; @@ -1530,7 +1531,9 @@ class DerivedPageDataUpdater implements IDBAccessObject, LoggerAwareInterface { if ( $this->options['changed'] && $title->getNamespace() == NS_USER_TALK && $shortTitle != $legacyUser->getTitleKey() - && !( $this->revision->isMinor() && $legacyUser->isAllowed( 'nominornewtalk' ) ) + && !( $this->revision->isMinor() && MediaWikiServices::getInstance() + ->getPermissionManager() + ->userHasRight( $legacyUser, 'nominornewtalk' ) ) ) { $recipient = User::newFromName( $shortTitle, false ); if ( !$recipient ) { diff --git a/includes/Storage/NameTableStore.php b/includes/Storage/NameTableStore.php index 5ef03042dc..88f301aee9 100644 --- a/includes/Storage/NameTableStore.php +++ b/includes/Storage/NameTableStore.php @@ -111,7 +111,7 @@ class NameTableStore { * @return IDatabase */ private function getDBConnection( $index, $flags = 0 ) { - return $this->loadBalancer->getConnection( $index, [], $this->domain, $flags ); + return $this->loadBalancer->getConnectionRef( $index, [], $this->domain, $flags ); } /** @@ -160,10 +160,7 @@ class NameTableStore { if ( $id === null ) { // RACE: $name was already in the db, probably just inserted, so load from master. // Use DBO_TRX to avoid missing inserts due to other threads or REPEATABLE-READs. - // ...but not during unit tests, because we need the fake DB tables of the default - // connection. - $connFlags = defined( 'MW_PHPUNIT_TEST' ) ? 0 : ILoadBalancer::CONN_TRX_AUTOCOMMIT; - $table = $this->reloadMap( $connFlags ); + $table = $this->reloadMap( ILoadBalancer::CONN_TRX_AUTOCOMMIT ); $searchResult = array_search( $name, $table, true ); if ( $searchResult === false ) { diff --git a/includes/Storage/PageEditStash.php b/includes/Storage/PageEditStash.php index a0ef07d651..826d52675b 100644 --- a/includes/Storage/PageEditStash.php +++ b/includes/Storage/PageEditStash.php @@ -231,7 +231,7 @@ class PageEditStash { return false; } - $age = time() - wfTimestamp( TS_UNIX, $editInfo->output->getCacheTime() ); + $age = time() - (int)wfTimestamp( TS_UNIX, $editInfo->output->getCacheTime() ); $context['age'] = $age; $isCacheUsable = true; @@ -450,7 +450,7 @@ class PageEditStash { ) { // If an item is renewed, mind the cache TTL determined by config and parser functions. // Put an upper limit on the TTL for sanity to avoid extreme template/file staleness. - $age = time() - wfTimestamp( TS_UNIX, $parserOutput->getCacheTime() ); + $age = time() - (int)wfTimestamp( TS_UNIX, $parserOutput->getCacheTime() ); $ttl = min( $parserOutput->getCacheExpiry() - $age, self::MAX_CACHE_TTL ); // Avoid extremely stale user signature timestamps (T84843) if ( $parserOutput->getFlag( 'user-signature' ) ) { diff --git a/includes/Storage/PageUpdater.php b/includes/Storage/PageUpdater.php index 7246238696..fd555f623e 100644 --- a/includes/Storage/PageUpdater.php +++ b/includes/Storage/PageUpdater.php @@ -882,6 +882,7 @@ class PageUpdater { // TODO: introduce something like an UnsavedRevisionFactory service instead! /** @var MutableRevisionRecord $rev */ $rev = $this->derivedDataUpdater->getRevision(); + '@phan-var MutableRevisionRecord $rev'; $rev->setPageId( $title->getArticleID() ); diff --git a/includes/Storage/SqlBlobStore.php b/includes/Storage/SqlBlobStore.php index d1b688b201..bcbc9e8a8f 100644 --- a/includes/Storage/SqlBlobStore.php +++ b/includes/Storage/SqlBlobStore.php @@ -26,12 +26,14 @@ namespace MediaWiki\Storage; +use AppendIterator; use DBAccessObjectUtils; use IDBAccessObject; use IExpiringStore; use InvalidArgumentException; use Language; use MWException; +use StatusValue; use WANObjectCache; use ExternalStoreAccess; use Wikimedia\Assert\Assert; @@ -101,10 +103,10 @@ class SqlBlobStore implements IDBAccessObject, BlobStore { * @param ExternalStoreAccess $extStoreAccess Access layer for external storage * @param WANObjectCache $cache A cache manager for caching blobs. This can be the local * wiki's default instance even if $dbDomain refers to a different wiki, since - * makeGlobalKey() is used to constructed a key that allows cached blobs from the - * same database to be re-used between wikis. For example, enwiki and frwiki will - * use the same cache keys for blobs from the wikidatawiki database, regardless of - * the cache's default key space. + * makeGlobalKey() is used to construct a key that allows cached blobs from the + * same database to be re-used between wikis. For example, wiki A and wiki B will + * use the same cache keys for blobs fetched from wiki C, regardless of the + * wiki-specific default key space. * @param bool|string $dbDomain The ID of the target wiki database. Use false for the local wiki. */ public function __construct( @@ -277,108 +279,185 @@ class SqlBlobStore implements IDBAccessObject, BlobStore { public function getBlob( $blobAddress, $queryFlags = 0 ) { Assert::parameterType( 'string', $blobAddress, '$blobAddress' ); - // No negative caching; negative hits on text rows may be due to corrupted replica DBs + $error = null; $blob = $this->cache->getWithSetCallback( $this->getCacheKey( $blobAddress ), $this->getCacheTTL(), - function ( $unused, &$ttl, &$setOpts ) use ( $blobAddress, $queryFlags ) { + function ( $unused, &$ttl, &$setOpts ) use ( $blobAddress, $queryFlags, &$error ) { // Ignore $setOpts; blobs are immutable and negatives are not cached - return $this->fetchBlob( $blobAddress, $queryFlags ); + list( $result, $errors ) = $this->fetchBlobs( [ $blobAddress ], $queryFlags ); + // No negative caching; negative hits on text rows may be due to corrupted replica DBs + $error = $errors[$blobAddress] ?? null; + return $result[$blobAddress]; }, [ 'pcGroup' => self::TEXT_CACHE_GROUP, 'pcTTL' => IExpiringStore::TTL_PROC_LONG ] ); - if ( $blob === false ) { - throw new BlobAccessException( 'Failed to load blob from address ' . $blobAddress ); + if ( $error ) { + throw new BlobAccessException( $error ); } + Assert::postcondition( is_string( $blob ), 'Blob must not be null' ); return $blob; } + /** + * A batched version of BlobStore::getBlob. + * + * @param string[] $blobAddresses An array of blob addresses. + * @param int $queryFlags See IDBAccessObject. + * @throws BlobAccessException + * @return StatusValue A status with a map of blobAddress => binary blob data or null + * if fetching the blob has failed. Fetch failures errors are the + * warnings in the status object. + * @since 1.34 + */ + public function getBlobBatch( $blobAddresses, $queryFlags = 0 ) { + $errors = null; + $addressByCacheKey = $this->cache->makeMultiKeys( + $blobAddresses, + function ( $blobAddress ) { + return $this->getCacheKey( $blobAddress ); + } + ); + $blobsByCacheKey = $this->cache->getMultiWithUnionSetCallback( + $addressByCacheKey, + $this->getCacheTTL(), + function ( array $blobAddresses, array &$ttls, array &$setOpts ) use ( $queryFlags, &$errors ) { + // Ignore $setOpts; blobs are immutable and negatives are not cached + list( $result, $errors ) = $this->fetchBlobs( $blobAddresses, $queryFlags ); + return $result; + }, + [ 'pcGroup' => self::TEXT_CACHE_GROUP, 'pcTTL' => IExpiringStore::TTL_PROC_LONG ] + ); + + // Remap back to incoming blob addresses. The return value of the + // WANObjectCache::getMultiWithUnionSetCallback is keyed on the internal + // keys from WANObjectCache::makeMultiKeys, so we need to remap them + // before returning to the client. + $blobsByAddress = []; + foreach ( $blobsByCacheKey as $cacheKey => $blob ) { + $blobsByAddress[ $addressByCacheKey[ $cacheKey ] ] = $blob !== false ? $blob : null; + } + + $result = StatusValue::newGood( $blobsByAddress ); + if ( $errors ) { + foreach ( $errors as $error ) { + $result->warning( 'internalerror', $error ); + } + } + return $result; + } + /** * MCR migration note: this corresponds to Revision::fetchText * - * @param string $blobAddress + * @param string[] $blobAddresses * @param int $queryFlags * * @throws BlobAccessException - * @return string|false - */ - private function fetchBlob( $blobAddress, $queryFlags ) { - list( $schema, $id, ) = self::splitBlobAddress( $blobAddress ); + * @return array [ $result, $errors ] A map of blob addresses to successfully fetched blobs + * or false if fetch failed, plus and array of errors + */ + private function fetchBlobs( $blobAddresses, $queryFlags ) { + $textIdToBlobAddress = []; + $result = []; + $errors = []; + foreach ( $blobAddresses as $blobAddress ) { + list( $schema, $id ) = self::splitBlobAddress( $blobAddress ); + //TODO: MCR: also support 'ex' schema with ExternalStore URLs, plus flags encoded in the URL! + if ( $schema === 'tt' ) { + $textId = intval( $id ); + $textIdToBlobAddress[$textId] = $blobAddress; + } else { + $errors[$blobAddress] = "Unknown blob address schema: $schema"; + $result[$blobAddress] = false; + continue; + } - //TODO: MCR: also support 'ex' schema with ExternalStore URLs, plus flags encoded in the URL! - if ( $schema === 'tt' ) { - $textId = intval( $id ); - } else { - // XXX: change to better exceptions! That makes migration more difficult, though. - throw new BlobAccessException( "Unknown blob address schema: $schema" ); + if ( !$textId || $id !== (string)$textId ) { + $errors[$blobAddress] = "Bad blob address: $blobAddress"; + $result[$blobAddress] = false; + } } - if ( !$textId || $id !== (string)$textId ) { - // XXX: change to better exceptions! That makes migration more difficult, though. - throw new BlobAccessException( "Bad blob address: $blobAddress" ); + $textIds = array_keys( $textIdToBlobAddress ); + if ( !$textIds ) { + return [ $result, $errors ]; } - // Callers doing updates will pass in READ_LATEST as usual. Since the text/blob tables // do not normally get rows changed around, set READ_LATEST_IMMUTABLE in those cases. $queryFlags |= DBAccessObjectUtils::hasFlags( $queryFlags, self::READ_LATEST ) ? self::READ_LATEST_IMMUTABLE : 0; - list( $index, $options, $fallbackIndex, $fallbackOptions ) = DBAccessObjectUtils::getDBOptions( $queryFlags ); - // Text data is immutable; check replica DBs first. - $row = $this->getDBConnection( $index )->selectRow( + $dbConnection = $this->getDBConnection( $index ); + $rows = $dbConnection->select( 'text', - [ 'old_text', 'old_flags' ], - [ 'old_id' => $textId ], + [ 'old_id', 'old_text', 'old_flags' ], + [ 'old_id' => $textIds ], __METHOD__, $options ); - // Fallback to DB_MASTER in some cases if the row was not found, using the appropriate + // Fallback to DB_MASTER in some cases if not all the rows were found, using the appropriate // options, such as FOR UPDATE to avoid missing rows due to REPEATABLE-READ. - if ( !$row && $fallbackIndex !== null ) { - $row = $this->getDBConnection( $fallbackIndex )->selectRow( + if ( $dbConnection->numRows( $rows ) !== count( $textIds ) && $fallbackIndex !== null ) { + $fetchedTextIds = []; + foreach ( $rows as $row ) { + $fetchedTextIds[] = $row->old_id; + } + $missingTextIds = array_diff( $textIds, $fetchedTextIds ); + $dbConnection = $this->getDBConnection( $fallbackIndex ); + $rowsFromFallback = $dbConnection->select( 'text', - [ 'old_text', 'old_flags' ], - [ 'old_id' => $textId ], + [ 'old_id', 'old_text', 'old_flags' ], + [ 'old_id' => $missingTextIds ], __METHOD__, $fallbackOptions ); + $appendIterator = new AppendIterator(); + $appendIterator->append( $rows ); + $appendIterator->append( $rowsFromFallback ); + $rows = $appendIterator; } - if ( !$row ) { - wfWarn( __METHOD__ . ": No text row with ID $textId." ); - return false; + foreach ( $rows as $row ) { + $blobAddress = $textIdToBlobAddress[$row->old_id]; + $blob = $this->expandBlob( $row->old_text, $row->old_flags, $blobAddress ); + if ( $blob === false ) { + $errors[$blobAddress] = "Bad data in text row {$row->old_id}."; + } + $result[$blobAddress] = $blob; } - $blob = $this->expandBlob( $row->old_text, $row->old_flags, $blobAddress ); - - if ( $blob === false ) { - wfLogWarning( __METHOD__ . ": Bad data in text row $textId." ); - return false; + // If we're still missing some of the rows, set errors for missing blobs. + if ( count( $result ) !== count( $blobAddresses ) ) { + foreach ( $blobAddresses as $blobAddress ) { + if ( !isset( $result[$blobAddress ] ) ) { + $errors[$blobAddress] = "Unable to fetch blob at $blobAddress"; + $result[$blobAddress] = false; + } + } } - - return $blob; + return [ $result, $errors ]; } /** * Get a cache key for a given Blob address. * * The cache key is constructed in a way that allows cached blobs from the same database - * to be re-used between wikis. For example, enwiki and frwiki will use the same cache keys - * for blobs from the wikidatawiki database. + * to be re-used between wikis. For example, wiki A and wiki B will use the same cache keys + * for blobs fetched from wiki C. * * @param string $blobAddress * @return string */ private function getCacheKey( $blobAddress ) { return $this->cache->makeGlobalKey( - 'BlobStore', - 'address', + 'SqlBlobStore-blob', $this->dbLoadBalancer->resolveDomainID( $this->dbDomain ), $blobAddress ); diff --git a/includes/StreamFile.php b/includes/StreamFile.php index 2ad42e5616..c9b2c33b9a 100644 --- a/includes/StreamFile.php +++ b/includes/StreamFile.php @@ -25,8 +25,10 @@ */ class StreamFile { // Do not send any HTTP headers unless requested by caller (e.g. body only) + /** @deprecated since 1.34 */ const STREAM_HEADLESS = HTTPFileStreamer::STREAM_HEADLESS; // Do not try to tear down any PHP output buffers + /** @deprecated since 1.34 */ const STREAM_ALLOW_OB = HTTPFileStreamer::STREAM_ALLOW_OB; /** @@ -66,8 +68,10 @@ class StreamFile { * @param string $fname Full name and path of the file to stream * @param int $flags Bitfield of STREAM_* constants * @since 1.24 + * @deprecated since 1.34, use HTTPFileStreamer::send404Message() instead */ public static function send404Message( $fname, $flags = 0 ) { + wfDeprecated( __METHOD__, '1.34' ); HTTPFileStreamer::send404Message( $fname, $flags ); } @@ -78,8 +82,10 @@ class StreamFile { * @param int $size File size * @return array|string Returns error string on failure (start, end, length) * @since 1.24 + * @deprecated since 1.34, use HTTPFileStreamer::parseRange() instead */ public static function parseRange( $range, $size ) { + wfDeprecated( __METHOD__, '1.34' ); return HTTPFileStreamer::parseRange( $range, $size ); } @@ -105,7 +111,6 @@ class StreamFile { case 'png': return 'image/png'; case 'jpg': - return 'image/jpeg'; case 'jpeg': return 'image/jpeg'; } diff --git a/includes/TemplatesOnThisPageFormatter.php b/includes/TemplatesOnThisPageFormatter.php index bca1ef651a..215e4ec813 100644 --- a/includes/TemplatesOnThisPageFormatter.php +++ b/includes/TemplatesOnThisPageFormatter.php @@ -20,6 +20,7 @@ use MediaWiki\Linker\LinkRenderer; use MediaWiki\Linker\LinkTarget; +use MediaWiki\MediaWikiServices; /** * Handles formatting for the "templates used on this page" @@ -158,11 +159,13 @@ class TemplatesOnThisPageFormatter { * Return a link to the edit page, with the text * saying "view source" if the user can't edit the page * - * @param Title $titleObj + * @param LinkTarget $titleObj * @return string */ - private function buildEditLink( Title $titleObj ) { - if ( $titleObj->quickUserCan( 'edit', $this->context->getUser() ) ) { + private function buildEditLink( LinkTarget $titleObj ) { + if ( MediaWikiServices::getInstance()->getPermissionManager() + ->quickUserCan( 'edit', $this->context->getUser(), $titleObj ) + ) { $linkMsg = 'editlink'; } else { $linkMsg = 'viewsourcelink'; diff --git a/includes/Title.php b/includes/Title.php index 281f75bac1..6c15a062b0 100644 --- a/includes/Title.php +++ b/includes/Title.php @@ -23,7 +23,7 @@ */ use MediaWiki\Permissions\PermissionManager; -use MediaWiki\Storage\RevisionRecord; +use MediaWiki\Revision\RevisionRecord; use Wikimedia\Assert\Assert; use Wikimedia\Rdbms\Database; use Wikimedia\Rdbms\IDatabase; @@ -51,10 +51,11 @@ class Title implements LinkTarget, IDBAccessObject { const CACHE_MAX = 1000; /** - * Used to be GAID_FOR_UPDATE define. Used with getArticleID() and friends - * to use the master DB + * Used to be GAID_FOR_UPDATE define(). Used with getArticleID() and friends + * to use the master DB and inject it into link cache. + * @deprecated since 1.34, use Title::READ_LATEST instead. */ - const GAID_FOR_UPDATE = 1; + const GAID_FOR_UPDATE = 512; /** * Flag for use with factory methods like newFromLinkTarget() that have @@ -74,25 +75,18 @@ class Title implements LinkTarget, IDBAccessObject { /** @var string Text form (spaces not underscores) of the main part */ public $mTextform = ''; - /** @var string URL-encoded form of the main part */ public $mUrlform = ''; - /** @var string Main part with underscores */ public $mDbkeyform = ''; - /** @var string Database key with the initial letter in the case specified by the user */ protected $mUserCaseDBKey; - /** @var int Namespace index, i.e. one of the NS_xxxx constants */ public $mNamespace = NS_MAIN; - /** @var string Interwiki prefix */ public $mInterwiki = ''; - /** @var bool Was this Title created from a string with a local interwiki prefix? */ private $mLocalInterwiki = false; - /** @var string Title fragment (i.e. the bit after the #) */ public $mFragment = ''; @@ -178,8 +172,8 @@ class Title implements LinkTarget, IDBAccessObject { /** @var bool Whether a page has any subpages */ private $mHasSubpages; - /** @var bool The (string) language code of the page's language and content code. */ - private $mPageLanguage = false; + /** @var array|null The (string) language code of the page's language and content code. */ + private $mPageLanguage; /** @var string|bool|null The page language code from the database, null if not saved in * the database or false if not loaded, yet. @@ -467,16 +461,18 @@ class Title implements LinkTarget, IDBAccessObject { * Create a new Title from an article ID * * @param int $id The page_id corresponding to the Title to create - * @param int $flags Use Title::GAID_FOR_UPDATE to use master + * @param int $flags Bitfield of class READ_* constants * @return Title|null The new object, or null on an error */ public static function newFromID( $id, $flags = 0 ) { - $db = ( $flags & self::GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_REPLICA ); - $row = $db->selectRow( + $flags |= ( $flags & self::GAID_FOR_UPDATE ) ? self::READ_LATEST : 0; // b/c + list( $index, $options ) = DBAccessObjectUtils::getDBOptions( $flags ); + $row = wfGetDB( $index )->selectRow( 'page', self::getSelectFields(), [ 'page_id' => $id ], - __METHOD__ + __METHOD__, + $options ); if ( $row !== false ) { $title = self::newFromRow( $row ); @@ -545,10 +541,10 @@ class Title implements LinkTarget, IDBAccessObject { if ( isset( $row->page_latest ) ) { $this->mLatestID = (int)$row->page_latest; } - if ( !$this->mForcedContentModel && isset( $row->page_content_model ) ) { - $this->mContentModel = (string)$row->page_content_model; - } elseif ( !$this->mForcedContentModel ) { - $this->mContentModel = false; # initialized lazily in getContentModel() + if ( isset( $row->page_content_model ) ) { + $this->lazyFillContentModel( $row->page_content_model ); + } else { + $this->lazyFillContentModel( false ); // lazily-load getContentModel() } if ( isset( $row->page_lang ) ) { $this->mDbPageLanguage = (string)$row->page_lang; @@ -561,9 +557,7 @@ class Title implements LinkTarget, IDBAccessObject { $this->mLength = 0; $this->mRedirect = false; $this->mLatestID = 0; - if ( !$this->mForcedContentModel ) { - $this->mContentModel = false; # initialized lazily in getContentModel() - } + $this->lazyFillContentModel( false ); // lazily-load getContentModel() } } @@ -598,7 +592,6 @@ class Title implements LinkTarget, IDBAccessObject { $t->mArticleID = ( $ns >= 0 ) ? -1 : 0; $t->mUrlform = wfUrlencode( $t->mDbkeyform ); $t->mTextform = strtr( $title, '_', ' ' ); - $t->mContentModel = false; # initialized lazily in getContentModel() return $t; } @@ -676,7 +669,7 @@ class Title implements LinkTarget, IDBAccessObject { * Get the prefixed DB key associated with an ID * * @param int $id The page_id of the article - * @return Title|null An object representing the article, or null if no such article was found + * @return string|null An object representing the article, or null if no such article was found */ public static function nameOf( $id ) { $dbr = wfGetDB( DB_REPLICA ); @@ -691,8 +684,7 @@ class Title implements LinkTarget, IDBAccessObject { return null; } - $n = self::makeName( $s->page_namespace, $s->page_title ); - return $n; + return self::makeName( $s->page_namespace, $s->page_title ); } /** @@ -1051,21 +1043,31 @@ class Title implements LinkTarget, IDBAccessObject { * * @todo Deprecate this in favor of SlotRecord::getModel() * - * @param int $flags A bit field; may be Title::GAID_FOR_UPDATE to select for update + * @param int $flags Either a bitfield of class READ_* constants or GAID_FOR_UPDATE * @return string Content model id */ public function getContentModel( $flags = 0 ) { - if ( !$this->mForcedContentModel - && ( !$this->mContentModel || $flags === self::GAID_FOR_UPDATE ) - && $this->getArticleID( $flags ) + if ( $this->mForcedContentModel ) { + if ( !$this->mContentModel ) { + throw new RuntimeException( 'Got out of sync; an empty model is being forced' ); + } + // Content model is locked to the currently loaded one + return $this->mContentModel; + } + + if ( DBAccessObjectUtils::hasFlags( $flags, self::READ_LATEST ) ) { + $this->lazyFillContentModel( $this->loadFieldFromDB( 'page_content_model', $flags ) ); + } elseif ( + ( !$this->mContentModel || $flags & self::GAID_FOR_UPDATE ) && + $this->getArticleID( $flags ) ) { $linkCache = MediaWikiServices::getInstance()->getLinkCache(); $linkCache->addLinkObj( $this ); # in case we already had an article ID - $this->mContentModel = $linkCache->getGoodLinkFieldObj( $this, 'model' ); + $this->lazyFillContentModel( $linkCache->getGoodLinkFieldObj( $this, 'model' ) ); } if ( !$this->mContentModel ) { - $this->mContentModel = ContentHandler::getDefaultModelFor( $this ); + $this->lazyFillContentModel( ContentHandler::getDefaultModelFor( $this ) ); } return $this->mContentModel; @@ -1082,21 +1084,38 @@ class Title implements LinkTarget, IDBAccessObject { } /** - * Set a proposed content model for the page for permissions - * checking. This does not actually change the content model - * of a title! + * Set a proposed content model for the page for permissions checking + * + * This does not actually change the content model of a title in the DB. + * It only affects this particular Title instance. The content model is + * forced to remain this value until another setContentModel() call. * - * Additionally, you should make sure you've checked - * ContentHandler::canBeUsedOn() first. + * ContentHandler::canBeUsedOn() should be checked before calling this + * if there is any doubt regarding the applicability of the content model * * @since 1.28 * @param string $model CONTENT_MODEL_XXX constant */ public function setContentModel( $model ) { + if ( (string)$model === '' ) { + throw new InvalidArgumentException( "Missing CONTENT_MODEL_* constant" ); + } + $this->mContentModel = $model; $this->mForcedContentModel = true; } + /** + * If the content model field is not frozen then update it with a retreived value + * + * @param string|bool $model CONTENT_MODEL_XXX constant or false + */ + private function lazyFillContentModel( $model ) { + if ( !$this->mForcedContentModel ) { + $this->mContentModel = ( $model === false ) ? false : (string)$model; + } + } + /** * Get the namespace text * @@ -1250,6 +1269,7 @@ class Title implements LinkTarget, IDBAccessObject { * @param int|int[] $namespaces,... The namespaces to check for * @return bool * @since 1.19 + * @suppress PhanCommentParamOnEmptyParamList Cannot make variadic due to HHVM bug, T191668#5263929 */ public function inNamespaces( /* ... */ ) { $namespaces = func_get_args(); @@ -1541,14 +1561,32 @@ class Title implements LinkTarget, IDBAccessObject { * Get a Title object associated with the talk page of this article * * @deprecated since 1.34, use getTalkPageIfDefined() or NamespaceInfo::getTalkPage() - * with NamespaceInfo::canHaveTalkPage(). + * with NamespaceInfo::canHaveTalkPage(). Note that the new method will + * throw if asked for the talk page of a section-only link, or of an interwiki + * link. * @return Title The object for the talk page * @throws MWException if $target doesn't have talk pages, e.g. because it's in NS_SPECIAL * or because it's a relative link, or an interwiki link. */ public function getTalkPage() { - return self::castFromLinkTarget( - MediaWikiServices::getInstance()->getNamespaceInfo()->getTalkPage( $this ) ); + // NOTE: The equivalent code in NamespaceInfo is less lenient about producing invalid titles. + // Instead of failing on invalid titles, let's just log the issue for now. + // See the discussion on T227817. + + // Is this the same title? + $talkNS = MediaWikiServices::getInstance()->getNamespaceInfo()->getTalk( $this->mNamespace ); + if ( $this->mNamespace == $talkNS ) { + return $this; + } + + $title = self::makeTitle( $talkNS, $this->mDbkeyform ); + + $this->warnIfPageCannotExist( $title, __METHOD__ ); + + return $title; + // TODO: replace the above with the code below: + // return self::castFromLinkTarget( + // MediaWikiServices::getInstance()->getNamespaceInfo()->getTalkPage( $this ) ); } /** @@ -1576,8 +1614,51 @@ class Title implements LinkTarget, IDBAccessObject { * @return Title The object for the subject page */ public function getSubjectPage() { - return self::castFromLinkTarget( - MediaWikiServices::getInstance()->getNamespaceInfo()->getSubjectPage( $this ) ); + // Is this the same title? + $subjectNS = MediaWikiServices::getInstance()->getNamespaceInfo() + ->getSubject( $this->mNamespace ); + if ( $this->mNamespace == $subjectNS ) { + return $this; + } + // NOTE: The equivalent code in NamespaceInfo is less lenient about producing invalid titles. + // Instead of failing on invalid titles, let's just log the issue for now. + // See the discussion on T227817. + $title = self::makeTitle( $subjectNS, $this->mDbkeyform ); + + $this->warnIfPageCannotExist( $title, __METHOD__ ); + + return $title; + // TODO: replace the above with the code below: + // return self::castFromLinkTarget( + // MediaWikiServices::getInstance()->getNamespaceInfo()->getSubjectPage( $this ) ); + } + + /** + * @param Title $title + * @param string $method + * + * @return bool whether a warning was issued + */ + private function warnIfPageCannotExist( Title $title, $method ) { + if ( $this->getText() == '' ) { + wfLogWarning( + $method . ': called on empty title ' . $this->getFullText() . ', returning ' + . $title->getFullText() + ); + + return true; + } + + if ( $this->getInterwiki() !== '' ) { + wfLogWarning( + $method . ': called on interwiki title ' . $this->getFullText() . ', returning ' + . $title->getFullText() + ); + + return true; + } + + return false; } /** @@ -1590,8 +1671,23 @@ class Title implements LinkTarget, IDBAccessObject { * @return Title */ public function getOtherPage() { - return self::castFromLinkTarget( - MediaWikiServices::getInstance()->getNamespaceInfo()->getAssociatedPage( $this ) ); + // NOTE: Depend on the methods in this class instead of their equivalent in NamespaceInfo, + // until their semantics has become exactly the same. + // See the discussion on T227817. + if ( $this->isSpecialPage() ) { + throw new MWException( 'Special pages cannot have other pages' ); + } + if ( $this->isTalkPage() ) { + return $this->getSubjectPage(); + } else { + if ( !$this->canHaveTalkPage() ) { + throw new MWException( "{$this->getPrefixedText()} does not have an other page" ); + } + return $this->getTalkPage(); + } + // TODO: replace the above with the code below: + // return self::castFromLinkTarget( + // MediaWikiServices::getInstance()->getNamespaceInfo()->getAssociatedPage( $this ) ); } /** @@ -1966,7 +2062,7 @@ class Title implements LinkTarget, IDBAccessObject { * * @see self::getLocalURL for the arguments. * @see wfExpandUrl - * @param string|string[] $query + * @param string|array $query * @param string|string[]|bool $query2 * @param string|int|null $proto Protocol type to use in URL * @return string The URL @@ -2027,7 +2123,7 @@ class Title implements LinkTarget, IDBAccessObject { * valid to link, locally, to the current Title. * @see self::newFromText to produce a Title object. * - * @param string|string[] $query An optional query string, + * @param string|array $query An optional query string, * not used for interwiki links. Can be specified as an associative array as well, * e.g., [ 'action' => 'edit' ] (keys and values will be URL-escaped). * Some query patterns will trigger various shorturl path replacements. @@ -2041,7 +2137,7 @@ class Title implements LinkTarget, IDBAccessObject { * @return string String of the URL. */ public function getLocalURL( $query = '', $query2 = false ) { - global $wgArticlePath, $wgScript, $wgServer, $wgRequest; + global $wgArticlePath, $wgScript, $wgServer, $wgRequest, $wgMainPageIsDomainRoot; $query = self::fixUrlQueryArgs( $query, $query2 ); @@ -2067,16 +2163,18 @@ class Title implements LinkTarget, IDBAccessObject { $url = false; $matches = []; - if ( !empty( $wgActionPaths ) + $articlePaths = PathRouter::getActionPaths( $wgActionPaths, $wgArticlePath ); + + if ( $articlePaths && preg_match( '/^(.*&|)action=([^&]*)(&(.*)|)$/', $query, $matches ) ) { $action = urldecode( $matches[2] ); - if ( isset( $wgActionPaths[$action] ) ) { + if ( isset( $articlePaths[$action] ) ) { $query = $matches[1]; if ( isset( $matches[4] ) ) { $query .= $matches[4]; } - $url = str_replace( '$1', $dbkey, $wgActionPaths[$action] ); + $url = str_replace( '$1', $dbkey, $articlePaths[$action] ); if ( $query != '' ) { $url = wfAppendQuery( $url, $query ); } @@ -2116,6 +2214,11 @@ class Title implements LinkTarget, IDBAccessObject { $url = $wgServer . $url; } } + + if ( $wgMainPageIsDomainRoot && $this->isMainPage() && $query === '' ) { + return '/'; + } + // Avoid PHP 7.1 warning from passing $this by reference $titleRef = $this; Hooks::run( 'GetLocalURL', [ &$titleRef, &$url, $query ] ); @@ -2160,7 +2263,7 @@ class Title implements LinkTarget, IDBAccessObject { * protocol-relative, the URL will be expanded to http:// * * @see self::getLocalURL for the arguments. - * @param string|string[] $query + * @param string|array $query * @param string|bool $query2 Deprecated * @return string The URL */ @@ -2183,7 +2286,7 @@ class Title implements LinkTarget, IDBAccessObject { * NOTE: Unlike getInternalURL(), the canonical URL includes the fragment * * @see self::getLocalURL for the arguments. - * @param string|string[] $query + * @param string|array $query * @param string|bool $query2 Deprecated * @return string The URL * @since 1.18 @@ -2499,6 +2602,7 @@ class Title implements LinkTarget, IDBAccessObject { * Determines if $user is unable to edit this page because it has been protected * by $wgNamespaceProtection. * + * @deprecated since 1.34 Don't use this function in new code. * @param User $user User object to check permissions * @return bool */ @@ -2506,8 +2610,9 @@ class Title implements LinkTarget, IDBAccessObject { global $wgNamespaceProtection; if ( isset( $wgNamespaceProtection[$this->mNamespace] ) ) { + $permissionManager = MediaWikiServices::getInstance()->getPermissionManager(); foreach ( (array)$wgNamespaceProtection[$this->mNamespace] as $right ) { - if ( $right != '' && !$user->isAllowed( $right ) ) { + if ( !$permissionManager->userHasRight( $user, $right ) ) { return true; } } @@ -2793,10 +2898,7 @@ class Title implements LinkTarget, IDBAccessObject { return; } - // TODO: should probably pass $flags into getArticleID, but it seems hacky - // to mix READ_LATEST and GAID_FOR_UPDATE, even if they have the same value. - // Maybe deprecate GAID_FOR_UPDATE now that we implement IDBAccessObject? - $id = $this->getArticleID(); + $id = $this->getArticleID( $flags ); if ( $id ) { $fname = __METHOD__; $loadRestrictionsFromDb = function ( IDatabase $dbr ) use ( $fname, $id ) { @@ -2952,7 +3054,7 @@ class Title implements LinkTarget, IDBAccessObject { } $dbr = wfGetDB( DB_REPLICA ); - $conds['page_namespace'] = $this->mNamespace; + $conds = [ 'page_namespace' => $this->mNamespace ]; $conds[] = 'page_title ' . $dbr->buildLike( $this->mDbkeyform . '/', $dbr->anyString() ); $options = []; if ( $limit > -1 ) { @@ -3020,24 +3122,28 @@ class Title implements LinkTarget, IDBAccessObject { * Get the article ID for this Title from the link cache, * adding it if necessary * - * @param int $flags A bit field; may be Title::GAID_FOR_UPDATE to select - * for update + * @param int $flags Either a bitfield of class READ_* constants or GAID_FOR_UPDATE * @return int The ID */ public function getArticleID( $flags = 0 ) { if ( $this->mNamespace < 0 ) { $this->mArticleID = 0; + return $this->mArticleID; } + $linkCache = MediaWikiServices::getInstance()->getLinkCache(); if ( $flags & self::GAID_FOR_UPDATE ) { $oldUpdate = $linkCache->forUpdate( true ); $linkCache->clearLink( $this ); $this->mArticleID = $linkCache->addLinkObj( $this ); $linkCache->forUpdate( $oldUpdate ); + } elseif ( DBAccessObjectUtils::hasFlags( $flags, self::READ_LATEST ) ) { + $this->mArticleID = (int)$this->loadFieldFromDB( 'page_id', $flags ); } elseif ( $this->mArticleID == -1 ) { $this->mArticleID = $linkCache->addLinkObj( $this ); } + return $this->mArticleID; } @@ -3045,33 +3151,27 @@ class Title implements LinkTarget, IDBAccessObject { * Is this an article that is a redirect page? * Uses link cache, adding it if necessary * - * @param int $flags A bit field; may be Title::GAID_FOR_UPDATE to select for update + * @param int $flags Either a bitfield of class READ_* constants or GAID_FOR_UPDATE * @return bool */ public function isRedirect( $flags = 0 ) { - if ( !is_null( $this->mRedirect ) ) { - return $this->mRedirect; - } - if ( !$this->getArticleID( $flags ) ) { - $this->mRedirect = false; - return $this->mRedirect; - } + if ( DBAccessObjectUtils::hasFlags( $flags, self::READ_LATEST ) ) { + $this->mRedirect = (bool)$this->loadFieldFromDB( 'page_is_redirect', $flags ); + } else { + if ( $this->mRedirect !== null ) { + return $this->mRedirect; + } elseif ( !$this->getArticleID( $flags ) ) { + $this->mRedirect = false; - $linkCache = MediaWikiServices::getInstance()->getLinkCache(); - $linkCache->addLinkObj( $this ); # in case we already had an article ID - $cached = $linkCache->getGoodLinkFieldObj( $this, 'redirect' ); - if ( $cached === null ) { - # Trust LinkCache's state over our own - # LinkCache is telling us that the page doesn't exist, despite there being cached - # data relating to an existing page in $this->mArticleID. Updaters should clear - # LinkCache as appropriate, or use $flags = Title::GAID_FOR_UPDATE. If that flag is - # set, then LinkCache will definitely be up to date here, since getArticleID() forces - # LinkCache to refresh its data from the master. - $this->mRedirect = false; - return $this->mRedirect; - } + return $this->mRedirect; + } - $this->mRedirect = (bool)$cached; + $linkCache = MediaWikiServices::getInstance()->getLinkCache(); + $linkCache->addLinkObj( $this ); // in case we already had an article ID + // Note that LinkCache returns null if it thinks the page does not exist; + // always trust the state of LinkCache over that of this Title instance. + $this->mRedirect = (bool)$linkCache->getGoodLinkFieldObj( $this, 'redirect' ); + } return $this->mRedirect; } @@ -3080,27 +3180,26 @@ class Title implements LinkTarget, IDBAccessObject { * What is the length of this page? * Uses link cache, adding it if necessary * - * @param int $flags A bit field; may be Title::GAID_FOR_UPDATE to select for update + * @param int $flags Either a bitfield of class READ_* constants or GAID_FOR_UPDATE * @return int */ public function getLength( $flags = 0 ) { - if ( $this->mLength != -1 ) { - return $this->mLength; - } - if ( !$this->getArticleID( $flags ) ) { - $this->mLength = 0; - return $this->mLength; - } - $linkCache = MediaWikiServices::getInstance()->getLinkCache(); - $linkCache->addLinkObj( $this ); # in case we already had an article ID - $cached = $linkCache->getGoodLinkFieldObj( $this, 'length' ); - if ( $cached === null ) { - # Trust LinkCache's state over our own, as for isRedirect() - $this->mLength = 0; - return $this->mLength; - } + if ( DBAccessObjectUtils::hasFlags( $flags, self::READ_LATEST ) ) { + $this->mLength = (int)$this->loadFieldFromDB( 'page_len', $flags ); + } else { + if ( $this->mLength != -1 ) { + return $this->mLength; + } elseif ( !$this->getArticleID( $flags ) ) { + $this->mLength = 0; + return $this->mLength; + } - $this->mLength = intval( $cached ); + $linkCache = MediaWikiServices::getInstance()->getLinkCache(); + $linkCache->addLinkObj( $this ); // in case we already had an article ID + // Note that LinkCache returns null if it thinks the page does not exist; + // always trust the state of LinkCache over that of this Title instance. + $this->mLength = (int)$linkCache->getGoodLinkFieldObj( $this, 'length' ); + } return $this->mLength; } @@ -3108,49 +3207,46 @@ class Title implements LinkTarget, IDBAccessObject { /** * What is the page_latest field for this page? * - * @param int $flags A bit field; may be Title::GAID_FOR_UPDATE to select for update + * @param int $flags Either a bitfield of class READ_* constants or GAID_FOR_UPDATE * @return int Int or 0 if the page doesn't exist */ public function getLatestRevID( $flags = 0 ) { - if ( !( $flags & self::GAID_FOR_UPDATE ) && $this->mLatestID !== false ) { - return intval( $this->mLatestID ); - } - if ( !$this->getArticleID( $flags ) ) { - $this->mLatestID = 0; - return $this->mLatestID; - } - $linkCache = MediaWikiServices::getInstance()->getLinkCache(); - $linkCache->addLinkObj( $this ); # in case we already had an article ID - $cached = $linkCache->getGoodLinkFieldObj( $this, 'revision' ); - if ( $cached === null ) { - # Trust LinkCache's state over our own, as for isRedirect() - $this->mLatestID = 0; - return $this->mLatestID; - } + if ( DBAccessObjectUtils::hasFlags( $flags, self::READ_LATEST ) ) { + $this->mLatestID = (int)$this->loadFieldFromDB( 'page_latest', $flags ); + } else { + if ( $this->mLatestID !== false ) { + return (int)$this->mLatestID; + } elseif ( !$this->getArticleID( $flags ) ) { + $this->mLatestID = 0; - $this->mLatestID = intval( $cached ); + return $this->mLatestID; + } + + $linkCache = MediaWikiServices::getInstance()->getLinkCache(); + $linkCache->addLinkObj( $this ); // in case we already had an article ID + // Note that LinkCache returns null if it thinks the page does not exist; + // always trust the state of LinkCache over that of this Title instance. + $this->mLatestID = (int)$linkCache->getGoodLinkFieldObj( $this, 'revision' ); + } return $this->mLatestID; } /** - * This clears some fields in this object, and clears any associated - * keys in the "bad links" section of the link cache. + * Inject a page ID, reset DB-loaded fields, and clear the link cache for this title * - * - This is called from WikiPage::doEditContent() and WikiPage::insertOn() to allow - * loading of the new page_id. It's also called from - * WikiPage::doDeleteArticleReal() + * This can be called on page insertion to allow loading of the new page_id without + * having to create a new Title instance. Likewise with deletion. * - * @param int $newid The new Article ID + * @note This overrides Title::setContentModel() + * + * @param int|bool $id Page ID, 0 for non-existant, or false for "unknown" (lazy-load) */ - public function resetArticleID( $newid ) { - $linkCache = MediaWikiServices::getInstance()->getLinkCache(); - $linkCache->clearLink( $this ); - - if ( $newid === false ) { + public function resetArticleID( $id ) { + if ( $id === false ) { $this->mArticleID = -1; } else { - $this->mArticleID = intval( $newid ); + $this->mArticleID = (int)$id; } $this->mRestrictionsLoaded = false; $this->mRestrictions = []; @@ -3159,10 +3255,13 @@ class Title implements LinkTarget, IDBAccessObject { $this->mLength = -1; $this->mLatestID = false; $this->mContentModel = false; + $this->mForcedContentModel = false; $this->mEstimateRevisions = null; - $this->mPageLanguage = false; + $this->mPageLanguage = null; $this->mDbPageLanguage = false; $this->mIsBigDeletion = null; + + MediaWikiServices::getInstance()->getLinkCache()->clearLink( $this ); } public static function clearCaches() { @@ -3183,7 +3282,7 @@ class Title implements LinkTarget, IDBAccessObject { public static function capitalize( $text, $ns = NS_MAIN ) { $services = MediaWikiServices::getInstance(); if ( $services->getNamespaceInfo()->isCapitalized( $ns ) ) { - return MediaWikiServices::getInstance()->getContentLanguage()->ucfirst( $text ); + return $services->getContentLanguage()->ucfirst( $text ); } else { return $text; } @@ -3209,6 +3308,7 @@ class Title implements LinkTarget, IDBAccessObject { // splitTitleString method, but the only implementation (MediaWikiTitleCodec) does /** @var MediaWikiTitleCodec $titleCodec */ $titleCodec = MediaWikiServices::getInstance()->getTitleParser(); + '@phan-var MediaWikiTitleCodec $titleCodec'; // MalformedTitleException can be thrown here $parts = $titleCodec->splitTitleString( $this->mDbkeyform, $this->mDefaultNamespace ); @@ -3461,7 +3561,7 @@ class Title implements LinkTarget, IDBAccessObject { return [ [ 'badtitletext' ] ]; } - $mp = new MovePage( $this, $nt ); + $mp = MediaWikiServices::getInstance()->getMovePageFactory()->newMovePage( $this, $nt ); $errors = $mp->isValidMove()->getErrorsArray(); if ( $auth ) { $errors = wfMergeErrorArrays( @@ -3493,8 +3593,9 @@ class Title implements LinkTarget, IDBAccessObject { global $wgUser; - $mp = new MovePage( $this, $nt ); + $mp = MediaWikiServices::getInstance()->getMovePageFactory()->newMovePage( $this, $nt ); $method = $auth ? 'moveIfAllowed' : 'move'; + /** @var Status $status */ $status = $mp->$method( $wgUser, $reason, $createRedirect, $changeTags ); if ( $status->isOK() ) { return true; @@ -3527,14 +3628,16 @@ class Title implements LinkTarget, IDBAccessObject { $mp = new MovePage( $this, $nt ); $method = $auth ? 'moveSubpagesIfAllowed' : 'moveSubpages'; + /** @var Status $result */ $result = $mp->$method( $wgUser, $reason, $createRedirect, $changeTags ); - if ( !$result->isOk() ) { + if ( !$result->isOK() ) { return $result->getErrorsArray(); } $retval = []; foreach ( $result->getValue() as $key => $status ) { + /** @var Status $status */ if ( $status->isOK() ) { $retval[$key] = $status->getValue(); } else { @@ -3545,8 +3648,9 @@ class Title implements LinkTarget, IDBAccessObject { } /** - * Checks if this page is just a one-rev redirect. - * Adds lock, so don't use just for light purposes. + * Locks the page row and check if this page is single revision redirect + * + * This updates the cached fields of this instance via Title::loadFromRow() * * @return bool */ @@ -3726,24 +3830,22 @@ class Title implements LinkTarget, IDBAccessObject { /** * Get next/previous revision ID relative to another revision ID * @param int $revId Revision ID. Get the revision that was before this one. - * @param int $flags Title::GAID_FOR_UPDATE + * @param int $flags Bitfield of class READ_* constants * @param string $dir 'next' or 'prev' * @return int|bool New revision ID, or false if none exists */ private function getRelativeRevisionID( $revId, $flags, $dir ) { $rl = MediaWikiServices::getInstance()->getRevisionLookup(); - $rlFlags = $flags === self::GAID_FOR_UPDATE ? IDBAccessObject::READ_LATEST : 0; - $rev = $rl->getRevisionById( $revId, $rlFlags ); + $rev = $rl->getRevisionById( $revId, $flags ); if ( !$rev ) { return false; } - $oldRev = $dir === 'next' - ? $rl->getNextRevision( $rev, $rlFlags ) - : $rl->getPreviousRevision( $rev, $rlFlags ); - if ( !$oldRev ) { - return false; - } - return $oldRev->getId(); + + $oldRev = ( $dir === 'next' ) + ? $rl->getNextRevision( $rev, $flags ) + : $rl->getPreviousRevision( $rev, $flags ); + + return $oldRev ? $oldRev->getId() : false; } /** @@ -3751,7 +3853,7 @@ class Title implements LinkTarget, IDBAccessObject { * * @deprecated since 1.34, use RevisionLookup::getPreviousRevision * @param int $revId Revision ID. Get the revision that was before this one. - * @param int $flags Title::GAID_FOR_UPDATE + * @param int $flags Bitfield of class READ_* constants * @return int|bool Old revision ID, or false if none exists */ public function getPreviousRevisionID( $revId, $flags = 0 ) { @@ -3763,7 +3865,7 @@ class Title implements LinkTarget, IDBAccessObject { * * @deprecated since 1.34, use RevisionLookup::getNextRevision * @param int $revId Revision ID. Get the revision that was after this one. - * @param int $flags Title::GAID_FOR_UPDATE + * @param int $flags Bitfield of class READ_* constants * @return int|bool Next revision ID, or false if none exists */ public function getNextRevisionID( $revId, $flags = 0 ) { @@ -3773,21 +3875,26 @@ class Title implements LinkTarget, IDBAccessObject { /** * Get the first revision of the page * - * @param int $flags Title::GAID_FOR_UPDATE + * @param int $flags Bitfield of class READ_* constants * @return Revision|null If page doesn't exist */ public function getFirstRevision( $flags = 0 ) { $pageId = $this->getArticleID( $flags ); if ( $pageId ) { - $db = ( $flags & self::GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_REPLICA ); + $flags |= ( $flags & self::GAID_FOR_UPDATE ) ? self::READ_LATEST : 0; // b/c + list( $index, $options ) = DBAccessObjectUtils::getDBOptions( $flags ); $revQuery = Revision::getQueryInfo(); - $row = $db->selectRow( $revQuery['tables'], $revQuery['fields'], + $row = wfGetDB( $index )->selectRow( + $revQuery['tables'], $revQuery['fields'], [ 'rev_page' => $pageId ], __METHOD__, - [ - 'ORDER BY' => 'rev_timestamp ASC, rev_id ASC', - 'IGNORE INDEX' => [ 'revision' => 'rev_timestamp' ], // See T159319 - ], + array_merge( + [ + 'ORDER BY' => 'rev_timestamp ASC, rev_id ASC', + 'IGNORE INDEX' => [ 'revision' => 'rev_timestamp' ], // See T159319 + ], + $options + ), $revQuery['joins'] ); if ( $row ) { @@ -3800,7 +3907,7 @@ class Title implements LinkTarget, IDBAccessObject { /** * Get the oldest revision timestamp of this page * - * @param int $flags Title::GAID_FOR_UPDATE + * @param int $flags Bitfield of class READ_* constants * @return string|null MW timestamp */ public function getEarliestRevTime( $flags = 0 ) { @@ -4031,8 +4138,7 @@ class Title implements LinkTarget, IDBAccessObject { * If you want to know if a title can be meaningfully viewed, you should * probably call the isKnown() method instead. * - * @param int $flags An optional bit field; may be Title::GAID_FOR_UPDATE to check - * from master/for update + * @param int $flags Either a bitfield of class READ_* constants or GAID_FOR_UPDATE * @return bool */ public function exists( $flags = 0 ) { @@ -4245,12 +4351,21 @@ class Title implements LinkTarget, IDBAccessObject { * on the number of links. Typically called on create and delete. */ public function touchLinks() { - DeferredUpdates::addUpdate( new HTMLCacheUpdate( $this, 'pagelinks', 'page-touch' ) ); + $jobs = []; + $jobs[] = HTMLCacheUpdateJob::newForBacklinks( + $this, + 'pagelinks', + [ 'causeAction' => 'page-touch' ] + ); if ( $this->mNamespace == NS_CATEGORY ) { - DeferredUpdates::addUpdate( - new HTMLCacheUpdate( $this, 'categorylinks', 'category-touch' ) + $jobs[] = HTMLCacheUpdateJob::newForBacklinks( + $this, + 'categorylinks', + [ 'causeAction' => 'category-touch' ] ); } + + JobQueueGroup::singleton()->lazyPush( $jobs ); } /** @@ -4628,6 +4743,27 @@ class Title implements LinkTarget, IDBAccessObject { return $notices; } + /** + * @param int $flags Bitfield of class READ_* constants + * @return string|bool + */ + private function loadFieldFromDB( $field, $flags ) { + if ( !in_array( $field, self::getSelectFields(), true ) ) { + return false; // field does not exist + } + + $flags |= ( $flags & self::GAID_FOR_UPDATE ) ? self::READ_LATEST : 0; // b/c + list( $index, $options ) = DBAccessObjectUtils::getDBOptions( $flags ); + + return wfGetDB( $index )->selectField( + 'page', + $field, + $this->pageCond(), + __METHOD__, + $options + ); + } + /** * @return array */ diff --git a/includes/TitleArray.php b/includes/TitleArray.php index f6969851d5..895b5a76b6 100644 --- a/includes/TitleArray.php +++ b/includes/TitleArray.php @@ -29,6 +29,8 @@ use Wikimedia\Rdbms\IResultWrapper; /** * The TitleArray class only exists to provide the newFromResult method at pre- * sent. + * + * @method int count() */ abstract class TitleArray implements Iterator { /** diff --git a/includes/WebRequest.php b/includes/WebRequest.php index 6593e49d8a..c94e8d4e23 100644 --- a/includes/WebRequest.php +++ b/includes/WebRequest.php @@ -27,6 +27,7 @@ use MediaWiki\MediaWikiServices; use MediaWiki\Session\Session; use MediaWiki\Session\SessionId; use MediaWiki\Session\SessionManager; +use Wikimedia\AtEase\AtEase; // The point of this class is to be a wrapper around super globals // phpcs:disable MediaWiki.Usage.SuperGlobalsUsage.SuperGlobals @@ -39,7 +40,29 @@ use MediaWiki\Session\SessionManager; * @ingroup HTTP */ class WebRequest { - protected $data, $headers = []; + /** + * The parameters from $_GET, $_POST and the path router + * @var array + */ + protected $data; + + /** + * The parameters from $_GET. The parameters from the path router are + * added by interpolateTitle() during Setup.php. + * @var array + */ + protected $queryAndPathParams; + + /** + * The parameters from $_GET only. + */ + protected $queryParams; + + /** + * Lazy-initialized request headers indexed by upper-case header name + * @var array + */ + protected $headers = []; /** * Flag to make WebRequest::getHeader return an array of values. @@ -96,6 +119,8 @@ class WebRequest { // POST overrides GET data // We don't use $_REQUEST here to avoid interference from cookies... $this->data = $_POST + $_GET; + + $this->queryAndPathParams = $this->queryParams = $_GET; } /** @@ -114,77 +139,80 @@ class WebRequest { * @return array Any query arguments found in path matches. */ public static function getPathInfo( $want = 'all' ) { - global $wgUsePathInfo; // PATH_INFO is mangled due to https://bugs.php.net/bug.php?id=31892 // And also by Apache 2.x, double slashes are converted to single slashes. // So we will use REQUEST_URI if possible. - $matches = []; - if ( !empty( $_SERVER['REQUEST_URI'] ) ) { + if ( isset( $_SERVER['REQUEST_URI'] ) ) { // Slurp out the path portion to examine... $url = $_SERVER['REQUEST_URI']; if ( !preg_match( '!^https?://!', $url ) ) { $url = 'http://unused' . $url; } - Wikimedia\suppressWarnings(); + AtEase::suppressWarnings(); $a = parse_url( $url ); - Wikimedia\restoreWarnings(); - if ( $a ) { - $path = $a['path'] ?? ''; - - global $wgScript; - if ( $path == $wgScript && $want !== 'all' ) { - // Script inside a rewrite path? - // Abort to keep from breaking... - return $matches; - } + AtEase::restoreWarnings(); + if ( !$a ) { + return []; + } + $path = $a['path'] ?? ''; - $router = new PathRouter; + global $wgScript; + if ( $path == $wgScript && $want !== 'all' ) { + // Script inside a rewrite path? + // Abort to keep from breaking... + return []; + } - // Raw PATH_INFO style - $router->add( "$wgScript/$1" ); + $router = new PathRouter; - if ( isset( $_SERVER['SCRIPT_NAME'] ) - && preg_match( '/\.php/', $_SERVER['SCRIPT_NAME'] ) - ) { - # Check for SCRIPT_NAME, we handle index.php explicitly - # But we do have some other .php files such as img_auth.php - # Don't let root article paths clober the parsing for them - $router->add( $_SERVER['SCRIPT_NAME'] . "/$1" ); - } - - global $wgArticlePath; - if ( $wgArticlePath ) { - $router->add( $wgArticlePath ); - } + // Raw PATH_INFO style + $router->add( "$wgScript/$1" ); - global $wgActionPaths; - if ( $wgActionPaths ) { - $router->add( $wgActionPaths, [ 'action' => '$key' ] ); - } + if ( isset( $_SERVER['SCRIPT_NAME'] ) + && strpos( $_SERVER['SCRIPT_NAME'], '.php' ) !== false + ) { + // Check for SCRIPT_NAME, we handle index.php explicitly + // But we do have some other .php files such as img_auth.php + // Don't let root article paths clober the parsing for them + $router->add( $_SERVER['SCRIPT_NAME'] . "/$1" ); + } - global $wgVariantArticlePath; - if ( $wgVariantArticlePath ) { - $router->add( $wgVariantArticlePath, - [ 'variant' => '$2' ], - [ '$2' => MediaWikiServices::getInstance()->getContentLanguage()-> - getVariants() ] - ); - } + global $wgArticlePath; + if ( $wgArticlePath ) { + $router->add( $wgArticlePath ); + } - Hooks::run( 'WebRequestPathInfoRouter', [ $router ] ); + global $wgActionPaths; + $articlePaths = PathRouter::getActionPaths( $wgActionPaths, $wgArticlePath ); + if ( $articlePaths ) { + $router->add( $articlePaths, [ 'action' => '$key' ] ); + } - $matches = $router->parse( $path ); + global $wgVariantArticlePath; + if ( $wgVariantArticlePath ) { + $router->add( $wgVariantArticlePath, + [ 'variant' => '$2' ], + [ '$2' => MediaWikiServices::getInstance()->getContentLanguage()-> + getVariants() ] + ); } - } elseif ( $wgUsePathInfo ) { - if ( isset( $_SERVER['ORIG_PATH_INFO'] ) && $_SERVER['ORIG_PATH_INFO'] != '' ) { - // Mangled PATH_INFO - // https://bugs.php.net/bug.php?id=31892 - // Also reported when ini_get('cgi.fix_pathinfo')==false - $matches['title'] = substr( $_SERVER['ORIG_PATH_INFO'], 1 ); - - } elseif ( isset( $_SERVER['PATH_INFO'] ) && $_SERVER['PATH_INFO'] != '' ) { - // Regular old PATH_INFO yay - $matches['title'] = substr( $_SERVER['PATH_INFO'], 1 ); + + Hooks::run( 'WebRequestPathInfoRouter', [ $router ] ); + + $matches = $router->parse( $path ); + } else { + global $wgUsePathInfo; + $matches = []; + if ( $wgUsePathInfo ) { + if ( !empty( $_SERVER['ORIG_PATH_INFO'] ) ) { + // Mangled PATH_INFO + // https://bugs.php.net/bug.php?id=31892 + // Also reported when ini_get('cgi.fix_pathinfo')==false + $matches['title'] = substr( $_SERVER['ORIG_PATH_INFO'], 1 ); + } elseif ( !empty( $_SERVER['PATH_INFO'] ) ) { + // Regular old PATH_INFO yay + $matches['title'] = substr( $_SERVER['PATH_INFO'], 1 ); + } } } @@ -329,7 +357,7 @@ class WebRequest { $matches = self::getPathInfo( 'title' ); foreach ( $matches as $key => $val ) { - $this->data[$key] = $_GET[$key] = $_REQUEST[$key] = $val; + $this->data[$key] = $this->queryAndPathParams[$key] = $val; } } @@ -395,18 +423,25 @@ class WebRequest { # https://www.php.net/variables.external#language.variables.external.dot-in-names # Work around PHP *feature* to avoid *bugs* elsewhere. $name = strtr( $name, '.', '_' ); - if ( isset( $arr[$name] ) ) { - $data = $arr[$name]; + + if ( !isset( $arr[$name] ) ) { + return $default; + } + + $data = $arr[$name]; + # Optimisation: Skip UTF-8 normalization and legacy transcoding for simple ASCII strings. + $isAsciiStr = ( is_string( $data ) && preg_match( '/[^\x20-\x7E]/', $data ) === 0 ); + if ( !$isAsciiStr ) { if ( isset( $_GET[$name] ) && is_string( $data ) ) { # Check for alternate/legacy character encoding. - $contLang = MediaWikiServices::getInstance()->getContentLanguage(); - $data = $contLang->checkTitleEncoding( $data ); + $data = MediaWikiServices::getInstance() + ->getContentLanguage() + ->checkTitleEncoding( $data ); } $data = $this->normalizeUnicode( $data ); - return $data; - } else { - return $default; } + + return $data; } /** @@ -512,7 +547,7 @@ class WebRequest { * * @param string $name * @param array|null $default Option default (or null) - * @return array Array of ints + * @return int[]|null */ public function getIntArray( $name, $default = null ) { $val = $this->getArray( $name, $default ); @@ -654,14 +689,27 @@ class WebRequest { } /** - * Get the values passed in the query string. + * Get the values passed in the query string and the path router parameters. * No transformation is performed on the values. * * @codeCoverageIgnore * @return array */ public function getQueryValues() { - return $_GET; + return $this->queryAndPathParams; + } + + /** + * Get the values passed in the query string only, not including the path + * router parameters. This is less suitable for self-links to index.php but + * useful for other entry points. No transformation is performed on the + * values. + * + * @since 1.34 + * @return array + */ + public function getQueryValuesOnly() { + return $this->queryParams; } /** diff --git a/includes/WebStart.php b/includes/WebStart.php index c83fdea511..957309137e 100644 --- a/includes/WebStart.php +++ b/includes/WebStart.php @@ -91,17 +91,20 @@ if ( !defined( 'MW_API' ) && header( 'Cache-Control: no-cache' ); header( 'Content-Type: text/html; charset=utf-8' ); HttpStatus::header( 400 ); - $error = wfMessage( 'nonwrite-api-promise-error' )->escaped(); - $content = <<useDatabase( false ) + ->inContentLanguage() + ->escaped(); + $content = << -$error +$errorHtml -EOT; +HTML; header( 'Content-Length: ' . strlen( $content ) ); echo $content; die(); diff --git a/includes/WikiMap.php b/includes/WikiMap.php index f2641f40f4..dba60f2d41 100644 --- a/includes/WikiMap.php +++ b/includes/WikiMap.php @@ -256,11 +256,10 @@ class WikiMap { * Get the wiki ID of a database domain * * This is like DatabaseDomain::getId() without encoding (for legacy reasons) and - * without the schema if it is the generic installer default of "mediawiki"/"dbo" + * without the schema if it is the generic installer default of "mediawiki" * * @see $wgDBmwschema * @see PostgresInstaller - * @see MssqlInstaller * * @param string|DatabaseDomain $domain * @return string @@ -273,7 +272,7 @@ class WikiMap { // the installer default then it is probably the case that the schema is the same for // all wikis in the farm. Historically, any wiki farm had to make the database/prefix // combination unique per wiki. Ommit the schema if it does not seem wiki specific. - if ( !in_array( $domain->getSchema(), [ null, 'mediawiki', 'dbo' ], true ) ) { + if ( !in_array( $domain->getSchema(), [ null, 'mediawiki' ], true ) ) { // This means a site admin may have specifically taylored the schemas. // Domain IDs might use the form -- or --_, // meaning that the schema portion must be accounted for to disambiguate wikis. diff --git a/includes/Xml.php b/includes/Xml.php index febf03e7a2..b368a131ca 100644 --- a/includes/Xml.php +++ b/includes/Xml.php @@ -266,7 +266,7 @@ class Xml { /** * Convenience function to build an HTML text input field * @param string $name Value of the name attribute - * @param int $size Value of the size attribute + * @param int|false $size Value of the size attribute * @param mixed $value Value of the value attribute * @param array $attribs Other attributes * @return string HTML @@ -289,7 +289,7 @@ class Xml { /** * Convenience function to build an HTML password input field * @param string $name Value of the name attribute - * @param int $size Value of the size attribute + * @param int|false $size Value of the size attribute * @param mixed $value Value of the value attribute * @param array $attribs Other attributes * @return string HTML @@ -600,7 +600,7 @@ class Xml { * * @param string|bool $legend Legend of the fieldset. If evaluates to false, * legend is not added. - * @param string $content Pre-escaped content for the fieldset. If false, + * @param string|false $content Pre-escaped content for the fieldset. If false, * only open fieldset is returned. * @param array $attribs Any attributes to fieldset-element. * diff --git a/includes/actions/Action.php b/includes/actions/Action.php index f892c5e34e..4be2f7d945 100644 --- a/includes/actions/Action.php +++ b/includes/actions/Action.php @@ -22,7 +22,7 @@ use MediaWiki\MediaWikiServices; /** - * @defgroup Actions Action done on pages + * @defgroup Actions Actions */ /** diff --git a/includes/actions/HistoryAction.php b/includes/actions/HistoryAction.php index 958ec06f47..6a6104570c 100644 --- a/includes/actions/HistoryAction.php +++ b/includes/actions/HistoryAction.php @@ -95,6 +95,7 @@ class HistoryAction extends FormlessAction { private function preCacheMessages() { // Precache various messages if ( !isset( $this->message ) ) { + $this->message = []; $msgs = [ 'cur', 'last', 'pipe-separator' ]; foreach ( $msgs as $msg ) { $this->message[$msg] = $this->msg( $msg )->escaped(); @@ -265,7 +266,8 @@ class HistoryAction extends FormlessAction { 'value' => $tagFilter, ] ]; - if ( $this->getUser()->isAllowed( 'deletedhistory' ) ) { + $permissionManager = MediaWikiServices::getInstance()->getPermissionManager(); + if ( $permissionManager->userHasRight( $this->getUser(), 'deletedhistory' ) ) { $fields[] = [ 'type' => 'check', 'label' => $this->msg( 'history-show-deleted' )->text(), @@ -431,27 +433,30 @@ class HistoryAction extends FormlessAction { * @return FeedItem */ function feedItem( $row ) { - $rev = new Revision( $row, 0, $this->getTitle() ); - + $revisionStore = MediaWikiServices::getInstance()->getRevisionStore(); + $rev = $revisionStore->newRevisionFromRow( $row, 0, $this->getTitle() ); + $prevRev = $revisionStore->getPreviousRevision( $rev ); + $revComment = $rev->getComment() === null ? null : $rev->getComment()->text; $text = FeedUtils::formatDiffRow( $this->getTitle(), - $this->getTitle()->getPreviousRevisionID( $rev->getId() ), + $prevRev ? $prevRev->getId() : false, $rev->getId(), $rev->getTimestamp(), - $rev->getComment() + $revComment ); - if ( $rev->getComment() == '' ) { + $revUserText = $rev->getUser() ? $rev->getUser()->getName() : ''; + if ( $revComment == '' ) { $contLang = MediaWikiServices::getInstance()->getContentLanguage(); $title = $this->msg( 'history-feed-item-nocomment', - $rev->getUserText(), + $revUserText, $contLang->timeanddate( $rev->getTimestamp() ), $contLang->date( $rev->getTimestamp() ), $contLang->time( $rev->getTimestamp() ) )->inContentLanguage()->text(); } else { - $title = $rev->getUserText() . + $title = $revUserText . $this->msg( 'colon-separator' )->inContentLanguage()->text() . - FeedItem::stripComment( $rev->getComment() ); + FeedItem::stripComment( $revComment ); } return new FeedItem( @@ -459,7 +464,7 @@ class HistoryAction extends FormlessAction { $text, $this->getTitle()->getFullURL( 'diff=' . $rev->getId() . '&oldid=prev' ), $rev->getTimestamp(), - $rev->getUserText(), + $revUserText, $this->getTitle()->getTalkPage()->getFullURL() ); } diff --git a/includes/actions/InfoAction.php b/includes/actions/InfoAction.php index 279c13bd04..0360fe44d2 100644 --- a/includes/actions/InfoAction.php +++ b/includes/actions/InfoAction.php @@ -23,7 +23,7 @@ */ use MediaWiki\MediaWikiServices; -use MediaWiki\Storage\RevisionRecord; +use MediaWiki\Revision\RevisionRecord; use Wikimedia\Rdbms\Database; /** @@ -280,11 +280,10 @@ class InfoAction extends FormlessAction { // Language in which the page content is (supposed to be) written $pageLang = $title->getPageLanguage()->getCode(); - $permissionManager = MediaWikiServices::getInstance()->getPermissionManager(); - $pageLangHtml = $pageLang . ' - ' . Language::fetchLanguageName( $pageLang, $lang->getCode() ); // Link to Special:PageLanguage with pre-filled page title if user has permissions + $permissionManager = $services->getPermissionManager(); if ( $config->get( 'PageLanguageUseDB' ) && $permissionManager->userCan( 'pagelang', $user, $title ) ) { @@ -344,8 +343,7 @@ class InfoAction extends FormlessAction { ]; $unwatchedPageThreshold = $config->get( 'UnwatchedPageThreshold' ); - if ( - $user->isAllowed( 'unwatchedpages' ) || + if ( $permissionManager->userHasRight( $user, 'unwatchedpages' ) || ( $unwatchedPageThreshold !== false && $pageCounts['watchers'] >= $unwatchedPageThreshold ) ) { @@ -360,7 +358,7 @@ class InfoAction extends FormlessAction { ) { $minToDisclose = $config->get( 'UnwatchedPageSecret' ); if ( $pageCounts['visitingWatchers'] > $minToDisclose || - $user->isAllowed( 'unwatchedpages' ) ) { + $permissionManager->userHasRight( $user, 'unwatchedpages' ) ) { $pageInfo['header-basic'][] = [ $this->msg( 'pageinfo-visiting-watchers' ), $lang->formatNum( $pageCounts['visitingWatchers'] ) @@ -743,8 +741,6 @@ class InfoAction extends FormlessAction { self::getCacheKey( $cache, $page->getTitle(), $page->getLatest() ), WANObjectCache::TTL_WEEK, function ( $oldValue, &$ttl, &$setOpts ) use ( $page, $config, $fname, $services ) { - global $wgActorTableSchemaMigrationStage; - $title = $page->getTitle(); $id = $title->getArticleID(); @@ -752,19 +748,11 @@ class InfoAction extends FormlessAction { $dbrWatchlist = wfGetDB( DB_REPLICA, 'watchlist' ); $setOpts += Database::getCacheSetOptions( $dbr, $dbrWatchlist ); - if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_READ_NEW ) { - $tables = [ 'revision_actor_temp' ]; - $field = 'revactor_actor'; - $pageField = 'revactor_page'; - $tsField = 'revactor_timestamp'; - $joins = []; - } else { - $tables = [ 'revision' ]; - $field = 'rev_user_text'; - $pageField = 'rev_page'; - $tsField = 'rev_timestamp'; - $joins = []; - } + $tables = [ 'revision_actor_temp' ]; + $field = 'revactor_actor'; + $pageField = 'revactor_page'; + $tsField = 'revactor_timestamp'; + $joins = []; $watchedItemStore = $services->getWatchedItemStore(); diff --git a/includes/actions/McrUndoAction.php b/includes/actions/McrUndoAction.php index 41cd24e701..68c0ea1243 100644 --- a/includes/actions/McrUndoAction.php +++ b/includes/actions/McrUndoAction.php @@ -289,8 +289,9 @@ class McrUndoAction extends FormAction { 'h2', [ 'id' => 'mw-previewheader' ], $this->context->msg( 'preview' )->text() ) . - $out->parseAsInterface( $note ) . - "
" + Html::rawElement( 'div', [ 'class' => 'warningbox' ], + $out->parseAsInterface( $note ) + ) ); $pageViewLang = $this->getTitle()->getPageViewLanguage(); diff --git a/includes/actions/RawAction.php b/includes/actions/RawAction.php index abb8ff5b1f..0586e093c5 100644 --- a/includes/actions/RawAction.php +++ b/includes/actions/RawAction.php @@ -111,7 +111,8 @@ class RawAction extends FormlessAction { $rootPage = strtok( $title->getText(), '/' ); $userFromTitle = User::newFromName( $rootPage, 'usable' ); if ( !$userFromTitle || $userFromTitle->getId() === 0 ) { - $elevated = $this->getUser()->isAllowed( 'editinterface' ); + $elevated = MediaWikiServices::getInstance()->getPermissionManager() + ->userHasRight( $this->getUser(), 'editinterface' ); $elevatedText = $elevated ? 'by elevated ' : ''; $log = LoggerFactory::getInstance( "security" ); $log->warning( @@ -237,23 +238,31 @@ class RawAction extends FormlessAction { */ public function getOldId() { $oldid = $this->getRequest()->getInt( 'oldid' ); + $rl = MediaWikiServices::getInstance()->getRevisionLookup(); switch ( $this->getRequest()->getText( 'direction' ) ) { case 'next': # output next revision, or nothing if there isn't one - $nextid = 0; + $nextRev = null; if ( $oldid ) { - $nextid = $this->getTitle()->getNextRevisionID( $oldid ); + $oldRev = $rl->getRevisionById( $oldid ); + if ( $oldRev ) { + $nextRev = $rl->getNextRevision( $oldRev ); + } } - $oldid = $nextid ?: -1; + $oldid = $nextRev ? $nextRev->getId() : -1; break; case 'prev': # output previous revision, or nothing if there isn't one + $prevRev = null; if ( !$oldid ) { # get the current revision so we can get the penultimate one $oldid = $this->page->getLatest(); } - $previd = $this->getTitle()->getPreviousRevisionID( $oldid ); - $oldid = $previd ?: -1; + $oldRev = $rl->getRevisionById( $oldid ); + if ( $oldRev ) { + $prevRev = $rl->getPreviousRevision( $oldRev ); + } + $oldid = $prevRev ? $prevRev->getId() : -1; break; case 'cur': $oldid = 0; diff --git a/includes/actions/RevertAction.php b/includes/actions/RevertAction.php index 8a5d7c9046..254f7a891c 100644 --- a/includes/actions/RevertAction.php +++ b/includes/actions/RevertAction.php @@ -118,7 +118,9 @@ class RevertAction extends FormAction { $this->useTransactionalTimeLimit(); $old = $this->getRequest()->getText( 'oldimage' ); + /** @var LocalFile $localFile */ $localFile = $this->page->getFile(); + '@phan-var LocalFile $localFile'; $oldFile = OldLocalFile::newFromArchiveName( $this->getTitle(), $localFile->getRepo(), $old ); $source = $localFile->getArchiveVirtualUrl( $old ); diff --git a/includes/actions/RollbackAction.php b/includes/actions/RollbackAction.php index 519da617ba..1c9e63b99e 100644 --- a/includes/actions/RollbackAction.php +++ b/includes/actions/RollbackAction.php @@ -20,7 +20,7 @@ * @ingroup Actions */ -use MediaWiki\Storage\RevisionRecord; +use MediaWiki\Revision\RevisionRecord; /** * User interface for the rollback action diff --git a/includes/actions/SpecialPageAction.php b/includes/actions/SpecialPageAction.php index 8a231cbe5f..56be456cac 100644 --- a/includes/actions/SpecialPageAction.php +++ b/includes/actions/SpecialPageAction.php @@ -35,6 +35,9 @@ class SpecialPageAction extends FormlessAction { 'editchangetags' => 'EditTags', ]; + /** + * @inheritDoc + */ public function getName() { $request = $this->getRequest(); $actionName = $request->getVal( 'action', 'view' ); diff --git a/includes/actions/WatchAction.php b/includes/actions/WatchAction.php index 0eba613a20..e88654ad2f 100644 --- a/includes/actions/WatchAction.php +++ b/includes/actions/WatchAction.php @@ -20,6 +20,8 @@ * @ingroup Actions */ +use MediaWiki\MediaWikiServices; + /** * Page addition to a user's watchlist * @@ -116,7 +118,8 @@ class WatchAction extends FormAction { User $user, $checkRights = User::CHECK_USER_RIGHTS ) { - if ( $checkRights && !$user->isAllowed( 'editmywatchlist' ) ) { + $permissionManager = MediaWikiServices::getInstance()->getPermissionManager(); + if ( $checkRights && !$permissionManager->userHasRight( $user, 'editmywatchlist' ) ) { return User::newFatalPermissionDeniedStatus( 'editmywatchlist' ); } @@ -140,7 +143,9 @@ class WatchAction extends FormAction { * @return Status */ public static function doUnwatch( Title $title, User $user ) { - if ( !$user->isAllowed( 'editmywatchlist' ) ) { + if ( !MediaWikiServices::getInstance() + ->getPermissionManager() + ->userHasRight( $user, 'editmywatchlist' ) ) { return User::newFatalPermissionDeniedStatus( 'editmywatchlist' ); } diff --git a/includes/actions/pagers/HistoryPager.php b/includes/actions/pagers/HistoryPager.php index c5c090d21b..f178911ab3 100644 --- a/includes/actions/pagers/HistoryPager.php +++ b/includes/actions/pagers/HistoryPager.php @@ -172,6 +172,7 @@ class HistoryPager extends ReverseChronologicalPager { * @return string HTML output */ protected function getStartBody() { + $permissionManager = MediaWikiServices::getInstance()->getPermissionManager(); $this->lastRow = false; $this->counter = 1; $this->oldIdChecked = 0; @@ -197,7 +198,7 @@ class HistoryPager extends ReverseChronologicalPager { $user = $this->getUser(); $actionButtons = ''; - if ( $user->isAllowed( 'deleterevision' ) ) { + if ( $permissionManager->userHasRight( $user, 'deleterevision' ) ) { $actionButtons .= $this->getRevisionButton( 'revisiondelete', 'showhideselectedversions' ); } @@ -210,7 +211,7 @@ class HistoryPager extends ReverseChronologicalPager { 'mw-history-revisionactions' ], $actionButtons ); } - if ( $user->isAllowed( 'deleterevision' ) || $this->showTagEditUI ) { + if ( $permissionManager->userHasRight( $user, 'deleterevision' ) || $this->showTagEditUI ) { $this->buttons .= ( new ListToggle( $this->getOutput() ) )->getHTML(); } @@ -305,6 +306,7 @@ class HistoryPager extends ReverseChronologicalPager { */ function historyLine( $row, $next, $notificationtimestamp = false, $dummy = false, $firstInList = false ) { + $permissionManager = MediaWikiServices::getInstance()->getPermissionManager(); $rev = new Revision( $row, 0, $this->getTitle() ); if ( is_object( $next ) ) { @@ -332,7 +334,7 @@ class HistoryPager extends ReverseChronologicalPager { $del = ''; $user = $this->getUser(); - $canRevDelete = $user->isAllowed( 'deleterevision' ); + $canRevDelete = $permissionManager->userHasRight( $user, 'deleterevision' ); // Show checkboxes for each revision, to allow for revision deletion and // change tags if ( $canRevDelete || $this->showTagEditUI ) { @@ -349,7 +351,8 @@ class HistoryPager extends ReverseChronologicalPager { [ 'name' => 'ids[' . $rev->getId() . ']' ] ); } // User can only view deleted revisions... - } elseif ( $rev->getVisibility() && $user->isAllowed( 'deletedhistory' ) ) { + } elseif ( $rev->getVisibility() && + $permissionManager->userHasRight( $user, 'deletedhistory' ) ) { // If revision was hidden from sysops, disable the link if ( !$rev->userCan( RevisionRecord::DELETED_RESTRICTED, $user ) ) { $del = Linker::revDeleteLinkDisabled( false ); @@ -398,8 +401,11 @@ class HistoryPager extends ReverseChronologicalPager { $tools = []; # Rollback and undo links - if ( $prevRev && $this->getTitle()->quickUserCan( 'edit', $user ) ) { - if ( $latest && $this->getTitle()->quickUserCan( 'rollback', $user ) ) { + + if ( $prevRev && $permissionManager->quickUserCan( 'edit', $user, $this->getTitle() ) ) { + if ( $latest && $permissionManager->quickUserCan( 'rollback', + $user, $this->getTitle() ) + ) { // Get a rollback link without the brackets $rollbackLink = Linker::generateRollback( $rev, @@ -419,7 +425,7 @@ class HistoryPager extends ReverseChronologicalPager { $undoTooltip = $latest ? [ 'title' => $this->msg( 'tooltip-undo' )->text() ] : []; - $undolink = MediaWikiServices::getInstance()->getLinkRenderer()->makeKnownLink( + $undolink = $this->getLinkRenderer()->makeKnownLink( $this->getTitle(), $this->msg( 'editundo' )->text(), $undoTooltip, @@ -499,7 +505,7 @@ class HistoryPager extends ReverseChronologicalPager { ) { return $cur; } else { - return MediaWikiServices::getInstance()->getLinkRenderer()->makeKnownLink( + return $this->getLinkRenderer()->makeKnownLink( $this->getTitle(), new HtmlArmor( $cur ), [], @@ -528,7 +534,7 @@ class HistoryPager extends ReverseChronologicalPager { return $last; } - $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer(); + $linkRenderer = $this->getLinkRenderer(); if ( $next === 'unknown' ) { # Next row probably exists but is unknown, use an oldid=prev link return $linkRenderer->makeKnownLink( diff --git a/includes/api/ApiAuthManagerHelper.php b/includes/api/ApiAuthManagerHelper.php index 2f66277ec4..9a3f75eb4d 100644 --- a/includes/api/ApiAuthManagerHelper.php +++ b/includes/api/ApiAuthManagerHelper.php @@ -306,9 +306,10 @@ class ApiAuthManagerHelper { /** * Clean up a field array for output - * @param ApiBase $module For context and parameters 'mergerequestfields' - * and 'messageformat' * @param array $fields + * @codingStandardsIgnoreStart + * @phan-param array{type:string,options:array,value:string,label:Message,help:Message,optional:bool,sensitive:bool,skippable:bool} $fields + * @codingStandardsIgnoreEnd * @return array */ private function formatFields( array $fields ) { diff --git a/includes/api/ApiBase.php b/includes/api/ApiBase.php index a7b872ce15..7518008f9d 100644 --- a/includes/api/ApiBase.php +++ b/includes/api/ApiBase.php @@ -274,7 +274,7 @@ abstract class ApiBase extends ContextSource { /** @var array Maps extension paths to info arrays */ private static $extensionInfo = null; - /** @var int[][][] Cache for self::filterIDs() */ + /** @var stdClass[][] Cache for self::filterIDs() */ private static $filterIDsCache = []; /** $var array Map of web UI block messages to corresponding API messages and codes */ @@ -992,7 +992,7 @@ abstract class ApiBase extends ContextSource { return; } - $queryValues = $this->getRequest()->getQueryValues(); + $queryValues = $this->getRequest()->getQueryValuesOnly(); $badParams = []; foreach ( $params as $param ) { if ( $prefix !== 'noprefix' ) { @@ -1308,8 +1308,15 @@ abstract class ApiBase extends ContextSource { } break; case 'limit': + // Must be a number or 'max' + if ( $value !== 'max' ) { + $value = (int)$value; + } + if ( $multi ) { + self::dieDebug( __METHOD__, "Multi-values not supported for $encParamName" ); + } if ( !$parseLimit ) { - // Don't do any validation whatsoever + // Don't do min/max validation and don't parse 'max' break; } if ( !isset( $paramSettings[self::PARAM_MAX] ) @@ -1320,21 +1327,16 @@ abstract class ApiBase extends ContextSource { "MAX1 or MAX2 are not defined for the limit $encParamName" ); } - if ( $multi ) { - self::dieDebug( __METHOD__, "Multi-values not supported for $encParamName" ); - } - $min = $paramSettings[self::PARAM_MIN] ?? 0; - if ( $value == 'max' ) { + if ( $value === 'max' ) { $value = $this->getMain()->canApiHighLimits() ? $paramSettings[self::PARAM_MAX2] : $paramSettings[self::PARAM_MAX]; $this->getResult()->addParsedLimit( $this->getModuleName(), $value ); } else { - $value = (int)$value; $this->validateLimit( $paramName, $value, - $min, + $paramSettings[self::PARAM_MIN] ?? 0, $paramSettings[self::PARAM_MAX], $paramSettings[self::PARAM_MAX2] ); @@ -1583,6 +1585,7 @@ abstract class ApiBase extends ContextSource { 'integeroutofrange', [ 'min' => $min, 'max' => $max, 'botMax' => $botMax ?: $max ] ); + // @phan-suppress-next-line PhanTypeMismatchArgument $this->warnOrDie( $msg, $enforceLimits ); $value = $min; } @@ -1604,6 +1607,7 @@ abstract class ApiBase extends ContextSource { 'integeroutofrange', [ 'min' => $min, 'max' => $max, 'botMax' => $botMax ?: $max ] ); + // @phan-suppress-next-line PhanTypeMismatchArgument $this->warnOrDie( $msg, $enforceLimits ); $value = $botMax; } @@ -1614,6 +1618,7 @@ abstract class ApiBase extends ContextSource { 'integeroutofrange', [ 'min' => $min, 'max' => $max, 'botMax' => $botMax ?: $max ] ); + // @phan-suppress-next-line PhanTypeMismatchArgument $this->warnOrDie( $msg, $enforceLimits ); $value = $max; } @@ -2020,6 +2025,7 @@ abstract class ApiBase extends ContextSource { */ public function dieWithException( $exception, array $options = [] ) { $this->dieWithError( + // @phan-suppress-next-line PhanTypeMismatchArgument $this->getErrorFormatter()->getMessageFromException( $exception, $options ) ); } @@ -2126,7 +2132,9 @@ abstract class ApiBase extends ContextSource { $user = $this->getUser(); } $rights = (array)$rights; - if ( !$user->isAllowedAny( ...$rights ) ) { + if ( !$this->getPermissionManager() + ->userHasAnyRight( $user, ...$rights ) + ) { $this->dieWithError( [ 'apierror-permissiondenied', $this->msg( "action-{$rights[0]}" ) ] ); } } @@ -2460,6 +2468,7 @@ abstract class ApiBase extends ContextSource { if ( $m ) { $m = new ApiHelpParamValueMessage( $value, + // @phan-suppress-next-line PhanTypeMismatchArgument [ $m->getKey(), 'api-help-param-no-description' ], $m->getParams(), isset( $deprecatedValues[$value] ) diff --git a/includes/api/ApiBlock.php b/includes/api/ApiBlock.php index 480126760e..30a9242c3b 100644 --- a/includes/api/ApiBlock.php +++ b/includes/api/ApiBlock.php @@ -98,7 +98,8 @@ class ApiBlock extends ApiBase { } } - if ( $params['hidename'] && !$user->isAllowed( 'hideuser' ) ) { + if ( $params['hidename'] && + !$this->getPermissionManager()->userHasRight( $user, 'hideuser' ) ) { $this->dieWithError( 'apierror-canthide' ); } if ( $params['noemail'] && !SpecialBlock::canBlockEmail( $user ) ) { @@ -139,8 +140,10 @@ class ApiBlock extends ApiBase { $this->dieStatus( $this->errorArrayToStatus( $retval ) ); } - list( $target, /*...*/ ) = SpecialBlock::getTargetAndType( $params['user'] ); + $res = []; + $res['user'] = $params['user']; + list( $target, /*...*/ ) = SpecialBlock::getTargetAndType( $params['user'] ); $res['userID'] = $target instanceof User ? $target->getId() : 0; $block = DatabaseBlock::newFromTarget( $target, null, true ); diff --git a/includes/api/ApiComparePages.php b/includes/api/ApiComparePages.php index e09691558c..6e788d550d 100644 --- a/includes/api/ApiComparePages.php +++ b/includes/api/ApiComparePages.php @@ -26,6 +26,9 @@ use MediaWiki\Revision\RevisionArchiveRecord; use MediaWiki\Revision\RevisionStore; use MediaWiki\Revision\SlotRecord; +/** + * @ingroup API + */ class ApiComparePages extends ApiBase { /** @var RevisionStore */ @@ -231,7 +234,9 @@ class ApiComparePages extends ApiBase { */ private function getRevisionById( $id ) { $rev = $this->revisionStore->getRevisionById( $id ); - if ( !$rev && $this->getUser()->isAllowedAny( 'deletedtext', 'undelete' ) ) { + if ( !$rev && $this->getPermissionManager() + ->userHasAnyRight( $this->getUser(), 'deletedtext', 'undelete' ) + ) { // Try the 'archive' table $arQuery = $this->revisionStore->getArchiveQueryInfo(); $row = $this->getDB()->selectRow( @@ -247,6 +252,7 @@ class ApiComparePages extends ApiBase { ); if ( $row ) { $rev = $this->revisionStore->newRevisionFromArchiveRow( $row ); + // @phan-suppress-next-line PhanUndeclaredProperty $rev->isArchive = true; } } @@ -615,6 +621,7 @@ class ApiComparePages extends ApiBase { } } + // @phan-suppress-next-line PhanUndeclaredProperty if ( !empty( $rev->isArchive ) ) { $this->getMain()->setCacheMode( 'private' ); $vals["{$prefix}archive"] = true; diff --git a/includes/api/ApiDelete.php b/includes/api/ApiDelete.php index 0e13d705ff..ad171c66de 100644 --- a/includes/api/ApiDelete.php +++ b/includes/api/ApiDelete.php @@ -42,6 +42,7 @@ class ApiDelete extends ApiBase { $pageObj = $this->getTitleOrPageId( $params, 'fromdbmaster' ); $titleObj = $pageObj->getTitle(); if ( !$pageObj->exists() && + // @phan-suppress-next-line PhanUndeclaredMethod !( $titleObj->getNamespace() == NS_FILE && self::canDeleteFile( $pageObj->getFile() ) ) ) { $this->dieWithError( 'apierror-missingtitle' ); @@ -156,6 +157,7 @@ class ApiDelete extends ApiBase { ) { $title = $page->getTitle(); + // @phan-suppress-next-line PhanUndeclaredMethod There's no right typehint for it $file = $page->getFile(); if ( !self::canDeleteFile( $file ) ) { return self::delete( $page, $user, $reason, $tags ); diff --git a/includes/api/ApiEditPage.php b/includes/api/ApiEditPage.php index 3f63a0085a..1936407ac3 100644 --- a/includes/api/ApiEditPage.php +++ b/includes/api/ApiEditPage.php @@ -20,7 +20,8 @@ * @file */ -use MediaWiki\Storage\RevisionRecord; +use MediaWiki\MediaWikiServices; +use MediaWiki\Revision\RevisionRecord; /** * A module that allows for editing and creating pages. @@ -62,9 +63,7 @@ class ApiEditPage extends ApiBase { /** @var Title $newTitle */ foreach ( $titles as $id => $newTitle ) { - if ( !isset( $titles[$id - 1] ) ) { - $titles[$id - 1] = $oldTitle; - } + $titles[ $id - 1 ] = $titles[ $id - 1 ] ?? $oldTitle; $redirValues[] = [ 'from' => $titles[$id - 1]->getPrefixedText(), @@ -241,11 +240,15 @@ class ApiEditPage extends ApiBase { $params['text'] = $newContent->serialize( $contentFormat ); // If no summary was given and we only undid one rev, // use an autosummary - if ( is_null( $params['summary'] ) && - $titleObj->getNextRevisionID( $undoafterRev->getId() ) == $params['undo'] - ) { - $params['summary'] = wfMessage( 'undo-summary' ) - ->params( $params['undo'], $undoRev->getUserText() )->inContentLanguage()->text(); + + if ( is_null( $params['summary'] ) ) { + $nextRev = MediaWikiServices::getInstance()->getRevisionLookup() + ->getNextRevision( $undoafterRev->getRevisionRecord() ); + if ( $nextRev && $nextRev->getId() == $params['undo'] ) { + $params['summary'] = wfMessage( 'undo-summary' ) + ->params( $params['undo'], $undoRev->getUserText() ) + ->inContentLanguage()->text(); + } } } @@ -380,6 +383,7 @@ class ApiEditPage extends ApiBase { $status = $ep->attemptSave( $result ); $wgRequest = $oldRequest; + $r = []; switch ( $status->value ) { case EditPage::AS_HOOK_ERROR: case EditPage::AS_HOOK_ERROR_EXPECTED: diff --git a/includes/api/ApiErrorFormatter.php b/includes/api/ApiErrorFormatter.php index 8049cd84b9..9dfd3705a5 100644 --- a/includes/api/ApiErrorFormatter.php +++ b/includes/api/ApiErrorFormatter.php @@ -26,6 +26,7 @@ * ApiResult. * @since 1.25 * @ingroup API + * @phan-file-suppress PhanUndeclaredMethod Undeclared methods in IApiMessage */ class ApiErrorFormatter { /** @var Title Dummy title to silence warnings from MessageCache::parse() */ @@ -236,6 +237,7 @@ class ApiErrorFormatter { */ public function formatException( $exception, array $options = [] ) { return $this->formatMessage( + // @phan-suppress-next-line PhanTypeMismatchArgument $this->getMessageFromException( $exception, $options ), $options['format'] ?? null ); diff --git a/includes/api/ApiExpandTemplates.php b/includes/api/ApiExpandTemplates.php index 851373d5e3..4b74a3d4d9 100644 --- a/includes/api/ApiExpandTemplates.php +++ b/includes/api/ApiExpandTemplates.php @@ -76,10 +76,6 @@ class ApiExpandTemplates extends ApiBase { $this->addWarning( [ 'apierror-revwrongpage', $rev->getId(), wfEscapeWikiText( $pTitleObj->getPrefixedText() ) ] ); } - } else { - // Consider the title derived from the revid as having - // been provided. - $titleProvided = true; } } @@ -103,9 +99,12 @@ class ApiExpandTemplates extends ApiBase { if ( isset( $prop['parsetree'] ) || $params['generatexml'] ) { $parser->startExternalParse( $titleObj, $options, Parser::OT_PREPROCESS ); $dom = $parser->preprocessToDom( $params['text'] ); + // @phan-suppress-next-line PhanUndeclaredMethodInCallable if ( is_callable( [ $dom, 'saveXML' ] ) ) { + // @phan-suppress-next-line PhanUndeclaredMethod $xml = $dom->saveXML(); } else { + // @phan-suppress-next-line PhanUndeclaredMethod $xml = $dom->__toString(); } if ( isset( $prop['parsetree'] ) ) { diff --git a/includes/api/ApiFeedContributions.php b/includes/api/ApiFeedContributions.php index 28b0a4b714..fabe4a2e7c 100644 --- a/includes/api/ApiFeedContributions.php +++ b/includes/api/ApiFeedContributions.php @@ -71,18 +71,15 @@ class ApiFeedContributions extends ApiBase { ' [' . $config->get( 'LanguageCode' ) . ']'; $feedUrl = SpecialPage::getTitleFor( 'Contributions', $params['user'] )->getFullURL(); - $target = 'newbies'; - if ( $params['user'] != 'newbies' ) { - try { - $target = $this->titleParser - ->parseTitle( $params['user'], NS_USER ) - ->getText(); - } catch ( MalformedTitleException $e ) { - $this->dieWithError( - [ 'apierror-baduser', 'user', wfEscapeWikiText( $params['user'] ) ], - 'baduser_' . $this->encodeParamName( 'user' ) - ); - } + try { + $target = $this->titleParser + ->parseTitle( $params['user'], NS_USER ) + ->getText(); + } catch ( MalformedTitleException $e ) { + $this->dieWithError( + [ 'apierror-baduser', 'user', wfEscapeWikiText( $params['user'] ) ], + 'baduser_' . $this->encodeParamName( 'user' ) + ); } $feed = new $feedClasses[$params['feedformat']] ( diff --git a/includes/api/ApiFeedWatchlist.php b/includes/api/ApiFeedWatchlist.php index c4977f4115..953c4d8d93 100644 --- a/includes/api/ApiFeedWatchlist.php +++ b/includes/api/ApiFeedWatchlist.php @@ -150,6 +150,7 @@ class ApiFeedWatchlist extends ApiBase { if ( $e instanceof ApiUsageException ) { foreach ( $e->getStatusValue()->getErrors() as $error ) { + // @phan-suppress-next-line PhanUndeclaredMethod $msg = ApiMessage::create( $error ) ->inLanguage( $this->getLanguage() ); $errorTitle = $this->msg( 'api-feed-error-title', $msg->getApiCode() ); diff --git a/includes/api/ApiFormatXmlRsd.php b/includes/api/ApiFormatXmlRsd.php index 6b892fa8ce..3052b89431 100644 --- a/includes/api/ApiFormatXmlRsd.php +++ b/includes/api/ApiFormatXmlRsd.php @@ -21,6 +21,9 @@ * @file */ +/** + * @ingroup API + */ class ApiFormatXmlRsd extends ApiFormatXml { public function __construct( ApiMain $main, $format ) { parent::__construct( $main, $format ); diff --git a/includes/api/ApiHelp.php b/includes/api/ApiHelp.php index 988957b6d8..2627715d5a 100644 --- a/includes/api/ApiHelp.php +++ b/includes/api/ApiHelp.php @@ -66,6 +66,23 @@ class ApiHelp extends ApiBase { ApiResult::setSubelementsList( $data, 'help' ); $result->addValue( null, $this->getModuleName(), $data ); } else { + // Show any errors at the top of the HTML + $transform = [ + 'Types' => [ 'AssocAsObject' => true ], + 'Strip' => 'all', + ]; + $errors = array_filter( [ + 'errors' => $this->getResult()->getResultData( [ 'errors' ], $transform ), + 'warnings' => $this->getResult()->getResultData( [ 'warnings' ], $transform ), + ] ); + if ( $errors ) { + $json = FormatJson::encode( $errors, true, FormatJson::UTF8_OK ); + // Escape any "--", some parsers might interpret that as end-of-comment. + // The above already escaped any "<" and ">". + $json = str_replace( '--', '-\u002D', $json ); + $html = "\n$html"; + } + $result->reset(); $result->addValue( null, 'text', $html, ApiResult::NO_SIZE_CHECK ); $result->addValue( null, 'mime', 'text/html', ApiResult::NO_SIZE_CHECK ); diff --git a/includes/api/ApiHelpParamValueMessage.php b/includes/api/ApiHelpParamValueMessage.php index 7aebf90e3c..0272dcd426 100644 --- a/includes/api/ApiHelpParamValueMessage.php +++ b/includes/api/ApiHelpParamValueMessage.php @@ -28,6 +28,7 @@ * 'APIGetParamDescriptionMessages' hook simple. * * @since 1.25 + * @ingroup API */ class ApiHelpParamValueMessage extends Message { diff --git a/includes/api/ApiImageRotate.php b/includes/api/ApiImageRotate.php index 668bd0e427..8a0e8c974d 100644 --- a/includes/api/ApiImageRotate.php +++ b/includes/api/ApiImageRotate.php @@ -1,7 +1,4 @@ getTempFSFileFactory() + ->newTempFSFile( 'rotate_', $ext ); $dstPath = $tmpFile->getPath(); + // @phan-suppress-next-line PhanUndeclaredMethod $err = $handler->rotate( $file, [ 'srcPath' => $srcPath, 'dstPath' => $dstPath, @@ -112,6 +116,7 @@ class ApiImageRotate extends ApiBase { $comment = wfMessage( 'rotate-comment' )->numParams( $rotation )->inContentLanguage()->text(); + // @phan-suppress-next-line PhanUndeclaredMethod $status = $file->upload( $dstPath, $comment, diff --git a/includes/api/ApiImport.php b/includes/api/ApiImport.php index b36045e1f4..e787e2671a 100644 --- a/includes/api/ApiImport.php +++ b/includes/api/ApiImport.php @@ -29,7 +29,6 @@ class ApiImport extends ApiBase { public function execute() { $this->useTransactionalTimeLimit(); - $user = $this->getUser(); $params = $this->extractRequestParams(); @@ -37,7 +36,7 @@ class ApiImport extends ApiBase { $isUpload = false; if ( isset( $params['interwikisource'] ) ) { - if ( !$user->isAllowed( 'import' ) ) { + if ( !$this->getPermissionManager()->userHasRight( $user, 'import' ) ) { $this->dieWithError( 'apierror-cantimport' ); } if ( !isset( $params['interwikipage'] ) ) { @@ -52,7 +51,7 @@ class ApiImport extends ApiBase { $usernamePrefix = $params['interwikisource']; } else { $isUpload = true; - if ( !$user->isAllowed( 'importupload' ) ) { + if ( !$this->getPermissionManager()->userHasRight( $user, 'importupload' ) ) { $this->dieWithError( 'apierror-cantimport-upload' ); } $source = ImportStreamSource::newFromUpload( 'xml' ); diff --git a/includes/api/ApiImportReporter.php b/includes/api/ApiImportReporter.php index be53c67c33..c4a432cf64 100644 --- a/includes/api/ApiImportReporter.php +++ b/includes/api/ApiImportReporter.php @@ -34,6 +34,7 @@ class ApiImportReporter extends ImportReporter { * @param int $successCount * @param array $pageInfo * @return void + * @suppress PhanParamSignatureMismatch */ public function reportPage( $title, $foreignTitle, $revisionCount, $successCount, $pageInfo ) { // Add a result entry diff --git a/includes/api/ApiMain.php b/includes/api/ApiMain.php index 554ab6a285..7bbce976aa 100644 --- a/includes/api/ApiMain.php +++ b/includes/api/ApiMain.php @@ -153,6 +153,7 @@ class ApiMain extends ApiBase { private $mModule; private $mCacheMode = 'private'; + /** @var array */ private $mCacheControl = []; private $mParamsUsed = []; private $mParamsSensitive = []; @@ -166,6 +167,7 @@ class ApiMain extends ApiBase { * @param IContextSource|WebRequest|null $context If this is an instance of * FauxRequest, errors are thrown and no printing occurs * @param bool $enableWrite Should be set to true if the api may modify data + * @suppress PhanUndeclaredMethod */ public function __construct( $context = null, $enableWrite = false ) { if ( $context === null ) { @@ -279,7 +281,10 @@ class ApiMain extends ApiBase { } $this->mResult->setErrorFormatter( $this->getErrorFormatter() ); - $this->mModuleMgr = new ApiModuleManager( $this ); + $this->mModuleMgr = new ApiModuleManager( + $this, + MediaWikiServices::getInstance()->getObjectFactory() + ); $this->mModuleMgr->addModules( self::$Modules, 'action' ); $this->mModuleMgr->addModules( $config->get( 'APIModules' ), 'action' ); $this->mModuleMgr->addModules( self::$Formats, 'format' ); @@ -291,7 +296,6 @@ class ApiMain extends ApiBase { $this->mEnableWrite = $enableWrite; $this->mCdnMaxAge = -1; // flag for executeActionWithErrorHandling() - $this->mCommit = false; } /** @@ -1410,8 +1414,8 @@ class ApiMain extends ApiBase { */ protected function checkExecutePermissions( $module ) { $user = $this->getUser(); - if ( $module->isReadMode() && !User::isEveryoneAllowed( 'read' ) && - !$user->isAllowed( 'read' ) + if ( $module->isReadMode() && !$this->getPermissionManager()->isEveryoneAllowed( 'read' ) && + !$this->getPermissionManager()->userHasRight( $user, 'read' ) ) { $this->dieWithError( 'apierror-readapidenied' ); } @@ -1419,7 +1423,7 @@ class ApiMain extends ApiBase { if ( $module->isWriteMode() ) { if ( !$this->mEnableWrite ) { $this->dieWithError( 'apierror-noapiwrite' ); - } elseif ( !$user->isAllowed( 'writeapi' ) ) { + } elseif ( !$this->getPermissionManager()->userHasRight( $user, 'writeapi' ) ) { $this->dieWithError( 'apierror-writeapidenied' ); } elseif ( $this->getRequest()->getHeader( 'Promise-Non-Write-API-Action' ) ) { $this->dieWithError( 'apierror-promised-nonwrite-api' ); @@ -1504,7 +1508,7 @@ class ApiMain extends ApiBase { } break; case 'bot': - if ( !$user->isAllowed( 'bot' ) ) { + if ( !$this->getPermissionManager()->userHasRight( $user, 'bot' ) ) { $this->dieWithError( 'apierror-assertbotfailed' ); } break; @@ -1539,6 +1543,12 @@ class ApiMain extends ApiBase { $this->dieWithErrorOrDebug( [ 'apierror-mustbeposted', $this->mAction ] ); } + if ( $request->wasPosted() && !$request->getHeader( 'Content-Type' ) ) { + $this->addDeprecation( + 'apiwarn-deprecation-post-without-content-type', 'post-without-content-type' + ); + } + // See if custom printer is used $this->mPrinter = $module->getCustomPrinter(); if ( is_null( $this->mPrinter ) ) { @@ -1700,7 +1710,7 @@ class ApiMain extends ApiBase { * @return string */ protected function encodeRequestLogValue( $s ) { - static $table; + static $table = []; if ( !$table ) { $chars = ';@$!*(),/:'; $numChars = strlen( $chars ); @@ -1906,6 +1916,10 @@ class ApiMain extends ApiBase { ]; } + /** + * @inheritDoc + * @phan-param array{nolead?:bool,headerlevel?:int,tocnumber?:int[]} $options + */ public function modifyHelp( array &$help, array $options, array &$tocData ) { // Wish PHP had an "array_insert_before". Instead, we have to manually // reindex the array to get 'permissions' in the right place. @@ -1939,7 +1953,7 @@ class ApiMain extends ApiBase { $groups = array_map( function ( $group ) { return $group == '*' ? 'all' : $group; - }, User::getGroupsWithPermission( $right ) ); + }, $this->getPermissionManager()->getGroupsWithPermission( $right ) ); $help['permissions'] .= Html::rawElement( 'dd', null, $this->msg( 'api-help-permissions-granted-to' ) @@ -2052,7 +2066,8 @@ class ApiMain extends ApiBase { */ public function canApiHighLimits() { if ( !isset( $this->mCanApiHighLimits ) ) { - $this->mCanApiHighLimits = $this->getUser()->isAllowed( 'apihighlimits' ); + $this->mCanApiHighLimits = $this->getPermissionManager() + ->userHasRight( $this->getUser(), 'apihighlimits' ); } return $this->mCanApiHighLimits; diff --git a/includes/api/ApiManageTags.php b/includes/api/ApiManageTags.php index 42de161018..6cd717a680 100644 --- a/includes/api/ApiManageTags.php +++ b/includes/api/ApiManageTags.php @@ -31,10 +31,10 @@ class ApiManageTags extends ApiBase { // make sure the user is allowed if ( $params['operation'] !== 'delete' - && !$this->getUser()->isAllowed( 'managechangetags' ) + && !$this->getPermissionManager()->userHasRight( $user, 'managechangetags' ) ) { $this->dieWithError( 'tags-manage-no-permission', 'permissiondenied' ); - } elseif ( !$this->getUser()->isAllowed( 'deletechangetags' ) ) { + } elseif ( !$this->getPermissionManager()->userHasRight( $user, 'deletechangetags' ) ) { $this->dieWithError( 'tags-delete-no-permission', 'permissiondenied' ); } diff --git a/includes/api/ApiMessageTrait.php b/includes/api/ApiMessageTrait.php index 73e4ac26d5..528a8b5ac2 100644 --- a/includes/api/ApiMessageTrait.php +++ b/includes/api/ApiMessageTrait.php @@ -22,6 +22,8 @@ * Trait to implement the IApiMessage interface for Message subclasses * @since 1.27 * @ingroup API + * @phan-file-suppress PhanTraitParentReference + * @phan-file-suppress PhanUndeclaredMethod */ trait ApiMessageTrait { diff --git a/includes/api/ApiModuleManager.php b/includes/api/ApiModuleManager.php index d2df013c08..8d5a82b92e 100644 --- a/includes/api/ApiModuleManager.php +++ b/includes/api/ApiModuleManager.php @@ -21,6 +21,9 @@ * @since 1.21 */ +use MediaWiki\MediaWikiServices; +use Wikimedia\ObjectFactory; + /** * This class holds a list of modules and handles instantiation * @@ -45,64 +48,35 @@ class ApiModuleManager extends ContextSource { * @var array[] */ private $mModules = []; + /** + * @var ObjectFactory + */ + private $objectFactory; /** * Construct new module manager + * * @param ApiBase $parentModule Parent module instance will be used during instantiation + * @param ObjectFactory|null $objectFactory Object factory to use when instantiating modules */ - public function __construct( ApiBase $parentModule ) { + public function __construct( ApiBase $parentModule, ObjectFactory $objectFactory = null ) { $this->mParent = $parentModule; + $this->objectFactory = $objectFactory ?? MediaWikiServices::getInstance()->getObjectFactory(); } /** * Add a list of modules to the manager. Each module is described - * by a module spec. - * - * Each module spec is an associative array containing at least - * the 'class' key for the module's class, and optionally a - * 'factory' key for the factory function to use for the module. + * by an ObjectFactory spec. * - * That factory function will be called with two parameters, - * the parent module (an instance of ApiBase, usually ApiMain) - * and the name the module was registered under. The return - * value must be an instance of the class given in the 'class' - * field. + * This simply calls `addModule()` for each module in `$modules`. * - * For backward compatibility, the module spec may also be a - * simple string containing the module's class name. In that - * case, the class' constructor will be called with the parent - * module and module name as parameters, as described above. - * - * Examples for defining module specs: - * - * @code - * $modules['foo'] = 'ApiFoo'; - * $modules['bar'] = [ - * 'class' => ApiBar::class, - * 'factory' => function( $main, $name ) { ... } - * ]; - * $modules['xyzzy'] = [ - * 'class' => ApiXyzzy::class, - * 'factory' => [ XyzzyFactory::class, 'newApiModule' ] - * ]; - * @endcode - * - * @param array $modules A map of ModuleName => ModuleSpec; The ModuleSpec - * is either a string containing the module's class name, or an associative - * array (see above for details). + * @see ApiModuleManager::addModule() + * @param array $modules A map of ModuleName => ModuleSpec * @param string $group Which group modules belong to (action,format,...) */ public function addModules( array $modules, $group ) { foreach ( $modules as $name => $moduleSpec ) { - if ( is_array( $moduleSpec ) ) { - $class = $moduleSpec['class']; - $factory = ( $moduleSpec['factory'] ?? null ); - } else { - $class = $moduleSpec; - $factory = null; - } - - $this->addModule( $name, $group, $class, $factory ); + $this->addModule( $name, $group, $moduleSpec ); } } @@ -111,14 +85,21 @@ class ApiModuleManager extends ContextSource { * classes who wish to add their own modules to their lexicon or override the * behavior of inherent ones. * + * ObjectFactory is used to instantiate the module when needed. The parent module + * (`$parentModule` from `__construct()`) and the `$name` are passed as extraArgs. + * + * @since 1.34, accepts an ObjectFactory spec as the third parameter. The old calling convention, + * passing a class name as parameter #3 and an optional factory callable as parameter #4, is + * deprecated. * @param string $name The identifier for this module. * @param string $group Name of the module group - * @param string $class The class where this module is implemented. - * @param callable|null $factory Callback for instantiating the module. + * @param string|array $spec The ObjectFactory spec for instantiating the module, + * or a class name to instantiate. + * @param callable|null $factory Callback for instantiating the module (deprecated). * * @throws InvalidArgumentException */ - public function addModule( $name, $group, $class, $factory = null ) { + public function addModule( $name, $group, $spec, $factory = null ) { if ( !is_string( $name ) ) { throw new InvalidArgumentException( '$name must be a string' ); } @@ -127,16 +108,23 @@ class ApiModuleManager extends ContextSource { throw new InvalidArgumentException( '$group must be a string' ); } - if ( !is_string( $class ) ) { - throw new InvalidArgumentException( '$class must be a string' ); - } + if ( is_string( $spec ) ) { + $spec = [ + 'class' => $spec + ]; - if ( $factory !== null && !is_callable( $factory ) ) { - throw new InvalidArgumentException( '$factory must be a callable (or null)' ); + if ( is_callable( $factory ) ) { + wfDeprecated( __METHOD__ . ' with $class and $factory', '1.34' ); + $spec['factory'] = $factory; + } + } elseif ( !is_array( $spec ) ) { + throw new InvalidArgumentException( '$spec must be a string or an array' ); + } elseif ( !isset( $spec['class'] ) ) { + throw new InvalidArgumentException( '$spec must define a class name' ); } $this->mGroups[$group] = null; - $this->mModules[$name] = [ $group, $class, $factory ]; + $this->mModules[$name] = [ $group, $spec ]; } /** @@ -153,7 +141,7 @@ class ApiModuleManager extends ContextSource { return null; } - list( $moduleGroup, $moduleClass, $moduleFactory ) = $this->mModules[$moduleName]; + list( $moduleGroup, $spec ) = $this->mModules[$moduleName]; if ( $group !== null && $moduleGroup !== $group ) { return null; @@ -164,7 +152,7 @@ class ApiModuleManager extends ContextSource { return $this->mInstances[$moduleName]; } else { // new instance - $instance = $this->instantiateModule( $moduleName, $moduleClass, $moduleFactory ); + $instance = $this->instantiateModule( $moduleName, $spec ); if ( !$ignoreCache ) { // cache this instance in case it is needed later @@ -179,28 +167,22 @@ class ApiModuleManager extends ContextSource { * Instantiate the module using the given class or factory function. * * @param string $name The identifier for this module. - * @param string $class The class where this module is implemented. - * @param callable|null $factory Callback for instantiating the module. + * @param array $spec The ObjectFactory spec for instantiating the module. * - * @throws MWException + * @throws UnexpectedValueException * @return ApiBase */ - private function instantiateModule( $name, $class, $factory = null ) { - if ( $factory !== null ) { - // create instance from factory - $instance = call_user_func( $factory, $this->mParent, $name ); - - if ( !$instance instanceof $class ) { - throw new MWException( - "The factory function for module $name did not return an instance of $class!" - ); - } - } else { - // create instance from class name - $instance = new $class( $this->mParent, $name ); - } - - return $instance; + private function instantiateModule( $name, $spec ) { + return $this->objectFactory->createObject( + $spec, + [ + 'extraArgs' => [ + $this->mParent, + $name + ], + 'assertClass' => $spec['class'] + ] + ); } /** @@ -213,8 +195,8 @@ class ApiModuleManager extends ContextSource { return array_keys( $this->mModules ); } $result = []; - foreach ( $this->mModules as $name => $grpCls ) { - if ( $grpCls[0] === $group ) { + foreach ( $this->mModules as $name => $groupAndSpec ) { + if ( $groupAndSpec[0] === $group ) { $result[] = $name; } } @@ -229,9 +211,9 @@ class ApiModuleManager extends ContextSource { */ public function getNamesWithClasses( $group = null ) { $result = []; - foreach ( $this->mModules as $name => $grpCls ) { - if ( $group === null || $grpCls[0] === $group ) { - $result[$name] = $grpCls[1]; + foreach ( $this->mModules as $name => $groupAndSpec ) { + if ( $group === null || $groupAndSpec[0] === $group ) { + $result[$name] = $groupAndSpec[1]['class']; } } @@ -247,7 +229,7 @@ class ApiModuleManager extends ContextSource { */ public function getClassName( $module ) { if ( isset( $this->mModules[$module] ) ) { - return $this->mModules[$module][1]; + return $this->mModules[$module][1]['class']; } return false; diff --git a/includes/api/ApiMove.php b/includes/api/ApiMove.php index 540860b3a9..74c6f8fccd 100644 --- a/includes/api/ApiMove.php +++ b/includes/api/ApiMove.php @@ -59,13 +59,15 @@ class ApiMove extends ApiBase { } $toTalk = $toTitle->getTalkPageIfDefined(); + $repoGroup = MediaWikiServices::getInstance()->getRepoGroup(); if ( $toTitle->getNamespace() == NS_FILE - && !RepoGroup::singleton()->getLocalRepo()->findFile( $toTitle ) - && MediaWikiServices::getInstance()->getRepoGroup()->findFile( $toTitle ) + && !$repoGroup->getLocalRepo()->findFile( $toTitle ) + && $repoGroup->findFile( $toTitle ) ) { - if ( !$params['ignorewarnings'] && $user->isAllowed( 'reupload-shared' ) ) { + if ( !$params['ignorewarnings'] && + $this->getPermissionManager()->userHasRight( $user, 'reupload-shared' ) ) { $this->dieWithError( 'apierror-fileexists-sharedrepo-perm' ); - } elseif ( !$user->isAllowed( 'reupload-shared' ) ) { + } elseif ( !$this->getPermissionManager()->userHasRight( $user, 'reupload-shared' ) ) { $this->dieWithError( 'apierror-cantoverwrite-sharedfile' ); } } @@ -172,7 +174,7 @@ class ApiMove extends ApiBase { * @return Status */ protected function movePage( Title $from, Title $to, $reason, $createRedirect, $changeTags ) { - $mp = new MovePage( $from, $to ); + $mp = MediaWikiServices::getInstance()->getMovePageFactory()->newMovePage( $from, $to ); $valid = $mp->isValidMove(); if ( !$valid->isOK() ) { return $valid; @@ -185,7 +187,7 @@ class ApiMove extends ApiBase { } // Check suppressredirect permission - if ( !$user->isAllowed( 'suppressredirect' ) ) { + if ( !$this->getPermissionManager()->userHasRight( $user, 'suppressredirect' ) ) { $createRedirect = true; } @@ -206,7 +208,7 @@ class ApiMove extends ApiBase { $mp = new MovePage( $fromTitle, $toTitle ); $result = $mp->moveSubpagesIfAllowed( $this->getUser(), $reason, !$noredirect, $changeTags ); - if ( !$result->isOk() ) { + if ( !$result->isOK() ) { // This means the whole thing failed return [ 'errors' => $this->getErrorFormatter()->arrayFromStatus( $result ) ]; } diff --git a/includes/api/ApiOpenSearch.php b/includes/api/ApiOpenSearch.php index 8e2837b324..7fcb818418 100644 --- a/includes/api/ApiOpenSearch.php +++ b/includes/api/ApiOpenSearch.php @@ -71,6 +71,7 @@ class ApiOpenSearch extends ApiBase { case 'xml': $printer = $this->getMain()->createPrinterByName( 'xml' . $this->fm ); + '@phan-var ApiFormatXML $printer'; $printer->setRootElement( 'SearchSuggestion' ); return $printer; @@ -96,6 +97,7 @@ class ApiOpenSearch extends ApiBase { // Trim extracts, if necessary $length = $this->getConfig()->get( 'OpenSearchDescriptionLength' ); foreach ( $results as &$r ) { + // @phan-suppress-next-line PhanTypeInvalidDimOffset if ( is_string( $r['extract'] ) && !$r['extract trimmed'] ) { $r['extract'] = self::trimExtract( $r['extract'], $length ); } @@ -111,6 +113,8 @@ class ApiOpenSearch extends ApiBase { * @param string $search the search query * @param array $params api request params * @return array search results. Keys are integers. + * @phan-return array + * Note that phan annotations don't support keys containing a space. */ private function search( $search, array $params ) { $searchEngine = $this->buildSearchEngine( $params ); @@ -247,6 +251,7 @@ class ApiOpenSearch extends ApiBase { if ( is_string( $r['extract'] ) && $r['extract'] !== '' ) { $item['Description'] = $r['extract']; } + // @phan-suppress-next-line PhanTypeArraySuspiciousNullable if ( is_array( $r['image'] ) && isset( $r['image']['source'] ) ) { $item['Image'] = array_intersect_key( $r['image'], $imageKeys ); } diff --git a/includes/api/ApiPageSet.php b/includes/api/ApiPageSet.php index 6b24b6347a..e68676a94e 100644 --- a/includes/api/ApiPageSet.php +++ b/includes/api/ApiPageSet.php @@ -77,6 +77,7 @@ class ApiPageSet extends ApiBase { private $mGeneratorData = []; // [ns][dbkey] => data array private $mFakePageId = -1; private $mCacheMode = 'public'; + /** @var array */ private $mRequestedPageFields = []; /** @var int */ private $mDefaultNamespace = NS_MAIN; @@ -971,7 +972,8 @@ class ApiPageSet extends ApiBase { // If the user can see deleted revisions, pull out the corresponding // titles from the archive table and include them too. We ignore // ar_page_id because deleted revisions are tied by title, not page_id. - if ( $goodRemaining && $this->getUser()->isAllowed( 'deletedhistory' ) ) { + if ( $goodRemaining && + $this->getPermissionManager()->userHasRight( $this->getUser(), 'deletedhistory' ) ) { $tables = [ 'archive' ]; $fields = [ 'ar_rev_id', 'ar_namespace', 'ar_title' ]; $where = [ 'ar_rev_id' => array_keys( $goodRemaining ) ]; @@ -1164,7 +1166,8 @@ class ApiPageSet extends ApiBase { $services = MediaWikiServices::getInstance(); $contLang = $services->getContentLanguage(); - foreach ( $titles as $title ) { + $titleObjects = []; + foreach ( $titles as $index => $title ) { if ( is_string( $title ) ) { try { $titleObj = Title::newFromTextThrow( $title, $this->mDefaultNamespace ); @@ -1183,6 +1186,16 @@ class ApiPageSet extends ApiBase { } else { $titleObj = $title; } + + $titleObjects[$index] = $titleObj; + } + + // Get gender information + $genderCache = $services->getGenderCache(); + $genderCache->doTitlesArray( $titleObjects, __METHOD__ ); + + foreach ( $titleObjects as $index => $titleObj ) { + $title = is_string( $titles[$index] ) ? $titles[$index] : false; $unconvertedTitle = $titleObj->getPrefixedText(); $titleWasConverted = false; if ( $titleObj->isExternal() ) { @@ -1195,7 +1208,7 @@ class ApiPageSet extends ApiBase { ) { // Language::findVariantLink will modify titleText and titleObj into // the canonical variant if possible - $titleText = is_string( $title ) ? $title : $titleObj->getPrefixedText(); + $titleText = $title !== false ? $title : $titleObj->getPrefixedText(); $contLang->findVariantLink( $titleText, $titleObj ); $titleWasConverted = $unconvertedTitle !== $titleObj->getPrefixedText(); } @@ -1243,24 +1256,13 @@ class ApiPageSet extends ApiBase { if ( $titleWasConverted ) { $this->mConvertedTitles[$unconvertedTitle] = $titleObj->getPrefixedText(); // In this case the page can't be Special. - if ( is_string( $title ) && $title !== $unconvertedTitle ) { + if ( $title !== false && $title !== $unconvertedTitle ) { $this->mNormalizedTitles[$title] = $unconvertedTitle; } - } elseif ( is_string( $title ) && $title !== $titleObj->getPrefixedText() ) { + } elseif ( $title !== false && $title !== $titleObj->getPrefixedText() ) { $this->mNormalizedTitles[$title] = $titleObj->getPrefixedText(); } - - // Need gender information - if ( - MediaWikiServices::getInstance()->getNamespaceInfo()-> - hasGenderDistinction( $titleObj->getNamespace() ) - ) { - $usernames[] = $titleObj->getText(); - } } - // Get gender information - $genderCache = $services->getGenderCache(); - $genderCache->doQuery( $usernames, __METHOD__ ); return $linkBatch; } diff --git a/includes/api/ApiParse.php b/includes/api/ApiParse.php index a7390e617a..2e7db78327 100644 --- a/includes/api/ApiParse.php +++ b/includes/api/ApiParse.php @@ -21,7 +21,7 @@ */ use MediaWiki\MediaWikiServices; -use MediaWiki\Storage\RevisionRecord; +use MediaWiki\Revision\RevisionRecord; /** * @ingroup API @@ -491,6 +491,7 @@ class ApiParse extends ApiBase { $parser = MediaWikiServices::getInstance()->getParser(); $parser->startExternalParse( $titleObj, $popts, Parser::OT_PREPROCESS ); + // @phan-suppress-next-line PhanUndeclaredMethod $xml = $parser->preprocessToDom( $this->content->getText() )->__toString(); $result_array['parsetree'] = $xml; $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'parsetree'; diff --git a/includes/api/ApiQuery.php b/includes/api/ApiQuery.php index bdb0dc22aa..a7ff729e84 100644 --- a/includes/api/ApiQuery.php +++ b/includes/api/ApiQuery.php @@ -20,6 +20,7 @@ * @file */ +use MediaWiki\MediaWikiServices; use Wikimedia\Rdbms\IDatabase; /** @@ -134,7 +135,10 @@ class ApiQuery extends ApiBase { public function __construct( ApiMain $main, $action ) { parent::__construct( $main, $action ); - $this->mModuleMgr = new ApiModuleManager( $this ); + $this->mModuleMgr = new ApiModuleManager( + $this, + MediaWikiServices::getInstance()->getObjectFactory() + ); // Allow custom modules to be added in LocalSettings.php $config = $this->getConfig(); @@ -223,7 +227,9 @@ class ApiQuery extends ApiBase { // Filter modules based on continue parameter $continuationManager = new ApiContinuationManager( $this, $allModules, $propModules ); $this->setContinuationManager( $continuationManager ); + /** @var ApiQueryBase[] $modules */ $modules = $continuationManager->getRunModules(); + '@phan-var ApiQueryBase[] $modules'; if ( !$continuationManager->isGeneratorDone() ) { // Query modules may optimize data requests through the $this->getPageSet() @@ -242,7 +248,6 @@ class ApiQuery extends ApiBase { $cacheMode = $this->mPageSet->getCacheMode(); // Execute all unfinished modules - /** @var ApiQueryBase $module */ foreach ( $modules as $module ) { $params = $module->extractRequestParams(); $cacheMode = $this->mergeCacheMode( diff --git a/includes/api/ApiQueryAllDeletedRevisions.php b/includes/api/ApiQueryAllDeletedRevisions.php index 85ca6480f1..2a499844a4 100644 --- a/includes/api/ApiQueryAllDeletedRevisions.php +++ b/includes/api/ApiQueryAllDeletedRevisions.php @@ -43,9 +43,6 @@ class ApiQueryAllDeletedRevisions extends ApiQueryRevisionsBase { * @return void */ protected function run( ApiPageSet $resultPageSet = null ) { - // Before doing anything at all, let's check permissions - $this->checkUserRightsAny( 'deletedhistory' ); - $user = $this->getUser(); $db = $this->getDB(); $params = $this->extractRequestParams( false ); @@ -134,7 +131,7 @@ class ApiQueryAllDeletedRevisions extends ApiQueryRevisionsBase { $this->addJoinConds( [ 'change_tag' => [ 'JOIN', [ 'ar_rev_id=ct_rev_id' ] ] ] ); - $changeTagDefStore = MediaWikiServices::getInstance()->getChangeTagDefStore(); + $changeTagDefStore = $services->getChangeTagDefStore(); try { $this->addWhereFld( 'ct_tag_id', $changeTagDefStore->getId( $params['tag'] ) ); } catch ( NameTableAccessException $exception ) { @@ -144,8 +141,15 @@ class ApiQueryAllDeletedRevisions extends ApiQueryRevisionsBase { } // This means stricter restrictions - if ( $this->fetchContent ) { - $this->checkUserRightsAny( [ 'deletedtext', 'undelete' ] ); + if ( ( $this->fld_comment || $this->fld_parsedcomment ) && + !$this->getPermissionManager()->userHasRight( $user, 'deletedhistory' ) + ) { + $this->dieWithError( 'apierror-cantview-deleted-comment', 'permissiondenied' ); + } + if ( $this->fetchContent && + !$this->getPermissionManager()->userHasAnyRight( $user, 'deletedtext', 'undelete' ) + ) { + $this->dieWithError( 'apierror-cantview-deleted-revision-content', 'permissiondenied' ); } $miser_ns = null; @@ -235,11 +239,11 @@ class ApiQueryAllDeletedRevisions extends ApiQueryRevisionsBase { if ( !is_null( $params['user'] ) || !is_null( $params['excludeuser'] ) ) { // Paranoia: avoid brute force searches (T19342) - // (shouldn't be able to get here without 'deletedhistory', but - // check it again just in case) - if ( !$user->isAllowed( 'deletedhistory' ) ) { + if ( !$this->getPermissionManager()->userHasRight( $user, 'deletedhistory' ) ) { $bitmask = RevisionRecord::DELETED_USER; - } elseif ( !$user->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) { + } elseif ( !$this->getPermissionManager() + ->userHasAnyRight( $user, 'suppressrevision', 'viewsuppressed' ) + ) { $bitmask = RevisionRecord::DELETED_USER | RevisionRecord::DELETED_RESTRICTED; } else { $bitmask = 0; diff --git a/includes/api/ApiQueryAllImages.php b/includes/api/ApiQueryAllImages.php index 40cd149181..b181710558 100644 --- a/includes/api/ApiQueryAllImages.php +++ b/includes/api/ApiQueryAllImages.php @@ -205,7 +205,7 @@ class ApiQueryAllImages extends ApiQueryGeneratorBase { $this->addJoinConds( [ 'user_groups' => [ 'LEFT JOIN', [ - 'ug_group' => User::getGroupsWithPermission( 'bot' ), + 'ug_group' => $this->getPermissionManager()->getGroupsWithPermission( 'bot' ), 'ug_user = ' . $actorQuery['fields']['img_user'], 'ug_expiry IS NULL OR ug_expiry >= ' . $db->addQuotes( $db->timestamp() ) ] diff --git a/includes/api/ApiQueryAllRevisions.php b/includes/api/ApiQueryAllRevisions.php index 050bc0f81e..3d4c49bb0f 100644 --- a/includes/api/ApiQueryAllRevisions.php +++ b/includes/api/ApiQueryAllRevisions.php @@ -40,8 +40,6 @@ class ApiQueryAllRevisions extends ApiQueryRevisionsBase { * @return void */ protected function run( ApiPageSet $resultPageSet = null ) { - global $wgActorTableSchemaMigrationStage; - $db = $this->getDB(); $params = $this->extractRequestParams( false ); $services = MediaWikiServices::getInstance(); @@ -54,9 +52,7 @@ class ApiQueryAllRevisions extends ApiQueryRevisionsBase { $tsField = 'rev_timestamp'; $idField = 'rev_id'; $pageField = 'rev_page'; - if ( $params['user'] !== null && - ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_READ_NEW ) - ) { + if ( $params['user'] !== null ) { // The query is probably best done using the actor_timestamp index on // revision_actor_temp. Use the denormalized fields from that table. $tsField = 'revactor_timestamp'; @@ -154,9 +150,11 @@ class ApiQueryAllRevisions extends ApiQueryRevisionsBase { if ( $params['user'] !== null || $params['excludeuser'] !== null ) { // Paranoia: avoid brute force searches (T19342) - if ( !$this->getUser()->isAllowed( 'deletedhistory' ) ) { + if ( !$this->getPermissionManager()->userHasRight( $this->getUser(), 'deletedhistory' ) ) { $bitmask = RevisionRecord::DELETED_USER; - } elseif ( !$this->getUser()->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) { + } elseif ( !$this->getPermissionManager() + ->userHasAnyRight( $this->getUser(), 'suppressrevision', 'viewsuppressed' ) + ) { $bitmask = RevisionRecord::DELETED_USER | RevisionRecord::DELETED_RESTRICTED; } else { $bitmask = 0; diff --git a/includes/api/ApiQueryAllUsers.php b/includes/api/ApiQueryAllUsers.php index 59e92e1538..0ea6af3247 100644 --- a/includes/api/ApiQueryAllUsers.php +++ b/includes/api/ApiQueryAllUsers.php @@ -20,12 +20,16 @@ * @file */ +use MediaWiki\Block\DatabaseBlock; + /** * Query module to enumerate all registered users. * * @ingroup API */ class ApiQueryAllUsers extends ApiQueryBase { + use ApiQueryBlockInfoTrait; + public function __construct( ApiQuery $query, $moduleName ) { parent::__construct( $query, $moduleName, 'au' ); } @@ -41,8 +45,6 @@ class ApiQueryAllUsers extends ApiQueryBase { } public function execute() { - global $wgActorTableSchemaMigrationStage; - $params = $this->extractRequestParams(); $activeUserDays = $this->getConfig()->get( 'ActiveUserDays' ); @@ -90,7 +92,8 @@ class ApiQueryAllUsers extends ApiQueryBase { if ( !is_null( $params['rights'] ) && count( $params['rights'] ) ) { $groups = []; foreach ( $params['rights'] as $r ) { - $groups = array_merge( $groups, User::getGroupsWithPermission( $r ) ); + $groups = array_merge( $groups, $this->getPermissionManager() + ->getGroupsWithPermission( $r ) ); } // no group with the given right(s) exists, no need for a query @@ -154,7 +157,7 @@ class ApiQueryAllUsers extends ApiQueryBase { $this->addWhere( 'user_editcount > 0' ); } - $this->showHiddenUsersAddBlockInfo( $fld_blockinfo ); + $this->addBlockInfoToQuery( $fld_blockinfo ); if ( $fld_groups || $fld_rights ) { $this->addFields( [ 'groups' => @@ -180,22 +183,17 @@ class ApiQueryAllUsers extends ApiQueryBase { ] ] ); // Actually count the actions using a subquery (T66505 and T66507) - $tables = [ 'recentchanges' ]; - $joins = []; - if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_READ_OLD ) { - $userCond = 'rc_user_text = user_name'; - } else { - $tables[] = 'actor'; - $joins['actor'] = [ 'JOIN', 'rc_actor = actor_id' ]; - $userCond = 'actor_user = user_id'; - } + $tables = [ 'recentchanges', 'actor' ]; + $joins = [ + 'actor' => [ 'JOIN', 'rc_actor = actor_id' ], + ]; $timestamp = $db->timestamp( wfTimestamp( TS_UNIX ) - $activeUserSeconds ); $this->addFields( [ 'recentactions' => '(' . $db->selectSQLText( $tables, 'COUNT(*)', [ - $userCond, + 'actor_user = user_id', 'rc_type != ' . $db->addQuotes( RC_EXTERNAL ), // no wikidata 'rc_log_type IS NULL OR rc_log_type != ' . $db->addQuotes( 'newusers' ), 'rc_timestamp >= ' . $db->addQuotes( $timestamp ), @@ -269,13 +267,8 @@ class ApiQueryAllUsers extends ApiQueryBase { ); } - if ( $fld_blockinfo && !is_null( $row->ipb_by_text ) ) { - $data['blockid'] = (int)$row->ipb_id; - $data['blockedby'] = $row->ipb_by_text; - $data['blockedbyid'] = (int)$row->ipb_by; - $data['blockedtimestamp'] = wfTimestamp( TS_ISO_8601, $row->ipb_timestamp ); - $data['blockreason'] = $commentStore->getComment( 'ipb_reason', $row )->text; - $data['blockexpiry'] = $row->ipb_expiry; + if ( $fld_blockinfo && !is_null( $row->ipb_id ) ) { + $data += $this->getBlockDetails( DatabaseBlock::newFromRow( $row ) ); } if ( $row->ipb_deleted ) { $data['hidden'] = true; @@ -312,7 +305,7 @@ class ApiQueryAllUsers extends ApiQueryBase { } if ( $fld_rights ) { - $data['rights'] = User::getGroupPermissions( $groups ); + $data['rights'] = $this->getPermissionManager()->getGroupPermissions( $groups ); ApiResult::setIndexedTagName( $data['rights'], 'r' ); ApiResult::setArrayType( $data['rights'], 'array' ); } @@ -355,7 +348,7 @@ class ApiQueryAllUsers extends ApiQueryBase { ApiBase::PARAM_ISMULTI => true, ], 'rights' => [ - ApiBase::PARAM_TYPE => User::getAllRights(), + ApiBase::PARAM_TYPE => $this->getPermissionManager()->getAllPermissions(), ApiBase::PARAM_ISMULTI => true, ], 'prop' => [ diff --git a/includes/api/ApiQueryBacklinks.php b/includes/api/ApiQueryBacklinks.php index f82a559f1e..a6b15e997b 100644 --- a/includes/api/ApiQueryBacklinks.php +++ b/includes/api/ApiQueryBacklinks.php @@ -35,9 +35,15 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase { */ private $rootTitle; - private $params, $cont, $redirect; + private $params; + /** @var array */ + private $cont; + private $redirect; private $bl_ns, $bl_from, $bl_from_ns, $bl_table, $bl_code, $bl_title, $bl_fields, $hasNS; + /** @var string */ + private $helpUrl; + /** * Maps ns and title to pageid * @@ -306,7 +312,7 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase { } if ( is_null( $resultPageSet ) ) { - $a['pageid'] = (int)$row->page_id; + $a = [ 'pageid' => (int)$row->page_id ]; ApiQueryBase::addTitleInfo( $a, Title::makeTitle( $row->page_namespace, $row->page_title ) ); if ( $row->page_is_redirect ) { $a['redirect'] = true; diff --git a/includes/api/ApiQueryBacklinksprop.php b/includes/api/ApiQueryBacklinksprop.php index b8672ee6b7..022fd9b848 100644 --- a/includes/api/ApiQueryBacklinksprop.php +++ b/includes/api/ApiQueryBacklinksprop.php @@ -286,6 +286,8 @@ class ApiQueryBacklinksprop extends ApiQueryGeneratorBase { $res = $this->select( __METHOD__ ); if ( is_null( $resultPageSet ) ) { + $this->executeGenderCacheFromResultWrapper( $res, __METHOD__ ); + $count = 0; foreach ( $res as $row ) { if ( ++$count > $params['limit'] ) { diff --git a/includes/api/ApiQueryBase.php b/includes/api/ApiQueryBase.php index 50ca99a45d..059c438a37 100644 --- a/includes/api/ApiQueryBase.php +++ b/includes/api/ApiQueryBase.php @@ -20,6 +20,7 @@ * @file */ +use MediaWiki\MediaWikiServices; use Wikimedia\Rdbms\IDatabase; use Wikimedia\Rdbms\IResultWrapper; @@ -31,6 +32,7 @@ use Wikimedia\Rdbms\IResultWrapper; * @ingroup API */ abstract class ApiQueryBase extends ApiBase { + use ApiQueryBlockInfoTrait; private $mQueryModule, $mDb, $tables, $where, $fields, $options, $join_conds; @@ -424,47 +426,6 @@ abstract class ApiQueryBase extends ApiBase { return Hooks::run( 'ApiQueryBaseProcessRow', [ $this, $row, &$data, &$hookData ] ); } - /** - * Filters hidden users (where the user doesn't have the right to view them) - * Also adds relevant block information - * - * @param bool $showBlockInfo - * @return void - */ - public function showHiddenUsersAddBlockInfo( $showBlockInfo ) { - $db = $this->getDB(); - - $tables = [ 'ipblocks' ]; - $fields = [ 'ipb_deleted' ]; - $joinConds = [ - 'blk' => [ 'LEFT JOIN', [ - 'ipb_user=user_id', - 'ipb_expiry > ' . $db->addQuotes( $db->timestamp() ), - ] ], - ]; - - if ( $showBlockInfo ) { - $actorQuery = ActorMigration::newMigration()->getJoin( 'ipb_by' ); - $commentQuery = CommentStore::getStore()->getJoin( 'ipb_reason' ); - $tables += $actorQuery['tables'] + $commentQuery['tables']; - $joinConds += $actorQuery['joins'] + $commentQuery['joins']; - $fields = array_merge( $fields, [ - 'ipb_id', - 'ipb_expiry', - 'ipb_timestamp' - ], $actorQuery['fields'], $commentQuery['fields'] ); - } - - $this->addTables( [ 'blk' => $tables ] ); - $this->addFields( $fields ); - $this->addJoinConds( $joinConds ); - - // Don't show hidden names - if ( !$this->getUser()->isAllowed( 'hideuser' ) ) { - $this->addWhere( 'ipb_deleted = 0 OR ipb_deleted IS NULL' ); - } - } - /** @} */ /************************************************************************//** @@ -600,7 +561,8 @@ abstract class ApiQueryBase extends ApiBase { * @return bool */ public function userCanSeeRevDel() { - return $this->getUser()->isAllowedAny( + return $this->getPermissionManager()->userHasAnyRight( + $this->getUser(), 'deletedhistory', 'deletedtext', 'suppressrevision', @@ -608,5 +570,61 @@ abstract class ApiQueryBase extends ApiBase { ); } + /** + * Preprocess the result set to fill the GenderCache with the necessary information + * before using self::addTitleInfo + * + * @param IResultWrapper $res Result set to work on. + * The result set must have _namespace and _title fields with the provided field prefix + * @param string $fname The caller function name, always use __METHOD__ + * @param string $fieldPrefix Prefix for fields to check gender for + */ + protected function executeGenderCacheFromResultWrapper( + IResultWrapper $res, $fname = __METHOD__, $fieldPrefix = 'page' + ) { + if ( !$res->numRows() ) { + return; + } + + $services = MediaWikiServices::getInstance(); + $nsInfo = $services->getNamespaceInfo(); + $namespaceField = $fieldPrefix . '_namespace'; + $titleField = $fieldPrefix . '_title'; + + $usernames = []; + foreach ( $res as $row ) { + if ( $nsInfo->hasGenderDistinction( $row->$namespaceField ) ) { + $usernames[] = $row->$titleField; + } + } + + if ( $usernames === [] ) { + return; + } + + $genderCache = $services->getGenderCache(); + $genderCache->doQuery( $usernames, $fname ); + } + + /** @} */ + + /************************************************************************//** + * @name Deprecated methods + * @{ + */ + + /** + * Filters hidden users (where the user doesn't have the right to view them) + * Also adds relevant block information + * + * @deprecated since 1.34, use ApiQueryBlockInfoTrait instead + * @param bool $showBlockInfo + * @return void + */ + public function showHiddenUsersAddBlockInfo( $showBlockInfo ) { + wfDeprecated( __METHOD__, '1.34' ); + return $this->addBlockInfoToQuery( $showBlockInfo ); + } + /** @} */ } diff --git a/includes/api/ApiQueryBlockInfoTrait.php b/includes/api/ApiQueryBlockInfoTrait.php new file mode 100644 index 0000000000..a3be35650e --- /dev/null +++ b/includes/api/ApiQueryBlockInfoTrait.php @@ -0,0 +1,94 @@ +getDB(); + + if ( $showBlockInfo ) { + $queryInfo = DatabaseBlock::getQueryInfo(); + } else { + $queryInfo = [ + 'tables' => [ 'ipblocks' ], + 'fields' => [ 'ipb_deleted' ], + 'joins' => [], + ]; + } + + $this->addTables( [ 'blk' => $queryInfo['tables'] ] ); + $this->addFields( $queryInfo['fields'] ); + $this->addJoinConds( $queryInfo['joins'] ); + $this->addJoinConds( [ + 'blk' => [ 'LEFT JOIN', [ + 'ipb_user=user_id', + 'ipb_expiry > ' . $db->addQuotes( $db->timestamp() ), + ] ], + ] ); + + // Don't show hidden names + if ( !$this->getPermissionManager()->userHasRight( $this->getUser(), 'hideuser' ) ) { + $this->addWhere( 'ipb_deleted = 0 OR ipb_deleted IS NULL' ); + } + } + + /** + * @name Methods required from ApiQueryBase + * @{ + */ + + /** @see ApiBase::getDB */ + abstract protected function getDB(); + + /** @see ApiBase::getPermissionManager */ + abstract protected function getPermissionManager(): PermissionManager; + + /** @see IContextSource::getUser */ + abstract public function getUser(); + + /** @see ApiQueryBase::addTables */ + abstract protected function addTables( $tables, $alias = null ); + + /** @see ApiQueryBase::addFields */ + abstract protected function addFields( $fields ); + + /** @see ApiQueryBase::addWhere */ + abstract protected function addWhere( $conds ); + + /** @see ApiQueryBase::addJoinConds */ + abstract protected function addJoinConds( $conds ); + + /**@}*/ + +} diff --git a/includes/api/ApiQueryBlocks.php b/includes/api/ApiQueryBlocks.php index 5615f46213..f9da9a3108 100644 --- a/includes/api/ApiQueryBlocks.php +++ b/includes/api/ApiQueryBlocks.php @@ -176,7 +176,7 @@ class ApiQueryBlocks extends ApiQueryBase { $this->addWhereIf( 'ipb_range_end > ipb_range_start', isset( $show['range'] ) ); } - if ( !$this->getUser()->isAllowed( 'hideuser' ) ) { + if ( !$this->getPermissionManager()->userHasRight( $this->getUser(), 'hideuser' ) ) { $this->addWhereFld( 'ipb_deleted', 0 ); } @@ -305,6 +305,8 @@ class ApiQueryBlocks extends ApiQueryBase { $id = $restriction->getBlockId(); switch ( $restriction->getType() ) { case 'page': + /** @var \MediaWiki\Block\Restriction\PageRestriction $restriction */ + '@phan-var \MediaWiki\Block\Restriction\PageRestriction $restriction'; $value = [ 'id' => $restriction->getValue() ]; if ( $restriction->getTitle() ) { self::addTitleInfo( $value, $restriction->getTitle() ); diff --git a/includes/api/ApiQueryCategories.php b/includes/api/ApiQueryCategories.php index 547a4e89db..79347e625a 100644 --- a/includes/api/ApiQueryCategories.php +++ b/includes/api/ApiQueryCategories.php @@ -127,6 +127,7 @@ class ApiQueryCategories extends ApiQueryGeneratorBase { 'cl_to' . $sort ] ); } + $this->addOption( 'LIMIT', $params['limit'] + 1 ); $res = $this->select( __METHOD__ ); diff --git a/includes/api/ApiQueryContributors.php b/includes/api/ApiQueryContributors.php index 9057f1055b..f2e306fe83 100644 --- a/includes/api/ApiQueryContributors.php +++ b/includes/api/ApiQueryContributors.php @@ -46,8 +46,6 @@ class ApiQueryContributors extends ApiQueryBase { } public function execute() { - global $wgActorTableSchemaMigrationStage; - $db = $this->getDB(); $params = $this->extractRequestParams(); $this->requireMaxOneParameter( $params, 'group', 'excludegroup', 'rights', 'excluderights' ); @@ -80,14 +78,10 @@ class ApiQueryContributors extends ApiQueryBase { $result = $this->getResult(); $revQuery = MediaWikiServices::getInstance()->getRevisionStore()->getQueryInfo(); - // For SCHEMA_COMPAT_READ_NEW, target indexes on the - // revision_actor_temp table, otherwise on the revision table. - $pageField = ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_READ_NEW ) - ? 'revactor_page' : 'rev_page'; - $idField = ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_READ_NEW ) - ? 'revactor_actor' : $revQuery['fields']['rev_user']; - $countField = ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_READ_NEW ) - ? 'revactor_actor' : $revQuery['fields']['rev_user_text']; + // Target indexes on the revision_actor_temp table. + $pageField = 'revactor_page'; + $idField = 'revactor_actor'; + $countField = 'revactor_actor'; // First, count anons $this->addTables( $revQuery['tables'] ); @@ -152,7 +146,8 @@ class ApiQueryContributors extends ApiQueryBase { } elseif ( $params['rights'] ) { $excludeGroups = false; foreach ( $params['rights'] as $r ) { - $limitGroups = array_merge( $limitGroups, User::getGroupsWithPermission( $r ) ); + $limitGroups = array_merge( $limitGroups, $this->getPermissionManager() + ->getGroupsWithPermission( $r ) ); } // If no group has the rights requested, no need to query @@ -168,7 +163,8 @@ class ApiQueryContributors extends ApiQueryBase { } elseif ( $params['excluderights'] ) { $excludeGroups = true; foreach ( $params['excluderights'] as $r ) { - $limitGroups = array_merge( $limitGroups, User::getGroupsWithPermission( $r ) ); + $limitGroups = array_merge( $limitGroups, $this->getPermissionManager() + ->getGroupsWithPermission( $r ) ); } } @@ -229,7 +225,7 @@ class ApiQueryContributors extends ApiQueryBase { public function getAllowedParams() { $userGroups = User::getAllGroups(); - $userRights = User::getAllRights(); + $userRights = $this->getPermissionManager()->getAllPermissions(); return [ 'group' => [ diff --git a/includes/api/ApiQueryDeletedRevisions.php b/includes/api/ApiQueryDeletedRevisions.php index bbb987f760..12fd20ae59 100644 --- a/includes/api/ApiQueryDeletedRevisions.php +++ b/includes/api/ApiQueryDeletedRevisions.php @@ -40,8 +40,6 @@ class ApiQueryDeletedRevisions extends ApiQueryRevisionsBase { protected function run( ApiPageSet $resultPageSet = null ) { $user = $this->getUser(); - // Before doing anything at all, let's check permissions - $this->checkUserRightsAny( 'deletedhistory' ); $pageSet = $this->getPageSet(); $pageMap = $pageSet->getGoodAndMissingTitlesByNamespace(); @@ -95,8 +93,15 @@ class ApiQueryDeletedRevisions extends ApiQueryRevisionsBase { } // This means stricter restrictions - if ( $this->fetchContent ) { - $this->checkUserRightsAny( [ 'deletedtext', 'undelete' ] ); + if ( ( $this->fld_comment || $this->fld_parsedcomment ) && + !$this->getPermissionManager()->userHasRight( $user, 'deletedhistory' ) + ) { + $this->dieWithError( 'apierror-cantview-deleted-comment', 'permissiondenied' ); + } + if ( $this->fetchContent && + !$this->getPermissionManager()->userHasAnyRight( $user, 'deletedtext', 'undelete' ) + ) { + $this->dieWithError( 'apierror-cantview-deleted-revision-content', 'permissiondenied' ); } $dir = $params['dir']; @@ -130,11 +135,11 @@ class ApiQueryDeletedRevisions extends ApiQueryRevisionsBase { if ( !is_null( $params['user'] ) || !is_null( $params['excludeuser'] ) ) { // Paranoia: avoid brute force searches (T19342) - // (shouldn't be able to get here without 'deletedhistory', but - // check it again just in case) - if ( !$user->isAllowed( 'deletedhistory' ) ) { + if ( !$this->getPermissionManager()->userHasRight( $user, 'deletedhistory' ) ) { $bitmask = RevisionRecord::DELETED_USER; - } elseif ( !$user->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) { + } elseif ( !$this->getPermissionManager() + ->userHasAnyRight( $this->getUser(), 'suppressrevision', 'viewsuppressed' ) + ) { $bitmask = RevisionRecord::DELETED_USER | RevisionRecord::DELETED_RESTRICTED; } else { $bitmask = 0; diff --git a/includes/api/ApiQueryDeletedrevs.php b/includes/api/ApiQueryDeletedrevs.php index a6366f2c8d..a47ca09522 100644 --- a/includes/api/ApiQueryDeletedrevs.php +++ b/includes/api/ApiQueryDeletedrevs.php @@ -22,7 +22,8 @@ use MediaWiki\MediaWikiServices; use MediaWiki\Storage\NameTableAccessException; -use MediaWiki\Storage\RevisionRecord; +use MediaWiki\Revision\RevisionRecord; +use MediaWiki\Revision\SlotRecord; /** * Query module to enumerate all deleted revisions. @@ -67,7 +68,7 @@ class ApiQueryDeletedrevs extends ApiQueryBase { } // If user can't undelete, no tokens - if ( !$user->isAllowed( 'undelete' ) ) { + if ( !$this->getPermissionManager()->userHasRight( $user, 'undelete' ) ) { $fld_token = false; } @@ -197,9 +198,11 @@ class ApiQueryDeletedrevs extends ApiQueryBase { // Paranoia: avoid brute force searches (T19342) // (shouldn't be able to get here without 'deletedhistory', but // check it again just in case) - if ( !$user->isAllowed( 'deletedhistory' ) ) { + if ( !$this->getPermissionManager()->userHasRight( $user, 'deletedhistory' ) ) { $bitmask = RevisionRecord::DELETED_USER; - } elseif ( !$user->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) { + } elseif ( !$this->getPermissionManager() + ->userHasAnyRight( $user, 'suppressrevision', 'viewsuppressed' ) + ) { $bitmask = RevisionRecord::DELETED_USER | RevisionRecord::DELETED_RESTRICTED; } else { $bitmask = 0; @@ -345,7 +348,9 @@ class ApiQueryDeletedrevs extends ApiQueryBase { $anyHidden = true; } if ( Revision::userCanBitfield( $row->ar_deleted, RevisionRecord::DELETED_TEXT, $user ) ) { - ApiResult::setContentValue( $rev, 'text', Revision::getRevisionText( $row, 'ar_' ) ); + ApiResult::setContentValue( $rev, 'text', + $revisionStore->newRevisionFromArchiveRow( $row ) + ->getContent( SlotRecord::MAIN )->serialize() ); } } @@ -366,7 +371,7 @@ class ApiQueryDeletedrevs extends ApiQueryBase { if ( !isset( $pageMap[$row->ar_namespace][$row->ar_title] ) ) { $pageID = $newPageID++; $pageMap[$row->ar_namespace][$row->ar_title] = $pageID; - $a['revisions'] = [ $rev ]; + $a = [ 'revisions' => [ $rev ] ]; ApiResult::setIndexedTagName( $a['revisions'], 'rev' ); $title = Title::makeTitle( $row->ar_namespace, $row->ar_title ); ApiQueryBase::addTitleInfo( $a, $title ); diff --git a/includes/api/ApiQueryFilearchive.php b/includes/api/ApiQueryFilearchive.php index 8e464d0195..c84f457918 100644 --- a/includes/api/ApiQueryFilearchive.php +++ b/includes/api/ApiQueryFilearchive.php @@ -38,9 +38,6 @@ class ApiQueryFilearchive extends ApiQueryBase { } public function execute() { - // Before doing anything at all, let's check permissions - $this->checkUserRightsAny( 'deletedhistory' ); - $user = $this->getUser(); $db = $this->getDB(); $commentStore = CommentStore::getStore(); @@ -60,6 +57,17 @@ class ApiQueryFilearchive extends ApiQueryBase { $fld_bitdepth = isset( $prop['bitdepth'] ); $fld_archivename = isset( $prop['archivename'] ); + if ( $fld_description && + !$this->getPermissionManager()->userHasRight( $user, 'deletedhistory' ) + ) { + $this->dieWithError( 'apierror-cantview-deleted-description', 'permissiondenied' ); + } + if ( $fld_metadata && + !$this->getPermissionManager()->userHasAnyRight( $user, 'deletedtext', 'undelete' ) + ) { + $this->dieWithError( 'apierror-cantview-deleted-metadata', 'permissiondenied' ); + } + $fileQuery = ArchivedFile::getQueryInfo(); $this->addTables( $fileQuery['tables'] ); $this->addFields( $fileQuery['fields'] ); @@ -110,21 +118,22 @@ class ApiQueryFilearchive extends ApiQueryBase { } if ( $sha1 ) { $this->addWhereFld( 'fa_sha1', $sha1 ); + // Paranoia: avoid brute force searches (T19342) + if ( !$this->getPermissionManager()->userHasRight( $user, 'deletedtext' ) ) { + $bitmask = File::DELETED_FILE; + } elseif ( !$this->getPermissionManager() + ->userHasAnyRight( $user, 'suppressrevision', 'viewsuppressed' ) + ) { + $bitmask = File::DELETED_FILE | File::DELETED_RESTRICTED; + } else { + $bitmask = 0; + } + if ( $bitmask ) { + $this->addWhere( $this->getDB()->bitAnd( 'fa_deleted', $bitmask ) . " != $bitmask" ); + } } } - // Exclude files this user can't view. - if ( !$user->isAllowed( 'deletedtext' ) ) { - $bitmask = File::DELETED_FILE; - } elseif ( !$user->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) { - $bitmask = File::DELETED_FILE | File::DELETED_RESTRICTED; - } else { - $bitmask = 0; - } - if ( $bitmask ) { - $this->addWhere( $this->getDB()->bitAnd( 'fa_deleted', $bitmask ) . " != $bitmask" ); - } - $limit = $params['limit']; $this->addOption( 'LIMIT', $limit + 1 ); $sort = ( $params['dir'] == 'descending' ? ' DESC' : '' ); @@ -148,6 +157,8 @@ class ApiQueryFilearchive extends ApiQueryBase { break; } + $canViewFile = RevisionRecord::userCanBitfield( $row->fa_deleted, File::DELETED_FILE, $user ); + $file = []; $file['id'] = (int)$row->fa_id; $file['name'] = $row->fa_name; @@ -169,13 +180,13 @@ class ApiQueryFilearchive extends ApiQueryBase { $file['userid'] = (int)$row->fa_user; $file['user'] = $row->fa_user_text; } - if ( $fld_sha1 ) { + if ( $fld_sha1 && $canViewFile ) { $file['sha1'] = Wikimedia\base_convert( $row->fa_sha1, 36, 16, 40 ); } if ( $fld_timestamp ) { $file['timestamp'] = wfTimestamp( TS_ISO_8601, $row->fa_timestamp ); } - if ( $fld_size || $fld_dimensions ) { + if ( ( $fld_size || $fld_dimensions ) && $canViewFile ) { $file['size'] = $row->fa_size; $pageCount = ArchivedFile::newFromRow( $row )->pageCount(); @@ -186,18 +197,18 @@ class ApiQueryFilearchive extends ApiQueryBase { $file['height'] = $row->fa_height; $file['width'] = $row->fa_width; } - if ( $fld_mediatype ) { + if ( $fld_mediatype && $canViewFile ) { $file['mediatype'] = $row->fa_media_type; } - if ( $fld_metadata ) { + if ( $fld_metadata && $canViewFile ) { $file['metadata'] = $row->fa_metadata ? ApiQueryImageInfo::processMetaData( unserialize( $row->fa_metadata ), $result ) : null; } - if ( $fld_bitdepth ) { + if ( $fld_bitdepth && $canViewFile ) { $file['bitdepth'] = $row->fa_bits; } - if ( $fld_mime ) { + if ( $fld_mime && $canViewFile ) { $file['mime'] = "$row->fa_major_mime/$row->fa_minor_mime"; } if ( $fld_archivename && !is_null( $row->fa_archive_name ) ) { diff --git a/includes/api/ApiQueryImageInfo.php b/includes/api/ApiQueryImageInfo.php index 0791426f77..285c0bfcc9 100644 --- a/includes/api/ApiQueryImageInfo.php +++ b/includes/api/ApiQueryImageInfo.php @@ -63,7 +63,7 @@ class ApiQueryImageInfo extends ApiQueryBase { $this->dieWithError( [ 'apierror-bad-badfilecontexttitle', $p ], 'invalid-title' ); } } else { - $badFileContextTitle = false; + $badFileContextTitle = null; } $pageIds = $this->getPageSet()->getGoodAndMissingTitlesByNamespace(); @@ -144,7 +144,8 @@ class ApiQueryImageInfo extends ApiQueryBase { $info['imagerepository'] = $img->getRepoName(); } if ( isset( $prop['badfile'] ) ) { - $info['badfile'] = (bool)wfIsBadImage( $title, $badFileContextTitle ); + $info['badfile'] = (bool)MediaWikiServices::getInstance()->getBadFileLookup() + ->isBadFile( $title, $badFileContextTitle ); } $fit = $result->addValue( [ 'query', 'pages' ], (int)$pageId, $info ); @@ -386,9 +387,13 @@ class ApiQueryImageInfo extends ApiQueryBase { $vals = [ ApiResult::META_TYPE => 'assoc', ]; + + // Some information will be unavailable if the file does not exist. T221812 + $exists = $file->exists(); + // Timestamp is shown even if the file is revdelete'd in interface // so do same here. - if ( isset( $prop['timestamp'] ) ) { + if ( isset( $prop['timestamp'] ) && $exists ) { $vals['timestamp'] = wfTimestamp( TS_ISO_8601, $file->getTimestamp() ); } @@ -407,7 +412,7 @@ class ApiQueryImageInfo extends ApiQueryBase { $user = isset( $prop['user'] ); $userid = isset( $prop['userid'] ); - if ( $user || $userid ) { + if ( ( $user || $userid ) && $exists ) { if ( $file->isDeleted( File::DELETED_USER ) ) { $vals['userhidden'] = true; $anyHidden = true; @@ -427,7 +432,7 @@ class ApiQueryImageInfo extends ApiQueryBase { // This is shown even if the file is revdelete'd in interface // so do same here. - if ( isset( $prop['size'] ) || isset( $prop['dimensions'] ) ) { + if ( ( isset( $prop['size'] ) || isset( $prop['dimensions'] ) ) && $exists ) { $vals['size'] = (int)$file->getSize(); $vals['width'] = (int)$file->getWidth(); $vals['height'] = (int)$file->getHeight(); @@ -448,7 +453,7 @@ class ApiQueryImageInfo extends ApiQueryBase { $pcomment = isset( $prop['parsedcomment'] ); $comment = isset( $prop['comment'] ); - if ( $pcomment || $comment ) { + if ( ( $pcomment || $comment ) && $exists ) { if ( $file->isDeleted( File::DELETED_COMMENT ) ) { $vals['commenthidden'] = true; $anyHidden = true; @@ -499,7 +504,7 @@ class ApiQueryImageInfo extends ApiQueryBase { } if ( $url ) { - if ( $file->exists() ) { + if ( $exists ) { if ( !is_null( $thumbParams ) ) { $mto = $file->transform( $thumbParams ); self::$transformCount++; @@ -522,12 +527,12 @@ class ApiQueryImageInfo extends ApiQueryBase { $vals['thumbmime'] = $mime; } } elseif ( $mto && $mto->isError() ) { + /** @var MediaTransformError $mto */ + '@phan-var MediaTransformError $mto'; $vals['thumberror'] = $mto->toText(); } } $vals['url'] = wfExpandUrl( $file->getFullUrl(), PROTO_CURRENT ); - } else { - $vals['filemissing'] = true; } $vals['descriptionurl'] = wfExpandUrl( $file->getDescriptionUrl(), PROTO_CURRENT ); @@ -537,11 +542,15 @@ class ApiQueryImageInfo extends ApiQueryBase { } } - if ( $sha1 ) { + if ( !$exists ) { + $vals['filemissing'] = true; + } + + if ( $sha1 && $exists ) { $vals['sha1'] = Wikimedia\base_convert( $file->getSha1(), 36, 16, 40 ); } - if ( $meta ) { + if ( $meta && $exists ) { Wikimedia\suppressWarnings(); $metadata = unserialize( $file->getMetadata() ); Wikimedia\restoreWarnings(); @@ -550,17 +559,18 @@ class ApiQueryImageInfo extends ApiQueryBase { } $vals['metadata'] = $metadata ? static::processMetaData( $metadata, $result ) : null; } - if ( $commonmeta ) { + if ( $commonmeta && $exists ) { $metaArray = $file->getCommonMetaArray(); $vals['commonmetadata'] = $metaArray ? static::processMetaData( $metaArray, $result ) : []; } - if ( $extmetadata ) { + if ( $extmetadata && $exists ) { // Note, this should return an array where all the keys // start with a letter, and all the values are strings. // Thus there should be no issue with format=xml. $format = new FormatMetadata; $format->setSingleLanguage( !$opts['multilang'] ); + // @phan-suppress-next-line PhanUndeclaredMethod $format->getContext()->setLanguage( $opts['language'] ); $extmetaArray = $format->fetchExtendedMetadata( $file ); if ( $opts['extmetadatafilter'] ) { @@ -571,19 +581,21 @@ class ApiQueryImageInfo extends ApiQueryBase { $vals['extmetadata'] = $extmetaArray; } - if ( $mime ) { + if ( $mime && $exists ) { $vals['mime'] = $file->getMimeType(); } - if ( $mediatype ) { + if ( $mediatype && $exists ) { $vals['mediatype'] = $file->getMediaType(); } if ( $archive && $file->isOld() ) { + /** @var OldLocalFile $file */ + '@phan-var OldLocalFile $file'; $vals['archivename'] = $file->getArchiveName(); } - if ( $bitdepth ) { + if ( $bitdepth && $exists ) { $vals['bitdepth'] = $file->getBitDepth(); } diff --git a/includes/api/ApiQueryInfo.php b/includes/api/ApiQueryInfo.php index 90f1340eb5..98474c763b 100644 --- a/includes/api/ApiQueryInfo.php +++ b/includes/api/ApiQueryInfo.php @@ -118,6 +118,7 @@ class ApiQueryInfo extends ApiQueryBase { return $this->tokenFunctions; } + /** @var string[] */ protected static $cachedTokens = []; /** @@ -135,7 +136,8 @@ class ApiQueryInfo extends ApiQueryBase { // but that's too expensive for this purpose // and would break caching global $wgUser; - if ( !$wgUser->isAllowed( 'edit' ) ) { + if ( !MediaWikiServices::getInstance()->getPermissionManager() + ->userHasRight( $wgUser, 'edit' ) ) { return false; } @@ -152,7 +154,8 @@ class ApiQueryInfo extends ApiQueryBase { */ public static function getDeleteToken( $pageid, $title ) { global $wgUser; - if ( !$wgUser->isAllowed( 'delete' ) ) { + if ( !MediaWikiServices::getInstance()->getPermissionManager() + ->userHasRight( $wgUser, 'delete' ) ) { return false; } @@ -169,7 +172,8 @@ class ApiQueryInfo extends ApiQueryBase { */ public static function getProtectToken( $pageid, $title ) { global $wgUser; - if ( !$wgUser->isAllowed( 'protect' ) ) { + if ( !MediaWikiServices::getInstance()->getPermissionManager() + ->userHasRight( $wgUser, 'protect' ) ) { return false; } @@ -186,7 +190,8 @@ class ApiQueryInfo extends ApiQueryBase { */ public static function getMoveToken( $pageid, $title ) { global $wgUser; - if ( !$wgUser->isAllowed( 'move' ) ) { + if ( !MediaWikiServices::getInstance()->getPermissionManager() + ->userHasRight( $wgUser, 'move' ) ) { return false; } @@ -203,7 +208,8 @@ class ApiQueryInfo extends ApiQueryBase { */ public static function getBlockToken( $pageid, $title ) { global $wgUser; - if ( !$wgUser->isAllowed( 'block' ) ) { + if ( !MediaWikiServices::getInstance()->getPermissionManager() + ->userHasRight( $wgUser, 'block' ) ) { return false; } @@ -245,7 +251,9 @@ class ApiQueryInfo extends ApiQueryBase { */ public static function getImportToken( $pageid, $title ) { global $wgUser; - if ( !$wgUser->isAllowedAny( 'import', 'importupload' ) ) { + if ( !MediaWikiServices::getInstance() + ->getPermissionManager() + ->userHasAnyRight( $wgUser, 'import', 'importupload' ) ) { return false; } @@ -808,7 +816,7 @@ class ApiQueryInfo extends ApiQueryBase { $user = $this->getUser(); if ( $user->isAnon() || count( $this->everything ) == 0 - || !$user->isAllowed( 'viewmywatchlist' ) + || !$this->getPermissionManager()->userHasRight( $user, 'viewmywatchlist' ) ) { return; } @@ -843,7 +851,7 @@ class ApiQueryInfo extends ApiQueryBase { } $user = $this->getUser(); - $canUnwatchedpages = $user->isAllowed( 'unwatchedpages' ); + $canUnwatchedpages = $this->getPermissionManager()->userHasRight( $user, 'unwatchedpages' ); $unwatchedPageThreshold = $this->getConfig()->get( 'UnwatchedPageThreshold' ); if ( !$canUnwatchedpages && !is_int( $unwatchedPageThreshold ) ) { return; @@ -873,7 +881,7 @@ class ApiQueryInfo extends ApiQueryBase { $user = $this->getUser(); $db = $this->getDB(); - $canUnwatchedpages = $user->isAllowed( 'unwatchedpages' ); + $canUnwatchedpages = $this->getPermissionManager()->userHasRight( $user, 'unwatchedpages' ); $unwatchedPageThreshold = $this->getConfig()->get( 'UnwatchedPageThreshold' ); if ( !$canUnwatchedpages && !is_int( $unwatchedPageThreshold ) ) { return; diff --git a/includes/api/ApiQueryLogEvents.php b/includes/api/ApiQueryLogEvents.php index 962d956130..2c19c7d1aa 100644 --- a/includes/api/ApiQueryLogEvents.php +++ b/includes/api/ApiQueryLogEvents.php @@ -186,6 +186,7 @@ class ApiQueryLogEvents extends ApiQueryBase { // T71222: MariaDB's optimizer, at least 10.1.37 and .38, likes to choose a wildly bad plan for // some reason for this code path. Tell it not to use the wrong index it wants to pick. + // @phan-suppress-next-line PhanTypeMismatchArgument $this->addOption( 'IGNORE INDEX', [ 'logging' => [ 'times' ] ] ); } @@ -220,10 +221,12 @@ class ApiQueryLogEvents extends ApiQueryBase { // Paranoia: avoid brute force searches (T19342) if ( $params['namespace'] !== null || !is_null( $title ) || !is_null( $user ) ) { - if ( !$this->getUser()->isAllowed( 'deletedhistory' ) ) { + if ( !$this->getPermissionManager()->userHasRight( $this->getUser(), 'deletedhistory' ) ) { $titleBits = LogPage::DELETED_ACTION; $userBits = LogPage::DELETED_USER; - } elseif ( !$this->getUser()->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) { + } elseif ( !$this->getPermissionManager() + ->userHasAnyRight( $this->getUser(), 'suppressrevision', 'viewsuppressed' ) + ) { $titleBits = LogPage::DELETED_ACTION | LogPage::DELETED_RESTRICTED; $userBits = LogPage::DELETED_USER | LogPage::DELETED_RESTRICTED; } else { diff --git a/includes/api/ApiQueryQueryPage.php b/includes/api/ApiQueryQueryPage.php index 26c17c59b8..12e908ff11 100644 --- a/includes/api/ApiQueryQueryPage.php +++ b/includes/api/ApiQueryQueryPage.php @@ -61,7 +61,7 @@ class ApiQueryQueryPage extends ApiQueryGeneratorBase { * @param string $name * @return QueryPage */ - private function getSpecialPage( $name ) { + private function getSpecialPage( $name ) : QueryPage { $qp = $this->specialPageFactory->getPage( $name ); if ( !$qp ) { self::dieDebug( diff --git a/includes/api/ApiQueryRecentChanges.php b/includes/api/ApiQueryRecentChanges.php index 8ae1b668b4..143d4662a1 100644 --- a/includes/api/ApiQueryRecentChanges.php +++ b/includes/api/ApiQueryRecentChanges.php @@ -312,12 +312,6 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase { /* Add fields to our query if they are specified as a needed parameter. */ $this->addFieldsIf( [ 'rc_this_oldid', 'rc_last_oldid' ], $this->fld_ids ); - if ( $this->fld_user || $this->fld_userid ) { - $actorQuery = ActorMigration::newMigration()->getJoin( 'rc_user' ); - $this->addTables( $actorQuery['tables'] ); - $this->addFields( $actorQuery['fields'] ); - $this->addJoinConds( $actorQuery['joins'] ); - } $this->addFieldsIf( [ 'rc_minor', 'rc_type', 'rc_bot' ], $this->fld_flags ); $this->addFieldsIf( [ 'rc_old_len', 'rc_new_len' ], $this->fld_sizes ); $this->addFieldsIf( [ 'rc_patrolled', 'rc_log_type' ], $this->fld_patrolled ); @@ -367,9 +361,11 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase { // Paranoia: avoid brute force searches (T19342) if ( !is_null( $params['user'] ) || !is_null( $params['excludeuser'] ) ) { - if ( !$user->isAllowed( 'deletedhistory' ) ) { + if ( !$this->getPermissionManager()->userHasRight( $user, 'deletedhistory' ) ) { $bitmask = RevisionRecord::DELETED_USER; - } elseif ( !$user->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) { + } elseif ( !$this->getPermissionManager() + ->userHasAnyRight( $user, 'suppressrevision', 'viewsuppressed' ) + ) { $bitmask = RevisionRecord::DELETED_USER | RevisionRecord::DELETED_RESTRICTED; } else { $bitmask = 0; @@ -380,9 +376,11 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase { } if ( $this->getRequest()->getCheck( 'namespace' ) ) { // LogPage::DELETED_ACTION hides the affected page, too. - if ( !$user->isAllowed( 'deletedhistory' ) ) { + if ( !$this->getPermissionManager()->userHasRight( $user, 'deletedhistory' ) ) { $bitmask = LogPage::DELETED_ACTION; - } elseif ( !$user->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) { + } elseif ( !$this->getPermissionManager() + ->userHasAnyRight( $user, 'suppressrevision', 'viewsuppressed' ) + ) { $bitmask = LogPage::DELETED_ACTION | LogPage::DELETED_RESTRICTED; } else { $bitmask = 0; @@ -405,6 +403,14 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase { $this->addJoinConds( $commentQuery['joins'] ); } + if ( $this->fld_user || $this->fld_userid || !is_null( $this->token ) ) { + // Token needs rc_user for RecentChange::newFromRow/User::newFromAnyId (T228425) + $actorQuery = ActorMigration::newMigration()->getJoin( 'rc_user' ); + $this->addTables( $actorQuery['tables'] ); + $this->addFields( $actorQuery['fields'] ); + $this->addJoinConds( $actorQuery['joins'] ); + } + $this->addOption( 'LIMIT', $params['limit'] + 1 ); $hookData = []; diff --git a/includes/api/ApiQueryRevisions.php b/includes/api/ApiQueryRevisions.php index fe3ae87d52..5b2b1b72b9 100644 --- a/includes/api/ApiQueryRevisions.php +++ b/includes/api/ApiQueryRevisions.php @@ -76,7 +76,8 @@ class ApiQueryRevisions extends ApiQueryRevisionsBase { */ public static function getRollbackToken( $pageid, $title, $rev ) { global $wgUser; - if ( !$wgUser->isAllowed( 'rollback' ) ) { + if ( !MediaWikiServices::getInstance()->getPermissionManager() + ->userHasRight( $wgUser, 'rollback' ) ) { return false; } @@ -84,8 +85,6 @@ class ApiQueryRevisions extends ApiQueryRevisionsBase { } protected function run( ApiPageSet $resultPageSet = null ) { - global $wgActorTableSchemaMigrationStage; - $params = $this->extractRequestParams( false ); $revisionStore = MediaWikiServices::getInstance()->getRevisionStore(); @@ -136,9 +135,7 @@ class ApiQueryRevisions extends ApiQueryRevisionsBase { $idField = 'rev_id'; $tsField = 'rev_timestamp'; $pageField = 'rev_page'; - if ( $params['user'] !== null && - ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_READ_NEW ) - ) { + if ( $params['user'] !== null ) { // We're going to want to use the page_actor_timestamp index (on revision_actor_temp) // so use that table's denormalized fields. $idField = 'revactor_rev'; @@ -332,9 +329,11 @@ class ApiQueryRevisions extends ApiQueryRevisionsBase { } if ( $params['user'] !== null || $params['excludeuser'] !== null ) { // Paranoia: avoid brute force searches (T19342) - if ( !$this->getUser()->isAllowed( 'deletedhistory' ) ) { + if ( !$this->getPermissionManager()->userHasRight( $this->getUser(), 'deletedhistory' ) ) { $bitmask = RevisionRecord::DELETED_USER; - } elseif ( !$this->getUser()->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) { + } elseif ( !$this->getPermissionManager() + ->userHasAnyRight( $this->getUser(), 'suppressrevision', 'viewsuppressed' ) + ) { $bitmask = RevisionRecord::DELETED_USER | RevisionRecord::DELETED_RESTRICTED; } else { $bitmask = 0; diff --git a/includes/api/ApiQueryRevisionsBase.php b/includes/api/ApiQueryRevisionsBase.php index 0d284c0c01..90e54804e6 100644 --- a/includes/api/ApiQueryRevisionsBase.php +++ b/includes/api/ApiQueryRevisionsBase.php @@ -501,6 +501,8 @@ abstract class ApiQueryRevisionsBase extends ApiQueryGeneratorBase { if ( $this->fld_parsetree || ( $this->fld_content && $this->generateXML ) ) { if ( $content->getModel() === CONTENT_MODEL_WIKITEXT ) { + /** @var WikitextContent $content */ + '@phan-var WikitextContent $content'; $t = $content->getText(); # note: don't set $text $parser = MediaWikiServices::getInstance()->getParser(); @@ -510,9 +512,12 @@ abstract class ApiQueryRevisionsBase extends ApiQueryGeneratorBase { Parser::OT_PREPROCESS ); $dom = $parser->preprocessToDom( $t ); + // @phan-suppress-next-line PhanUndeclaredMethodInCallable if ( is_callable( [ $dom, 'saveXML' ] ) ) { + // @phan-suppress-next-line PhanUndeclaredMethod $xml = $dom->saveXML(); } else { + // @phan-suppress-next-line PhanUndeclaredMethod $xml = $dom->__toString(); } $vals['parsetree'] = $xml; @@ -534,6 +539,8 @@ abstract class ApiQueryRevisionsBase extends ApiQueryGeneratorBase { if ( $this->expandTemplates && !$this->parseContent ) { if ( $content->getModel() === CONTENT_MODEL_WIKITEXT ) { + /** @var WikitextContent $content */ + '@phan-var WikitextContent $content'; $text = $content->getText(); $text = MediaWikiServices::getInstance()->getParser()->preprocess( diff --git a/includes/api/ApiQuerySiteinfo.php b/includes/api/ApiQuerySiteinfo.php index 7e4a891adf..47212b321a 100644 --- a/includes/api/ApiQuerySiteinfo.php +++ b/includes/api/ApiQuerySiteinfo.php @@ -279,6 +279,8 @@ class ApiQuerySiteinfo extends ApiQueryBase { } protected function appendNamespaces( $property ) { + $nsProtection = $this->getConfig()->get( 'NamespaceProtection' ); + $data = [ ApiResult::META_TYPE => 'assoc', ]; @@ -303,6 +305,17 @@ class ApiQuerySiteinfo extends ApiQueryBase { $data[$ns]['content'] = $nsInfo->isContent( $ns ); $data[$ns]['nonincludable'] = $nsInfo->isNonincludable( $ns ); + if ( isset( $nsProtection[$ns] ) ) { + if ( is_array( $nsProtection[$ns] ) ) { + $specificNs = implode( "|", array_filter( $nsProtection[$ns] ) ); + } elseif ( $nsProtection[$ns] !== '' ) { + $specificNs = $nsProtection[$ns]; + } + if ( isset( $specificNs ) && $specificNs !== '' ) { + $data[$ns]['namespaceprotection'] = $specificNs; + } + } + $contentmodel = $nsInfo->getNamespaceContentModel( $ns ); if ( $contentmodel ) { $data[$ns]['defaultcontentmodel'] = $contentmodel; diff --git a/includes/api/ApiQueryStashImageInfo.php b/includes/api/ApiQueryStashImageInfo.php index 1924ca0339..c84de8c974 100644 --- a/includes/api/ApiQueryStashImageInfo.php +++ b/includes/api/ApiQueryStashImageInfo.php @@ -70,11 +70,37 @@ class ApiQueryStashImageInfo extends ApiQueryImageInfo { } } - private $propertyFilter = [ + private static $propertyFilter = [ 'user', 'userid', 'comment', 'parsedcomment', 'mediatype', 'archivename', 'uploadwarning', ]; + /** + * Returns all possible parameters to siiprop + * + * @param array|null $filter List of properties to filter out + * @return array + */ + public static function getPropertyNames( $filter = null ) { + if ( $filter === null ) { + $filter = self::$propertyFilter; + } + return parent::getPropertyNames( $filter ); + } + + /** + * Returns messages for all possible parameters to siiprop + * + * @param array|null $filter List of properties to filter out + * @return array + */ + public static function getPropertyMessages( $filter = null ) { + if ( $filter === null ) { + $filter = self::$propertyFilter; + } + return parent::getPropertyMessages( $filter ); + } + public function getAllowedParams() { return [ 'filekey' => [ @@ -87,9 +113,9 @@ class ApiQueryStashImageInfo extends ApiQueryImageInfo { 'prop' => [ ApiBase::PARAM_ISMULTI => true, ApiBase::PARAM_DFLT => 'timestamp|url', - ApiBase::PARAM_TYPE => self::getPropertyNames( $this->propertyFilter ), + ApiBase::PARAM_TYPE => self::getPropertyNames(), ApiBase::PARAM_HELP_MSG => 'apihelp-query+imageinfo-param-prop', - ApiBase::PARAM_HELP_MSG_PER_VALUE => self::getPropertyMessages( $this->propertyFilter ) + ApiBase::PARAM_HELP_MSG_PER_VALUE => self::getPropertyMessages() ], 'urlwidth' => [ ApiBase::PARAM_TYPE => 'integer', diff --git a/includes/api/ApiQueryUserContribs.php b/includes/api/ApiQueryUserContribs.php index 379f1afd39..189f9572ee 100644 --- a/includes/api/ApiQueryUserContribs.php +++ b/includes/api/ApiQueryUserContribs.php @@ -42,8 +42,6 @@ class ApiQueryUserContribs extends ApiQueryBase { $fld_patrolled = false, $fld_tags = false, $fld_size = false, $fld_sizediff = false; public function execute() { - global $wgActorTableSchemaMigrationStage; - // Parse some parameters $this->params = $this->extractRequestParams(); @@ -82,8 +80,6 @@ class ApiQueryUserContribs extends ApiQueryBase { // a wiki with users "Test00000001" to "Test99999999"), use a // generator with batched lookup and continuation. $userIter = call_user_func( function () use ( $dbSecondary, $sort, $op, $fname ) { - global $wgActorTableSchemaMigrationStage; - $fromName = false; if ( !is_null( $this->params['continue'] ) ) { $continue = explode( '|', $this->params['continue'] ); @@ -97,26 +93,13 @@ class ApiQueryUserContribs extends ApiQueryBase { do { $from = $fromName ? "$op= " . $dbSecondary->addQuotes( $fromName ) : false; - - // For the new schema, pull from the actor table. For the - // old, pull from rev_user. - if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_READ_NEW ) { - $res = $dbSecondary->select( - 'actor', - [ 'actor_id', 'user_id' => 'COALESCE(actor_user,0)', 'user_name' => 'actor_name' ], - array_merge( [ "actor_name$like" ], $from ? [ "actor_name $from" ] : [] ), - $fname, - [ 'ORDER BY' => [ "user_name $sort" ], 'LIMIT' => $limit ] - ); - } else { - $res = $dbSecondary->select( - 'revision', - [ 'actor_id' => 'NULL', 'user_id' => 'rev_user', 'user_name' => 'rev_user_text' ], - array_merge( [ "rev_user_text$like" ], $from ? [ "rev_user_text $from" ] : [] ), - $fname, - [ 'DISTINCT', 'ORDER BY' => [ "rev_user_text $sort" ], 'LIMIT' => $limit ] - ); - } + $res = $dbSecondary->select( + 'actor', + [ 'actor_id', 'user_id' => 'COALESCE(actor_user,0)', 'user_name' => 'actor_name' ], + array_merge( [ "actor_name$like" ], $from ? [ "actor_name $from" ] : [] ), + $fname, + [ 'ORDER BY' => [ "user_name $sort" ], 'LIMIT' => $limit ] + ); $count = 0; $fromName = false; @@ -159,25 +142,13 @@ class ApiQueryUserContribs extends ApiQueryBase { $from = "$op= $fromId"; } - // For the new schema, just select from the actor table. For the - // old, select from user. - if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_READ_NEW ) { - $res = $dbSecondary->select( - 'actor', - [ 'actor_id', 'user_id' => 'actor_user', 'user_name' => 'actor_name' ], - array_merge( [ 'actor_user' => $ids ], $from ? [ "actor_id $from" ] : [] ), - __METHOD__, - [ 'ORDER BY' => "user_id $sort" ] - ); - } else { - $res = $dbSecondary->select( - 'user', - [ 'actor_id' => 'NULL', 'user_id' => 'user_id', 'user_name' => 'user_name' ], - array_merge( [ 'user_id' => $ids ], $from ? [ "user_id $from" ] : [] ), - __METHOD__, - [ 'ORDER BY' => "user_id $sort" ] - ); - } + $res = $dbSecondary->select( + 'actor', + [ 'actor_id', 'user_id' => 'actor_user', 'user_name' => 'actor_name' ], + array_merge( [ 'actor_user' => $ids ], $from ? [ "actor_id $from" ] : [] ), + __METHOD__, + [ 'ORDER BY' => "user_id $sort" ] + ); $userIter = UserArray::newFromResult( $res ); $batchSize = count( $ids ); } else { @@ -222,57 +193,22 @@ class ApiQueryUserContribs extends ApiQueryBase { $from = "$op= " . $dbSecondary->addQuotes( $fromName ); } - // For the new schema, just select from the actor table. For the - // old, select from user then merge in any unknown users (IPs and imports). - if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_READ_NEW ) { - $res = $dbSecondary->select( - 'actor', - [ 'actor_id', 'user_id' => 'actor_user', 'user_name' => 'actor_name' ], - array_merge( [ 'actor_name' => array_keys( $names ) ], $from ? [ "actor_id $from" ] : [] ), - __METHOD__, - [ 'ORDER BY' => "actor_name $sort" ] - ); - $userIter = UserArray::newFromResult( $res ); - } else { - $res = $dbSecondary->select( - 'user', - [ 'actor_id' => 'NULL', 'user_id', 'user_name' ], - array_merge( [ 'user_name' => array_keys( $names ) ], $from ? [ "user_name $from" ] : [] ), - __METHOD__ - ); - foreach ( $res as $row ) { - $names[$row->user_name] = $row; - } - if ( $this->params['dir'] == 'newer' ) { - ksort( $names, SORT_STRING ); - } else { - krsort( $names, SORT_STRING ); - } - $neg = $op === '>' ? -1 : 1; - $userIter = call_user_func( function () use ( $names, $fromName, $neg ) { - foreach ( $names as $name => $row ) { - if ( $fromName === false || $neg * strcmp( $name, $fromName ) <= 0 ) { - $user = $row ? User::newFromRow( $row ) : User::newFromName( $name, false ); - yield $user; - } - } - } ); - } + $res = $dbSecondary->select( + 'actor', + [ 'actor_id', 'user_id' => 'actor_user', 'user_name' => 'actor_name' ], + array_merge( [ 'actor_name' => array_keys( $names ) ], $from ? [ "actor_id $from" ] : [] ), + __METHOD__, + [ 'ORDER BY' => "actor_name $sort" ] + ); + $userIter = UserArray::newFromResult( $res ); $batchSize = count( $names ); } - // With the new schema, the DB query will order by actor so update $this->orderBy to match. - if ( $batchSize > 1 && ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_READ_NEW ) ) { + // The DB query will order by actor so update $this->orderBy to match. + if ( $batchSize > 1 ) { $this->orderBy = 'actor'; } - // Use the 'contributions' replica, but only if we're querying by user ID (T216656). - if ( $this->orderBy === 'id' && - !( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_READ_NEW ) - ) { - $this->selectNamedDB( 'contributions', DB_REPLICA, 'contributions' ); - } - $count = 0; $limit = $this->params['limit']; $userIter->rewind(); @@ -325,47 +261,33 @@ class ApiQueryUserContribs extends ApiQueryBase { * @param int $limit */ private function prepareQuery( array $users, $limit ) { - global $wgActorTableSchemaMigrationStage; - $this->resetQueryParams(); $db = $this->getDB(); $revQuery = MediaWikiServices::getInstance()->getRevisionStore()->getQueryInfo( [ 'page' ] ); - if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_READ_NEW ) { - $revWhere = ActorMigration::newMigration()->getWhere( $db, 'rev_user', $users ); - $orderUserField = 'rev_actor'; - $userField = $this->orderBy === 'actor' ? 'revactor_actor' : 'actor_name'; - $tsField = 'revactor_timestamp'; - $idField = 'revactor_rev'; - - // T221511: MySQL/MariaDB (10.1.37) can sometimes irrationally decide that querying `actor` - // before `revision_actor_temp` and filesorting is somehow better than querying $limit+1 rows - // from `revision_actor_temp`. Tell it not to reorder the query (and also reorder it ourselves - // because as generated by RevisionStore it'll have `revision` first rather than - // `revision_actor_temp`). But not when uctag is used, as it seems as likely to be harmed as - // helped in that case, and not when there's only one User because in that case it fetches - // the one `actor` row as a constant and doesn't filesort. - if ( count( $users ) > 1 && !isset( $this->params['tag'] ) ) { - $revQuery['joins']['revision'] = $revQuery['joins']['temp_rev_user']; - unset( $revQuery['joins']['temp_rev_user'] ); - $this->addOption( 'STRAIGHT_JOIN' ); - // It isn't actually necesssary to reorder $revQuery['tables'] as Database does the right thing - // when join conditions are given for all joins, but Gergő is wary of relying on that so pull - // `revision_actor_temp` to the start. - $revQuery['tables'] = - [ 'temp_rev_user' => $revQuery['tables']['temp_rev_user'] ] + $revQuery['tables']; - } - } else { - // If we're dealing with user names (rather than IDs) in read-old mode, - // pass false for ActorMigration::getWhere()'s $useId parameter so - // $revWhere['conds'] isn't an OR. - $revWhere = ActorMigration::newMigration() - ->getWhere( $db, 'rev_user', $users, $this->orderBy === 'id' ); - $orderUserField = $this->orderBy === 'id' ? 'rev_user' : 'rev_user_text'; - $userField = $revQuery['fields'][$orderUserField]; - $tsField = 'rev_timestamp'; - $idField = 'rev_id'; + $revWhere = ActorMigration::newMigration()->getWhere( $db, 'rev_user', $users ); + $orderUserField = 'rev_actor'; + $userField = $this->orderBy === 'actor' ? 'revactor_actor' : 'actor_name'; + $tsField = 'revactor_timestamp'; + $idField = 'revactor_rev'; + + // T221511: MySQL/MariaDB (10.1.37) can sometimes irrationally decide that querying `actor` + // before `revision_actor_temp` and filesorting is somehow better than querying $limit+1 rows + // from `revision_actor_temp`. Tell it not to reorder the query (and also reorder it ourselves + // because as generated by RevisionStore it'll have `revision` first rather than + // `revision_actor_temp`). But not when uctag is used, as it seems as likely to be harmed as + // helped in that case, and not when there's only one User because in that case it fetches + // the one `actor` row as a constant and doesn't filesort. + if ( count( $users ) > 1 && !isset( $this->params['tag'] ) ) { + $revQuery['joins']['revision'] = $revQuery['joins']['temp_rev_user']; + unset( $revQuery['joins']['temp_rev_user'] ); + $this->addOption( 'STRAIGHT_JOIN' ); + // It isn't actually necesssary to reorder $revQuery['tables'] as Database does the right thing + // when join conditions are given for all joins, but Gergő is wary of relying on that so pull + // `revision_actor_temp` to the start. + $revQuery['tables'] = + [ 'temp_rev_user' => $revQuery['tables']['temp_rev_user'] ] + $revQuery['tables']; } $this->addTables( $revQuery['tables'] ); @@ -408,9 +330,11 @@ class ApiQueryUserContribs extends ApiQueryBase { // Don't include any revisions where we're not supposed to be able to // see the username. $user = $this->getUser(); - if ( !$user->isAllowed( 'deletedhistory' ) ) { + if ( !$this->getPermissionManager()->userHasRight( $user, 'deletedhistory' ) ) { $bitmask = RevisionRecord::DELETED_USER; - } elseif ( !$user->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) { + } elseif ( !$this->getPermissionManager() + ->userHasAnyRight( $user, 'suppressrevision', 'viewsuppressed' ) + ) { $bitmask = RevisionRecord::DELETED_USER | RevisionRecord::DELETED_RESTRICTED; } else { $bitmask = 0; diff --git a/includes/api/ApiQueryUserInfo.php b/includes/api/ApiQueryUserInfo.php index ba7280da10..28dea3b9e4 100644 --- a/includes/api/ApiQueryUserInfo.php +++ b/includes/api/ApiQueryUserInfo.php @@ -34,7 +34,9 @@ class ApiQueryUserInfo extends ApiQueryBase { const WL_UNREAD_LIMIT = 1000; + /** @var array */ private $params = []; + /** @var array */ private $prop = []; public function __construct( ApiQuery $query, $moduleName ) { @@ -159,8 +161,7 @@ class ApiQueryUserInfo extends ApiQueryBase { } if ( isset( $this->prop['rights'] ) ) { - // User::getRights() may return duplicate values, strip them - $vals['rights'] = array_values( array_unique( $user->getRights() ) ); + $vals['rights'] = $this->getPermissionManager()->getUserPermissions( $user ); ApiResult::setArrayType( $vals['rights'], 'array' ); // even if empty ApiResult::setIndexedTagName( $vals['rights'], 'r' ); // even if empty } @@ -180,7 +181,7 @@ class ApiQueryUserInfo extends ApiQueryBase { if ( isset( $this->prop['preferencestoken'] ) && !$this->lacksSameOriginSecurity() && - $user->isAllowed( 'editmyoptions' ) + $this->getPermissionManager()->userHasRight( $user, 'editmyoptions' ) ) { $vals['preferencestoken'] = $user->getEditToken( '', $this->getMain()->getRequest() ); } @@ -201,7 +202,8 @@ class ApiQueryUserInfo extends ApiQueryBase { $vals['realname'] = $user->getRealName(); } - if ( $user->isAllowed( 'viewmyprivateinfo' ) && isset( $this->prop['email'] ) ) { + if ( $this->getPermissionManager()->userHasRight( $user, 'viewmyprivateinfo' ) && + isset( $this->prop['email'] ) ) { $vals['email'] = $user->getEmail(); $auth = $user->getEmailAuthenticationTimestamp(); if ( $auth !== null ) { @@ -301,32 +303,17 @@ class ApiQueryUserInfo extends ApiQueryBase { * @return string|null ISO 8601 timestamp of current user's last contribution or null if none */ protected function getLatestContributionTime() { - global $wgActorTableSchemaMigrationStage; - $user = $this->getUser(); $dbr = $this->getDB(); - if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_READ_NEW ) { - if ( $user->getActorId() === null ) { - return null; - } - $res = $dbr->selectField( 'revision_actor_temp', - 'MAX(revactor_timestamp)', - [ 'revactor_actor' => $user->getActorId() ], - __METHOD__ - ); - } else { - if ( $user->isLoggedIn() ) { - $conds = [ 'rev_user' => $user->getId() ]; - } else { - $conds = [ 'rev_user_text' => $user->getName() ]; - } - $res = $dbr->selectField( 'revision', - 'MAX(rev_timestamp)', - $conds, - __METHOD__ - ); + if ( $user->getActorId() === null ) { + return null; } + $res = $dbr->selectField( 'revision_actor_temp', + 'MAX(revactor_timestamp)', + [ 'revactor_actor' => $user->getActorId() ], + __METHOD__ + ); return $res ? wfTimestamp( TS_ISO_8601, $res ) : null; } diff --git a/includes/api/ApiQueryUsers.php b/includes/api/ApiQueryUsers.php index 66d8db4987..0171a37733 100644 --- a/includes/api/ApiQueryUsers.php +++ b/includes/api/ApiQueryUsers.php @@ -20,12 +20,15 @@ * @file */ +use MediaWiki\Block\DatabaseBlock; + /** * Query module to get information about a list of users * * @ingroup API */ class ApiQueryUsers extends ApiQueryBase { + use ApiQueryBlockInfoTrait; private $tokenFunctions, $prop; @@ -150,7 +153,7 @@ class ApiQueryUsers extends ApiQueryBase { $this->addWhereFld( 'user_id', $userids ); } - $this->showHiddenUsersAddBlockInfo( isset( $this->prop['blockinfo'] ) ); + $this->addBlockInfoToQuery( isset( $this->prop['blockinfo'] ) ); $data = []; $res = $this->select( __METHOD__ ); @@ -225,19 +228,14 @@ class ApiQueryUsers extends ApiQueryBase { } if ( isset( $this->prop['rights'] ) ) { - $data[$key]['rights'] = $user->getRights(); + $data[$key]['rights'] = $this->getPermissionManager() + ->getUserPermissions( $user ); } if ( $row->ipb_deleted ) { $data[$key]['hidden'] = true; } if ( isset( $this->prop['blockinfo'] ) && !is_null( $row->ipb_by_text ) ) { - $data[$key]['blockid'] = (int)$row->ipb_id; - $data[$key]['blockedby'] = $row->ipb_by_text; - $data[$key]['blockedbyid'] = (int)$row->ipb_by; - $data[$key]['blockedtimestamp'] = wfTimestamp( TS_ISO_8601, $row->ipb_timestamp ); - $data[$key]['blockreason'] = $commentStore->getComment( 'ipb_reason', $row ) - ->text; - $data[$key]['blockexpiry'] = $row->ipb_expiry; + $data[$key] += $this->getBlockDetails( DatabaseBlock::newFromRow( $row ) ); } if ( isset( $this->prop['emailable'] ) ) { @@ -331,8 +329,8 @@ class ApiQueryUsers extends ApiQueryBase { } } - $fit = $result->addValue( [ 'query', $this->getModuleName() ], - null, $data[$u] ); + // @phan-suppress-next-line PhanTypeArraySuspiciousNullable + $fit = $result->addValue( [ 'query', $this->getModuleName() ], null, $data[$u] ); if ( !$fit ) { if ( $useNames ) { $this->setContinueEnumParameter( 'users', diff --git a/includes/api/ApiResetPassword.php b/includes/api/ApiResetPassword.php index 77838269b4..6b1c217017 100644 --- a/includes/api/ApiResetPassword.php +++ b/includes/api/ApiResetPassword.php @@ -21,6 +21,7 @@ */ use MediaWiki\Auth\AuthManager; +use MediaWiki\MediaWikiServices; /** * Reset password, with AuthManager @@ -63,7 +64,11 @@ class ApiResetPassword extends ApiBase { $this->requireOnlyOneParameter( $params, 'user', 'email' ); - $passwordReset = new PasswordReset( $this->getConfig(), AuthManager::singleton() ); + $passwordReset = new PasswordReset( + $this->getConfig(), + AuthManager::singleton(), + MediaWikiServices::getInstance()->getPermissionManager() + ); $status = $passwordReset->isAllowed( $this->getUser() ); if ( !$status->isOK() ) { diff --git a/includes/api/ApiRevisionDelete.php b/includes/api/ApiRevisionDelete.php index 1ee91c21f0..60b24f09f1 100644 --- a/includes/api/ApiRevisionDelete.php +++ b/includes/api/ApiRevisionDelete.php @@ -38,12 +38,6 @@ class ApiRevisionDelete extends ApiBase { $user = $this->getUser(); $this->checkUserRightsAny( RevisionDeleter::getRestriction( $params['type'] ) ); - // @TODO Use PermissionManager::isBlockedFrom() instead. - $block = $user->getBlock(); - if ( $block ) { - $this->dieBlocked( $block ); - } - if ( !$params['ids'] ) { $this->dieWithError( [ 'apierror-paramempty', 'ids' ], 'paramempty_ids' ); } @@ -97,6 +91,10 @@ class ApiRevisionDelete extends ApiBase { $this->dieWithError( [ 'apierror-revdel-needtarget' ], 'needtarget' ); } + if ( $this->getPermissionManager()->isBlockedFrom( $user, $targetObj ) ) { + $this->dieBlocked( $user->getBlock() ); + } + $list = RevisionDeleter::createList( $params['type'], $this->getContext(), $targetObj, $params['ids'] ); diff --git a/includes/api/ApiSetNotificationTimestamp.php b/includes/api/ApiSetNotificationTimestamp.php index d2bbe7bb88..3f6f14c1b3 100644 --- a/includes/api/ApiSetNotificationTimestamp.php +++ b/includes/api/ApiSetNotificationTimestamp.php @@ -93,13 +93,14 @@ class ApiSetNotificationTimestamp extends ApiBase { $titles = $pageSet->getGoodTitles(); $title = reset( $titles ); if ( $title ) { - $revid = $title->getNextRevisionID( $params['newerthanrevid'], Title::GAID_FOR_UPDATE ); - if ( $revid ) { - $timestamp = $dbw->timestamp( - MediaWikiServices::getInstance()->getRevisionStore()->getTimestampFromId( $title, $revid ) - ); - } else { - $timestamp = null; + $timestamp = null; + $rl = MediaWikiServices::getInstance()->getRevisionLookup(); + $currRev = $rl->getRevisionById( $params['newerthanrevid'], Title::READ_LATEST ); + if ( $currRev ) { + $nextRev = $rl->getNextRevision( $currRev, Title::READ_LATEST ); + if ( $nextRev ) { + $timestamp = $dbw->timestamp( $nextRev->getTimestamp() ); + } } } } diff --git a/includes/api/ApiStashEdit.php b/includes/api/ApiStashEdit.php index c3cf5f1089..478b0bcfba 100644 --- a/includes/api/ApiStashEdit.php +++ b/includes/api/ApiStashEdit.php @@ -131,9 +131,6 @@ class ApiStashEdit extends ApiBase { return; } - // The user will abort the AJAX request by pressing "save", so ignore that - ignore_user_abort( true ); - if ( $user->pingLimiter( 'stashedit' ) ) { $status = 'ratelimited'; } else { diff --git a/includes/api/ApiTag.php b/includes/api/ApiTag.php index aff01830e0..f2b52cd33a 100644 --- a/includes/api/ApiTag.php +++ b/includes/api/ApiTag.php @@ -20,7 +20,6 @@ */ use MediaWiki\MediaWikiServices; -use MediaWiki\Revision\RevisionStore; /** * @ingroup API @@ -28,7 +27,9 @@ use MediaWiki\Revision\RevisionStore; */ class ApiTag extends ApiBase { - /** @var RevisionStore */ + use ApiBlockInfoTrait; + + /** @var \MediaWiki\Revision\RevisionStore */ private $revisionStore; public function execute() { @@ -40,9 +41,9 @@ class ApiTag extends ApiBase { // make sure the user is allowed $this->checkUserRightsAny( 'changetags' ); - // @TODO Use PermissionManager::isBlockedFrom() instead. + // Fail early if the user is sitewide blocked. $block = $user->getBlock(); - if ( $block ) { + if ( $block && $block->isSitewide() ) { $this->dieBlocked( $block ); } @@ -85,6 +86,7 @@ class ApiTag extends ApiBase { } protected function processIndividual( $type, $params, $id ) { + $user = $this->getUser(); $idResult = [ $type => $id ]; // validate the ID @@ -92,9 +94,32 @@ class ApiTag extends ApiBase { switch ( $type ) { case 'rcid': $valid = RecentChange::newFromId( $id ); + if ( $valid && $this->getPermissionManager()->isBlockedFrom( $user, $valid->getTitle() ) ) { + $idResult['status'] = 'error'; + // @phan-suppress-next-line PhanTypeMismatchArgument + $idResult += $this->getErrorFormatter()->formatMessage( ApiMessage::create( + 'apierror-blocked', + 'blocked', + [ 'blockinfo' => $this->getBlockDetails( $user->getBlock() ) ] + ) ); + return $idResult; + } break; case 'revid': $valid = $this->revisionStore->getRevisionById( $id ); + if ( + $valid && + $this->getPermissionManager()->isBlockedFrom( $user, $valid->getPageAsLinkTarget() ) + ) { + $idResult['status'] = 'error'; + // @phan-suppress-next-line PhanTypeMismatchArgument + $idResult += $this->getErrorFormatter()->formatMessage( ApiMessage::create( + 'apierror-blocked', + 'blocked', + [ 'blockinfo' => $this->getBlockDetails( $user->getBlock() ) ] + ) ); + return $idResult; + } break; case 'logid': $valid = self::validateLogId( $id ); diff --git a/includes/api/ApiUnblock.php b/includes/api/ApiUnblock.php index 5cef194f36..15c2564488 100644 --- a/includes/api/ApiUnblock.php +++ b/includes/api/ApiUnblock.php @@ -41,7 +41,7 @@ class ApiUnblock extends ApiBase { $this->requireOnlyOneParameter( $params, 'id', 'user', 'userid' ); - if ( !$user->isAllowed( 'block' ) ) { + if ( !$this->getPermissionManager()->userHasRight( $user, 'block' ) ) { $this->dieWithError( 'apierror-permissiondenied-unblock', 'permissiondenied' ); } # T17810: blocked admins should have limited access here @@ -86,11 +86,13 @@ class ApiUnblock extends ApiBase { $this->dieStatus( $this->errorArrayToStatus( $retval ) ); } - $res['id'] = $block->getId(); $target = $block->getType() == DatabaseBlock::TYPE_AUTO ? '' : $block->getTarget(); - $res['user'] = $target instanceof User ? $target->getName() : $target; - $res['userid'] = $target instanceof User ? $target->getId() : 0; - $res['reason'] = $params['reason']; + $res = [ + 'id' => $block->getId(), + 'user' => $target instanceof User ? $target->getName() : $target, + 'userid' => $target instanceof User ? $target->getId() : 0, + 'reason' => $params['reason'] + ]; $this->getResult()->addValue( null, $this->getModuleName(), $res ); } diff --git a/includes/api/ApiUndelete.php b/includes/api/ApiUndelete.php index ba9be81bea..9ef17e64df 100644 --- a/includes/api/ApiUndelete.php +++ b/includes/api/ApiUndelete.php @@ -84,10 +84,12 @@ class ApiUndelete extends ApiBase { $this->setWatch( $params['watchlist'], $titleObj ); - $info['title'] = $titleObj->getPrefixedText(); - $info['revisions'] = (int)$retval[0]; - $info['fileversions'] = (int)$retval[1]; - $info['reason'] = $retval[2]; + $info = [ + 'title' => $titleObj->getPrefixedText(), + 'revisions' => (int)$retval[0], + 'fileversions' => (int)$retval[1], + 'reason' => $retval[2] + ]; $this->getResult()->addValue( null, $this->getModuleName(), $info ); } diff --git a/includes/api/ApiUpload.php b/includes/api/ApiUpload.php index b15b9989a0..4678e1f0cc 100644 --- a/includes/api/ApiUpload.php +++ b/includes/api/ApiUpload.php @@ -636,6 +636,7 @@ class ApiUpload extends ApiBase { } ApiResult::setIndexedTagName( $details, 'detail' ); $msg->setApiData( $msg->getApiData() + [ 'details' => $details ] ); + // @phan-suppress-next-line PhanTypeMismatchArgument $this->dieWithError( $msg ); break; @@ -794,6 +795,7 @@ class ApiUpload extends ApiBase { } // No errors, no warnings: do the upload + $result = []; if ( $this->mParams['async'] ) { $progress = UploadBase::getSessionStatus( $this->getUser(), $this->mParams['filekey'] ); if ( $progress && $progress['result'] === 'Poll' ) { diff --git a/includes/api/ApiUserrights.php b/includes/api/ApiUserrights.php index 8f3c404116..3aaae70483 100644 --- a/includes/api/ApiUserrights.php +++ b/includes/api/ApiUserrights.php @@ -51,7 +51,7 @@ class ApiUserrights extends ApiBase { // Deny if the user is blocked and doesn't have the full 'userrights' permission. // This matches what Special:UserRights does for the web UI. - if ( !$pUser->isAllowed( 'userrights' ) ) { + if ( !$this->getPermissionManager()->userHasRight( $pUser, 'userrights' ) ) { $block = $pUser->getBlock(); if ( $block && $block->isSitewide() ) { $this->dieBlocked( $block ); @@ -112,6 +112,7 @@ class ApiUserrights extends ApiBase { $form = $this->getUserRightsPage(); $form->setContext( $this->getContext() ); + $r = []; $r['user'] = $user->getName(); $r['userid'] = $user->getId(); list( $r['added'], $r['removed'] ) = $form->doSaveUserGroups( diff --git a/includes/api/ApiValidatePassword.php b/includes/api/ApiValidatePassword.php index 943149da0f..c36759ac9d 100644 --- a/includes/api/ApiValidatePassword.php +++ b/includes/api/ApiValidatePassword.php @@ -33,6 +33,7 @@ class ApiValidatePassword extends ApiBase { $user = $this->getUser(); } + $r = []; $validity = $user->checkPasswordValidity( $params['password'] ); $r['validity'] = $validity->isGood() ? 'Good' : ( $validity->isOK() ? 'Change' : 'Invalid' ); $messages = array_merge( diff --git a/includes/api/SearchApi.php b/includes/api/SearchApi.php index 02abb1e1c4..6f46c565cd 100644 --- a/includes/api/SearchApi.php +++ b/includes/api/SearchApi.php @@ -99,6 +99,7 @@ trait SearchApi { * * @return array array containing available additional api param definitions. * Empty if profiles are not supported by the searchEngine implementation. + * @suppress PhanTypeMismatchDimFetch */ private function buildProfileApiParam() { $configs = $this->getSearchProfileParams(); @@ -119,6 +120,7 @@ trait SearchApi { if ( isset( $profile['desc-message'] ) ) { $helpMessages[$profile['name']] = $profile['desc-message']; } + if ( !empty( $profile['default'] ) ) { $defaultProfile = $profile['name']; } diff --git a/includes/api/i18n/ar.json b/includes/api/i18n/ar.json index af9723616f..58d2deed51 100644 --- a/includes/api/i18n/ar.json +++ b/includes/api/i18n/ar.json @@ -1607,6 +1607,10 @@ "apierror-cantoverwrite-sharedfile": "الملف الهدف موجود في مستودع مشترك وليست لديك صلاحية لتجاوزه.", "apierror-cantsend": "لم تقم بتسجيل الدخول أو ليس لديك عنوان بريد إلكتروني مؤكد أو غير مسموح لك بإرسال بريد إلكتروني إلى مستخدمين آخرين; لذلك لا يمكنك إرسال بريد إلكتروني.", "apierror-cantundelete": "تعذر الاسترجاع: قد لا تكون المراجعات المطلوبة موجودة، أو ربما تم الاسترجاع بالفعل.", + "apierror-cantview-deleted-comment": "ليست لديك صلاحية لعرض التعليقات المحذوفة.", + "apierror-cantview-deleted-description": "ليست لديك صلاحية لعرض أوصاف الملفات المحذوفة.", + "apierror-cantview-deleted-metadata": "ليست لديك صلاحية لعرض البيانات الوصفية للملفات المحذوفة.", + "apierror-cantview-deleted-revision-content": "ليست لديك صلاحية لعرض محتوى المراجعات المحذوفة.", "apierror-changeauth-norequest": "فشل في إنشاء طلب التغيير.", "apierror-chunk-too-small": "الحد الأدنى لحجم القطعة هو $1 {{PLURAL:$1|بايت}} للقطع غير النهائية.", "apierror-cidrtoobroad": "لا يُقبَل مدى $1 CIDR أكبر من /$2.", @@ -1786,6 +1790,7 @@ "apiwarn-deprecation-missingparam": "نظرا لعدم تحديد $1; تم استخدام تنسيق قديم للإخراج، تم إيقاف هذا التنسيق، وسيتم دائما استخدام التنسيق الجديد في المستقبل.", "apiwarn-deprecation-parameter": "تم إيقاف الوسيط $1.", "apiwarn-deprecation-parse-headitems": "تم إيقاف prop=headitems منذ ميدياويكي 1.28; استخدم prop=headhtml عند إنشاء مستندات HTML جديدة، أو prop=modules|jsconfigvars عند تحديث مستند من جانب العميل.", + "apiwarn-deprecation-post-without-content-type": "تم تقديم طلب POST بدون عنوان Content-Type، هذا لا يعمل بشكل موثوق.", "apiwarn-deprecation-purge-get": "تم إيقاف استخدام action=purge عبر GET; استخدم POST بدلا من ذلك.", "apiwarn-deprecation-withreplacement": "تم إيقاف $1; الرجاء استخدام $2 بدلا من ذلك.", "apiwarn-difftohidden": "لا يمكنك إجراء مقارنة مع r$1: المحتوى مخفي.", diff --git a/includes/api/i18n/en.json b/includes/api/i18n/en.json index 6625863716..cc5e872cee 100644 --- a/includes/api/i18n/en.json +++ b/includes/api/i18n/en.json @@ -1727,6 +1727,10 @@ "apierror-cantoverwrite-sharedfile": "The target file exists on a shared repository and you do not have permission to override it.", "apierror-cantsend": "You are not logged in, you do not have a confirmed email address, or you are not allowed to send email to other users, so you cannot send email.", "apierror-cantundelete": "Couldn't undelete: the requested revisions may not exist, or may have been undeleted already.", + "apierror-cantview-deleted-comment": "You don't have permission to view deleted comments.", + "apierror-cantview-deleted-description": "You don't have permission to view descriptions of deleted files.", + "apierror-cantview-deleted-metadata": "You don't have permission to view metadata of deleted files.", + "apierror-cantview-deleted-revision-content": "You don't have permission to view content of deleted revisions.", "apierror-changeauth-norequest": "Failed to create change request.", "apierror-chunk-too-small": "Minimum chunk size is $1 {{PLURAL:$1|byte|bytes}} for non-final chunks.", "apierror-cidrtoobroad": "$1 CIDR ranges broader than /$2 are not accepted.", @@ -1908,6 +1912,7 @@ "apiwarn-deprecation-missingparam": "Because $1 was not specified, a legacy format has been used for the output. This format is deprecated, and in the future the new format will always be used.", "apiwarn-deprecation-parameter": "The parameter $1 has been deprecated.", "apiwarn-deprecation-parse-headitems": "prop=headitems is deprecated since MediaWiki 1.28. Use prop=headhtml when creating new HTML documents, or prop=modules|jsconfigvars when updating a document client-side.", + "apiwarn-deprecation-post-without-content-type": "A POST request was made without a Content-Type header. This does not work reliably.", "apiwarn-deprecation-purge-get": "Use of action=purge via GET is deprecated. Use POST instead.", "apiwarn-deprecation-withreplacement": "$1 has been deprecated. Please use $2 instead.", "apiwarn-difftohidden": "Couldn't diff to r$1: content is hidden.", diff --git a/includes/api/i18n/es.json b/includes/api/i18n/es.json index e9fd0a7030..55fc06167d 100644 --- a/includes/api/i18n/es.json +++ b/includes/api/i18n/es.json @@ -40,8 +40,8 @@ "apihelp-main-param-action": "Qué acción se realizará.", "apihelp-main-param-format": "El formato de la salida.", "apihelp-main-param-maxlag": "Se puede usar el retardo máximo cuando se instala MediaWiki en un clúster replicado de base de datos. Para evitar acciones que causen más retardo en la replicación del sitio, este parámetro puede hacer que el cliente espere hasta que el retardo en la replicación sea menor que el valor especificado. En caso de retardo excesivo, se devuelve el código de error maxlag con un mensaje como Esperando a $host: $lag segundos de retardo.
Consulta [[mw:Special:MyLanguage/Manual:Maxlag_parameter|Manual: parámetro Maxlag]] para más información.", - "apihelp-main-param-smaxage": "Establece la cabecera HTTP s-maxage de control de caché a esta cantidad de segundos. Los errores nunca se almacenan en caché.", - "apihelp-main-param-maxage": "Establece la cabecera HTTP max-age de control de caché a esta cantidad de segundos. Los errores nunca se almacenan en la caché.", + "apihelp-main-param-smaxage": "Establece la cabecera HTTP s-maxage de control de antememoria a esta cantidad de segundos. Los errores nunca se almacenan en la antememoria.", + "apihelp-main-param-maxage": "Establece la cabecera HTTP max-age de control de antememoria a esta cantidad de segundos. Los errores nunca se almacenan en la antememoria.", "apihelp-main-param-assert": "Comprobar que el usuario haya iniciado sesión si el valor es user o si tiene el permiso de bot si es bot.", "apihelp-main-param-assertuser": "Verificar el usuario actual es el usuario nombrado.", "apihelp-main-param-requestid": "Cualquier valor dado aquí se incluirá en la respuesta. Se puede utilizar para distinguir solicitudes.", @@ -122,7 +122,7 @@ "apihelp-edit-param-text": "Contenido de la página.", "apihelp-edit-param-summary": "Editar resumen. Además de la sección del título cuando $1section=new y $1sectiontitle no están establecidos.", "apihelp-edit-param-tags": "Cambia las etiquetas para aplicarlas a la revisión.", - "apihelp-edit-param-minor": "Edición menor.", + "apihelp-edit-param-minor": "Marcar esta edición como menor.", "apihelp-edit-param-notminor": "Edición no menor.", "apihelp-edit-param-bot": "Marcar esta como una edición de bot.", "apihelp-edit-param-basetimestamp": "Marca de tiempo de la revisión base, usada para detectar conflictos de edición. Se puede obtener mediante [[Special:ApiHelp/query+revisions|action=query&prop=revisions&rvprop=timestamp]]", diff --git a/includes/api/i18n/fr.json b/includes/api/i18n/fr.json index 591bf31ae2..12ece4c5e3 100644 --- a/includes/api/i18n/fr.json +++ b/includes/api/i18n/fr.json @@ -37,13 +37,13 @@ "Lucas Werkmeister (WMDE)" ] }, - "apihelp-main-extended-description": "
\n* [[mw:Special:MyLanguage/API:Main_page|Documentation]]\n* [[mw:Special:MyLanguage/API:FAQ|FAQ]]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api Liste de diffusion]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce Annonces de l’API]\n* [https://phabricator.wikimedia.org/maniphest/query/GebfyV4uCaLd/#R Bogues et demandes]\n
\nÉtat : L’API MediaWiki est une interface stable et mature qui est supportée et améliorée de façon active. Bien que nous essayions de l’éviter, nous pouvons avoir parfois besoin de faire des modifications impactantes ; inscrivez-vous à [https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ la liste de diffusion mediawiki-api-announce] pour être informé des mises à jour.\n\nRequêtes erronées : Si des requêtes erronées sont envoyées à l’API, un entête HTTP sera renvoyé avec la clé « MediaWiki-API-Error ». La valeur de cet entête et le code d’erreur renvoyé prendront la même valeur. Pour plus d’information, voyez [[mw:Special:MyLanguage/API:Errors_and_warnings|API:Errors and warnings]].\n\n

Test : Pour faciliter le test des requêtes à l’API, voyez [[Special:ApiSandbox]].

", + "apihelp-main-extended-description": "
\n* [[mw:Special:MyLanguage/API:Main_page|Documentation]]\n* [[mw:Special:MyLanguage/API:FAQ|FAQ]]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api Liste de diffusion]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce Annonces de l’API]\n* [https://phabricator.wikimedia.org/maniphest/query/GebfyV4uCaLd/#R Bogues et demandes]\n
\nÉtat : l’API de MediaWiki est une interface stable et mature qui est supportée et améliorée de façon active. Bien que nous essayions de l’éviter, nous pouvons avoir parfois besoin de faire des modifications impactantes ; inscrivez-vous à [https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ la liste de diffusion ''mediawiki-api-announce''] pour être informé des mises à jour.\n\nRequêtes erronées : si des requêtes erronées sont envoyées à l’API, un entête HTTP sera renvoyé avec la clé « MediaWiki-API-Error ». La valeur de cet entête et le code d’erreur renvoyé prendront la même valeur. Pour plus d’information, voyez [[mw:Special:MyLanguage/API:Errors_and_warnings|API:Errors and warnings]].\n\n

Test : Pour faciliter le test des requêtes à l’API, voyez [[Special:ApiSandbox]].

", "apihelp-main-param-action": "Quelle action effectuer.", "apihelp-main-param-format": "Le format de sortie.", - "apihelp-main-param-maxlag": "La latence maximale peut être utilisée quand MédiaWiki est installé sur un cluster de base de données répliqué. Pour éviter des actions provoquant un supplément de latence de réplication de site, ce paramètre peut faire attendre le client jusqu’à ce que la latence de réplication soit inférieure à une valeur spécifiée. En cas de latence excessive, le code d’erreur maxlag est renvoyé avec un message tel que Attente de $host : $lag secondes de délai.
Voyez [[mw:Special:MyLanguage/Manual:Maxlag_parameter|Manuel: paramètre Maxlag]] pour plus d’information.", + "apihelp-main-param-maxlag": "La latence maximale peut être utilisée quand MediaWiki est installé sur une grappe de réplication de base de données. Pour éviter des actions provoquant un supplément de latence de réplication de site, ce paramètre peut faire attendre le client jusqu’à ce que la latence de réplication soit inférieure à une valeur spécifiée. En cas de latence excessive, le code d’erreur maxlag est renvoyé avec un message tel que Attente de $host : $lag secondes de délai.
Voyez [[mw:Special:MyLanguage/Manual:Maxlag_parameter|Manuel : paramètre Maxlag]] pour plus d’informations.", "apihelp-main-param-smaxage": "Fixer l’entête HTTP de contrôle de cache s-maxage à ce nombre de secondes. Les erreurs ne sont jamais mises en cache.", "apihelp-main-param-maxage": "Fixer l’entête HTTP de contrôle de cache max-age à ce nombre de secondes. Les erreurs ne sont jamais mises en cache.", - "apihelp-main-param-assert": "Vérifier si l’utilisateur est connecté si la valeur est user, ou s’il a le droit d’un utilisateur robot si la valeur est bot.", + "apihelp-main-param-assert": "Vérifier que l’utilisateur est connecté lorsque la valeur est user ou qu’il a le droit d’un utilisateur robot lorsque la valeur est bot.", "apihelp-main-param-assertuser": "Vérifier que l’utilisateur actuel est l’utilisateur nommé.", "apihelp-main-param-requestid": "Toute valeur fournie ici sera incluse dans la réponse. Peut être utilisé pour distinguer des demandes.", "apihelp-main-param-servedby": "Inclure le nom d’hôte qui a renvoyé la requête dans les résultats.", @@ -51,7 +51,7 @@ "apihelp-main-param-responselanginfo": "Inclure les langues utilisées pour uselang et errorlang dans le résultat.", "apihelp-main-param-origin": "En accédant à l’API en utilisant une requête AJAX inter-domaines (CORS), mettre le domaine d’origine dans ce paramètre. Il doit être inclus dans toute requête de pre-flight, et doit donc faire partie de l’URI de la requête (pas du corps du POST).\n\nPour les requêtes authentifiées, il doit correspondre exactement à une des origines dans l’entête Origin header, donc il doit être fixé avec quelque chose comme https://en.wikipedia.org ou https://meta.wikimedia.org. Si ce paramètre ne correspond pas à l’entête Origin, une réponse 403 sera renvoyée. Si ce paramètre correspond à l’entête Origin et que l’origine est en liste blanche, des entêtes Access-Control-Allow-Origin et Access-Control-Allow-Credentials seront positionnés.\n\nPour les requêtes non authentifiées, spécifiez la valeur *. Cela positionnera l’entête Access-Control-Allow-Origin, mais Access-Control-Allow-Credentials vaudra false et toutes les données spécifiques à l’utilisateur seront filtrées.", "apihelp-main-param-uselang": "Langue à utiliser pour les traductions de message. [[Special:ApiHelp/query+siteinfo|action=query&meta=siteinfo]] avec siprop=languages renvoie une liste de codes de langue, ou en spécifiant user pour utiliser la préférence de langue de l’utilisateur actuel, ou en spécifiant content pour utiliser le langage du contenu de ce wiki.", - "apihelp-main-param-errorformat": "Format à utiliser pour la sortie du texte d’avertissement et d’erreur.\n; plaintext: Wikitexte avec balises HTML supprimées et les entités remplacées.\n; wikitext: wikitexte non analysé.\n; html: HTML.\n; raw: Clé de message et paramètres.\n; none: Aucune sortie de texte, uniquement les codes erreur.\n; bc: Format utilisé avant MédiaWiki 1.29. errorlang et errorsuselocal sont ignorés.", + "apihelp-main-param-errorformat": "Format à utiliser pour la sortie du texte d’avertissement et d’erreur.\n; plaintext: Wikitexte avec balises HTML supprimées et les entités remplacées.\n; wikitext: wikitexte non analysé.\n; html: HTML.\n; raw: Clé de message et paramètres.\n; none: Aucune sortie de texte, uniquement les codes erreur.\n; bc: Format utilisé avant MediaWiki 1.29. errorlang et errorsuselocal sont ignorés.", "apihelp-main-param-errorlang": "Langue à utiliser pour les avertissements et les erreurs. [[Special:ApiHelp/query+siteinfo|action=query&meta=siteinfo]] avec siprop=languages renvoyant une liste de codes de langue, ou spécifier content pour utiliser la langue du contenu de ce wiki, ou spécifier uselang pour utiliser la même valeur que le paramètre uselang.", "apihelp-main-param-errorsuselocal": "S’il est fourni, les textes d’erreur utiliseront des messages adaptés à la langue dans l’espace de noms {{ns:MediaWiki}}.", "apihelp-block-summary": "Bloquer un utilisateur.", @@ -86,7 +86,7 @@ "apihelp-clientlogin-example-login": "Commencer le processus de connexion au wiki en tant qu’utilisateur Exemple avec le mot de passe ExempleMotDePasse.", "apihelp-clientlogin-example-login2": "Continuer la connexion après une réponse de l’IHM pour l’authentification à deux facteurs, en fournissant un OATHToken valant 987654.", "apihelp-compare-summary": "Obtenir la différence entre deux pages.", - "apihelp-compare-extended-description": "Vous devez passer un numéro de révision, un titre de page, ou un ID de page, à la fois pour « from » et « to ».", + "apihelp-compare-extended-description": "Vous devez passer un numéro de révision, un titre de page, ou un ID de page, à la fois pour « from » et « to ».", "apihelp-compare-param-fromtitle": "Premier titre à comparer.", "apihelp-compare-param-fromid": "ID de la première page à comparer.", "apihelp-compare-param-fromrev": "Première révision à comparer.", @@ -153,7 +153,7 @@ "apihelp-edit-param-summary": "Modifier le résumé. Également le titre de la section quand $1section=new et $1sectiontitle n’est pas défini.", "apihelp-edit-param-tags": "Modifier les balises à appliquer à la version.", "apihelp-edit-param-minor": "Marquer cette modification comme étant mineure.", - "apihelp-edit-param-notminor": "Ne pas marquer cette modification comme mineure, même si la préférence utilisateur « {{int:tog-minordefault}} » est positionnée.", + "apihelp-edit-param-notminor": "Ne pas marquer cette modification comme mineure, même si la préférence utilisateur « {{int:tog-minordefault}} » est positionnée.", "apihelp-edit-param-bot": "Marquer cette modification comme effectuée par un robot.", "apihelp-edit-param-basetimestamp": "Horodatage de la révision de base, utilisé pour détecter les conflits de modification. Peut être obtenu via [[Special:ApiHelp/query+revisions|action=query&prop=revisions&rvprop=timestamp]].", "apihelp-edit-param-starttimestamp": "L'horodatage, lorsque le processus d'édition est démarré, est utilisé pour détecter les conflits de modification. Une valeur appropriée peut être obtenue en utilisant [[Special:ApiHelp/main|curtimestamp]] lors du démarrage du processus d'édition (par ex. en chargeant le contenu de la page à modifier).", @@ -323,7 +323,7 @@ "apihelp-opensearch-param-limit": "Nombre maximal de résultats à renvoyer.", "apihelp-opensearch-param-namespace": "Espaces de nom à rechercher. Ignoré if $1search commence avec le préfixe d'un espace de noms valide.", "apihelp-opensearch-param-suggest": "Ne rien faire si [[mw:Special:MyLanguage/Manual:$wgEnableOpenSearchSuggest|$wgEnableOpenSearchSuggest]] vaut faux.", - "apihelp-opensearch-param-redirects": "Comment gérer les redirections :\n;return:Renvoie la redirection elle-même.\n;resolve:Renvoie la page cible. Peut renvoyer moins de $1limit résultats.\nPour des raisons historiques, la valeur par défaut est « return » pour $1format=json et « resolve » pour les autres formats.", + "apihelp-opensearch-param-redirects": "Comment gérer les redirections :\n;return:Renvoie la redirection elle-même.\n;resolve:Renvoie la page cible. Peut renvoyer moins de $1limit résultats.\nPour des raisons historiques, la valeur par défaut est « return » pour $1format=json et « resolve » pour les autres formats.", "apihelp-opensearch-param-format": "Le format de sortie.", "apihelp-opensearch-param-warningsaserror": "Si des avertissements apparaissent avec format=json, renvoyer une erreur d’API au lieu de les ignorer.", "apihelp-opensearch-example-te": "Trouver les pages commençant par Te.", @@ -387,7 +387,7 @@ "apihelp-parse-param-effectivelanglinks": "Inclut les liens de langue fournis par les extensions (à utiliser avec $1prop=langlinks).", "apihelp-parse-param-section": "Traiter uniquement le contenu de la section ayant ce numéro.\n\nQuand la valeur est new, traite $1text et $1sectiontitle comme s’ils correspondaient à une nouvelle section de la page.\n\nLa valeur new n’est autorisée que si text est défini.", "apihelp-parse-param-sectiontitle": "Nouveau titre de section quand section vaut nouveau.\n\nÀ la différence de la modification de page, cela ne revient pas à summary quand il est omis ou vide.", - "apihelp-parse-param-disablelimitreport": "Omettre le rapport de limite (« rapport de limite du nouveau PP ») de la sortie de l’analyseur.", + "apihelp-parse-param-disablelimitreport": "Omettre le rapport de limite (« rapport de limite du nouveau PP ») de la sortie de l’analyseur.", "apihelp-parse-param-disablepp": "Utiliser $1disablelimitreport à la place.", "apihelp-parse-param-disableeditsection": "Omettre les liens de modification de section de la sortie de l’analyseur.", "apihelp-parse-param-disabletidy": "Ne pas exécuter de nettoyage du code HTML (par exemple, réagencer) sur la sortie de l'analyseur.", @@ -747,11 +747,11 @@ "apihelp-query+embeddedin-param-limit": "Combien de pages renvoyer au total.", "apihelp-query+embeddedin-example-simple": "Afficher les pages incluant Template:Stub.", "apihelp-query+embeddedin-example-generator": "Obtenir des informations sur les pages incluant Template:Stub.", - "apihelp-query+extlinks-summary": "Renvoyer toutes les URLs externes (non interwikis) des pages données.", + "apihelp-query+extlinks-summary": "Renvoyer toutes les URL externes (non interwikis) des pages données.", "apihelp-query+extlinks-param-limit": "Combien de liens renvoyer.", "apihelp-query+extlinks-param-protocol": "Protocole de l’URL. Si vide et $1query est positionné, le protocole est http. Laisser à la fois ceci et $1query vides pour lister tous les liens externes.", "apihelp-query+extlinks-param-query": "Rechercher une chaîne sans protocole. Utile pour vérifier si une certaine page contient une certaine URL externe.", - "apihelp-query+extlinks-param-expandurl": "Étendre les URLs relatives au protocole avec le protocole canonique.", + "apihelp-query+extlinks-param-expandurl": "Étendre les URL relatives au protocole avec le protocole canonique.", "apihelp-query+extlinks-example-simple": "Obtenir une liste des liens externes de Main Page.", "apihelp-query+exturlusage-summary": "Énumérer les pages contenant une URL donnée.", "apihelp-query+exturlusage-param-prop": "Quelles informations inclure :", @@ -762,7 +762,7 @@ "apihelp-query+exturlusage-param-query": "Rechercher une chaîne sans protocole. Voyez [[Special:LinkSearch]]. Le laisser vide pour lister tous les liens externes.", "apihelp-query+exturlusage-param-namespace": "Les espaces de nom à énumérer.", "apihelp-query+exturlusage-param-limit": "Combien de pages renvoyer.", - "apihelp-query+exturlusage-param-expandurl": "Étendre les URLs relatives au protocole avec le protocole canonique.", + "apihelp-query+exturlusage-param-expandurl": "Étendre les URL relatives au protocole avec le protocole canonique.", "apihelp-query+exturlusage-example-simple": "Afficher les pages avec un lien vers https://www.mediawiki.org.", "apihelp-query+filearchive-summary": "Énumérer séquentiellement tous les fichiers supprimés.", "apihelp-query+filearchive-param-from": "Le titre de l’image auquel démarrer l’énumération.", @@ -771,7 +771,7 @@ "apihelp-query+filearchive-param-limit": "Combien d’images renvoyer au total.", "apihelp-query+filearchive-param-dir": "La direction dans laquelle lister.", "apihelp-query+filearchive-param-sha1": "Hachage SHA1 de l’image. Écrase $1sha1base36.", - "apihelp-query+filearchive-param-sha1base36": "Hachage SHA1 de l’image en base 36 (utilisé dans MédiaWiki).", + "apihelp-query+filearchive-param-sha1base36": "Hachage SHA1 de l’image en base 36 (utilisé dans MediaWiki).", "apihelp-query+filearchive-param-prop": "Quelle information obtenir sur l’image :", "apihelp-query+filearchive-paramvalue-prop-sha1": "Ajoute le hachage SHA-1 pour l’image.", "apihelp-query+filearchive-paramvalue-prop-timestamp": "Ajoute l’horodatage à la version téléversée.", @@ -798,7 +798,7 @@ "apihelp-query+filerepoinfo-paramvalue-prop-local": "Si ce dépôt est local ou non.", "apihelp-query+filerepoinfo-paramvalue-prop-name": "La clé du dépôt — utilisée dans les valeurs de retour, par ex. [[mw:Special:MyLanguage/Manual:$wgForeignFileRepos|$wgForeignFileRepos]] et [[Special:ApiHelp/query+imageinfo|imageinfo]] return values.", "apihelp-query+filerepoinfo-paramvalue-prop-rootUrl": "Chemin de l’URL racine pour les chemins d’image.", - "apihelp-query+filerepoinfo-paramvalue-prop-scriptDirUrl": "Chemin de l’URL racine pour l’installation de MédiaWiki du wiki du dépôt.", + "apihelp-query+filerepoinfo-paramvalue-prop-scriptDirUrl": "Chemin de l’URL racine pour l’installation de MediaWiki du wiki du dépôt.", "apihelp-query+filerepoinfo-paramvalue-prop-server": "[[mw:Special:MyLanguage/Manual:$wgServer|$wgServer]] du wiki du dépôt, ou équivalent.", "apihelp-query+filerepoinfo-paramvalue-prop-thumbUrl": "Chemin de l’URL racine pour les chemins des vignettes.", "apihelp-query+filerepoinfo-paramvalue-prop-url": "Chemin de l’URL de la zone publique.", @@ -833,7 +833,7 @@ "apihelp-query+imageinfo-paramvalue-prop-extmetadata": "Liste les métadonnées mises en forme combinées depuis diverses sources. Les résultats sont au format HTML.", "apihelp-query+imageinfo-paramvalue-prop-archivename": "Ajoute le nom de fichier de la version d’archive pour les versions autres que la dernière.", "apihelp-query+imageinfo-paramvalue-prop-bitdepth": "Ajoute la profondeur de bits de la version.", - "apihelp-query+imageinfo-paramvalue-prop-uploadwarning": "Utilisé par la page Special:Upload pour obtenir de l’information sur un fichier existant. Non prévu pour être utilisé en dehors du cœur de MédiaWiki.", + "apihelp-query+imageinfo-paramvalue-prop-uploadwarning": "Utilisé par la page Special:Upload pour obtenir de l’information sur un fichier existant. Non prévu pour être utilisé en dehors du cœur de MediaWiki.", "apihelp-query+imageinfo-paramvalue-prop-badfile": "Ajoute l'indication que le fichier est sur [[MediaWiki:Bad image list]]", "apihelp-query+imageinfo-param-limit": "Combien de révisions de fichier renvoyer par fichier.", "apihelp-query+imageinfo-param-start": "Horodatage auquel démarrer la liste.", @@ -888,7 +888,7 @@ "apihelp-query+info-example-simple": "Obtenir des informations sur la page Main Page.", "apihelp-query+info-example-protection": "Obtenir des informations générales et de protection sur la page Main Page.", "apihelp-query+iwbacklinks-summary": "Trouver toutes les pages qui ont un lien vers le lien interwiki indiqué.", - "apihelp-query+iwbacklinks-extended-description": "Peut être utilisé pour trouver tous les liens avec un préfixe, ou tous les liens vers un titre (avec un préfixe donné). Sans paramètre, équivaut à « tous les liens interwiki ».", + "apihelp-query+iwbacklinks-extended-description": "Peut être utilisé pour trouver tous les liens avec un préfixe, ou tous les liens vers un titre (avec un préfixe donné). Sans paramètre, équivaut à « tous les liens interwiki ».", "apihelp-query+iwbacklinks-param-prefix": "Préfixe pour l’interwiki.", "apihelp-query+iwbacklinks-param-title": "Lien interwiki à rechercher. Doit être utilisé avec $1blprefix.", "apihelp-query+iwbacklinks-param-limit": "Combien de pages renvoyer.", @@ -908,7 +908,7 @@ "apihelp-query+iwlinks-param-dir": "La direction dans laquelle lister.", "apihelp-query+iwlinks-example-simple": "Obtenir les liens interwiki de la page Main Page.", "apihelp-query+langbacklinks-summary": "Trouver toutes les pages qui ont un lien vers le lien de langue indiqué.", - "apihelp-query+langbacklinks-extended-description": "Peut être utilisé pour trouver tous les liens avec un code de langue, ou tous les liens vers un titre (avec une langue donnée). Sans paramètre équivaut à « tous les liens de langue ».\n\nNotez que cela peut ne pas prendre en compte les liens de langue ajoutés par les extensions.", + "apihelp-query+langbacklinks-extended-description": "Peut être utilisé pour trouver tous les liens avec un code de langue, ou tous les liens vers un titre (avec une langue donnée). Sans paramètre équivaut à « tous les liens de langue ».\n\nNotez que cela peut ne pas prendre en compte les liens de langue ajoutés par les extensions.", "apihelp-query+langbacklinks-param-lang": "Langue pour le lien de langue.", "apihelp-query+langbacklinks-param-title": "Lien interlangue à rechercher. Doit être utilisé avec $1lang.", "apihelp-query+langbacklinks-param-limit": "Combien de pages renvoyer au total.", @@ -933,7 +933,7 @@ "apihelp-query+languageinfo-summary": "Renvoyer des informations sur les langues disponibles.", "apihelp-query+languageinfo-extended-description": "[[mw:API:Query#Continuing queries|Un prolongement]] peut être appliqué si la récupération de l’information prend trop de temps pour une requête.", "apihelp-query+languageinfo-param-prop": "Quelle information obtenir pour chaque langue.", - "apihelp-query+languageinfo-paramvalue-prop-code": "Le code de langue (ce code est spécifique à MédiaWiki, bien qu’il y ait des recouvrements avec d’autres standards).", + "apihelp-query+languageinfo-paramvalue-prop-code": "Le code de langue (ce code est spécifique à MediaWiki, bien qu’il y ait des recouvrements avec d’autres standards).", "apihelp-query+languageinfo-paramvalue-prop-bcp47": "Le code de langue BCP-47.", "apihelp-query+languageinfo-paramvalue-prop-dir": "La direction d’écriture de la langue (ltr ou rtl).", "apihelp-query+languageinfo-paramvalue-prop-autonym": "L’autonyme d’une langue, c’est-à-dire son nom dans cette langue.", @@ -1169,7 +1169,7 @@ "apihelp-query+siteinfo-paramvalue-prop-fileextensions": "Renvoie la liste des extensions de fichiers (types de fichiers) autorisées au téléversement.", "apihelp-query+siteinfo-paramvalue-prop-rightsinfo": "Renvoie l’information sur les droits du wiki (sa licence), si elle est disponible.", "apihelp-query+siteinfo-paramvalue-prop-restrictions": "Renvoie l’information sur les types de restriction disponibles (protection).", - "apihelp-query+siteinfo-paramvalue-prop-languages": "Renvoie une liste des langues que MédiaWiki prend en charge (éventuellement localisée en utilisant $1inlanguagecode).", + "apihelp-query+siteinfo-paramvalue-prop-languages": "Renvoie une liste des langues que MediaWiki prend en charge (éventuellement localisée en utilisant $1inlanguagecode).", "apihelp-query+siteinfo-paramvalue-prop-languagevariants": "Renvoie une liste de codes de langue pour lesquels [[mw:Special:MyLanguage/LanguageConverter|LanguageConverter]] est activé, et les variantes prises en charge pour chacun.", "apihelp-query+siteinfo-paramvalue-prop-skins": "Renvoie une liste de tous les habillages activés (éventuellement localisé en utilisant $1inlanguagecode, sinon dans la langue du contenu).", "apihelp-query+siteinfo-paramvalue-prop-extensiontags": "Renvoie une liste des balises d’extension de l’analyseur.", @@ -1282,7 +1282,7 @@ "apihelp-query+users-paramvalue-prop-editcount": "Ajoute le compteur de modifications de l’utilisateur.", "apihelp-query+users-paramvalue-prop-registration": "Ajoute l’horodatage d’inscription de l’utilisateur.", "apihelp-query+users-paramvalue-prop-emailable": "Marque si l’utilisateur peut et veut recevoir des courriels via [[Special:Emailuser]].", - "apihelp-query+users-paramvalue-prop-gender": "Marque le sexe de l’utilisateur. Renvoie « male », « female », ou « unknown ».", + "apihelp-query+users-paramvalue-prop-gender": "Marque le sexe de l’utilisateur. Renvoie « male », « female », ou « unknown ».", "apihelp-query+users-paramvalue-prop-centralids": "Ajoute les IDs centraux et l’état d’attachement de l’utilisateur.", "apihelp-query+users-paramvalue-prop-cancreate": "Indique si un compte peut être créé pour les noms d’utilisateurs valides mais non enregistrés.", "apihelp-query+users-param-attachedwiki": "Avec $1prop=centralids, indiquer si l’utilisateur est attaché au wiki identifié par cet ID.", @@ -1374,7 +1374,7 @@ "apihelp-rsd-summary": "Exporter un schéma RSD (Découverte Très Simple).", "apihelp-rsd-example-simple": "Exporter le schéma RSD", "apihelp-setnotificationtimestamp-summary": "Mettre à jour l’horodatage de notification pour les pages suivies.", - "apihelp-setnotificationtimestamp-extended-description": "Cela affecte la mise en évidence des pages modifiées dans la liste de suivi et l’historique, et l’envoi de courriel quand la préférence « {{int:tog-enotifwatchlistpages}} » est activée.", + "apihelp-setnotificationtimestamp-extended-description": "Cela affecte la mise en évidence des pages modifiées dans la liste de suivi et l’historique, et l’envoi de courriel quand la préférence « {{int:tog-enotifwatchlistpages}} » est activée.", "apihelp-setnotificationtimestamp-param-entirewatchlist": "Travailler sur toutes les pages suivies.", "apihelp-setnotificationtimestamp-param-timestamp": "Horodatage auquel dater la notification.", "apihelp-setnotificationtimestamp-param-torevid": "Révision pour laquelle fixer l’horodatage de notification (une page uniquement).", @@ -1439,7 +1439,7 @@ "apihelp-unlinkaccount-summary": "Supprimer un compte tiers lié de l’utilisateur actuel.", "apihelp-unlinkaccount-example-simple": "Essayer de supprimer le lien de l’utilisateur actuel pour le fournisseur associé avec FooAuthenticationRequest.", "apihelp-upload-summary": "Téléverser un fichier, ou obtenir l’état des téléversements en cours.", - "apihelp-upload-extended-description": "Plusieurs méthodes sont disponibles :\n* Téléverser directement le contenu du fichier, en utilisant le paramètre $1file.\n* Téléverser le fichier par morceaux, en utilisant les paramètres $1filesize, $1chunk, and $1offset.\n* Pour que le serveur MédiaWiki cherche un fichier depuis une URL, utilisez le paramètre $1url.\n* Terminer un téléversement précédent qui a échoué à cause d’avertissements, en utilisant le paramètre $1filekey.\nNoter que le POST HTTP doit être fait comme un téléversement de fichier (par exemple en utilisant multipart/form-data) en envoyant le $1file.", + "apihelp-upload-extended-description": "Plusieurs méthodes sont disponibles :\n* Téléverser directement le contenu du fichier, en utilisant le paramètre $1file.\n* Téléverser le fichier par morceaux, en utilisant les paramètres $1filesize, $1chunk, and $1offset.\n* Pour que le serveur MediaWiki cherche un fichier depuis une URL, utilisez le paramètre $1url.\n* Terminer un téléversement précédent qui a échoué à cause d’avertissements, en utilisant le paramètre $1filekey.\nNoter que le POST HTTP doit être fait comme un téléversement de fichier (par exemple en utilisant multipart/form-data) en envoyant le $1file.", "apihelp-upload-param-filename": "Nom de fichier cible.", "apihelp-upload-param-comment": "Téléverser le commentaire. Utilisé aussi comme texte de la page initiale pour les nouveaux fichiers si $1text n’est pas spécifié.", "apihelp-upload-param-tags": "Modifier les balises à appliquer à l’entrée du journal de téléversement et à la révision de la page du fichier.", @@ -1513,7 +1513,7 @@ "api-pageset-param-titles": "Une liste des titres sur lesquels travailler.", "api-pageset-param-pageids": "Une liste des IDs de pages sur lesquelles travailler.", "api-pageset-param-revids": "Une liste des IDs de révisions sur lesquelles travailler.", - "api-pageset-param-generator": "Obtenir la liste des pages sur lesquelles travailler en exécutant le module de requête spécifié.\n\nNOTE : les noms de paramètre du générateur doivent être préfixés avec un « g », voir les exemples.", + "api-pageset-param-generator": "Obtenir la liste des pages sur lesquelles travailler en exécutant le module de requête spécifié.\n\nNOTE : les noms de paramètre du générateur doivent être préfixés avec un « g », voir les exemples.", "api-pageset-param-redirects-generator": "Résoudre automatiquement les redirections dans $1titles, $1pageids et $1revids, et dans les pages renvoyées par $1generator.", "api-pageset-param-redirects-nogenerator": "Résoudre automatiquement les redirections dans $1titles, $1pageids et $1revids.", "api-pageset-param-converttitles": "Convertir les titres dans d’autres variantes si nécessaire. Fonctionne uniquement si la langue de contenu du wiki prend en charge la conversion en variantes. Les langues qui prennent en charge la conversion en variantes incluent $1.", @@ -1539,7 +1539,7 @@ "api-help-param-templated-var-first": "{$1} dans le nom du paramètre doit être remplacé par des valeurs de $2", "api-help-param-templated-var": "{$1} par les valeurs de $2", "api-help-datatypes-header": "Type de données", - "api-help-datatypes": "Les entrées dans MédiaWiki doivent être en UTF-8 à la norme NFC. MédiaWiki peut tenter de convertir d’autres types d’entrées, mais cela peut faire échouer certaines opérations (comme les [[Special:ApiHelp/edit|modifications]] avec contrôles MD5).\n\nCertains types de paramètres dans les requêtes de l’API nécessitent plus d’explication :\n;boolean\n:Les paramètres booléens fonctionnent comme des cases à cocher HTML : si le paramètre est spécifié, quelle que soit sa valeur, il est considéré comme vrai. Pour une valeur fausse, enlever complètement le paramètre.\n;timestamp\n:Les horodatages peuvent être spécifiés sous différentes formes, voir [[mw:Special:MyLanguage/Timestamp|les formats d’entrées de la librairie Timestampdocumentés sur mediawiki.org]] pour plus de détails. La date et heure ISO 8601 est recommandée : 2001-01-15T14:56:00Z. De plus, la chaîne de caractères now peut être utilisée pour spécifier le fuseau horaire actuel.\n;séparateur multi-valeurs alternatif\n:Les paramètres prenant plusieurs valeurs sont normalement validés lorsque celles-ci sont séparées par le caractère « pipe Â» (|), ex. paramètre=valeur1|valeur2 ou paramètre=valeur1%7Cvaleur2. Si une valeur doit contenir le caractère « pipe Â», utiliser U+001F (séparateur de sous-articles) comme séparateur ''et'' la préfixer de U+001F, ex. paramètre=%1Fvaleur1%1Fvaleur2.", + "api-help-datatypes": "Les entrées dans MediaWiki doivent être en UTF-8 à la norme NFC. MediaWiki peut tenter de convertir d’autres types d’entrées, mais cela peut faire échouer certaines opérations (comme les [[Special:ApiHelp/edit|modifications]] avec contrôles MD5).\n\nCertains types de paramètres dans les requêtes de l’API nécessitent plus d’explication :\n;boolean\n:Les paramètres booléens fonctionnent comme des cases à cocher HTML : si le paramètre est spécifié, quelle que soit sa valeur, il est considéré comme vrai. Pour une valeur fausse, enlever complètement le paramètre.\n;timestamp\n:Les horodatages peuvent être spécifiés sous différentes formes, voir [[mw:Special:MyLanguage/Timestamp|les formats d’entrées de la librairie Timestampdocumentés sur mediawiki.org]] pour plus de détails. La date et heure ISO 8601 est recommandée : 2001-01-15T14:56:00Z. De plus, la chaîne de caractères now peut être utilisée pour spécifier le fuseau horaire actuel.\n;séparateur multi-valeurs alternatif\n:Les paramètres prenant plusieurs valeurs sont normalement validés lorsque celles-ci sont séparées par le caractère « pipe Â» (|), ex. paramètre=valeur1|valeur2 ou paramètre=valeur1%7Cvaleur2. Si une valeur doit contenir le caractère « pipe Â», utiliser U+001F (séparateur de sous-articles) comme séparateur ''et'' la préfixer de U+001F, ex. paramètre=%1Fvaleur1%1Fvaleur2.", "api-help-templatedparams-header": "Paramètres de modèle", "api-help-templatedparams": "Les paramètres de modèle supportent les cas où un module d’API a besoin d’une valeur pour chaque valeur d’un autre paramètre quelconque. Par exemple, s’il y avait un module d’API pour demander un fruit, il pourrait avoir un paramètre fruits pour spécifier quels fruits sont demandés et un paramètre de modèle {fruit}-quantité pour spécifier la quantité demandée de chaque fruit. Un client de l’API qui voudrait une pomme, cinq bananes et vingt fraises pourrait alors faire une requête comme fruits=pommes|bananes|fraises&pommes-quantité=1&bananes-quantité=5&fraises-quantité=20.", "api-help-param-type-limit": "Type : entier ou max", @@ -1561,7 +1561,7 @@ "api-help-param-multi-all": "Pour spécifier toutes les valeurs, utiliser $1.", "api-help-param-default": "Par défaut : $1", "api-help-param-default-empty": "Par défaut : (vide)", - "api-help-param-token": "Un jeton « $1 » récupéré par [[Special:ApiHelp/query+tokens|action=query&meta=tokens]]", + "api-help-param-token": "Un jeton « $1 » récupéré par [[Special:ApiHelp/query+tokens|action=query&meta=tokens]]", "api-help-param-token-webui": "Pour rester compatible, le jeton utilisé dans l’IHM web est aussi accepté.", "api-help-param-disabled-in-miser-mode": "Désactivé à cause du [[mw:Special:MyLanguage/Manual:$wgMiserMode|mode minimal]].", "api-help-param-limited-in-miser-mode": "NOTE : Du fait du [[mw:Special:MyLanguage/Manual:$wgMiserMode|mode minimal]], utiliser cela peut aboutir à moins de résultats que $1limit renvoyés avant de continuer ; dans les cas extrêmes, zéro résultat peut être renvoyé.", @@ -1589,7 +1589,7 @@ "apierror-appendnotsupported": "Impossible d’ajouter aux pages utilisant le modèle de contenu $1.", "apierror-articleexists": "L’article que vous essayez de créer l’a déjà été.", "apierror-assertbotfailed": "La vérification que l’utilisateur a le droit bot a échoué.", - "apierror-assertnameduserfailed": "La vérification que l’utilisateur est « $1 » a échoué.", + "apierror-assertnameduserfailed": "La vérification que l’utilisateur est « $1 » a échoué.", "apierror-assertuserfailed": "La vérification que l’utilisateur est connecté a échoué.", "apierror-autoblocked": "Votre adresse IP a été bloquée automatiquement, parce qu’elle a été utilisée par un utilisateur bloqué.", "apierror-bad-badfilecontexttitle": "Titre invalide dans le paramètre $1badfilecontexttitle .", @@ -1603,15 +1603,15 @@ "apierror-badgenerator-unknown": "generator=$1 inconnu.", "apierror-badip": "Paramètre IP non valide.", "apierror-badmd5": "Le hachage MD5 fourni n’était pas correct.", - "apierror-badmodule-badsubmodule": "Le module $1 n’a pas de sous-module « $2 ».", + "apierror-badmodule-badsubmodule": "Le module $1 n’a pas de sous-module « $2 ».", "apierror-badmodule-nosubmodules": "Le module $1 n’a pas de sous-modules.", "apierror-badparameter": "Valeur non valide pour le paramètre $1.", "apierror-badquery": "Requête invalide.", - "apierror-badtimestamp": "Valeur non valide « $2 » pour le paramètre de référence horaire $1.", + "apierror-badtimestamp": "Valeur non valide « $2 » pour le paramètre de référence horaire $1.", "apierror-badtoken": "Jeton CSRF non valide.", "apierror-badupload": "Le paramètre de téléversement de fichier $1 n’est pas un téléversement de fichier ; assurez-vous d’utiliser multipart/form-data pour votre POST et d’inclure un nom de fichier dans l’entête Content-Disposition.", - "apierror-badurl": "Valeur « $2 » non valide pour le paramètre d’URL $1.", - "apierror-baduser": "Valeur « $2 » non valide pour le paramètre utilisateur $1.", + "apierror-badurl": "Valeur « $2 » non valide pour le paramètre d’URL $1.", + "apierror-baduser": "Valeur « $2 » non valide pour le paramètre utilisateur $1.", "apierror-badvalue-notmultivalue": "La séparation multi-valeur U+001F ne peut être utilisée que pour des paramètres multi-valeurs.", "apierror-bad-watchlist-token": "Jeton de liste de suivi fourni non valide. Veuillez mettre un jeton valide dans [[Special:Preferences]].", "apierror-blockedfrommail": "Vous avez été bloqué pour l’envoi de courriel.", @@ -1630,6 +1630,10 @@ "apierror-cantoverwrite-sharedfile": "Le fichier cible existe dans un dépôt partagé et vous n’avez pas le droit de l’écraser.", "apierror-cantsend": "Vous n’êtes pas connecté, vous n’avez pas d’adresse de courriel confirmée, ou vous n’êtes pas autorisé à envoyer des courriels aux autres utilisateurs, donc vous ne pouvez envoyer de courriel.", "apierror-cantundelete": "Impossible d’annuler : les révisions demandées peuvent ne plus exister, ou avoir déjà été annulées.", + "apierror-cantview-deleted-comment": "Vous n’avez pas la permission de visualiser les commentaires supprimés.", + "apierror-cantview-deleted-description": "Vous n’avez pas le droit d’afficher les descriptions des fichiers supprimés.", + "apierror-cantview-deleted-metadata": "Vous n’avez pas le droit d’afficher les métadonnées des fichiers supprimés.", + "apierror-cantview-deleted-revision-content": "Vous n’avez pas la permission de visualiser le contenu des révisions supprimées.", "apierror-changeauth-norequest": "Échec à la création de la requête de modification.", "apierror-chunk-too-small": "La taille minimale d’un segment est de $1 {{PLURAL:$1|octet|octets}} pour les segments hors le dernier.", "apierror-cidrtoobroad": "Les plages CIDR $1 plus large que /$2 ne sont pas acceptées.", @@ -1678,7 +1682,7 @@ "apierror-invalidsection": "Le paramètre section doit être un ID de section valide ou new.", "apierror-invalidsha1base36hash": "Le hachage SHA1Base36 fourni n’est pas valide.", "apierror-invalidsha1hash": "Le hachage SHA1 fourni n’est pas valide.", - "apierror-invalidtitle": "Mauvais titre « $1 ».", + "apierror-invalidtitle": "Mauvais titre « $1 ».", "apierror-invalidurlparam": "Valeur non valide pour $1urlparam ($2=$3).", "apierror-invaliduser": "Nom d'utilisateur invalide \"$1\".", "apierror-invaliduserid": "L'ID d'utilisateur $1 n'est pas valide.", @@ -1735,15 +1739,15 @@ "apierror-paramempty": "Le paramètre $1 ne peut pas être vide.", "apierror-parsetree-notwikitext": "prop=parsetree n’est pris en charge que pour le contenu wikitexte.", "apierror-parsetree-notwikitext-title": "prop=parsetree n’est pris en charge que pour le contenu wikitexte. $1 utilise le modèle de contenu $2.", - "apierror-pastexpiry": "La date d’expiration « $1 » est dépassée.", + "apierror-pastexpiry": "La date d’expiration « $1 » est dépassée.", "apierror-permissiondenied": "Vous n’avez pas le droit de $1.", "apierror-permissiondenied-generic": "Autorisation refusée.", "apierror-permissiondenied-patrolflag": "Vous avez besoin du droit patrol ou patrolmarks pour demander le drapeau patrouillé.", "apierror-permissiondenied-unblock": "Vous n’avez pas le droit de débloquer les utilisateurs.", "apierror-prefixsearchdisabled": "La recherche de préfixe est désactivée en mode misérable.", "apierror-promised-nonwrite-api": "L’entête HTTP Promise-Non-Write-API-Action ne peut pas être envoyé aux modules de l’API en mode écriture.", - "apierror-protect-invalidaction": "Type de protection non valide « $1 ».", - "apierror-protect-invalidlevel": "Niveau de protection non valide « $1 ».", + "apierror-protect-invalidaction": "Type de protection non valide « $1 ».", + "apierror-protect-invalidlevel": "Niveau de protection non valide « $1 ».", "apierror-ratelimited": "Vous avez dépassé votre limite de débit. Veuillez attendre un peu et réessayer.", "apierror-readapidenied": "Vous avez besoin du droit de lecture pour utiliser ce module.", "apierror-readonly": "Ce wiki est actuellement en mode lecture seule.", @@ -1808,28 +1812,29 @@ "apiwarn-deprecation-login-token": "La récupération d’un jeton via action=login est désuète. Utilisez action=query&meta=tokens&type=login à la place.", "apiwarn-deprecation-missingparam": "Comme $1 n’a pas été spécifié, un format ancien a été utilisé pour la sortie. Ce format est obsolète, et dans le futur, le nouveau format sera toujours utilisé.", "apiwarn-deprecation-parameter": "Le paramètre $1 est désuet.", - "apiwarn-deprecation-parse-headitems": "prop=headitems est désuet depuis MédiaWiki 1.28. Utilisez prop=headhtml lors de la création de nouveaux documents HTML, ou prop=modules|jsconfigvars lors de la mise à jour d’un document côté client.", + "apiwarn-deprecation-parse-headitems": "prop=headitems est désuet depuis MediaWiki 1.28. Utilisez prop=headhtml lors de la création de nouveaux documents HTML, ou prop=modules|jsconfigvars lors de la mise à jour d’un document côté client.", + "apiwarn-deprecation-post-without-content-type": "Une requête POST a été faite sans entête Content-Type. Cela ne fonctionne pas de façon fiable.", "apiwarn-deprecation-purge-get": "L’utilisation de action=purge via un GET est désuète. Utiliser POST à la place.", "apiwarn-deprecation-withreplacement": "$1 est désuet. Veuillez utiliser $2 à la place.", "apiwarn-difftohidden": "Impossible de faire un diff avec r$1 : le contenu est masqué.", "apiwarn-errorprinterfailed": "Erreur échec imprimante. Nouvel essai sans paramètres.", "apiwarn-ignoring-invalid-templated-value": "Ignorer la valeur $2 dans $1 en traitant les paramètres de modèle.", - "apiwarn-invalidcategory": "« $1 » n'est pas une catégorie.", - "apiwarn-invalidtitle": "« $1 » n’est pas un titre valide.", + "apiwarn-invalidcategory": "« $1 » n'est pas une catégorie.", + "apiwarn-invalidtitle": "« $1 » n’est pas un titre valide.", "apiwarn-invalidxmlstylesheetext": "Une feuille de style doit avoir une extension .xsl.", "apiwarn-invalidxmlstylesheet": "Feuille de style spécifiée non valide ou inexistante.", "apiwarn-invalidxmlstylesheetns": "La feuille de style devrait être dans l’espace de noms {{ns:MediaWiki}}.", "apiwarn-moduleswithoutvars": "La propriété modules a été définie mais pas jsconfigvars ni encodedjsconfigvars. Les variables de configuration sont nécessaires pour une utilisation correcte du module.", - "apiwarn-notfile": "« $1 » n'est pas un fichier.", + "apiwarn-notfile": "« $1 » n'est pas un fichier.", "apiwarn-nothumb-noimagehandler": "Impossible de créer la vignette car $1 n’a pas de gestionnaire d’image associé.", "apiwarn-parse-nocontentmodel": "Ni title ni contentmodel n’ont été fournis, $1 est supposé.", "apiwarn-parse-revidwithouttext": "revid utilisé sans text, et les propriétés de la page analysée ont été demandées. Vouliez-vous utiliser oldid au lieu de revid ?", "apiwarn-parse-titlewithouttext": "title utilisé sans text, et les propriétés de page analysées sont nécessaires. Voulez-vous dire que vous voulez utiliser page à la place de title ?", "apiwarn-redirectsandrevids": "La résolution de la redirection ne peut pas être utilisée avec le paramètre revids. Toutes les redirections vers lesquelles pointent revids n’ont pas été résolues.", - "apiwarn-tokennotallowed": "L'action « $1 » n'est pas autorisée pour l'utilisateur actuel.", + "apiwarn-tokennotallowed": "L'action « $1 » n'est pas autorisée pour l'utilisateur actuel.", "apiwarn-tokens-origin": "Les jetons ne peuvent pas être obtenus quand la politique de même origine n’est pas appliquée.", "apiwarn-truncatedresult": "Ce résultat a été tronqué parce que sinon, il dépasserait la limite de $1 octets.", - "apiwarn-unclearnowtimestamp": "Passer « $2 » comme paramètre d’horodatage $1 est obsolète. Si, pour une raison quelconque, vous avez besoin de spécifier explicitement l’heure courante sans la recalculer du côté client, utilisez now.", + "apiwarn-unclearnowtimestamp": "Passer « $2 » comme paramètre d’horodatage $1 est obsolète. Si, pour une raison quelconque, vous avez besoin de spécifier explicitement l’heure courante sans la recalculer du côté client, utilisez now.", "apiwarn-unrecognizedvalues": "{{PLURAL:$3|Valeur non reconnue|Valeurs non reconnues}} pour le paramètre $1 : $2.", "apiwarn-unsupportedarray": "Le paramètre $1 utilise une syntaxe PHP de tableau non prise en charge.", "apiwarn-urlparamwidth": "Valeur de la largeur définie dans $1urlparam ($2) ignorée en faveur de la largeur calculée à partir de $1urlwidth/$1urlheight ($3).", diff --git a/includes/api/i18n/gl.json b/includes/api/i18n/gl.json index cb4cd81d5b..f6076b4d52 100644 --- a/includes/api/i18n/gl.json +++ b/includes/api/i18n/gl.json @@ -47,6 +47,9 @@ "apihelp-block-param-reblock": "Se o usuario xa está bloqueado, sobreescribir o bloqueo existente.", "apihelp-block-param-watchuser": "Vixiar a páxina de usuario ou direccións IP e a de conversa deste usuario", "apihelp-block-param-tags": "Cambiar as etiquetas a aplicar á entrada no rexistro de bloqueos.", + "apihelp-block-param-partial": "Bloquear a un usuario en determinadas páxinas ou espazos de nomes no canto de todo o sitio.", + "apihelp-block-param-pagerestrictions": "Lista de títulos que o bloqueo impedirá editar ó usuario. Só se aplica cando partial (parcial) está definido como 'true' (verdadeiro).", + "apihelp-block-param-namespacerestrictions": "Lista de identificadores de espazos de nomes que o bloqueo impedirá que edite o usuario. Só se aplica cando partial (parcial) está definido como 'true' (verdadeiro).", "apihelp-block-example-ip-simple": "Bloquear dirección IP 192.0.2.5 durante tres días coa razón Primeiro aviso.", "apihelp-block-example-user-complex": "Bloquear indefinidamente ó usuario Vandal coa razón Vandalism, e impedir a creación de novas contas e envío de correos electrónicos.", "apihelp-changeauthenticationdata-summary": "Cambiar os datos de autenticación do usuario actual.", diff --git a/includes/api/i18n/he.json b/includes/api/i18n/he.json index e2c5fe7761..e7cf3d4f5e 100644 --- a/includes/api/i18n/he.json +++ b/includes/api/i18n/he.json @@ -418,6 +418,7 @@ "apihelp-query-param-indexpageids": "לכלול פסקת pageids נוספת עם רשימת כל מזהי הדף שהוחזרו.", "apihelp-query-param-export": "יצוא הגרסאות הנוכחיות של כל הדפים הנתונים המחוללים.", "apihelp-query-param-exportnowrap": "להחזיר את ה־XML של היצוא בלי לעטוף אותו בתוצאת XML (אותו תסדיר כמו [[Special:Export]]). אפשר להשתמש בזה רק עם $1export.", + "apihelp-query-param-exportschema": "להשתמש בגרסה הנתונה של תסדיר היטל XML בעת היצוא. יכול לשמש רק עם $1export.", "apihelp-query-param-iwurl": "האם לקבל את ה־URL המלא אם הכותרת היא קישור בינוויקי.", "apihelp-query-param-rawcontinue": "להחזיר נתוני query-continue גולמיים להמשך.", "apihelp-query-example-revisions": "אחזור [[Special:ApiHelp/query+siteinfo|site info]] ו־[[Special:ApiHelp/query+revisions|revisions]] של Main Page.", @@ -913,9 +914,20 @@ "apihelp-query+langlinks-param-inlanguagecode": "קוד שפה בשביל שמות שפות מתורגמות.", "apihelp-query+langlinks-example-simple": "קבלת קישורים בין־לשוניים מהדף Main Page.", "apihelp-query+languageinfo-summary": "מחזירה מידע על שפות זמינות.", + "apihelp-query+languageinfo-extended-description": "אפשר להחיל [[mw:API:Query#Continuing queries|המשך]] אם אחזור המידע לוקח יותר מדי זמן בשביל בקשה אחת.", "apihelp-query+languageinfo-param-prop": "איזה מידע לקבל עבור כל שפה.", + "apihelp-query+languageinfo-paramvalue-prop-code": "קוד השפה. (הקוד הזה ייחודי למדיה־ויקי, אם כי יש חפיפה עם תקנים אחרים.)", + "apihelp-query+languageinfo-paramvalue-prop-bcp47": "קוד שפת לפי BCP-47.", "apihelp-query+languageinfo-paramvalue-prop-dir": "כיוון הכתיבה של השפה (ltr או rtl).", + "apihelp-query+languageinfo-paramvalue-prop-autonym": "השם העצמי של השפה, כלומר השם באותה השפה.", + "apihelp-query+languageinfo-paramvalue-prop-name": "השם בשפה בשפה שצוינה בפרמטר uselang, עם שפת גיבוי כשזה נחוץ.", + "apihelp-query+languageinfo-paramvalue-prop-fallbacks": "קודי השפה של שפות הגיבוי שמוגדרים עבור השפה הזאת. הגיבוי המשתמע הסופי ל־\"en\" אינו כלול (אבל שפות אחדות יכולות להיות מגובות ב־\"en\" במפורש).", + "apihelp-query+languageinfo-paramvalue-prop-variants": "קודי השפה של ההגוונים שהשפה הזאת תומכת בהם.", + "apihelp-query+languageinfo-param-code": "קודי השפה של השפות שאמורות להיות מוחזרות, או * כדי לקבל את כל השפות.", "apihelp-query+languageinfo-example-simple": "קבלת קודי שפה של כל השפות הנתמכות.", + "apihelp-query+languageinfo-example-autonym-name-de": "קבלת השמות העצמיים והשמות הגרמניים של כל השפות הנתמכות.", + "apihelp-query+languageinfo-example-fallbacks-variants-oc": "קבלת שפות הגיבוי וההגוונים של אוקסיטנית.", + "apihelp-query+languageinfo-example-bcp47-dir": "קבלת קודי שפה ב־BCP-47 וכיוון עבור כל השפות הנתמכות.", "apihelp-query+links-summary": "החזרת כל הקישורים מהדפים שצוינו.", "apihelp-query+links-param-namespace": "להציג קישורים רק במרחבי השם האלה.", "apihelp-query+links-param-limit": "כמה קישורים להחזיר.", @@ -1510,7 +1522,7 @@ "api-help-param-templated-var-first": "יש להחליף את הטקסט {$1} (בשם הפרמטר) עם הערכים של הפרמטר $2", "api-help-param-templated-var": "{$1} עם הערכים של הפרמטר $2", "api-help-datatypes-header": "סוגי נתונים", - "api-help-datatypes": "קלט למדיה־ויקי צריך להיות בקידוד UTF-8 מנורמל ב־NFC. מדיה־ויקי יכולה לנסות להמיר קלט אחר, אבל זה עלול לגרום לפעולות מסוימות (כגון [[Special:ApiHelp/edit|עריכות]] עם בדיקות MD5) להיכשל.\n\nחלק מסוגי הפרמטרים בבקשות API דורשים הסבר נוסף:\n;בוליאני (boolean)\n:פרמטרים בוליאניים עובדים כמו תיבות סימון של HTML: אם הפרמטר צוין, בלי קשר לערך שלו, הוא אמת (true). בשביל ערך שקר (false), יש להשמיט את הפרמטר לגמרי.\n;חותם־זמן (timestamp)\n:אפשר לכתוב חותמי־זמן במספר תסדירים. תאריך ושעה לפי ISO 8601 הוא הדבר המומלת. כל הזמנים מצוינים ב־ UTC, לא תהיה השפעה לשום אזור זמן שיצוין.\n:* תאריך ושעה לפי ISO 8601‏, 2001-01-15T14:56:00Z (לא חובה לכתוב פיסוק ו־Z)\n:* תאריך ושעה לפי ISO 8601 עם חלקי שנייה (שלא תהיה להם שום השפעה), 2001-01-15T14:56:00.00001Z (לא חובה לכתוב קווים מפרידים, נקודתיים ו־Z)\n:* תסדיר MediaWiki‏, 20010115145600\n:* תסדיר מספרי כללי, 2001-01-15 14:56:00 (לאזור זמן אופציונלי של GMT‏, +##, או -## אין השפעה)\n:* תסדיר EXIF‏, 2001:01:15 14:56:00\n:* תסדיר RFC 2822 (אפשר להשמיט את אזור הזמן), Mon, 15 Jan 2001 14:56:00\n:* תסדיר RFC 850 (אפשר להשמיט את אזור הזמן), Monday, 15-Jan-2001 14:56:00\n:* תסדיר C ctime‏, Mon Jan 15 14:56:00 2001\n:* שניות מאז 1970-01-01T00:00:00Z בתור מספר שלך בין 1 ל־13 (לא כולל 0)\n:* המחרוזת now\n;מפריד ערכים מרובים חלופי\n:פרמטרים שלוקחים ערכים מרובים בדרך־כלל נשלחים עם הערכים מופרדים באמצעות תו מקל, למשל param=value1|value2 או param=value1%7Cvalue2. אם הערך צריך להכיל את תו המקל, יש להשתמש ב־U+001F (מפריד יחידות) בתור המפריד ''וגם'' להוסיף לתחילת הערך U+001F, למשל param=%1Fvalue1%1Fvalue2.", + "api-help-datatypes": "קלט למדיה־ויקי צריך להיות בקידוד UTF-8 מנורמל ב־NFC. מדיה־ויקי יכולה לנסות להמיר קלט אחר, אבל זה עלול לגרום לפעולות מסוימות (כגון [[Special:ApiHelp/edit|עריכות]] עם בדיקות MD5) להיכשל.\n\nחלק מסוגי הפרמטרים בבקשות API דורשים הסבר נוסף:\n;בוליאני (boolean)\n:פרמטרים בוליאניים עובדים כמו תיבות סימון של HTML: אם הפרמטר צוין, בלי קשר לערך שלו, הוא אמת (true). בשביל ערך שקר (false), יש להשמיט את הפרמטר לגמרי.\n;חותם־זמן (timestamp)\n:אפשר לכתוב חותמי־זמן במספר תסדירים, ר' את [[mw:Special:MyLanguage/Timestamp|תיעוד תסדירי הקלט של ספריית Timestamp באתר mediawiki.org]] לפרטים. תאריך ושעה לפי ISO 8601 הוא הדבר המומלץ: 2001-01-15T14:56:00Z. בנוסף, המחרוזת now יכולה לשמש לציון חום־הזמן הנוכחי.\n;מפריד ערכים מרובים חלופי\n:פרמטרים שלוקחים ערכים מרובים בדרך־כלל נשלחים עם הערכים מופרדים באמצעות תו מקל, למשל param=value1|value2 או param=value1%7Cvalue2. אם הערך צריך להכיל את תו המקל, יש להשתמש ב־U+001F (מפריד יחידות) בתור המפריד ''וגם'' להוסיף לתחילת הערך U+001F, למשל param=%1Fvalue1%1Fvalue2.", "api-help-templatedparams-header": "פרמטרים בתבניות", "api-help-templatedparams": "התכונה \"פרמטרים בתבניות\" תומכת במקרים שבהם מודול של API זקוק לערך כלשהו עבור ערכים של פרמטרים אחרים. למשל, אם היה מודול API לבקשת פרי, ייתכן שהוא היה זקוק לפרמטר בשם פירות על־מנת לציין מהם הפירות המבוקשים, ולפרמטר בתבנית בשם {פרי}-כמות על־מנת לציין את הכמות של כל פרי עבור הבקשה. לשם כך, לקוח API שמעוניין לקבל תפוח אחד, 5 בננות ו־20 תותים יכול היה ליצור בקשה בסגנון פירות=תפוחים|בננות|תותים&תפוחים-כמות=1&בננות-כמות=5&תותים-כמות=20.", "api-help-param-type-limit": "סוג: מספר שלם או max", @@ -1601,6 +1613,10 @@ "apierror-cantoverwrite-sharedfile": "קובץ היעד קיים במאגר משותף ואין לך הרשאה לעקוף אותו.", "apierror-cantsend": "לא נכנסת לחשבון, אין לך חשבון דואר אלקטרוני מאושר, או שאסור לך לשלוח דואר אלקטרוני למשתמשים אחרים, ולכן אין לך אפשרות לשלוח דואר אלקטרוני.", "apierror-cantundelete": "לא היה אפשר לשחזר ממחיקה: אולי הגרסאות המבוקשות אינן קיימות, ואולי הן כבר נמחקו.", + "apierror-cantview-deleted-comment": "אין לך הרשאה לצפות בהערות מחוקות.", + "apierror-cantview-deleted-description": "אין לך הרשאה לצפות בתיאורים של קבצים מחוקים.", + "apierror-cantview-deleted-metadata": "אין לך הרשאה לצפות במטא־נתונים של קבצים מחוקים.", + "apierror-cantview-deleted-revision-content": "אין לך הרשאה לצפות בתוכן של גרסאות מחוקות.", "apierror-changeauth-norequest": "יצירת בקשת השינוי נכשלה.", "apierror-chunk-too-small": "גודל הפלח המזערי הוא {{PLURAL:$1|בית אחד|$1 בתים}} בשביל פלחים לא סופיים.", "apierror-cidrtoobroad": "טווחי CIDR של $1 שרחבים יותר מ־/$2 אינם קבילים.", @@ -1780,6 +1796,7 @@ "apiwarn-deprecation-missingparam": "מכיוון שלא צוין $1, ישמש תסדיר ישן לפלט. התסדיר הזה מוכרז בתור מיושן, ובעתיד ישמש רק התסדיר החדש.", "apiwarn-deprecation-parameter": "הפרמטר $1 מיושן.", "apiwarn-deprecation-parse-headitems": "prop=headitems מיושן מאז מדיה־ויקי 1.28. יש להשתמש ב־prop=headhtml בעת יצירת מסמכי HTML חדשים, או ב־prop=modules|jsconfigvars בעת עדכון מסמך בצד הלקוח.", + "apiwarn-deprecation-post-without-content-type": "בקשת POST נעשתה ללא כותר Content-Type. זה לא עובד באופן מהימן.", "apiwarn-deprecation-purge-get": "שימוש ב־action=purge דרך GET מיושן. יש להשתמש ב־POST במקום זה.", "apiwarn-deprecation-withreplacement": "$1 מיושן. יש להשתמש ב־$2 במקום זה.", "apiwarn-difftohidden": "לא היה אפשר לעשות השוואה עם גרסה $1: התוכן מוסתר.", diff --git a/includes/api/i18n/it.json b/includes/api/i18n/it.json index e29c34af24..9d5d349ef9 100644 --- a/includes/api/i18n/it.json +++ b/includes/api/i18n/it.json @@ -684,6 +684,9 @@ "api-help-authmanagerhelper-continue": "Questa richiesta è una continuazione dopo una precedente risposta UI o REDIRECT. È necessario fornirlo, oppure fornire $1returnurl.", "api-help-authmanagerhelper-additional-params": "Questo modulo accetta parametri aggiuntivi a seconda delle richieste di autenticazione disponibili. Utilizza [[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]] con amirequestsfor=$1 (o una precedente risposta da questo modulo, se applicabile) per determinare le richieste disponibili e i campi usati da queste.", "apierror-bad-badfilecontexttitle": "Titolo non valido nel parametro $1badfilecontexttitle.", + "apierror-cantview-deleted-description": "Non si dispone dei permessi necessari per vedere le descrizioni dei file cancellati.", + "apierror-cantview-deleted-metadata": "Non si dispone dei permessi necessari per vedere i metadati dei file cancellati.", + "apierror-cantview-deleted-revision-content": "Non si dispone dei permessi necessari per vedere il contenuto delle versioni cancellate.", "apierror-compare-notext": "Il parametro $1 non può essere usato senza $2.", "apierror-invalidoldimage": "Il parametro oldimage ha un formato non valido.", "apierror-invaliduserid": "L'ID utente $1 non è valido.", diff --git a/includes/api/i18n/ko.json b/includes/api/i18n/ko.json index c02963643e..6f390f992c 100644 --- a/includes/api/i18n/ko.json +++ b/includes/api/i18n/ko.json @@ -16,7 +16,8 @@ "Jonghaya", "Jerrykim306", "코코아", - "Macofe" + "Macofe", + "렌즈" ] }, "apihelp-main-extended-description": "
\n* [[mw:Special:MyLanguage/API:Main_page|설명문서]]\n* [[mw:Special:MyLanguage/API:FAQ|FAQ]]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api 메일링 리스트]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce API 알림 사항]\n* [https://phabricator.wikimedia.org/maniphest/query/GebfyV4uCaLd/#R 버그 및 요청]\n
\n상태: 이 페이지에 보이는 모든 기능은 정상적으로 작동하지만, API는 여전히 활발하게 개발되고 있으며, 언제든지 변경될 수 있습니다. 업데이트 공지를 받아보려면 [https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ mediawiki-api-announce 메일링 리스트]를 구독하십시오.\n\n잘못된 요청: API에 잘못된 요청이 전송되면 \"MediaWiki-API-Error\" 키가 포함된 HTTP 헤더가 전송되며 반환되는 헤더와 오류 코드의 값은 모두 동일한 값으로 설정됩니다. 자세한 정보에 대해서는 [[mw:Special:MyLanguage/API:Errors and warnings/ko|API:오류와 경고]]를 참조하십시오.\n\n테스트하기: API 요청 테스트를 용이하게 하려면, [[Special:ApiSandbox]]를 보십시오.", @@ -99,7 +100,7 @@ "apihelp-compare-paramvalue-prop-title": "'from'과 'to' 판의 문서 제목입니다.", "apihelp-compare-paramvalue-prop-user": "'from'과 'to' 판의 사용자 이름과 ID입니다.", "apihelp-compare-paramvalue-prop-comment": "'from'과 'to' 판의 설명입니다.", - "apihelp-compare-paramvalue-prop-parsedcomment": "'from'과 to' 판의 구문 분석된 설명입니다.", + "apihelp-compare-paramvalue-prop-parsedcomment": "'from'과 to' 판의 변환된 설명입니다.", "apihelp-compare-paramvalue-prop-size": "'from'과 'to' 판의 크기입니다.", "apihelp-compare-example-1": "판 1과 2의 차이를 생성합니다.", "apihelp-createaccount-summary": "새 사용자 계정을 만듭니다.", @@ -256,39 +257,39 @@ "apihelp-options-example-complex": "모든 환경 설정을 초기화하고 skin과 nickname을 설정합니다.", "apihelp-paraminfo-summary": "API 모듈의 정보를 가져옵니다.", "apihelp-paraminfo-param-helpformat": "도움말 문자열 포맷.", - "apihelp-parse-summary": "내용의 구문을 분석하고 파서 출력을 반환합니다.", - "apihelp-parse-param-summary": "구문 분석할 요약입니다.", - "apihelp-parse-paramvalue-prop-text": "위키텍스트의 구문 분석된 텍스트를 제공합니다.", - "apihelp-parse-paramvalue-prop-langlinks": "구문 분석된 위키텍스트의 언어 링크를 제공합니다.", - "apihelp-parse-paramvalue-prop-categories": "구문 분석된 위키텍스트의 분류를 제공합니다.", + "apihelp-parse-summary": "내용을 변환하고 출력을 반환합니다.", + "apihelp-parse-param-summary": "변환할 요약입니다.", + "apihelp-parse-paramvalue-prop-text": "위키텍스트로 변환된 텍스트를 제공합니다.", + "apihelp-parse-paramvalue-prop-langlinks": "언어 링크를 위키텍스트로 변환하여 제공합니다.", + "apihelp-parse-paramvalue-prop-categories": "분류를 변환된 위키텍스트로 제공합니다.", "apihelp-parse-paramvalue-prop-categorieshtml": "분류의 HTML 버전을 제공합니다.", - "apihelp-parse-paramvalue-prop-links": "구문 분석된 위키텍스트의 내부 링크를 제공합니다.", - "apihelp-parse-paramvalue-prop-templates": "구문 분석된 위키텍스트의 틀을 제공합니다.", - "apihelp-parse-paramvalue-prop-images": "구문 분석된 위키텍스트의 그림을 제공합니다.", - "apihelp-parse-paramvalue-prop-externallinks": "구문 분석된 위키텍스트의 외부 링크를 제공합니다.", - "apihelp-parse-paramvalue-prop-sections": "구문 분석된 위키텍스트의 문단을 제공합니다.", - "apihelp-parse-paramvalue-prop-revid": "구문 분석된 페이지의 판 ID를 추가합니다.", - "apihelp-parse-paramvalue-prop-displaytitle": "구문 분석된 위키텍스트의 제목을 추가합니다.", + "apihelp-parse-paramvalue-prop-links": "내부 링크를 위키텍스트로 변환하여 제공합니다.", + "apihelp-parse-paramvalue-prop-templates": "틀을 변환된 위키텍스트로 제공합니다.", + "apihelp-parse-paramvalue-prop-images": "그림을 위키텍스트로 변환하여 제공합니다.", + "apihelp-parse-paramvalue-prop-externallinks": "외부 링크를 위키텍스트로 변환하여 제공합니다.", + "apihelp-parse-paramvalue-prop-sections": "문단을 변환된 위키텍스트로 제공합니다.", + "apihelp-parse-paramvalue-prop-revid": "변환할 페이지의 판 ID를 추가합니다.", + "apihelp-parse-paramvalue-prop-displaytitle": "제목을 변환된 위키텍스트로 추가합니다.", "apihelp-parse-paramvalue-prop-headitems": "문서의 <head> 안에 넣을 항목을 제공합니다.", - "apihelp-parse-paramvalue-prop-headhtml": "문서의 구문 분석된 <head>를 제공합니다.", + "apihelp-parse-paramvalue-prop-headhtml": "문서의 파싱된 doctype, 여는 <html>, <head>, <body>를 제공합니다.", "apihelp-parse-paramvalue-prop-modules": "문서에 사용되는 ResourceLoader 모듈을 제공합니다. 불러오려면, mw.loader.using()을 사용하세요. jsconfigvars 또는 encodedjsconfigvars는 modules와 함께 요청해야 합니다.", "apihelp-parse-paramvalue-prop-jsconfigvars": "문서에 특화된 자바스크립트 구성 변수를 제공합니다. 적용하려면 mw.config.set()을 사용하세요.", - "apihelp-parse-paramvalue-prop-iwlinks": "구문 분석된 위키텍스트의 인터위키 링크를 제공합니다.", - "apihelp-parse-paramvalue-prop-wikitext": "구문 분석된 위키텍스트 원문을 제공합니다.", - "apihelp-parse-paramvalue-prop-properties": "구문 분석된 위키텍스트에 정의된 다양한 속성을 제공합니다.", - "apihelp-parse-param-pst": "구문 분석 이전에 입력에 대한 사전 저장 변환을 수행합니다. 텍스트로 사용할 때에만 유효합니다.", + "apihelp-parse-paramvalue-prop-iwlinks": "인터위키 링크를 위키텍스트로 변환하여 제공합니다.", + "apihelp-parse-paramvalue-prop-wikitext": "변환한 원문 위키텍스트를 제공합니다.", + "apihelp-parse-paramvalue-prop-properties": "정의된 다양한 속성을 변환된 위키텍스트로 제공합니다.", + "apihelp-parse-param-pst": "파싱에 앞서 입력에 대한 저장 직전의 변환을 수행합니다. 텍스트로 사용할 때에만 유효합니다.", "apihelp-parse-param-disablelimitreport": "파서 출력에서 제한 보고서(\"NewPP limit report\")를 제외합니다.", "apihelp-parse-param-disablepp": "$1disablelimitreport를 대신 사용합니다.", "apihelp-parse-param-disableeditsection": "파서 출력에서 문단 편집 링크를 제외합니다.", "apihelp-parse-param-disabletidy": "파서 출력에서 HTML 정리(예: tidy)를 수행하지 않습니다.", - "apihelp-parse-param-preview": "미리 보기 모드에서 구문 분석을 합니다.", - "apihelp-parse-param-sectionpreview": "문단 미리 보기 모드에서 구문 분석을 합니다. (미리 보기 모드도 활성화함)", + "apihelp-parse-param-preview": "미리 보기 모드에서 파싱합니다.", + "apihelp-parse-param-sectionpreview": "문단 미리 보기 모드에서 파싱합니다. (미리 보기 모드도 활성화함)", "apihelp-parse-param-disabletoc": "출력에서 목차를 제외합니다.", "apihelp-parse-param-useskin": "선택한 스킨을 파서 출력에 적용합니다. 다음의 속성에 영향을 줄 수 있습니다: langlinks, headitems, modules, jsconfigvars, indicators.", "apihelp-parse-param-contentformat": "입력 텍스트에 사용할 내용 직렬화 포맷입니다. $1text와 함께 사용할 때에만 유효합니다.", - "apihelp-parse-example-page": "페이지의 구문을 분석합니다.", + "apihelp-parse-example-page": "페이지를 파싱합니다.", "apihelp-parse-example-text": "위키텍스트의 구문을 분석합니다.", - "apihelp-parse-example-summary": "요약을 구문 분석합니다.", + "apihelp-parse-example-summary": "요약을 변환합니다.", "apihelp-patrol-summary": "문서나 판을 점검하기.", "apihelp-patrol-param-rcid": "점검할 최근 바뀜 ID입니다.", "apihelp-patrol-param-revid": "점검할 판 ID입니다.", @@ -438,7 +439,7 @@ "apihelp-query+imageinfo-summary": "파일 정보와 업로드 역사를 반환합니다.", "apihelp-query+imageinfo-param-prop": "가져올 파일 정보입니다:", "apihelp-query+imageinfo-paramvalue-prop-timestamp": "업로드된 판에 대한 타임스탬프를 추가합니다.", - "apihelp-query+imageinfo-paramvalue-prop-parsedcomment": "판의 설명을 구문 분석합니다.", + "apihelp-query+imageinfo-paramvalue-prop-parsedcomment": "판의 설명을 변환합니다.", "apihelp-query+imageinfo-paramvalue-prop-sha1": "파일에 대한 SHA-1 해시를 추가합니다.", "apihelp-query+imageinfo-paramvalue-prop-mediatype": "파일의 미디어 유형을 추가합니다.", "apihelp-query+imageinfo-param-urlheight": "$1urlwidth와 유사합니다.", @@ -523,7 +524,7 @@ "apihelp-query+revisions+base-paramvalue-prop-contentmodel": "판의 콘텐츠 모델 ID.", "apihelp-query+revisions+base-paramvalue-prop-content": "판의 텍스트.", "apihelp-query+revisions+base-paramvalue-prop-tags": "판의 태그.", - "apihelp-query+revisions+base-param-parse": "[[Special:ApiHelp/parse|action=parse]]를 대신 사용합니다. 판 내용의 구문을 분석합니다. ($1prop=content 필요) 성능 상의 이유로 이 옵션을 사용할 경우 $1limit은 1로 강제됩니다.", + "apihelp-query+revisions+base-param-parse": "[[Special:ApiHelp/parse|action=parse]]를 대신 사용하세요. 판 내용을 파싱합니다. ($1prop=content 필요) 성능 상의 이유로 이 옵션을 사용할 경우 $1limit은 1로 강제됩니다.", "apihelp-query+search-summary": "전문 검색을 수행합니다.", "apihelp-query+search-param-qiprofile": "쿼리 독립적인 프로파일 사용(순위 알고리즘에 영향있음)", "apihelp-query+search-paramvalue-prop-size": "바이트 단위로 문서의 크기를 추가합니다.", @@ -845,6 +846,9 @@ "apierror-cantimport-upload": "업로드된 페이지를 가져올 권한이 없습니다.", "apierror-cantimport": "페이지를 가져올 권한이 없습니다.", "apierror-cantsend": "로그인하지 않았거나 인증된 이메일 주소가 없거나 다른 사용자로 이메일을 보낼 권한이 없기 때문에 이메일을 보낼 수 없습니다.", + "apierror-cantview-deleted-description": "삭제된 파일의 설명을 볼 권한이 없습니다.", + "apierror-cantview-deleted-metadata": "삭제된 파일의 메타데이터를 볼 권한이 없습니다.", + "apierror-cantview-deleted-revision-content": "삭제된 판의 내용을 볼 권한이 없습니다.", "apierror-compare-maintextrequired": "$1slots에 main이 포함되어 있다면 $1text-main 변수는 필수입니다. (메인 슬롯을 삭제할 수 없습니다)", "apierror-compare-notext": "$1 변수는 $2 없이 사용할 수 없습니다.", "apierror-emptynewsection": "비어있는 새 문단을 만들 수 없습니다.", diff --git a/includes/api/i18n/lt.json b/includes/api/i18n/lt.json index 907be26912..51350a176a 100644 --- a/includes/api/i18n/lt.json +++ b/includes/api/i18n/lt.json @@ -3,7 +3,8 @@ "authors": [ "Zygimantus", "Eitvys200", - "Hugo.arg" + "Hugo.arg", + "Homo" ] }, "apihelp-main-param-action": "Kurį veiksmą atlikti.", @@ -19,7 +20,7 @@ "apihelp-createaccount-summary": "Kurti naują vartotojo paskyrą.", "apihelp-delete-summary": "Ištrinti puslapį.", "apihelp-delete-param-watch": "Pridėti puslapį prie dabartinio vartotojo stebimųjų sąrašo.", - "apihelp-delete-param-unwatch": "Pašalinti puslapį iš dabartinio vartotojo stebimųjų sąrašo.", + "apihelp-delete-param-unwatch": "Pašalinti puslapį iš dabartinio naudotojo stebimųjų sąrašo.", "apihelp-delete-example-simple": "Ištrinti Main Page.", "apihelp-delete-example-reason": "Ištrinti Main Page su priežastimi Preparing for move.", "apihelp-disabled-summary": "Šis modulis buvo išjungtas.", @@ -36,7 +37,7 @@ "apihelp-edit-param-createonly": "Neredaguoti puslapio jei jis jau egzistuoja.", "apihelp-edit-param-nocreate": "Parodyti klaidą, jei puslapis neegzistuoja.", "apihelp-edit-param-watch": "Pridėti puslapį į dabartinio vartotojo stebimųjų sąrašą.", - "apihelp-edit-param-unwatch": "Pašalinti puslapį iš dabartinio vartotojo stebimųjų sąrašo.", + "apihelp-edit-param-unwatch": "Pašalinti puslapį iš dabartinio naudotojo stebimųjų sąrašo.", "apihelp-edit-param-redirect": "Automatiškai išspręsti peradresavimus.", "apihelp-edit-param-contentmodel": "Naujam turiniui taikomas turinio modelis.", "apihelp-edit-example-edit": "Redaguoti puslapį.", @@ -104,7 +105,7 @@ "apihelp-move-param-movetalk": "Pervadinti aptarimo puslapį, jei jis egzistuoja.", "apihelp-move-param-noredirect": "Nekurti nukreipimo.", "apihelp-move-param-watch": "Pridėti puslapį ir nukreipimą į dabartinio vartotojo stebimųjų sąrašą.", - "apihelp-move-param-unwatch": "Pašalinti puslapį ir nukreipimą iš dabartinio vartotojo stebimųjų sąrašo.", + "apihelp-move-param-unwatch": "Pašalinti puslapį ir nukreipimą iš dabartinio naudotojo stebimųjų sąrašo.", "apihelp-move-param-ignorewarnings": "Ignuoruoti bet kokius įspėjimus.", "apihelp-move-example-move": "Perkelti Badtitle į Goodtitle nepaliekant nukreipimo.", "apihelp-opensearch-summary": "Ieškoti viki naudojant OpenSearch protokolą.", @@ -301,7 +302,7 @@ "apihelp-query+watchlist-paramvalue-type-new": "Puslapio sukūrimai.", "apihelp-query+watchlist-paramvalue-type-log": "Žurnalo įrašai.", "apihelp-resetpassword-param-user": "Iš naujo nustatomas vartotojas.", - "apihelp-resetpassword-param-email": "Iš naujo nustatomo vartotojo el. pašto adresas.", + "apihelp-resetpassword-param-email": "Iš naujo nustatomo naudotojo el. pašto adresas.", "apihelp-setpagelanguage-summary": "Keisti puslapio kalbą.", "apihelp-setpagelanguage-param-reason": "Keitimo priežastis.", "apihelp-stashedit-param-title": "Puslapio pavadinimas buvo redaguotas.", diff --git a/includes/api/i18n/mk.json b/includes/api/i18n/mk.json index 28453409ea..f72c344fb9 100644 --- a/includes/api/i18n/mk.json +++ b/includes/api/i18n/mk.json @@ -48,6 +48,8 @@ "apihelp-compare-param-totitle": "Втор наслов за споредба.", "apihelp-compare-param-toid": "Втора назнака на страница за споредба.", "apihelp-compare-param-torev": "Бтора преработка за споредба.", + "apihelp-compare-param-topst": "Направи преобразување пред зачувување на totext.", + "apihelp-compare-paramvalue-prop-diff": "HTML на разликата.", "apihelp-compare-example-1": "Дај разлика помеѓу преработките 1 и 2", "apihelp-createaccount-summary": "Создај нова корисничка сметка.", "apihelp-delete-summary": "Избриши страница.", @@ -71,7 +73,7 @@ "apihelp-edit-param-summary": "Опис на уредувањето. Ова е и назив на поднасловот кога не се зададени $1section=new и $1sectiontitle.", "apihelp-edit-param-tags": "Ознаки за измена што се однесуваат на преработката.", "apihelp-edit-param-minor": "Означи го уредувањево како ситно.", - "apihelp-edit-param-notminor": "Неситно уредување.", + "apihelp-edit-param-notminor": "Да не се одбележува ова уредување како ситно дури и при поставена корисничката поставка „{{int:tog-minordefаult}}“.", "apihelp-edit-param-bot": "Означи го уредувањето како ботовско.", "apihelp-edit-param-basetimestamp": "Датум и време на преработката на базата, кои се користат за утврдување на спротиставености во уредувањето. Може да се добие преку [[Special:ApiHelp/query+revisions|action=query&prop=revisions&rvprop=timestamp]].", "apihelp-edit-param-starttimestamp": "Датум и време кога сте почнало уредувањето, кои се користат за утврдување на спротиставености во уредувањата. Соодветната вредност се добива користејќи [[Special:ApiHelp/main|curtimestamp]] кога ќе почнете со уредување (на пр. кога ќе се вчита содржината што ќе ја уредувате).", @@ -447,11 +449,16 @@ "apihelp-query+watchlistraw-param-dir": "Насока на исписот.", "apihelp-revisiondelete-param-suppress": "Дали се притајуваат податоци од администраторите на ист начин како и за останатите.", "apihelp-revisiondelete-param-tags": "Ознаки за примена врз ставката во дневникот на бришења.", + "apihelp-rollback-param-title": "Наслов на страницата што сакате да ја отповикате. Не може да се користи заедно со $1pageid.", + "apihelp-rollback-param-pageid": "Назнака на страницата што сакате да ја отповикате. Не може да се користи заедно со $1title.", + "apihelp-setpagelanguage-param-reason": "Причина за промената.", "apihelp-stashedit-param-section": "Број на поднасловот. 0 за првиот, new за нов.", "apihelp-stashedit-param-sectiontitle": "Назив за нов поднаслов.", "apihelp-stashedit-param-text": "Содржина на страницата.", "apihelp-stashedit-param-contentmodel": "Содржински модел на новата содржина.", "apihelp-stashedit-param-contentformat": "Форматот за серијализација на содржината што се користи во вносниот текст.", + "apihelp-stashedit-param-summary": "Опис на промената.", + "apihelp-tag-summary": "Додавање или отстранување ознаки за промени од поединечни преработки или дневнички записи.", "apihelp-tag-param-reason": "Причина за промената.", "apihelp-unblock-summary": "Одблокирај корисник.", "apihelp-unblock-param-user": "Корисничко име, IP-адреса или IP-опсег за одблокирање. Не може да се користи заедно со $1id или $1userid.", @@ -564,6 +571,15 @@ "api-help-right-apihighlimits": "Употреба на повисоки ограничувања за приложни барања (бавни барања: $1; брзи барања: $2). Ограничувањата за бавни барања важат и за повеќевредносни параметри.", "apierror-badgenerator-unknown": "Непознат generator=$1.", "apierror-badquery": "Неважечко барање.", + "apierror-cannotviewtitle": "Не ви е дозволено да ја прегледате $1.", + "apierror-cantblock": "Немате дозвола за блокирање корисници.", + "apierror-cantchangecontentmodel": "Немате дозвола за менување содржинскиот модел на страница.", + "apierror-cantimport-upload": "Немате дозвола да увезувате подигнати страници.", + "apierror-cantimport": "Немате дозвола за увезуваање страници.", + "apierror-cantview-deleted-comment": "Немате дозвола за прегледување на избришаните коментари.", + "apierror-cantview-deleted-description": "Немате дозвола за прегледување описи на избришаните податотеки.", + "apierror-cantview-deleted-metadata": "Немате дозвола за прегледување метаподатоци на избришаните податотеки.", + "apierror-cantview-deleted-revision-content": "Немате дозвола за прегледување содржина на избришаните преработки.", "apierror-copyuploadbaddomain": "Подигањето преку URL не е дозволено од овој домен.", "apierror-copyuploadbadurl": "Подигањето не е дозволено од оваа URL-адреса.", "apierror-emptynewsection": "Создавањето на нови празни поднаслови не е дозволено.", diff --git a/includes/api/i18n/pl.json b/includes/api/i18n/pl.json index e71a9dc78e..65a091389c 100644 --- a/includes/api/i18n/pl.json +++ b/includes/api/i18n/pl.json @@ -17,7 +17,8 @@ "InternerowyGołąb", "CiaPan", "Vlad5250", - "Railfail536" + "Railfail536", + "Rail" ] }, "apihelp-main-extended-description": "
\n* [[mw:Special:MyLanguage/API:Main_page|Dokumentacja]]\n* [[mw:Special:MyLanguage/API:FAQ|FAQ]]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api Lista dyskusyjna]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce Ogłoszenia dotyczące API]\n* [https://phabricator.wikimedia.org/maniphest/query/GebfyV4uCaLd/#R Błędy i propozycje]\n
\nStan: Wszystkie funkcje opisane na tej stronie powinny działać, ale API nadal jest aktywnie rozwijane i mogą się zmienić w dowolnym czasie. Subskrybuj [https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ listę dyskusyjną mediawiki-api-announce], aby móc na bieżąco dowiadywać się o aktualizacjach.\n\nBłędne żądania: Gdy zostanie wysłane błędne żądanie do API, zostanie wysłany w odpowiedzi nagłówek HTTP z kluczem \"MediaWiki-API-Error\" i zarówno jego wartość jak i wartość kodu błędu wysłanego w odpowiedzi będą miały taką samą wartość. Aby uzyskać więcej informacji, zobacz [[mw:Special:MyLanguage/API:Errors_and_warnings|API: Błędy i ostrzeżenia]].\n\nTestowanie: Aby łatwo testować żądania API, zobacz [[Special:ApiSandbox]].", @@ -658,6 +659,10 @@ "apierror-cantimport": "Nie masz uprawnień do importowania stron.", "apierror-cantsend": "Nie jesteś zalogowany, nie masz potwierdzonego adresu e-mail, albo nie masz prawa wysyłać e-maili do innych użytkowników, więc nie możesz wysłać wiadomości e-mail.", "apierror-cantundelete": "Nie można przywrócić: dana wersja nie istnieje albo została już przywrócona.", + "apierror-cantview-deleted-comment": "Nie masz uprawnień do podglądu usuniętych komentarzy.", + "apierror-cantview-deleted-description": "Nie masz uprawnień do podglądu opisów usuniętych plików.", + "apierror-cantview-deleted-metadata": "Nie masz uprawnień do podglądu metadanych usuniętych plików.", + "apierror-cantview-deleted-revision-content": "Nie masz uprawnień do podglądu treści usuniętych wersji.", "apierror-exceptioncaught": "[$1] Stwierdzono wyjątek: $2", "apierror-filedoesnotexist": "Plik nie istnieje.", "apierror-import-unknownerror": "Nieznany błąd podczas importowania: $1.", diff --git a/includes/api/i18n/pt-br.json b/includes/api/i18n/pt-br.json index 0ad76879b6..77f927dd8e 100644 --- a/includes/api/i18n/pt-br.json +++ b/includes/api/i18n/pt-br.json @@ -1610,6 +1610,10 @@ "apierror-cantoverwrite-sharedfile": "O arquivo de destino existe em um repositório compartilhado e você não tem permissão para substituí-lo.", "apierror-cantsend": "Você não está logado, não possui um endereço de e-mail confirmado ou não tem permissão para enviar e-mails para outros usuários, por isso não pode enviar e-mails.", "apierror-cantundelete": "Não foi possível recuperar arquivos: as revisões solicitadas podem não existir ou talvez já tenham sido eliminadas.", + "apierror-cantview-deleted-comment": "Você não tem permissão para visualizar comentários excluídos.", + "apierror-cantview-deleted-description": "Você não tem permissão para visualizar descrições de arquivos excluídos.", + "apierror-cantview-deleted-metadata": "Você não tem permissão para visualizar os metadados dos arquivos excluídos.", + "apierror-cantview-deleted-revision-content": "Você não tem permissão para visualizar o conteúdo das revisões excluídas.", "apierror-changeauth-norequest": "Falha ao criar pedido de mudança.", "apierror-chunk-too-small": "O tamanho mínimo do bloco é $1 {{PLURAL:$1|byte|bytes}} para os pedaços não finais.", "apierror-cidrtoobroad": "Os intervalos CIDR $1 maiores que /$2 não são aceitos.", @@ -1789,6 +1793,7 @@ "apiwarn-deprecation-missingparam": "Foi usado um formato antigo para a saída, porque $1 não foi especificado. Este formato foi descontinuado e de futuro será sempre usado o formato novo.", "apiwarn-deprecation-parameter": "O parâmetro $1 é obsoleto.", "apiwarn-deprecation-parse-headitems": "prop=headitems está depreciado desde o MediaWiki 1.28. Use prop=headhtml quando for criar novos documentos HTML, ou prop=modules|jsconfigvars quando for atualizar um documento no lado do cliente.", + "apiwarn-deprecation-post-without-content-type": "Um pedido POST foi feito sem um cabeçalho Content-Type. Isto não funciona de forma fiável.", "apiwarn-deprecation-purge-get": "O uso de action=purge via GET está obsoleto. Use o POST em vez disso.", "apiwarn-deprecation-withreplacement": "$1 está obsoleto. Por favor, use $2 em vez.", "apiwarn-difftohidden": "Não foi possível diferenciar r$1: o conteúdo está oculto.", diff --git a/includes/api/i18n/pt.json b/includes/api/i18n/pt.json index 6c30749e21..0986c31b90 100644 --- a/includes/api/i18n/pt.json +++ b/includes/api/i18n/pt.json @@ -1605,6 +1605,10 @@ "apierror-cantoverwrite-sharedfile": "O ficheiro alvo existe num repositório partilhado e você não tem permissão para o substituir.", "apierror-cantsend": "Não está autenticado, não tem um endereço de correio eletrónico confirmado, ou não lhe é permitido enviar correio a outros utilizadores, por isso não pode enviar correios eletrónicos.", "apierror-cantundelete": "Não foi possível restaurar: as revisões solicitadas podem não existir ou podem já ter sido restauradas.", + "apierror-cantview-deleted-comment": "Não tem permissão para ver comentários eliminados.", + "apierror-cantview-deleted-description": "Não tem permissão para ver descrições de ficheiros eliminados.", + "apierror-cantview-deleted-metadata": "Não tem permissão para ver metadados de ficheiros eliminados.", + "apierror-cantview-deleted-revision-content": "Não tem permissão para ver o conteúdo de revisões eliminadas.", "apierror-changeauth-norequest": "A criação do pedido de modificação falhou.", "apierror-chunk-too-small": "O tamanho de segmento mínimo é $1 {{PLURAL:$1|byte|bytes}} para segmentos que não sejam segmentos finais.", "apierror-cidrtoobroad": "Não são aceites intervalos CIDR $1 maiores do que /$2.", @@ -1784,6 +1788,7 @@ "apiwarn-deprecation-missingparam": "Foi usado um formato antigo para a saída, porque $1 não foi especificado. Este formato foi descontinuado e de futuro será sempre usado o formato novo.", "apiwarn-deprecation-parameter": "O parâmetro $1 foi descontinuado.", "apiwarn-deprecation-parse-headitems": "prop=headitems está obsoleto desde o MediaWiki 1.28. Use prop=headhtml ao criar novos documentos de HTML, ou prop=modules|jsconfigvars ao atualizar um documento no lado do cliente.", + "apiwarn-deprecation-post-without-content-type": "Um pedido POST foi feito sem um cabeçalho Content-Type. Isto não funciona de forma fiável.", "apiwarn-deprecation-purge-get": "O uso de action=purge através de um GET foi descontinuado. Em substituição, use um POST.", "apiwarn-deprecation-withreplacement": "$1 foi descontinuado. Em substituição, use $2, por favor.", "apiwarn-difftohidden": "Não foi possível criar uma lista das diferenças em relação à r$1: o conteúdo está ocultado.", diff --git a/includes/api/i18n/qqq.json b/includes/api/i18n/qqq.json index d5de23f9bf..25e7522f66 100644 --- a/includes/api/i18n/qqq.json +++ b/includes/api/i18n/qqq.json @@ -40,10 +40,10 @@ "apihelp-block-param-userid": "{{doc-apihelp-param|block|userid}}", "apihelp-block-param-expiry": "{{doc-apihelp-param|block|expiry}}\n{{doc-important|Do not translate \"5 months\", \"2 weeks\", \"infinite\", \"indefinite\" or \"never\"!}}", "apihelp-block-param-reason": "{{doc-apihelp-param|block|reason}}", - "apihelp-block-param-anononly": "{{doc-apihelp-param|block|anononly}}\n* See also {{msg-mw|ipb-hardblock}}", + "apihelp-block-param-anononly": "{{doc-apihelp-param|block|anononly}}\n\n* See also {{msg-mw|ipb-hardblock}}", "apihelp-block-param-nocreate": "{{doc-apihelp-param|block|nocreate}}\n* See also {{msg-mw|ipbcreateaccount}}", "apihelp-block-param-autoblock": "{{doc-singularthey}}\n{{doc-apihelp-param|block|autoblock}}\n* See also {{msg-mw|ipbenableautoblock}}", - "apihelp-block-param-noemail": "{{doc-apihelp-param|block|noemail}}\n* See also {{msg-mw|ipbemailban}}", + "apihelp-block-param-noemail": "{{doc-apihelp-param|block|noemail}}\n\n* See also {{msg-mw|ipbemailban}}", "apihelp-block-param-hidename": "{{doc-apihelp-param|block|hidename}}", "apihelp-block-param-allowusertalk": "{{doc-apihelp-param|block|allowusertalk}}\n* See also {{msg-mw|ipb-disableusertalk}}", "apihelp-block-param-reblock": "{{doc-apihelp-param|block|reblock}}", @@ -1615,6 +1615,10 @@ "apierror-cantoverwrite-sharedfile": "{{doc-apierror}}", "apierror-cantsend": "{{doc-apierror}}", "apierror-cantundelete": "{{doc-apierror}}", + "apierror-cantview-deleted-comment": "{{doc-apierror}}", + "apierror-cantview-deleted-description": "{{doc-apierror}}", + "apierror-cantview-deleted-metadata": "{{doc-apierror}}", + "apierror-cantview-deleted-revision-content": "{{doc-apierror}}", "apierror-changeauth-norequest": "{{doc-apierror}}", "apierror-chunk-too-small": "{{doc-apierror}}\n\nParameters:\n* $1 - Minimum size in bytes.", "apierror-cidrtoobroad": "{{doc-apierror}}\n\nParameters:\n* $1 - \"IPv4\" or \"IPv6\"\n* $2 - Minimum CIDR mask length.", @@ -1795,6 +1799,7 @@ "apiwarn-deprecation-missingparam": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name.", "apiwarn-deprecation-parameter": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name.", "apiwarn-deprecation-parse-headitems": "{{doc-apierror}}", + "apiwarn-deprecation-post-without-content-type": "{{doc-apierror}}", "apiwarn-deprecation-purge-get": "{{doc-apierror}}", "apiwarn-deprecation-withreplacement": "{{doc-apierror}}\n\nParameters:\n* $1 - Query string fragment that is deprecated, e.g. \"action=tokens\".\n* $2 - Query string fragment to use instead, e.g. \"action=tokens\".", "apiwarn-difftohidden": "{{doc-apierror}}\n\nParameters:\n* $1 - Revision ID number.\n\n\"r\" is short for \"revision\". You may translate it.", diff --git a/includes/api/i18n/ru.json b/includes/api/i18n/ru.json index 599cbb8707..3e5da0ad07 100644 --- a/includes/api/i18n/ru.json +++ b/includes/api/i18n/ru.json @@ -37,7 +37,9 @@ "Edward Chernenko", "Vlad5250", "Diralik", - "DmitTrix" + "DmitTrix", + "Марио", + "Katunchik" ] }, "apihelp-main-extended-description": "
\n* [[mw:Special:MyLanguage/API:Main_page|Документация]]\n* [[mw:Special:MyLanguage/API:FAQ|ЧаВО]]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api Почтовая рассылка]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce Новости API]\n* [https://phabricator.wikimedia.org/maniphest/query/GebfyV4uCaLd/#R Ошибки и запросы]\n
\nСтатус: MediaWiki API — зрелый и стабильный интерфейс, активно поддерживаемый и улучшаемый. Мы стараемся избегать ломающих изменений, однако изредка они могут быть необходимы. Подпишитесь на [https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ почтовую рассылку mediawiki-api-announce], чтобы быть в курсе обновлений.\n\nОшибочные запросы: Если API получает запрос с ошибкой, вернётся заголовок HTTP с ключом «MediaWiki-API-Error», после чего значение заголовка и код ошибки будут отправлены обратно и установлены в то же значение. Более подробную информацию см. [[mw:Special:MyLanguage/API:Errors_and_warnings|API: Ошибки и предупреждения]].\n\n

Тестирование: для удобства тестирования API-запросов, см. [[Special:ApiSandbox]].

", @@ -94,9 +96,10 @@ "apihelp-compare-param-fromid": "Идентификатор первой сравниваемой страницы.", "apihelp-compare-param-fromrev": "Первая сравниваемая версия.", "apihelp-compare-param-frompst": "Выполнить преобразование перед записью правки (PST) над fromtext-{slot}.", - "apihelp-compare-param-fromtext": "Используйте этот текст вместо содержимого версии, заданной fromtitle, fromid или fromrev.", - "apihelp-compare-param-fromcontentmodel": "Модель содержимого fromtext. Если не задана, будет угадана по другим параметрам.", - "apihelp-compare-param-fromcontentformat": "Формат сериализации содержимого fromtext.", + "apihelp-compare-param-fromslots": "Переопределение содержимого версии, заданной параметром fromtitle, fromid или fromrev.\n\nЭтот параметр определяет слоты, которые должны быть изменены. Используйте fromtext-{slot}, fromcontentmodel-{slot}, и fromcontentformat-{slot} для определения содержимого для каждого слота.", + "apihelp-compare-param-fromtext": "Укажите fromslots=main и используйте fromtext-main.", + "apihelp-compare-param-fromcontentmodel": "Укажите fromslots=main и используйте fromcontentmodel-main.", + "apihelp-compare-param-fromcontentformat": "Укажите fromslots=main и используйте fromcontentformat-main.", "apihelp-compare-param-fromsection": "Использовать только указанную секцию из содержимого «from».", "apihelp-compare-param-totitle": "Заголовок второй сравниваемой страницы.", "apihelp-compare-param-toid": "Идентификатор второй сравниваемой страницы.", @@ -104,9 +107,9 @@ "apihelp-compare-param-torelative": "Использовать версию, относящуюся к определённой fromtitle, fromid или fromrev. Все другие опции 'to' будут проигнорированы.", "apihelp-compare-param-topst": "Выполнить преобразование перед записью правки (PST) над totext.", "apihelp-compare-param-tocontentmodel-{slot}": "Модель содержимого totext-{slot}. Если не задана, будет угадана по другим параметрам.", - "apihelp-compare-param-totext": "Используйте этот текст вместо содержимого версии, заданной totitle, toid или torev.", - "apihelp-compare-param-tocontentmodel": "Модель содержимого totext. Если не задана, будет угадана по другим параметрам.", - "apihelp-compare-param-tocontentformat": "Формат сериализации содержимого totext.", + "apihelp-compare-param-totext": "Укажите toslots=main и используйте totext-main.", + "apihelp-compare-param-tocontentmodel": "Укажите toslots=main и используйте tocontentmodel-main.", + "apihelp-compare-param-tocontentformat": "Укажите toslots=main и используйте tocontentformat-main.", "apihelp-compare-param-tosection": "Использовать только указанную секцию из содержимого «to».", "apihelp-compare-param-prop": "Какую информацию получить.", "apihelp-compare-paramvalue-prop-diff": "HTML-код разницы.", @@ -362,7 +365,7 @@ "apihelp-parse-paramvalue-prop-revid": "Добавляет идентификатор версии распарсенной страницы.", "apihelp-parse-paramvalue-prop-displaytitle": "Добавляет название проанализированного вики-текста.", "apihelp-parse-paramvalue-prop-headitems": "Возвращает элементы, которые следует поместить в <head> страницы.", - "apihelp-parse-paramvalue-prop-headhtml": "Возвращает распарсенный <head> страницы.", + "apihelp-parse-paramvalue-prop-headhtml": "Возвращает распарсенный тип документа, начальный <html>, элемент <head> и открывающий <body>.", "apihelp-parse-paramvalue-prop-modules": "Возвращает использованные на странице модули ResourceLoader. Для загрузки, используйте mw.loader.using(). Одновременно с modules должно быть запрошено либо jsconfigvars, либо encodedjsconfigvars.", "apihelp-parse-paramvalue-prop-jsconfigvars": "Возвращает переменные JavaScript с данными настроек для этой страницы. Для их применения используйте mw.config.set().", "apihelp-parse-paramvalue-prop-encodedjsconfigvars": "Возвращает переменные JavaScript с данными настроек для этой страницы в виде JSON-строки.", @@ -646,6 +649,7 @@ "apihelp-query+blocks-paramvalue-prop-reason": "Добавляет причину блокировки.", "apihelp-query+blocks-paramvalue-prop-range": "Добавляет диапазон IP-адресов, затронутых блокировкой.", "apihelp-query+blocks-paramvalue-prop-flags": "Добавляет бану метку (autoblock, anonoly, и так далее).", + "apihelp-query+blocks-paramvalue-prop-restrictions": "Добавляет ограничения частичных блокировок, если блокировка не действует во всём проекте.", "apihelp-query+blocks-param-show": "Показать только элементы, удовлетворяющие этим критериям.\nНапример, чтобы отобразить только бессрочные блокировки IP-адресов, установите $1show=ip|!temp.", "apihelp-query+blocks-example-simple": "Список блокировок.", "apihelp-query+blocks-example-users": "Список блокировок участников Alice и Bob.", @@ -1066,10 +1070,10 @@ "apihelp-query+revisions+base-paramvalue-prop-userid": "Идентификатор создателя версии.", "apihelp-query+revisions+base-paramvalue-prop-size": "Длина версии (в байтах).", "apihelp-query+revisions+base-paramvalue-prop-sha1": "SHA-1-хэш (base 16) версии.", - "apihelp-query+revisions+base-paramvalue-prop-contentmodel": "Идентификатор модели содержимого версии.", + "apihelp-query+revisions+base-paramvalue-prop-contentmodel": "Идентификатор модели содержимого каждого слота версии.", "apihelp-query+revisions+base-paramvalue-prop-comment": "Описание правки.", "apihelp-query+revisions+base-paramvalue-prop-parsedcomment": "Распарсенное описание правки.", - "apihelp-query+revisions+base-paramvalue-prop-content": "Текст версии.", + "apihelp-query+revisions+base-paramvalue-prop-content": "Содержание каждого слота версии.", "apihelp-query+revisions+base-paramvalue-prop-tags": "Метки версии.", "apihelp-query+revisions+base-paramvalue-prop-parsetree": "Используйте [[Special:ApiHelp/expandtemplates|action=expandtemplates]] или [[Special:ApiHelp/parse|action=parse]]. Дерево парсинга XML-содержимого версии (требуется модель содержимого $1).", "apihelp-query+revisions+base-param-limit": "Сколько версий вернуть.", @@ -1495,7 +1499,7 @@ "api-help-param-templated-var-first": "{$1} в названии параметра должно быть заменено значениями $2", "api-help-param-templated-var": "{$1} — значениями $2", "api-help-datatypes-header": "Типы данных", - "api-help-datatypes": "Ввод в MediaWiki должен быть NFC-нормализованным UTF-8. MediaWiki может попытаться преобразовать другой ввод, но это приведёт к провалу некоторых операций (таких, как [[Special:ApiHelp/edit|редактирование]] со сверкой MD5).\n\nНекоторые типы параметров в запросах API требуют дополнительных пояснений:\n;логический\n:Логические параметры работают как флажки (checkboxes) в HTML: если параметр задан, независимо от его значения, он воспринимается за истину. Для передачи ложного значения просто опустите параметр.\n;временные метки\n:Временные метки могут быть заданы в нескольких форматах. Рекомендуемым является дата и время ISO 8601. Всё время считается в UTC, любые включённые часовые пояса игнорируются.\n:* Дата и время ISO 8601: 2001-01-15T14:56:00Z (знаки препинания и Z необязательны)\n:* Дата и время ISO 8601 с (игнорируемой) дробной частью секунд: 2001-01-15T14:56:00.00001Z (дефисы, двоеточия и Z необязательны)\n:* Формат MediaWiki: 20010115145600\n:* Общий числовой формат: 2001-01-15 14:56:00 (необязательный часовой пояс GMT, +## или -## игнорируется)\n:* Формат EXIF: 2001:01:15 14:56:00\n:* Формат RFC 2822 (часовой пояс может быть опущен): Mon, 15 Jan 2001 14:56:00\n:* Формат RFC 850 (часовой пояс может быть опущен): Monday, 15-Jan-2001 14:56:00\n:* Формат ctime языка программирования C: Mon Jan 15 14:56:00 2001\n:* Количество секунд, прошедших с 1970-01-01T00:00:00Z, в виде челого числа с от 1 до 13 знаками (исключая 0)\n:* Строка now\n;альтернативный разделитель значений\n:Параметры, принимающие несколько значений, обычно отправляются со значениями, разделёнными с помощью символа пайпа, например, param=value1|value2 или param=value1%7Cvalue2. Если значение должно содержать символ пайпа, используйте U+001F (Unit Separator) в качестве разделителя ''и'' добавьте в начало значения U+001F, например, param=%1Fvalue1%1Fvalue2.", + "api-help-datatypes": "Ввод в MediaWiki должен быть NFC-нормализованным UTF-8. MediaWiki может попытаться преобразовать другой ввод, но это приведёт к провалу некоторых операций (таких, как [[Special:ApiHelp/edit|редактирование]] со сверкой MD5).\n\nНекоторые типы параметров в запросах API требуют дополнительных пояснений:\n;логический\n:Логические параметры работают как флажки (checkboxes) в HTML: если параметр задан, независимо от его значения, он воспринимается за истину. Для передачи ложного значения просто опустите параметр.\n;временные метки\n:Временные метки могут быть заданы в нескольких форматах (детальнее см. [[mw:Special:MyLanguage/Timestamp|Timestamp library input formats on mediawiki.org]]). Рекомендуемым является дата и время ISO 8601.\n2001-01-15T14:56:00Z. Дополнительно, строка now может использоваться, чтобы указать текущую временную метку.\n;альтернативный разделитель значений\n:Параметры, принимающие несколько значений, обычно отправляются со значениями, разделёнными с помощью символа пайпа, например, param=value1|value2 или param=value1%7Cvalue2. Если значение должно содержать символ пайпа, используйте U+001F (Unit Separator) в качестве разделителя ''и'' добавьте в начало значения U+001F, например, param=%1Fvalue1%1Fvalue2.", "api-help-templatedparams-header": "Шаблонные параметры", "api-help-templatedparams": "Шаблонные параметры используются в случаях, когда модулю API нужно получить по параметру со значением на каждое значение другого параметра. Например, если бы был модуль API, запрашивающий фрукты, у него мог бы быть параметр фрукты, указывающий, какие фрукты запрашиваются, и шаблонный параметр {фрукт}-в-количестве, указывающий, сколько фруктов каждого вида запросить. Клиент API, который хочет запросить 1 яблоко, 5 бананов и 20 апельсинов, мог бы тогда сделать запрос наподобие фрукты=яблоки|бананы|апельсины&яблоки-в-количестве=1&бананы-в-количестве=5&апельсины-в-количестве=20.", "api-help-param-type-limit": "Тип: целое число или max", @@ -1572,6 +1576,7 @@ "apierror-bad-watchlist-token": "Предоставлен некорректный токен списка наблюдения. Пожалуйста, установите корректный токен в [[Special:Preferences]].", "apierror-blockedfrommail": "Отправка электронной почты была для вас заблокирована.", "apierror-blocked": "Редактирование было для вас заблокировано.", + "apierror-blocked-partial": "Вы были заблокированы от редактирования этой страницы.", "apierror-botsnotsupported": "Этот интерфейс не поддерживается для ботов.", "apierror-cannot-async-upload-file": "Параметры async и file не могут применяться вместе. Если вы хотите ассинхронно обработать загруженный файл, сначала загрузите его во временное хранилище (используя параметр stash), а затем опубликуйте этот файл ассинхронно (используя параметры filekey и async).", "apierror-cannotreauthenticate": "Это действие недоступно, так как ваша личность не может быть подтверждена.", @@ -1585,6 +1590,10 @@ "apierror-cantoverwrite-sharedfile": "Целевой файл существует в общем репозитории и у вас нет прав перезаписать его.", "apierror-cantsend": "Вы не авторизованы, ваш электронный адрес не подтверждён или у вас нет прав на отправку электронной почты другим участникам, поэтому вы не можете отправить электронное письмо.", "apierror-cantundelete": "Невозможно восстановить: возможно, запрашиваемые версии не существуют или уже были восстановлены.", + "apierror-cantview-deleted-comment": "У вас нет разрешения на просмотр удаленных комментариев.", + "apierror-cantview-deleted-description": "У вас нет разрешения на просмотр описаний удаленных файлов.", + "apierror-cantview-deleted-metadata": "У вас нет разрешения на просмотр метаданных удаленных файлов.", + "apierror-cantview-deleted-revision-content": "У вас нет разрешения на просмотр содержимого удаленных редакций.", "apierror-changeauth-norequest": "Попытка создать запрос правки провалилась.", "apierror-chunk-too-small": "Минимальный размер кусочка — $1 {{PLURAL:$1|байт|байта|байт}}, если кусочек не является последним.", "apierror-cidrtoobroad": "Диапазоны $1 CIDR, шире /$2, не разрешены.", diff --git a/includes/api/i18n/sv.json b/includes/api/i18n/sv.json index 75c41fc3b1..ea84a53da8 100644 --- a/includes/api/i18n/sv.json +++ b/includes/api/i18n/sv.json @@ -49,6 +49,8 @@ "apihelp-block-param-reblock": "Skriv över befintlig blockering om användaren redan är blockerad.", "apihelp-block-param-watchuser": "Bevaka användarens eller IP-adressens användarsida och diskussionssida", "apihelp-block-param-tags": "Ändra märken att tillämpa i blockloggens post.", + "apihelp-block-param-pagerestrictions": "Lista över titlar att blockera användaren från att redigera. Gäller endast när partial är \"true\".", + "apihelp-block-param-namespacerestrictions": "Lista över namnrymds-ID:n att blockera användaren från att redigera. Gäller endast när partial är \"true\".", "apihelp-block-example-ip-simple": "Blockera IP-adressen 192.0.2.5 i tre dagar med motivationen First strike", "apihelp-block-example-user-complex": "Blockera användare Vandal på obegränsad tid med motivationen Vandalism, och förhindra kontoskapande och e-post.", "apihelp-changeauthenticationdata-summary": "Ändra autentiseringsdata för aktuell användare.", @@ -377,6 +379,7 @@ "apihelp-query+blocks-paramvalue-prop-expiry": "Lägger till en tidsstämpel för när blockeringen går ut.", "apihelp-query+blocks-paramvalue-prop-reason": "Lägger till de skäl som angetts för blockeringen.", "apihelp-query+blocks-paramvalue-prop-range": "Lägger till intervallet av IP-adresser som berörs av blockeringen.", + "apihelp-query+blocks-paramvalue-prop-restrictions": "Lägger till partiella blockeringsbegränsningar om blockeringen inte gäller för hela webbplatsen.", "apihelp-query+blocks-example-simple": "Lista blockeringar.", "apihelp-query+blocks-example-users": "Lista blockeringar av användarna Alice och Bob.", "apihelp-query+categories-summary": "Lista alla kategorier sidorna tillhör.", @@ -548,5 +551,6 @@ "apierror-unknownformat": "Okänt format \"$1\".", "apiwarn-compare-no-next": "Sidversion $2 är den senaste sidversionen av $1, det finns ingen sidversion för torelative=next att jämföra med.", "apiwarn-compare-no-prev": "Sidversionen $2 är den tidigaste sidversion för $1, det finns ingen sidversion för torelative=prev att jämföra med.", + "apiwarn-deprecation-post-without-content-type": "En POST-begäran gjordes utan en Content-Type i sidhuvudet. Det fungerar inte ordentligt.", "api-feed-error-title": "Fel ($1)" } diff --git a/includes/api/i18n/zh-hans.json b/includes/api/i18n/zh-hans.json index 83e8314519..7e08e77b87 100644 --- a/includes/api/i18n/zh-hans.json +++ b/includes/api/i18n/zh-hans.json @@ -30,7 +30,8 @@ "科劳", "SolidBlock", "神樂坂秀吉", - "94rain" + "94rain", + "予弦" ] }, "apihelp-main-extended-description": "
\n* [[mw:Special:MyLanguage/API:Main_page|文档]]\n* [[mw:Special:MyLanguage/API:FAQ|常见问题]]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api 邮件列表]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce API公告]\n* [https://phabricator.wikimedia.org/maniphest/query/GebfyV4uCaLd/#R 程序错误与功能请求]\n
\n状态信息:MediaWiki API是一个成熟稳定的,不断受到支持和改进的界面。尽管我们尽力避免,但偶尔也需要作出重大更新;请订阅[https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ mediawiki-api-announce 邮件列表]以便获得更新通知。\n\n错误请求:当API收到错误请求时,HTTP header将会返回一个包含\"MediaWiki-API-Error\"的值,随后header的值与error code将会送回并设置为相同的值。详细信息请参阅[[mw:Special:MyLanguage/API:Errors_and_warnings|API:错误与警告]]。\n\n

测试中:测试API请求的易用性,请参见[[Special:ApiSandbox]]。

", @@ -64,6 +65,9 @@ "apihelp-block-param-reblock": "如果该用户已被封禁,则覆盖已有的封禁。", "apihelp-block-param-watchuser": "监视用户或该 IP 的用户页和讨论页。", "apihelp-block-param-tags": "要在封禁日志中应用到实体的更改标签。", + "apihelp-block-param-partial": "封禁用户于特定页面或名字空间而不是整个站点。", + "apihelp-block-param-pagerestrictions": "阻止用户编辑的标题列表。仅在partial设置为true时适用。", + "apihelp-block-param-namespacerestrictions": "用于阻止用户编辑的名字空间ID列表。仅在partial设置为true时适用。", "apihelp-block-example-ip-simple": "封禁IP地址192.0.2.5三天,原因First strike。", "apihelp-block-example-user-complex": "无限期封禁用户Vandal,原因Vandalism,并阻止新账户创建和电子邮件发送。", "apihelp-changeauthenticationdata-summary": "更改当前用户的身份验证数据。", diff --git a/includes/api/i18n/zh-hant.json b/includes/api/i18n/zh-hant.json index b7c60ed656..cd5b4cebb7 100644 --- a/includes/api/i18n/zh-hant.json +++ b/includes/api/i18n/zh-hant.json @@ -1613,6 +1613,10 @@ "apierror-cantoverwrite-sharedfile": "目標檔案存在於分享儲存庫上,因此您沒有權限來覆蓋掉。", "apierror-cantsend": "您尚未登入,您沒有已確認的電子郵件地址,或是您未被允許發送電子郵件給其他人,因此您不能發送電子郵件。", "apierror-cantundelete": "無法取消刪除:請求的修訂可能不存在,或是可能已被取消刪除。", + "apierror-cantview-deleted-comment": "您沒有權限來檢視被刪除的註釋。", + "apierror-cantview-deleted-description": "您沒有權限來檢視被刪除檔案的描述內容。", + "apierror-cantview-deleted-metadata": "您沒有權限來檢視被刪除檔案的詮釋資料。", + "apierror-cantview-deleted-revision-content": "您沒有權限來檢視被刪除修訂的內容。", "apierror-changeauth-norequest": "建立更改請求失敗。", "apierror-chunk-too-small": "對於非最終塊,最小塊的大小為 $1 {{PLURAL:$1|位元組|位元組}}。", "apierror-cidrtoobroad": "不能接受超出 /$2 的 $1 CIDR 範圍。", @@ -1792,6 +1796,7 @@ "apiwarn-deprecation-missingparam": "因為未指定 $1,輸出內容使用到過去舊有的格式。該格式已棄用,並且往後都只會使用新格式。", "apiwarn-deprecation-parameter": "參數 $1 已棄用。", "apiwarn-deprecation-parse-headitems": "prop=headitems 自 MediaWiki 的 1.28 版本後已被棄用。當建立新 HTML 文件時請使用 prop=headhtml,或是當更新文件客戶端時請使用 prop=modules|jsconfigvars。", + "apiwarn-deprecation-post-without-content-type": "POST 請求不需要 Content-Type 標頭,這會無法可靠運作。", "apiwarn-deprecation-purge-get": "透過 GET 方式使用的 action=purge 已棄用,請以 POST 替代。", "apiwarn-deprecation-withreplacement": "$1 已棄用,請改用 $2。", "apiwarn-difftohidden": "無法對 r$1 比較差異:內容被隱蔵。", diff --git a/includes/auth/AuthManager.php b/includes/auth/AuthManager.php index c871ce1cb3..4fcaf4e9e9 100644 --- a/includes/auth/AuthManager.php +++ b/includes/auth/AuthManager.php @@ -1639,8 +1639,9 @@ class AuthManager implements LoggerAwareInterface { // Is the IP user able to create accounts? $anon = new User; - if ( $source !== self::AUTOCREATE_SOURCE_MAINT && - !$anon->isAllowedAny( 'createaccount', 'autocreateaccount' ) + if ( $source !== self::AUTOCREATE_SOURCE_MAINT && !MediaWikiServices::getInstance() + ->getPermissionManager() + ->userHasAnyRight( $anon, 'createaccount', 'autocreateaccount' ) ) { $this->logger->debug( __METHOD__ . ': IP lacks the ability to create or autocreate accounts', [ 'username' => $username, diff --git a/includes/auth/AuthenticationRequest.php b/includes/auth/AuthenticationRequest.php index 420034191e..bc70d5efbd 100644 --- a/includes/auth/AuthenticationRequest.php +++ b/includes/auth/AuthenticationRequest.php @@ -122,6 +122,7 @@ abstract class AuthenticationRequest { * a 'password' field). * * @return array As above + * @phan-return array */ abstract public function getFieldInfo(); @@ -246,12 +247,18 @@ abstract class AuthenticationRequest { /** * Select a request by class name. + * + * @codingStandardsIgnoreStart + * @phan-template T + * @codingStandardsIgnoreEnd * @param AuthenticationRequest[] $reqs * @param string $class Class name + * @phan-param class-string $class * @param bool $allowSubclasses If true, also returns any request that's a subclass of the given * class. * @return AuthenticationRequest|null Returns null if there is not exactly * one matching request. + * @phan-return T|null */ public static function getRequestByClass( array $reqs, $class, $allowSubclasses = false ) { $requests = array_filter( $reqs, function ( $req ) use ( $class, $allowSubclasses ) { @@ -297,6 +304,7 @@ abstract class AuthenticationRequest { * @param AuthenticationRequest[] $reqs * @return array * @throws \UnexpectedValueException If fields cannot be merged + * @suppress PhanTypeInvalidDimOffset */ public static function mergeFieldInfo( array $reqs ) { $merged = []; @@ -337,12 +345,13 @@ abstract class AuthenticationRequest { } $options['sensitive'] = !empty( $options['sensitive'] ); + $type = $options['type']; if ( !array_key_exists( $name, $merged ) ) { $merged[$name] = $options; - } elseif ( $merged[$name]['type'] !== $options['type'] ) { + } elseif ( $merged[$name]['type'] !== $type ) { throw new \UnexpectedValueException( "Field type conflict for \"$name\", " . - "\"{$merged[$name]['type']}\" vs \"{$options['type']}\"" + "\"{$merged[$name]['type']}\" vs \"$type\"" ); } else { if ( isset( $options['options'] ) ) { @@ -373,7 +382,7 @@ abstract class AuthenticationRequest { * @return AuthenticationRequest */ public static function __set_state( $data ) { - // @phan-suppress-next-line PhanTypeInstantiateAbstract + // @phan-suppress-next-line PhanTypeInstantiateAbstractStatic $ret = new static(); foreach ( $data as $k => $v ) { $ret->$k = $v; diff --git a/includes/auth/RememberMeAuthenticationRequest.php b/includes/auth/RememberMeAuthenticationRequest.php index 06060b16f9..39bcbf3e77 100644 --- a/includes/auth/RememberMeAuthenticationRequest.php +++ b/includes/auth/RememberMeAuthenticationRequest.php @@ -43,6 +43,7 @@ class RememberMeAuthenticationRequest extends AuthenticationRequest { public function __construct() { /** @var SessionProvider $provider */ $provider = SessionManager::getGlobalSession()->getProvider(); + '@phan-var SessionProvider $provider'; $this->expiration = $provider->getRememberUserDuration(); } diff --git a/includes/auth/ResetPasswordSecondaryAuthenticationProvider.php b/includes/auth/ResetPasswordSecondaryAuthenticationProvider.php index c831fc8c44..25a17544f1 100644 --- a/includes/auth/ResetPasswordSecondaryAuthenticationProvider.php +++ b/includes/auth/ResetPasswordSecondaryAuthenticationProvider.php @@ -96,7 +96,9 @@ class ResetPasswordSecondaryAuthenticationProvider extends AbstractSecondaryAuth } } + /** @var PasswordAuthenticationRequest $needReq */ $needReq = $data->req ?? new PasswordAuthenticationRequest(); + '@phan-var PasswordAuthenticationRequest $needReq'; if ( !$needReq->action ) { $needReq->action = AuthManager::ACTION_CHANGE; } diff --git a/includes/auth/Throttler.php b/includes/auth/Throttler.php index e6d9bd8de9..7128fe2d7e 100644 --- a/includes/auth/Throttler.php +++ b/includes/auth/Throttler.php @@ -40,7 +40,7 @@ class Throttler implements LoggerAwareInterface { /** * See documentation of $wgPasswordAttemptThrottle for format. Old (pre-1.27) format is not * allowed here. - * @var array + * @var array[] * @see https://www.mediawiki.org/wiki/Manual:$wgPasswordAttemptThrottle */ protected $conditions; @@ -127,14 +127,16 @@ class Throttler implements LoggerAwareInterface { continue; } - $throttleKey = $this->cache->makeGlobalKey( 'throttler', $this->type, $index, $ipKey, $userKey ); + $throttleKey = $this->cache->makeGlobalKey( + 'throttler', + $this->type, + $index, + $ipKey, + $userKey + ); $throttleCount = $this->cache->get( $throttleKey ); - - if ( !$throttleCount ) { // counter not started yet - $this->cache->add( $throttleKey, 1, $expiry ); - } elseif ( $throttleCount < $count ) { // throttle limited not yet reached - $this->cache->incr( $throttleKey ); - } else { // throttled + if ( $throttleCount && $throttleCount >= $count ) { + // Throttle limited reached $this->logRejection( [ 'throttle' => $this->type, 'index' => $index, @@ -147,13 +149,12 @@ class Throttler implements LoggerAwareInterface { // @codeCoverageIgnoreEnd ] ); - return [ - 'throttleIndex' => $index, - 'count' => $count, - 'wait' => $expiry, - ]; + return [ 'throttleIndex' => $index, 'count' => $count, 'wait' => $expiry ]; + } else { + $this->cache->incrWithInit( $throttleKey, $expiry, 1 ); } } + return false; } @@ -178,7 +179,7 @@ class Throttler implements LoggerAwareInterface { /** * Handles B/C for $wgPasswordAttemptThrottle. * @param array $throttleConditions - * @return array + * @return array[] * @see $wgPasswordAttemptThrottle for structure */ protected static function normalizeThrottleConditions( $throttleConditions ) { diff --git a/includes/block/AbstractBlock.php b/includes/block/AbstractBlock.php index f6544040bb..fa91909b60 100644 --- a/includes/block/AbstractBlock.php +++ b/includes/block/AbstractBlock.php @@ -23,6 +23,7 @@ namespace MediaWiki\Block; use IContextSource; use InvalidArgumentException; use IP; +use MediaWiki\MediaWikiServices; use RequestContext; use Title; use User; @@ -89,12 +90,13 @@ abstract class AbstractBlock { /** * Create a new block with specified parameters on a user, IP or IP range. * - * @param array $options Parameters of the block: - * address string|User Target user name, User object, IP address or IP range - * by int User ID of the blocker - * reason string Reason of the block - * timestamp string The time at which the block comes into effect - * byText string Username of the blocker (for foreign users) + * @param array $options Parameters of the block, with supported options: + * - address: (string|User) Target user name, User object, IP address or IP range + * - by: (int) User ID of the blocker + * - reason: (string) Reason of the block + * - timestamp: (string) The time at which the block comes into effect + * - byText: (string) Username of the blocker (for foreign users) + * - hideName: (bool) Hide the target user name */ public function __construct( array $options = [] ) { $defaults = [ @@ -103,6 +105,7 @@ abstract class AbstractBlock { 'reason' => '', 'timestamp' => '', 'byText' => '', + 'hideName' => false, ]; $options += $defaults; @@ -119,6 +122,7 @@ abstract class AbstractBlock { $this->setReason( $options['reason'] ); $this->setTimestamp( wfTimestamp( TS_MW, $options['timestamp'] ) ); + $this->setHideName( (bool)$options['hideName'] ); } /** @@ -246,8 +250,9 @@ abstract class AbstractBlock { * may be overridden according to global configs. * * @since 1.33 - * @param string $right Right to check - * @return bool|null null if unrecognized right or unset property + * @param string $right + * @return bool|null The block applies to the right, or null if + * unsure (e.g. unrecognized right or unset property) */ public function appliesToRight( $right ) { $config = RequestContext::getMain()->getConfig(); @@ -279,8 +284,9 @@ abstract class AbstractBlock { if ( !$res && $blockDisablesLogin ) { // If a block would disable login, then it should // prevent any right that all users cannot do + $permissionManager = MediaWikiServices::getInstance()->getPermissionManager(); $anon = new User; - $res = $anon->isAllowed( $right ) ? $res : true; + $res = $permissionManager->userHasRight( $anon, $right ) ? $res : true; } return $res; @@ -339,8 +345,9 @@ abstract class AbstractBlock { if ( !$res && $blockDisablesLogin ) { // If a block would disable login, then it should // prevent any action that all users cannot do + $permissionManager = MediaWikiServices::getInstance()->getPermissionManager(); $anon = new User; - $res = $anon->isAllowed( $action ) ? $res : true; + $res = $permissionManager->userHasRight( $anon, $action ) ? $res : true; } return $res; diff --git a/includes/block/BlockManager.php b/includes/block/BlockManager.php index b67703cab1..077bdca7c8 100644 --- a/includes/block/BlockManager.php +++ b/includes/block/BlockManager.php @@ -21,11 +21,15 @@ namespace MediaWiki\Block; use DateTime; +use DateTimeZone; use DeferredUpdates; +use Hooks; use IP; use MediaWiki\Config\ServiceOptions; +use MediaWiki\Permissions\PermissionManager; use MediaWiki\User\UserIdentity; use MWCryptHash; +use Psr\Log\LoggerInterface; use User; use WebRequest; use WebResponse; @@ -38,12 +42,11 @@ use Wikimedia\IPSet; * @since 1.34 Refactored from User and Block. */ class BlockManager { - // TODO: This should be UserIdentity instead of User - /** @var User */ - private $currentUser; + /** @var PermissionManager */ + private $permissionManager; - /** @var WebRequest */ - private $currentRequest; + /** @var ServiceOptions */ + private $options; /** * TODO Make this a const when HHVM support is dropped (T192166) @@ -63,20 +66,23 @@ class BlockManager { 'SoftBlockRanges', ]; + /** @var LoggerInterface */ + private $logger; + /** * @param ServiceOptions $options - * @param User $currentUser - * @param WebRequest $currentRequest + * @param PermissionManager $permissionManager + * @param LoggerInterface $logger */ public function __construct( ServiceOptions $options, - User $currentUser, - WebRequest $currentRequest + PermissionManager $permissionManager, + LoggerInterface $logger ) { $options->assertRequiredOptions( self::$constructorOptions ); $this->options = $options; - $this->currentUser = $currentUser; - $this->currentRequest = $currentRequest; + $this->permissionManager = $permissionManager; + $this->logger = $logger; } /** @@ -84,49 +90,116 @@ class BlockManager { * return a composite block that combines the strictest features of the applicable * blocks. * - * TODO: $user should be UserIdentity instead of User + * Different blocks may be sought, depending on the user and their permissions. The + * user may be: + * (1) The global user (and can be affected by IP blocks). The global request object + * is needed for checking the IP address, the XFF header and the cookies. + * (2) The global user (and exempt from IP blocks). The global request object is + * needed for checking the cookies. + * (3) Another user (not the global user). No request object is available or needed; + * just look for a block against the user account. + * + * Cases #1 and #2 check whether the global user is blocked in practice; the block + * may due to their user account being blocked or to an IP address block or cookie + * block (or multiple of these). Case #3 simply checks whether a user's account is + * blocked, and does not determine whether the person using that account is affected + * in practice by any IP address or cookie blocks. * * @internal This should only be called by User::getBlockedStatus * @param User $user + * @param WebRequest|null $request The global request object if the user is the + * global user (cases #1 and #2), otherwise null (case #3). The IP address and + * information from the request header are needed to find some types of blocks. * @param bool $fromReplica Whether to check the replica DB first. * To improve performance, non-critical checks are done against replica DBs. * Check when actually saving should be done against master. * @return AbstractBlock|null The most relevant block, or null if there is no block. */ - public function getUserBlock( User $user, $fromReplica ) { - $isAnon = $user->getId() === 0; + public function getUserBlock( User $user, $request, $fromReplica ) { + $fromMaster = !$fromReplica; + $ip = null; - // TODO: If $user is the current user, we should use the current request. Otherwise, - // we should not look for XFF or cookie blocks. - $request = $user->getRequest(); + // If this is the global user, they may be affected by IP blocks (case #1), + // or they may be exempt (case #2). If affected, look for additional blocks + // against the IP address. + $checkIpBlocks = $request && + !$this->permissionManager->userHasRight( $user, 'ipblock-exempt' ); - # We only need to worry about passing the IP address to the block generator if the - # user is not immune to autoblocks/hardblocks, and they are the current user so we - # know which IP address they're actually coming from - $ip = null; - $sessionUser = $this->currentUser; - // the session user is set up towards the end of Setup.php. Until then, - // assume it's a logged-out user. - $globalUserName = $sessionUser->isSafeToLoad() - ? $sessionUser->getName() - : IP::sanitizeIP( $this->currentRequest->getIP() ); - if ( $user->getName() === $globalUserName && !$user->isAllowed( 'ipblock-exempt' ) ) { - $ip = $this->currentRequest->getIP(); + if ( $request && $checkIpBlocks ) { + + // Case #1: checking the global user, including IP blocks + $ip = $request->getIP(); + // TODO: remove dependency on DatabaseBlock (T221075) + $blocks = DatabaseBlock::newListFromTarget( $user, $ip, $fromMaster ); + $this->getAdditionalIpBlocks( $blocks, $request, !$user->isRegistered(), $fromMaster ); + $this->getCookieBlock( $blocks, $user, $request ); + + } elseif ( $request ) { + + // Case #2: checking the global user, but they are exempt from IP blocks + // TODO: remove dependency on DatabaseBlock (T221075) + $blocks = DatabaseBlock::newListFromTarget( $user, null, $fromMaster ); + $this->getCookieBlock( $blocks, $user, $request ); + + } else { + + // Case #3: checking whether a user's account is blocked + // TODO: remove dependency on DatabaseBlock (T221075) + $blocks = DatabaseBlock::newListFromTarget( $user, null, $fromMaster ); + + } + + // Filter out any duplicated blocks, e.g. from the cookie + $blocks = $this->getUniqueBlocks( $blocks ); + + $block = null; + if ( count( $blocks ) > 0 ) { + if ( count( $blocks ) === 1 ) { + $block = $blocks[ 0 ]; + } else { + $block = new CompositeBlock( [ + 'address' => $ip, + 'byText' => 'MediaWiki default', + 'reason' => wfMessage( 'blockedtext-composite-reason' )->plain(), + 'originalBlocks' => $blocks, + ] ); + } } - // User/IP blocking - // After this, $blocks is an array of blocks or an empty array - // TODO: remove dependency on DatabaseBlock - $blocks = DatabaseBlock::newListFromTarget( $user, $ip, !$fromReplica ); + Hooks::run( 'GetUserBlock', [ clone $user, $ip, &$block ] ); + + return $block; + } - // Cookie blocking + /** + * Get the cookie block, if there is one. + * + * @param AbstractBlock[] &$blocks + * @param UserIdentity $user + * @param WebRequest $request + * @return void + */ + private function getCookieBlock( &$blocks, UserIdentity $user, WebRequest $request ) { $cookieBlock = $this->getBlockFromCookieValue( $user, $request ); - if ( $cookieBlock instanceof AbstractBlock ) { + if ( $cookieBlock instanceof DatabaseBlock ) { $blocks[] = $cookieBlock; } + } + + /** + * Check for any additional blocks against the IP address or any IPs in the XFF header. + * + * @param AbstractBlock[] &$blocks Blocks found so far + * @param WebRequest $request + * @param bool $isAnon The user is logged out + * @param bool $fromMaster + * @return void + */ + private function getAdditionalIpBlocks( &$blocks, WebRequest $request, $isAnon, $fromMaster ) { + $ip = $request->getIP(); // Proxy blocking - if ( $ip !== null && !in_array( $ip, $this->options->get( 'ProxyWhitelist' ) ) ) { + if ( !in_array( $ip, $this->options->get( 'ProxyWhitelist' ) ) ) { // Local list if ( $this->isLocallyBlockedProxy( $ip ) ) { $blocks[] = new SystemBlock( [ @@ -145,24 +218,8 @@ class BlockManager { } } - // (T25343) Apply IP blocks to the contents of XFF headers, if enabled - if ( $this->options->get( 'ApplyIpBlocksToXff' ) - && $ip !== null - && !in_array( $ip, $this->options->get( 'ProxyWhitelist' ) ) - ) { - $xff = $request->getHeader( 'X-Forwarded-For' ); - $xff = array_map( 'trim', explode( ',', $xff ) ); - $xff = array_diff( $xff, [ $ip ] ); - // TODO: remove dependency on DatabaseBlock - $xffblocks = DatabaseBlock::getBlocksForIPList( $xff, $isAnon, !$fromReplica ); - $blocks = array_merge( $blocks, $xffblocks ); - } - // Soft blocking - if ( $ip !== null - && $isAnon - && IP::isInRanges( $ip, $this->options->get( 'SoftBlockRanges' ) ) - ) { + if ( $isAnon && IP::isInRanges( $ip, $this->options->get( 'SoftBlockRanges' ) ) ) { $blocks[] = new SystemBlock( [ 'address' => $ip, 'byText' => 'MediaWiki default', @@ -172,24 +229,17 @@ class BlockManager { ] ); } - // Filter out any duplicated blocks, e.g. from the cookie - $blocks = $this->getUniqueBlocks( $blocks ); - - if ( count( $blocks ) > 0 ) { - if ( count( $blocks ) === 1 ) { - $block = $blocks[ 0 ]; - } else { - $block = new CompositeBlock( [ - 'address' => $ip, - 'byText' => 'MediaWiki default', - 'reason' => wfMessage( 'blockedtext-composite-reason' )->plain(), - 'originalBlocks' => $blocks, - ] ); - } - return $block; + // (T25343) Apply IP blocks to the contents of XFF headers, if enabled + if ( $this->options->get( 'ApplyIpBlocksToXff' ) + && !in_array( $ip, $this->options->get( 'ProxyWhitelist' ) ) + ) { + $xff = $request->getHeader( 'X-Forwarded-For' ); + $xff = array_map( 'trim', explode( ',', $xff ) ); + $xff = array_diff( $xff, [ $ip ] ); + // TODO: remove dependency on DatabaseBlock (T221075) + $xffblocks = DatabaseBlock::getBlocksForIPList( $xff, $isAnon, $fromMaster ); + $blocks = array_merge( $blocks, $xffblocks ); } - - return null; } /** @@ -210,6 +260,8 @@ class BlockManager { if ( $block instanceof SystemBlock ) { $systemBlocks[] = $block; } elseif ( $block->getType() === DatabaseBlock::TYPE_AUTO ) { + /** @var DatabaseBlock $block */ + '@phan-var DatabaseBlock $block'; if ( !isset( $databaseBlocks[$block->getParentBlockId()] ) ) { $databaseBlocks[$block->getParentBlockId()] = $block; } @@ -218,12 +270,12 @@ class BlockManager { } } - return array_merge( $systemBlocks, $databaseBlocks ); + return array_values( array_merge( $systemBlocks, $databaseBlocks ) ); } /** * Try to load a block from an ID given in a cookie value. If the block is invalid - * or doesn't exist, remove the cookie. + * doesn't exist, or the cookie value is malformed, remove the cookie. * * @param UserIdentity $user * @param WebRequest $request @@ -233,20 +285,25 @@ class BlockManager { UserIdentity $user, WebRequest $request ) { - $blockCookieId = $this->getIdFromCookieValue( $request->getCookie( 'BlockID' ) ); + $cookieValue = $request->getCookie( 'BlockID' ); + if ( is_null( $cookieValue ) ) { + return false; + } - if ( $blockCookieId !== null ) { - // TODO: remove dependency on DatabaseBlock + $blockCookieId = $this->getIdFromCookieValue( $cookieValue ); + if ( !is_null( $blockCookieId ) ) { + // TODO: remove dependency on DatabaseBlock (T221075) $block = DatabaseBlock::newFromID( $blockCookieId ); if ( $block instanceof DatabaseBlock && - $this->shouldApplyCookieBlock( $block, $user->isAnon() ) + $this->shouldApplyCookieBlock( $block, !$user->isRegistered() ) ) { return $block; } - $this->clearBlockCookie( $request->response() ); } + $this->clearBlockCookie( $request->response() ); + return false; } @@ -348,15 +405,14 @@ class BlockManager { $ipList = $this->checkHost( $hostname ); if ( $ipList ) { - wfDebugLog( - 'dnsblacklist', + $this->logger->info( "Hostname $hostname is {$ipList[0]}, it's a proxy says $basename!" ); $found = true; break; } - wfDebugLog( 'dnsblacklist', "Requested $hostname, not found in $basename." ); + $this->logger->debug( "Requested $hostname, not found in $basename." ); } } @@ -399,12 +455,14 @@ class BlockManager { // TODO: Improve on simply tracking the first trackable block (T225654) foreach ( $block->getOriginalBlocks() as $originalBlock ) { if ( $this->shouldTrackBlockWithCookie( $originalBlock, $isAnon ) ) { + '@phan-var DatabaseBlock $originalBlock'; $this->setBlockCookie( $originalBlock, $response ); return; } } } else { if ( $this->shouldTrackBlockWithCookie( $block, $isAnon ) ) { + '@phan-var DatabaseBlock $block'; $this->setBlockCookie( $block, $response ); } } @@ -435,7 +493,11 @@ class BlockManager { } // Set the cookie. Reformat the MediaWiki datetime as a Unix timestamp for the cookie. - $expiryValue = DateTime::createFromFormat( 'YmdHis', $expiryTime )->format( 'U' ); + $expiryValue = DateTime::createFromFormat( + 'YmdHis', + $expiryTime, + new DateTimeZone( 'UTC' ) + )->format( 'U' ); $cookieOptions = [ 'httpOnly' => false ]; $cookieValue = $this->getCookieValue( $block ); $response->setCookie( 'BlockID', $cookieValue, $expiryValue, $cookieOptions ); diff --git a/includes/block/CompositeBlock.php b/includes/block/CompositeBlock.php index 45a630149c..6f49f170ee 100644 --- a/includes/block/CompositeBlock.php +++ b/includes/block/CompositeBlock.php @@ -40,8 +40,9 @@ class CompositeBlock extends AbstractBlock { /** * Create a new block with specified parameters on a user, IP or IP range. * - * @param array $options Parameters of the block: - * originalBlocks Block[] Blocks that this block is composed from + * @param array $options Parameters of the block, with options supported by + * `AbstractBlock::__construct`, and also: + * - originalBlocks: (Block[]) Blocks that this block is composed from */ public function __construct( array $options = [] ) { parent::__construct( $options ); @@ -163,9 +164,28 @@ class CompositeBlock extends AbstractBlock { /** * @inheritDoc + * + * Determines whether the CompositeBlock applies to a right by checking + * whether the original blocks apply to that right. Each block can report + * true (applies), false (does not apply) or null (unsure). Then: + * - If any original blocks apply, this block applies + * - If no original blocks apply but any are unsure, this block is unsure + * - If all blocks do not apply, this block does not apply */ public function appliesToRight( $right ) { - return $this->methodReturnsValue( __FUNCTION__, true, $right ); + $isUnsure = false; + + foreach ( $this->originalBlocks as $block ) { + $appliesToRight = $block->appliesToRight( $right ); + + if ( $appliesToRight ) { + return true; + } elseif ( $appliesToRight === null ) { + $isUnsure = true; + } + } + + return $isUnsure ? null : false; } /** diff --git a/includes/block/DatabaseBlock.php b/includes/block/DatabaseBlock.php index 2fd62ee332..b1a8e215b8 100644 --- a/includes/block/DatabaseBlock.php +++ b/includes/block/DatabaseBlock.php @@ -24,7 +24,6 @@ namespace MediaWiki\Block; use ActorMigration; use AutoCommitUpdate; -use BadMethodCallException; use CommentStore; use DeferredUpdates; use Hooks; @@ -86,19 +85,18 @@ class DatabaseBlock extends AbstractBlock { /** * Create a new block with specified option parameters on a user, IP or IP range. * - * @param array $options Parameters of the block: - * user int Override target user ID (for foreign users) - * auto bool Is this an automatic block? - * expiry string Timestamp of expiration of the block or 'infinity' - * anonOnly bool Only disallow anonymous actions - * createAccount bool Disallow creation of new accounts - * enableAutoblock bool Enable automatic blocking - * hideName bool Hide the target user name - * blockEmail bool Disallow sending emails - * allowUsertalk bool Allow the target to edit its own talk page - * sitewide bool Disallow editing all pages and all contribution - * actions, except those specifically allowed by - * other block flags + * @param array $options Parameters of the block, with options supported by + * `AbstractBlock::__construct`, and also: + * - user: (int) Override target user ID (for foreign users) + * - auto: (bool) Is this an automatic block? + * - expiry: (string) Timestamp of expiration of the block or 'infinity' + * - anonOnly: (bool) Only disallow anonymous actions + * - createAccount: (bool) Disallow creation of new accounts + * - enableAutoblock: (bool) Enable automatic blocking + * - blockEmail: (bool) Disallow sending emails + * - allowUsertalk: (bool) Allow the target to edit its own talk page + * - sitewide: (bool) Disallow editing all pages and all contribution actions, + * except those specifically allowed by other block flags * * @since 1.26 $options array */ @@ -112,7 +110,6 @@ class DatabaseBlock extends AbstractBlock { 'anonOnly' => false, 'createAccount' => false, 'enableAutoblock' => false, - 'hideName' => false, 'blockEmail' => false, 'allowUsertalk' => false, 'sitewide' => true, @@ -129,7 +126,6 @@ class DatabaseBlock extends AbstractBlock { # Boolean settings $this->mAuto = (bool)$options['auto']; - $this->setHideName( (bool)$options['hideName'] ); $this->isHardblock( !$options['anonOnly'] ); $this->isAutoblocking( (bool)$options['enableAutoblock'] ); $this->isSitewide( (bool)$options['sitewide'] ); @@ -164,47 +160,6 @@ class DatabaseBlock extends AbstractBlock { } } - /** - * Return the list of ipblocks fields that should be selected to create - * a new block. - * @deprecated since 1.31, use self::getQueryInfo() instead. - * @return array - */ - public static function selectFields() { - global $wgActorTableSchemaMigrationStage; - - if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_READ_NEW ) { - // If code is using this instead of self::getQueryInfo(), there's a - // decent chance it's going to try to directly access - // $row->ipb_by or $row->ipb_by_text and we can't give it - // useful values here once those aren't being used anymore. - throw new BadMethodCallException( - 'Cannot use ' . __METHOD__ - . ' when $wgActorTableSchemaMigrationStage has SCHEMA_COMPAT_READ_NEW' - ); - } - - wfDeprecated( __METHOD__, '1.31' ); - return [ - 'ipb_id', - 'ipb_address', - 'ipb_by', - 'ipb_by_text', - 'ipb_by_actor' => 'NULL', - 'ipb_timestamp', - 'ipb_auto', - 'ipb_anon_only', - 'ipb_create_account', - 'ipb_enable_autoblock', - 'ipb_expiry', - 'ipb_deleted', - 'ipb_block_email', - 'ipb_allow_usertalk', - 'ipb_parent_block_id', - 'ipb_sitewide', - ] + CommentStore::getStore()->getFields( 'ipb_reason' ); - } - /** * Return the tables, fields, and join conditions to be selected to create * a new block object. diff --git a/includes/block/Restriction/AbstractRestriction.php b/includes/block/Restriction/AbstractRestriction.php index a010e833ab..e08b877aa0 100644 --- a/includes/block/Restriction/AbstractRestriction.php +++ b/includes/block/Restriction/AbstractRestriction.php @@ -99,7 +99,7 @@ abstract class AbstractRestriction implements Restriction { * @inheritDoc */ public static function newFromRow( \stdClass $row ) { - // @phan-suppress-next-line PhanTypeInstantiateAbstract + // @phan-suppress-next-line PhanTypeInstantiateAbstractStatic return new static( $row->ir_ipb_id, $row->ir_value ); } diff --git a/includes/block/Restriction/PageRestriction.php b/includes/block/Restriction/PageRestriction.php index 45aab46b7a..78d67228d9 100644 --- a/includes/block/Restriction/PageRestriction.php +++ b/includes/block/Restriction/PageRestriction.php @@ -87,7 +87,9 @@ class PageRestriction extends AbstractRestriction { * @inheritDoc */ public static function newFromRow( \stdClass $row ) { + /** @var self $restriction */ $restriction = parent::newFromRow( $row ); + '@phan-var self $restriction'; // If the page_namespace and the page_title were provided, add the title to // the restriction. diff --git a/includes/block/Restriction/Restriction.php b/includes/block/Restriction/Restriction.php index d717fe7e86..5dddd785d1 100644 --- a/includes/block/Restriction/Restriction.php +++ b/includes/block/Restriction/Restriction.php @@ -70,7 +70,7 @@ interface Restriction { * * @since 1.33 * @param \stdClass $row - * @return self + * @return static */ public static function newFromRow( \stdClass $row ); diff --git a/includes/block/SystemBlock.php b/includes/block/SystemBlock.php index 0cbf12508b..494a7b9ba2 100644 --- a/includes/block/SystemBlock.php +++ b/includes/block/SystemBlock.php @@ -39,11 +39,11 @@ class SystemBlock extends AbstractBlock { /** * Create a new block with specified parameters on a user, IP or IP range. * - * @param array $options Parameters of the block: - * systemBlock string Indicate that this block is automatically - * created by MediaWiki rather than being stored - * in the database. Value is a string to return - * from self::getSystemBlockType(). + * @param array $options Parameters of the block, with options supported by + * `AbstractBlock::__construct`, and also: + * - systemBlock: (string) Indicate that this block is automatically created by + * MediaWiki rather than being stored in the database. Value is a string to + * return from self::getSystemBlockType(). */ public function __construct( array $options = [] ) { parent::__construct( $options ); diff --git a/includes/cache/BacklinkCache.php b/includes/cache/BacklinkCache.php index c2fb52afdc..269630276f 100644 --- a/includes/cache/BacklinkCache.php +++ b/includes/cache/BacklinkCache.php @@ -135,7 +135,7 @@ class BacklinkCache { $this->partitionCache = []; $this->fullResultCache = []; $this->wanCache->touchCheckKey( $this->makeCheckKey() ); - unset( $this->db ); + $this->db = null; } /** @@ -153,7 +153,7 @@ class BacklinkCache { * @return IDatabase */ protected function getDB() { - if ( !isset( $this->db ) ) { + if ( $this->db === null ) { $this->db = wfGetDB( DB_REPLICA ); } diff --git a/includes/cache/CacheHelper.php b/includes/cache/CacheHelper.php index d798ddbcb3..d1261a8076 100644 --- a/includes/cache/CacheHelper.php +++ b/includes/cache/CacheHelper.php @@ -82,9 +82,9 @@ class CacheHelper implements ICacheHelper { * Function that gets called when initialization is done. * * @since 1.20 - * @var callable + * @var callable|null */ - protected $onInitHandler = false; + protected $onInitHandler; /** * Elements to build a cache key with. @@ -183,7 +183,7 @@ class CacheHelper implements ICacheHelper { $this->hasCached = is_array( $cachedChunks ); $this->cachedChunks = $this->hasCached ? $cachedChunks : []; - if ( $this->onInitHandler !== false ) { + if ( $this->onInitHandler !== null ) { call_user_func( $this->onInitHandler, $this->hasCached ); } } diff --git a/includes/cache/FileCacheBase.php b/includes/cache/FileCacheBase.php index ce5a019b7c..fb4253996f 100644 --- a/includes/cache/FileCacheBase.php +++ b/includes/cache/FileCacheBase.php @@ -230,31 +230,26 @@ abstract class FileCacheBase { */ public function incrMissesRecent( WebRequest $request ) { if ( mt_rand( 0, self::MISS_FACTOR - 1 ) == 0 ) { - $cache = ObjectCache::getLocalClusterInstance(); # Get a large IP range that should include the user even if that # person's IP address changes $ip = $request->getIP(); if ( !IP::isValid( $ip ) ) { return; } + $ip = IP::isIPv6( $ip ) ? IP::sanitizeRange( "$ip/32" ) : IP::sanitizeRange( "$ip/16" ); # Bail out if a request already came from this range... + $cache = ObjectCache::getLocalClusterInstance(); $key = $cache->makeKey( static::class, 'attempt', $this->mType, $this->mKey, $ip ); - if ( $cache->get( $key ) ) { + if ( !$cache->add( $key, 1, self::MISS_TTL_SEC ) ) { return; // possibly the same user } - $cache->set( $key, 1, self::MISS_TTL_SEC ); # Increment the number of cache misses... - $key = $this->cacheMissKey( $cache ); - if ( $cache->get( $key ) === false ) { - $cache->set( $key, 1, self::MISS_TTL_SEC ); - } else { - $cache->incr( $key ); - } + $cache->incrWithInit( $this->cacheMissKey( $cache ), self::MISS_TTL_SEC ); } } diff --git a/includes/cache/HTMLFileCache.php b/includes/cache/HTMLFileCache.php index a0d61b259e..ab78ee469b 100644 --- a/includes/cache/HTMLFileCache.php +++ b/includes/cache/HTMLFileCache.php @@ -94,10 +94,6 @@ class HTMLFileCache extends FileCacheBase { $config = MediaWikiServices::getInstance()->getMainConfig(); if ( !$config->get( 'UseFileCache' ) && $mode !== self::MODE_REBUILD ) { - return false; - } elseif ( $config->get( 'DebugToolbar' ) ) { - wfDebug( "HTML file cache skipped. \$wgDebugToolbar on\n" ); - return false; } diff --git a/includes/cache/LinkCache.php b/includes/cache/LinkCache.php index 80181179e4..b2f24525a5 100644 --- a/includes/cache/LinkCache.php +++ b/includes/cache/LinkCache.php @@ -89,6 +89,7 @@ class LinkCache { * * @param bool|null $update * @return bool + * @deprecated Since 1.34 */ public function forUpdate( $update = null ) { return wfSetVar( $this->mForUpdate, $update ); diff --git a/includes/cache/MessageCache.php b/includes/cache/MessageCache.php index 57454516f3..848d9c907a 100644 --- a/includes/cache/MessageCache.php +++ b/includes/cache/MessageCache.php @@ -45,6 +45,12 @@ class MessageCache { /** How long memcached locks last */ const LOCK_TTL = 30; + /** + * Lifetime for cache, for keys stored in $wanCache, in seconds. + * @var int + */ + const WAN_TTL = IExpiringStore::TTL_DAY; + /** * Process cache of loaded messages that are defined in MediaWiki namespace * @@ -70,12 +76,6 @@ class MessageCache { */ protected $mDisable; - /** - * Lifetime for cache, used by object caching. - * Set on construction, see __construct(). - */ - protected $mExpiry; - /** * Message cache has its own parser which it uses to transform messages * @var ParserOptions @@ -105,44 +105,14 @@ class MessageCache { private $loadedLanguages = []; /** - * Singleton instance - * - * @var MessageCache $instance - */ - private static $instance; - - /** - * Get the signleton instance of this class + * Get the singleton instance of this class * + * @deprecated in 1.34 inject an instance of this class instead of using global state * @since 1.18 * @return MessageCache */ public static function singleton() { - if ( self::$instance === null ) { - global $wgUseDatabaseMessages, $wgMsgCacheExpiry, $wgUseLocalMessageCache; - $services = MediaWikiServices::getInstance(); - self::$instance = new self( - $services->getMainWANObjectCache(), - wfGetMessageCacheStorage(), - $wgUseLocalMessageCache - ? $services->getLocalServerObjectCache() - : new EmptyBagOStuff(), - $wgUseDatabaseMessages, - $wgMsgCacheExpiry, - $services->getContentLanguage() - ); - } - - return self::$instance; - } - - /** - * Destroy the singleton instance - * - * @since 1.18 - */ - public static function destroyInstance() { - self::$instance = null; + return MediaWikiServices::getInstance()->getMessageCache(); } /** @@ -167,7 +137,6 @@ class MessageCache { * @param BagOStuff $clusterCache * @param BagOStuff $serverCache * @param bool $useDB Whether to look for message overrides (e.g. MediaWiki: pages) - * @param int $expiry Lifetime for cache. @see $mExpiry. * @param Language|null $contLang Content language of site */ public function __construct( @@ -175,7 +144,6 @@ class MessageCache { BagOStuff $clusterCache, BagOStuff $serverCache, $useDB, - $expiry, Language $contLang = null ) { $this->wanCache = $wanCache; @@ -185,7 +153,6 @@ class MessageCache { $this->cache = new MapCacheLRU( 5 ); // limit size for sanity $this->mDisable = !$useDB; - $this->mExpiry = $expiry; $this->contLang = $contLang ?? MediaWikiServices::getInstance()->getContentLanguage(); } @@ -198,8 +165,8 @@ class MessageCache { global $wgUser; if ( !$this->mParserOptions ) { - if ( !$wgUser->isSafeToLoad() ) { - // $wgUser isn't unstubbable yet, so don't try to get a + if ( !$wgUser || !$wgUser->isSafeToLoad() ) { + // $wgUser isn't available yet, so don't try to get a // ParserOptions for it. And don't cache this ParserOptions // either. $po = ParserOptions::newFromAnon(); @@ -534,6 +501,21 @@ class MessageCache { // Set the text for small software-defined messages in the main cache map $revisionStore = MediaWikiServices::getInstance()->getRevisionStore(); $revQuery = $revisionStore->getQueryInfo( [ 'page', 'user' ] ); + + // T231196: MySQL/MariaDB (10.1.37) can sometimes irrationally decide that querying `actor` then + // `revision` then `page` is somehow better than starting with `page`. Tell it not to reorder the + // query (and also reorder it ourselves because as generated by RevisionStore it'll have + // `revision` first rather than `page`). + $revQuery['joins']['revision'] = $revQuery['joins']['page']; + unset( $revQuery['joins']['page'] ); + // It isn't actually necesssary to reorder $revQuery['tables'] as Database does the right thing + // when join conditions are given for all joins, but Gergő is wary of relying on that so pull + // `page` to the start. + $revQuery['tables'] = array_merge( + [ 'page' ], + array_diff( $revQuery['tables'], [ 'page' ] ) + ); + $res = $dbr->select( $revQuery['tables'], $revQuery['fields'], @@ -542,7 +524,7 @@ class MessageCache { 'page_latest = rev_id' // get the latest revision only ] ), __METHOD__ . "($code)-small", - [], + [ 'STRAIGHT_JOIN' ], $revQuery['joins'] ); foreach ( $res as $row ) { @@ -581,7 +563,7 @@ class MessageCache { # messages larger than $wgMaxMsgCacheEntrySize, since those are only # stored and fetched from memcache. $cache['HASH'] = md5( serialize( $cache ) ); - $cache['EXPIRY'] = wfTimestamp( TS_MW, time() + $this->mExpiry ); + $cache['EXPIRY'] = wfTimestamp( TS_MW, time() + self::WAN_TTL ); unset( $cache['EXCESSIVE'] ); // only needed for hash return $cache; @@ -696,7 +678,7 @@ class MessageCache { $this->wanCache->set( $this->bigMessageCacheKey( $cache['HASH'], $title ), ' ' . $newTextByTitle[$title], - $this->mExpiry + self::WAN_TTL ); } // Mark this cache as definitely being "latest" (non-volatile) so @@ -1120,11 +1102,11 @@ class MessageCache { $fname = __METHOD__; return $this->srvCache->getWithSetCallback( $this->srvCache->makeKey( 'messages-big', $hash, $dbKey ), - IExpiringStore::TTL_MINUTE, + BagOStuff::TTL_HOUR, function () use ( $code, $dbKey, $hash, $fname ) { return $this->wanCache->getWithSetCallback( $this->bigMessageCacheKey( $hash, $dbKey ), - $this->mExpiry, + self::WAN_TTL, function ( $oldValue, &$ttl, &$setOpts ) use ( $dbKey, $code, $fname ) { // Try loading the message from the database $dbr = wfGetDB( DB_REPLICA ); @@ -1209,6 +1191,7 @@ class MessageCache { $class = $wgParserConf['class']; if ( $class == ParserDiffTest::class ) { # Uncloneable + // @phan-suppress-next-line PhanTypeMismatchProperty $this->mParser = new $class( $wgParserConf ); } else { $this->mParser = clone $parser; diff --git a/includes/cache/UserCache.php b/includes/cache/UserCache.php index 8f816d9013..bc0bbfa1ce 100644 --- a/includes/cache/UserCache.php +++ b/includes/cache/UserCache.php @@ -78,8 +78,6 @@ class UserCache { * @param string $caller The calling method */ public function doQuery( array $userIds, $options = [], $caller = '' ) { - global $wgActorTableSchemaMigrationStage; - $usersToCheck = []; $usersToQuery = []; @@ -100,21 +98,12 @@ class UserCache { // Lookup basic info for users not yet loaded... if ( count( $usersToQuery ) ) { $dbr = wfGetDB( DB_REPLICA ); - $tables = [ 'user' ]; + $tables = [ 'user', 'actor' ]; $conds = [ 'user_id' => $usersToQuery ]; - $fields = [ 'user_name', 'user_real_name', 'user_registration', 'user_id' ]; - $joinConds = []; - - // Technically we shouldn't allow this without SCHEMA_COMPAT_READ_NEW, - // but it does little harm and might be needed for write callers loading a User. - if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_NEW ) { - $tables[] = 'actor'; - $fields[] = 'actor_id'; - $joinConds['actor'] = [ - ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_READ_NEW ) ? 'JOIN' : 'LEFT JOIN', - [ 'actor_user = user_id' ] - ]; - } + $fields = [ 'user_name', 'user_real_name', 'user_registration', 'user_id', 'actor_id' ]; + $joinConds = [ + 'actor' => [ 'JOIN', 'actor_user = user_id' ], + ]; $comment = __METHOD__; if ( strval( $caller ) !== '' ) { @@ -127,9 +116,7 @@ class UserCache { $this->cache[$userId]['name'] = $row->user_name; $this->cache[$userId]['real_name'] = $row->user_real_name; $this->cache[$userId]['registration'] = $row->user_registration; - if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_NEW ) { - $this->cache[$userId]['actor'] = $row->actor_id; - } + $this->cache[$userId]['actor'] = $row->actor_id; $usersToCheck[$userId] = $row->user_name; } } diff --git a/includes/cache/localisation/LCStoreCDB.php b/includes/cache/localisation/LCStoreCDB.php index aad9439ad6..fd9af39515 100644 --- a/includes/cache/localisation/LCStoreCDB.php +++ b/includes/cache/localisation/LCStoreCDB.php @@ -33,7 +33,7 @@ use Cdb\Writer; */ class LCStoreCDB implements LCStore { - /** @var Reader[] */ + /** @var Reader[]|false[] */ private $readers; /** @var Writer */ diff --git a/includes/cache/localisation/LCStoreDB.php b/includes/cache/localisation/LCStoreDB.php index 88a70421fa..a79325aa61 100644 --- a/includes/cache/localisation/LCStoreDB.php +++ b/includes/cache/localisation/LCStoreDB.php @@ -27,18 +27,20 @@ use Wikimedia\Rdbms\DBQueryError; * This will work on any MediaWiki installation. */ class LCStoreDB implements LCStore { - /** @var string */ - private $currentLang; - /** @var bool */ - private $writesDone = false; + /** @var string Language code */ + private $code; + /** @var array Server configuration map */ + private $server; + + /** @var array Rows buffered for insertion */ + private $batch = []; + /** @var IDatabase|null */ private $dbw; - /** @var array */ - private $batch = []; - /** @var bool */ + /** @var bool Whether a batch of writes were recently written */ + private $writesDone = false; + /** @var bool Whether the DB is read-only or otherwise unavailable for writes */ private $readOnly = false; - /** @var array Server configuration map */ - private $server; public function __construct( $params ) { $this->server = $params['server'] ?? []; @@ -74,14 +76,14 @@ class LCStoreDB implements LCStore { $dbw = $this->getWriteConnection(); $this->readOnly = $dbw->isReadOnly(); - $this->currentLang = $code; + $this->code = $code; $this->batch = []; } public function finishWrite() { if ( $this->readOnly ) { return; - } elseif ( is_null( $this->currentLang ) ) { + } elseif ( is_null( $this->code ) ) { throw new MWException( __CLASS__ . ': must call startWrite() before finishWrite()' ); } @@ -91,7 +93,7 @@ class LCStoreDB implements LCStore { $dbw = $this->getWriteConnection(); $dbw->startAtomic( __METHOD__ ); try { - $dbw->delete( 'l10n_cache', [ 'lc_lang' => $this->currentLang ], __METHOD__ ); + $dbw->delete( 'l10n_cache', [ 'lc_lang' => $this->code ], __METHOD__ ); foreach ( array_chunk( $this->batch, 500 ) as $rows ) { $dbw->insert( 'l10n_cache', $rows, __METHOD__ ); } @@ -108,21 +110,21 @@ class LCStoreDB implements LCStore { $trxProfiler->setSilenced( $oldSilenced ); } - $this->currentLang = null; + $this->code = null; $this->batch = []; } public function set( $key, $value ) { if ( $this->readOnly ) { return; - } elseif ( is_null( $this->currentLang ) ) { + } elseif ( is_null( $this->code ) ) { throw new MWException( __CLASS__ . ': must call startWrite() before set()' ); } $dbw = $this->getWriteConnection(); $this->batch[] = [ - 'lc_lang' => $this->currentLang, + 'lc_lang' => $this->code, 'lc_key' => $key, 'lc_value' => $dbw->encodeBlob( serialize( $value ) ) ]; diff --git a/includes/cache/localisation/LCStoreStaticArray.php b/includes/cache/localisation/LCStoreStaticArray.php index 59116561c0..53893bdbe4 100644 --- a/includes/cache/localisation/LCStoreStaticArray.php +++ b/includes/cache/localisation/LCStoreStaticArray.php @@ -121,6 +121,8 @@ class LCStoreStaticArray implements LCStore { 'Generated by LCStoreStaticArray.php -- do not edit!' ); file_put_contents( $this->fname, $out ); + // Release the data to manage the memory in rebuildLocalisationCache + unset( $this->data[$this->currentLang] ); $this->currentLang = null; $this->fname = null; } diff --git a/includes/cache/localisation/LocalisationCache.php b/includes/cache/localisation/LocalisationCache.php index ffc7cd00d6..c6d6b8fa5b 100644 --- a/includes/cache/localisation/LocalisationCache.php +++ b/includes/cache/localisation/LocalisationCache.php @@ -519,17 +519,8 @@ class LocalisationCache { * @return array */ protected function readPHPFile( $_fileName, $_fileType ) { - // Disable APC caching - Wikimedia\suppressWarnings(); - $_apcEnabled = ini_set( 'apc.cache_by_default', '0' ); - Wikimedia\restoreWarnings(); - include $_fileName; - Wikimedia\suppressWarnings(); - ini_set( 'apc.cache_by_default', $_apcEnabled ); - Wikimedia\restoreWarnings(); - $data = []; if ( $_fileType == 'core' || $_fileType == 'extension' ) { foreach ( self::$allKeys as $key ) { @@ -731,6 +722,7 @@ class LocalisationCache { if ( in_array( $key, self::$mergeableMapKeys ) ) { $value = $value + $fallbackValue; } elseif ( in_array( $key, self::$mergeableListKeys ) ) { + // @phan-suppress-next-line PhanTypeMismatchArgumentInternal $value = array_unique( array_merge( $fallbackValue, $value ) ); } elseif ( in_array( $key, self::$mergeableAliasListKeys ) ) { $value = array_merge_recursive( $value, $fallbackValue ); @@ -826,7 +818,7 @@ class LocalisationCache { if ( !$code ) { throw new MWException( "Invalid language code requested" ); } - $this->recachedLangs[$code] = true; + $this->recachedLangs[ $code ] = true; # Initial values $initialData = array_fill_keys( self::$allKeys, null ); @@ -835,16 +827,11 @@ class LocalisationCache { # Load the primary localisation from the source file $data = $this->readSourceFilesAndRegisterDeps( $code, $deps ); - if ( $data === false ) { - $this->logger->debug( __METHOD__ . ": no localisation file for $code, using fallback to en" ); - $coreData['fallback'] = 'en'; - } else { - $this->logger->debug( __METHOD__ . ": got localisation for $code from source" ); + $this->logger->debug( __METHOD__ . ": got localisation for $code from source" ); - # Merge primary localisation - foreach ( $data as $key => $value ) { - $this->mergeItem( $key, $coreData[$key], $value ); - } + # Merge primary localisation + foreach ( $data as $key => $value ) { + $this->mergeItem( $key, $coreData[ $key ], $value ); } # Fill in the fallback if it's not there already @@ -932,16 +919,14 @@ class LocalisationCache { # Load the secondary localisation from the source file to # avoid infinite cycles on cyclic fallbacks $fbData = $this->readSourceFilesAndRegisterDeps( $csCode, $deps ); - if ( $fbData !== false ) { - # Only merge the keys that make sense to merge - foreach ( self::$allKeys as $key ) { - if ( !isset( $fbData[$key] ) ) { - continue; - } - - if ( is_null( $coreData[$key] ) || $this->isMergeableKey( $key ) ) { - $this->mergeItem( $key, $csData[$key], $fbData[$key] ); - } + # Only merge the keys that make sense to merge + foreach ( self::$allKeys as $key ) { + if ( !isset( $fbData[ $key ] ) ) { + continue; + } + + if ( is_null( $coreData[ $key ] ) || $this->isMergeableKey( $key ) ) { + $this->mergeItem( $key, $csData[ $key ], $fbData[ $key ] ); } } } diff --git a/includes/changes/CategoryMembershipChange.php b/includes/changes/CategoryMembershipChange.php index 2ef9c9f95f..5a7f45ea25 100644 --- a/includes/changes/CategoryMembershipChange.php +++ b/includes/changes/CategoryMembershipChange.php @@ -1,6 +1,7 @@ pageTitle->getPreviousRevisionID( $this->pageTitle->getLatestRevID() ) - ); - - return $previousRev ? $previousRev->getTimestamp() : null; + $rl = MediaWikiServices::getInstance()->getRevisionLookup(); + $latestRev = $rl->getRevisionByTitle( $this->pageTitle ); + if ( $latestRev ) { + $previousRev = $rl->getPreviousRevision( $latestRev ); + if ( $previousRev ) { + return $previousRev->getTimestamp(); + } + } + return null; } } diff --git a/includes/changes/ChangesFeed.php b/includes/changes/ChangesFeed.php index 79092ee37c..e60cc0937b 100644 --- a/includes/changes/ChangesFeed.php +++ b/includes/changes/ChangesFeed.php @@ -21,7 +21,7 @@ */ use MediaWiki\MediaWikiServices; -use MediaWiki\Storage\RevisionRecord; +use MediaWiki\Revision\RevisionRecord; /** * Feed to Special:RecentChanges and Special:RecentChangesLinked. diff --git a/includes/changes/ChangesList.php b/includes/changes/ChangesList.php index e2b35a8632..34d73d6505 100644 --- a/includes/changes/ChangesList.php +++ b/includes/changes/ChangesList.php @@ -23,7 +23,7 @@ */ use MediaWiki\Linker\LinkRenderer; use MediaWiki\MediaWikiServices; -use MediaWiki\Storage\RevisionRecord; +use MediaWiki\Revision\RevisionRecord; use Wikimedia\Rdbms\IResultWrapper; class ChangesList extends ContextSource { @@ -161,6 +161,7 @@ class ChangesList extends ContextSource { */ private function preCacheMessages() { if ( !isset( $this->message ) ) { + $this->message = []; foreach ( [ 'cur', 'diff', 'hist', 'enhancedrc-history', 'last', 'blocklink', 'history', 'semicolon-separator', 'pipe-separator' ] as $msg @@ -232,6 +233,13 @@ class ChangesList extends ContextSource { $classes[] = Sanitizer::escapeClass( self::CSS_CLASS_PREFIX . 'ns-' . $rc->mAttribs['rc_namespace'] ); + $nsInfo = MediaWikiServices::getInstance()->getNamespaceInfo(); + $classes[] = Sanitizer::escapeClass( + self::CSS_CLASS_PREFIX . + 'ns-' . + ( $nsInfo->isTalk( $rc->mAttribs['rc_namespace'] ) ? 'talk' : 'subject' ) + ); + if ( $this->filterGroups !== null ) { foreach ( $this->filterGroups as $filterGroup ) { foreach ( $filterGroup->getFilters() as $filter ) { @@ -447,13 +455,21 @@ class ChangesList extends ContextSource { * @param string &$s HTML to update * @param Title $title * @param string $logtype + * @param bool $useParentheses (optional) Wrap log entry in parentheses where needed */ - public function insertLog( &$s, $title, $logtype ) { + public function insertLog( &$s, $title, $logtype, $useParentheses = true ) { $page = new LogPage( $logtype ); $logname = $page->getName()->setContext( $this->getContext() )->text(); - $s .= Html::rawElement( 'span', [ - 'class' => 'mw-changeslist-links' - ], $this->linkRenderer->makeKnownLink( $title, $logname ) ); + $link = $this->linkRenderer->makeKnownLink( $title, $logname, [ + 'class' => $useParentheses ? '' : 'mw-changeslist-links' + ] ); + if ( $useParentheses ) { + $s .= $this->msg( 'parentheses' )->rawParams( + $link + )->escaped(); + } else { + $s .= $link; + } } /** @@ -617,7 +633,7 @@ class ChangesList extends ContextSource { */ public function insertComment( $rc ) { if ( $this->isDeleted( $rc, RevisionRecord::DELETED_COMMENT ) ) { - return ' ' . + return ' ' . $this->msg( 'rev-deleted-comment' )->escaped() . ''; } else { return Linker::commentBlock( $rc->mAttribs['rc_comment'], $rc->getTitle(), @@ -709,7 +725,9 @@ class ChangesList extends ContextSource { /** Check for rollback permissions, disallow special pages, and only * show a link on the top-most revision */ - if ( $title->quickUserCan( 'rollback', $this->getUser() ) ) { + if ( MediaWikiServices::getInstance()->getPermissionManager() + ->quickUserCan( 'rollback', $this->getUser(), $title ) + ) { $rev = new Revision( [ 'title' => $title, 'id' => $rc->mAttribs['rc_this_oldid'], diff --git a/includes/changes/ChangesListBooleanFilterGroup.php b/includes/changes/ChangesListBooleanFilterGroup.php index 4401378b28..59f59d1541 100644 --- a/includes/changes/ChangesListBooleanFilterGroup.php +++ b/includes/changes/ChangesListBooleanFilterGroup.php @@ -8,6 +8,7 @@ use Wikimedia\Rdbms\IDatabase; * but 'Bot' is unchecked, hidebots=1 will be sent. * * @since 1.29 + * @method ChangesListBooleanFilter[] getFilters() */ class ChangesListBooleanFilterGroup extends ChangesListFilterGroup { /** @@ -55,6 +56,7 @@ class ChangesListBooleanFilterGroup extends ChangesListFilterGroup { * Registers a filter in this group * * @param ChangesListBooleanFilter $filter + * @suppress PhanParamSignaturePHPDocMismatchHasParamType,PhanParamSignatureMismatch */ public function registerFilter( ChangesListBooleanFilter $filter ) { $this->filters[$filter->getName()] = $filter; diff --git a/includes/changes/ChangesListFilterGroup.php b/includes/changes/ChangesListFilterGroup.php index ec863073d5..5f0cd221ff 100644 --- a/includes/changes/ChangesListFilterGroup.php +++ b/includes/changes/ChangesListFilterGroup.php @@ -32,6 +32,7 @@ use Wikimedia\Rdbms\IDatabase; * Represents a filter group (used on ChangesListSpecialPage and descendants) * * @since 1.29 + * @method registerFilter($filter) */ abstract class ChangesListFilterGroup { /** diff --git a/includes/changes/ChangesListStringOptionsFilterGroup.php b/includes/changes/ChangesListStringOptionsFilterGroup.php index e06f0817ba..b18ae61ec1 100644 --- a/includes/changes/ChangesListStringOptionsFilterGroup.php +++ b/includes/changes/ChangesListStringOptionsFilterGroup.php @@ -155,6 +155,7 @@ class ChangesListStringOptionsFilterGroup extends ChangesListFilterGroup { * Registers a filter in this group * * @param ChangesListStringOptionsFilter $filter + * @suppress PhanParamSignaturePHPDocMismatchHasParamType,PhanParamSignatureMismatch */ public function registerFilter( ChangesListStringOptionsFilter $filter ) { $this->filters[$filter->getName()] = $filter; diff --git a/includes/changes/EnhancedChangesList.php b/includes/changes/EnhancedChangesList.php index 62cf39ee45..d2c4dd48fa 100644 --- a/includes/changes/EnhancedChangesList.php +++ b/includes/changes/EnhancedChangesList.php @@ -1,6 +1,6 @@ unpatrolled, $block[0]->watched ); } - $queryParams['curid'] = $curId; + $queryParams = [ 'curid' => $curId ]; # Sub-entries $lines = []; @@ -632,7 +632,7 @@ class EnhancedChangesList extends ChangesList { protected function recentChangesBlockLine( $rcObj ) { $data = []; - $query['curid'] = $rcObj->mAttribs['rc_cur_id']; + $query = [ 'curid' => $rcObj->mAttribs['rc_cur_id'] ]; $type = $rcObj->mAttribs['rc_type']; $logType = $rcObj->mAttribs['rc_log_type']; diff --git a/includes/changes/OldChangesList.php b/includes/changes/OldChangesList.php index c15701ba66..8228baea5b 100644 --- a/includes/changes/OldChangesList.php +++ b/includes/changes/OldChangesList.php @@ -87,7 +87,7 @@ class OldChangesList extends ChangesList { if ( $rc->mAttribs['rc_log_type'] ) { $logtitle = SpecialPage::getTitleFor( 'Log', $rc->mAttribs['rc_log_type'] ); - $this->insertLog( $html, $logtitle, $rc->mAttribs['rc_log_type'] ); + $this->insertLog( $html, $logtitle, $rc->mAttribs['rc_log_type'], false ); $flags = $this->recentChangesFlags( [ 'unpatrolled' => $unpatrolled, 'bot' => $rc->mAttribs['rc_bot'] ], '' ); if ( $flags !== '' ) { @@ -98,7 +98,7 @@ class OldChangesList extends ChangesList { list( $name, $htmlubpage ) = MediaWikiServices::getInstance()->getSpecialPageFactory()-> resolveAlias( $rc->mAttribs['rc_title'] ); if ( $name == 'Log' ) { - $this->insertLog( $html, $rc->getTitle(), $htmlubpage ); + $this->insertLog( $html, $rc->getTitle(), $htmlubpage, false ); } // Regular entries } else { diff --git a/includes/changes/RCCacheEntryFactory.php b/includes/changes/RCCacheEntryFactory.php index d448eae4a1..83720d3453 100644 --- a/includes/changes/RCCacheEntryFactory.php +++ b/includes/changes/RCCacheEntryFactory.php @@ -20,7 +20,7 @@ * @file */ use MediaWiki\Linker\LinkRenderer; -use MediaWiki\Storage\RevisionRecord; +use MediaWiki\Revision\RevisionRecord; class RCCacheEntryFactory { @@ -56,7 +56,6 @@ class RCCacheEntryFactory { */ public function newFromRecentChange( RecentChange $baseRC, $watched ) { $user = $this->context->getUser(); - $counter = $baseRC->counter; $cacheEntry = RCCacheEntry::newFromParent( $baseRC ); @@ -73,8 +72,8 @@ class RCCacheEntryFactory { // called too many times (50% of CPU time on RecentChanges!). $showDiffLinks = $this->showDiffLinks( $cacheEntry, $user ); - $cacheEntry->difflink = $this->buildDiffLink( $cacheEntry, $showDiffLinks, $counter ); - $cacheEntry->curlink = $this->buildCurLink( $cacheEntry, $showDiffLinks, $counter ); + $cacheEntry->difflink = $this->buildDiffLink( $cacheEntry, $showDiffLinks ); + $cacheEntry->curlink = $this->buildCurLink( $cacheEntry, $showDiffLinks ); $cacheEntry->lastlink = $this->buildLastLink( $cacheEntry, $showDiffLinks ); // Make user links @@ -109,11 +108,11 @@ class RCCacheEntryFactory { } /** - * @param RecentChange $cacheEntry + * @param RCCacheEntry $cacheEntry * * @return string */ - private function buildCLink( RecentChange $cacheEntry ) { + private function buildCLink( RCCacheEntry $cacheEntry ) { $type = $cacheEntry->mAttribs['rc_type']; // New unpatrolled pages @@ -182,11 +181,10 @@ class RCCacheEntryFactory { /** * @param RecentChange $cacheEntry * @param bool $showDiffLinks - * @param int $counter * * @return string */ - private function buildCurLink( RecentChange $cacheEntry, $showDiffLinks, $counter ) { + private function buildCurLink( RecentChange $cacheEntry, $showDiffLinks ) { $queryParams = $this->buildCurQueryParams( $cacheEntry ); $curMessage = $this->getMessage( 'cur' ); $logTypes = [ RC_LOG ]; @@ -217,11 +215,10 @@ class RCCacheEntryFactory { /** * @param RecentChange $cacheEntry * @param bool $showDiffLinks - * @param int $counter * * @return string */ - private function buildDiffLink( RecentChange $cacheEntry, $showDiffLinks, $counter ) { + private function buildDiffLink( RecentChange $cacheEntry, $showDiffLinks ) { $queryParams = $this->buildDiffQueryParams( $cacheEntry ); $diffMessage = $this->getMessage( 'diff' ); $logTypes = [ RC_NEW, RC_LOG ]; diff --git a/includes/changes/RecentChange.php b/includes/changes/RecentChange.php index 95c9fa6c63..e18482505c 100644 --- a/includes/changes/RecentChange.php +++ b/includes/changes/RecentChange.php @@ -20,6 +20,7 @@ * @file */ use MediaWiki\ChangeTags\Taggable; +use MediaWiki\MediaWikiServices; /** * Utility class for creating new RC entries @@ -89,16 +90,17 @@ class RecentChange implements Taggable { */ const SEND_FEED = false; + /** @var array */ public $mAttribs = []; public $mExtra = []; /** - * @var Title + * @var Title|false */ public $mTitle = false; /** - * @var User + * @var User|false */ private $mPerformer = false; @@ -219,55 +221,6 @@ class RecentChange implements Taggable { } } - /** - * Return the list of recentchanges fields that should be selected to create - * a new recentchanges object. - * @deprecated since 1.31, use self::getQueryInfo() instead. - * @return array - */ - public static function selectFields() { - global $wgActorTableSchemaMigrationStage; - - wfDeprecated( __METHOD__, '1.31' ); - if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_READ_NEW ) { - // If code is using this instead of self::getQueryInfo(), there's a - // decent chance it's going to try to directly access - // $row->rc_user or $row->rc_user_text and we can't give it - // useful values here once those aren't being used anymore. - throw new BadMethodCallException( - 'Cannot use ' . __METHOD__ - . ' when $wgActorTableSchemaMigrationStage has SCHEMA_COMPAT_READ_NEW' - ); - } - - return [ - 'rc_id', - 'rc_timestamp', - 'rc_user', - 'rc_user_text', - 'rc_actor' => 'NULL', - 'rc_namespace', - 'rc_title', - 'rc_minor', - 'rc_bot', - 'rc_new', - 'rc_cur_id', - 'rc_this_oldid', - 'rc_last_oldid', - 'rc_type', - 'rc_source', - 'rc_patrolled', - 'rc_ip', - 'rc_old_len', - 'rc_new_len', - 'rc_deleted', - 'rc_logid', - 'rc_log_type', - 'rc_log_action', - 'rc_params', - ] + CommentStore::getStore()->getFields( 'rc_comment' ); - } - /** * Return the tables, fields, and join conditions to be selected to create * a new recentchanges object. @@ -390,7 +343,7 @@ class RecentChange implements Taggable { } # If our database is strict about IP addresses, use NULL instead of an empty string - $strictIPs = in_array( $dbw->getType(), [ 'oracle', 'postgres' ] ); // legacy + $strictIPs = $dbw->getType() === 'postgres'; // legacy if ( $strictIPs && $this->mAttribs['rc_ip'] == '' ) { unset( $this->mAttribs['rc_ip'] ); } @@ -608,8 +561,9 @@ class RecentChange implements Taggable { } // Users without the 'autopatrol' right can't patrol their // own revisions - if ( $user->getName() === $this->getAttribute( 'rc_user_text' ) - && !$user->isAllowed( 'autopatrol' ) + if ( $user->getName() === $this->getAttribute( 'rc_user_text' ) && + !MediaWikiServices::getInstance()->getPermissionManager() + ->userHasRight( $user, 'autopatrol' ) ) { $errors[] = [ 'markedaspatrollederror-noautopatrol' ]; } @@ -857,6 +811,7 @@ class RecentChange implements Taggable { $type, $action, $target, $logComment, $params, $newId = 0, $actionCommentIRC = '', $revId = 0, $isPatrollable = false ) { global $wgRequest; + $permissionManager = MediaWikiServices::getInstance()->getPermissionManager(); # # Get pageStatus for email notification switch ( $type . '-' . $action ) { @@ -881,7 +836,8 @@ class RecentChange implements Taggable { } // Allow unpatrolled status for patrollable log entries - $markPatrolled = $isPatrollable ? $user->isAllowed( 'autopatrol' ) : true; + $canAutopatrol = $permissionManager->userHasRight( $user, 'autopatrol' ); + $markPatrolled = $isPatrollable ? $canAutopatrol : true; $rc = new RecentChange; $rc->mTitle = $target; @@ -902,7 +858,8 @@ class RecentChange implements Taggable { 'rc_comment_data' => null, 'rc_this_oldid' => $revId, 'rc_last_oldid' => 0, - 'rc_bot' => $user->isAllowed( 'bot' ) ? (int)$wgRequest->getBool( 'bot', true ) : 0, + 'rc_bot' => $permissionManager->userHasRight( $user, 'bot' ) ? + (int)$wgRequest->getBool( 'bot', true ) : 0, 'rc_ip' => self::checkIPAddress( $ip ), 'rc_patrolled' => $markPatrolled ? self::PRC_AUTOPATROLLED : self::PRC_UNPATROLLED, 'rc_new' => 0, # obsolete diff --git a/includes/changetags/ChangeTags.php b/includes/changetags/ChangeTags.php index 8c8125b0fb..ba6cb2ca6b 100644 --- a/includes/changetags/ChangeTags.php +++ b/includes/changetags/ChangeTags.php @@ -126,7 +126,7 @@ class ChangeTags { $markers = $context->msg( 'tag-list-wrapper' ) ->numParams( count( $displayTags ) ) - ->rawParams( $context->getLanguage()->commaList( $displayTags ) ) + ->rawParams( implode( ' ', $displayTags ) ) ->parse(); $markers = Xml::tags( 'span', [ 'class' => 'mw-tag-markers' ], $markers ); @@ -520,11 +520,11 @@ class ChangeTags { */ public static function canAddTagsAccompanyingChange( array $tags, User $user = null ) { if ( !is_null( $user ) ) { - if ( !$user->isAllowed( 'applychangetags' ) ) { + if ( !MediaWikiServices::getInstance()->getPermissionManager() + ->userHasRight( $user, 'applychangetags' ) + ) { return Status::newFatal( 'tags-apply-no-permission' ); - } elseif ( $user->getBlock() ) { - // @TODO Ensure that the block does not apply to the `applychangetags` - // right. + } elseif ( $user->getBlock() && $user->getBlock()->isSitewide() ) { return Status::newFatal( 'tags-apply-blocked', $user->getName() ); } } @@ -595,11 +595,11 @@ class ChangeTags { User $user = null ) { if ( !is_null( $user ) ) { - if ( !$user->isAllowed( 'changetags' ) ) { + if ( !MediaWikiServices::getInstance()->getPermissionManager() + ->userHasRight( $user, 'changetags' ) + ) { return Status::newFatal( 'tags-update-no-permission' ); - } elseif ( $user->getBlock() ) { - // @TODO Ensure that the block does not apply to the `changetags` - // right. + } elseif ( $user->getBlock() && $user->getBlock()->isSitewide() ) { return Status::newFatal( 'tags-update-blocked', $user->getName() ); } } @@ -997,7 +997,7 @@ class ChangeTags { } $logEntry->setParameters( $params ); $logEntry->setRelations( [ 'Tag' => $tag ] ); - $logEntry->setTags( $logEntryTags ); + $logEntry->addTags( $logEntryTags ); $logId = $logEntry->insert( $dbw ); $logEntry->publish( $logId ); @@ -1015,11 +1015,11 @@ class ChangeTags { */ public static function canActivateTag( $tag, User $user = null ) { if ( !is_null( $user ) ) { - if ( !$user->isAllowed( 'managechangetags' ) ) { + if ( !MediaWikiServices::getInstance()->getPermissionManager() + ->userHasRight( $user, 'managechangetags' ) + ) { return Status::newFatal( 'tags-manage-no-permission' ); - } elseif ( $user->getBlock() ) { - // @TODO Ensure that the block does not apply to the `managechangetags` - // right. + } elseif ( $user->getBlock() && $user->getBlock()->isSitewide() ) { return Status::newFatal( 'tags-manage-blocked', $user->getName() ); } } @@ -1089,11 +1089,11 @@ class ChangeTags { */ public static function canDeactivateTag( $tag, User $user = null ) { if ( !is_null( $user ) ) { - if ( !$user->isAllowed( 'managechangetags' ) ) { + if ( !MediaWikiServices::getInstance()->getPermissionManager() + ->userHasRight( $user, 'managechangetags' ) + ) { return Status::newFatal( 'tags-manage-no-permission' ); - } elseif ( $user->getBlock() ) { - // @TODO Ensure that the block does not apply to the `managechangetags` - // right. + } elseif ( $user->getBlock() && $user->getBlock()->isSitewide() ) { return Status::newFatal( 'tags-manage-blocked', $user->getName() ); } } @@ -1188,11 +1188,11 @@ class ChangeTags { */ public static function canCreateTag( $tag, User $user = null ) { if ( !is_null( $user ) ) { - if ( !$user->isAllowed( 'managechangetags' ) ) { + if ( !MediaWikiServices::getInstance()->getPermissionManager() + ->userHasRight( $user, 'managechangetags' ) + ) { return Status::newFatal( 'tags-manage-no-permission' ); - } elseif ( $user->getBlock() ) { - // @TODO Ensure that the block does not apply to the `managechangetags` - // right. + } elseif ( $user->getBlock() && $user->getBlock()->isSitewide() ) { return Status::newFatal( 'tags-manage-blocked', $user->getName() ); } } @@ -1308,11 +1308,11 @@ class ChangeTags { $tagUsage = self::tagUsageStatistics(); if ( !is_null( $user ) ) { - if ( !$user->isAllowed( 'deletechangetags' ) ) { + if ( !MediaWikiServices::getInstance()->getPermissionManager() + ->userHasRight( $user, 'deletechangetags' ) + ) { return Status::newFatal( 'tags-delete-no-permission' ); - } elseif ( $user->getBlock() ) { - // @TODO Ensure that the block does not apply to the `deletechangetags` - // right. + } elseif ( $user->getBlock() && $user->getBlock()->isSitewide() ) { return Status::newFatal( 'tags-manage-blocked', $user->getName() ); } } @@ -1566,6 +1566,8 @@ class ChangeTags { * @return bool */ public static function showTagEditingUI( User $user ) { - return $user->isAllowed( 'changetags' ) && (bool)self::listExplicitlyDefinedTags(); + return MediaWikiServices::getInstance()->getPermissionManager() + ->userHasRight( $user, 'changetags' ) && + (bool)self::listExplicitlyDefinedTags(); } } diff --git a/includes/changetags/ChangeTagsList.php b/includes/changetags/ChangeTagsList.php index 89f8f76350..fc53d13ed3 100644 --- a/includes/changetags/ChangeTagsList.php +++ b/includes/changetags/ChangeTagsList.php @@ -21,6 +21,12 @@ /** * Generic list for change tagging. + * + * @property ChangeTagsLogItem $current + * @method ChangeTagsLogItem next() + * @method ChangeTagsLogItem reset() + * @method ChangeTagsLogItem current() + * @phan-file-suppress PhanParamSignatureMismatch */ abstract class ChangeTagsList extends RevisionListBase { function __construct( IContextSource $context, Title $title, array $ids ) { diff --git a/includes/changetags/ChangeTagsLogItem.php b/includes/changetags/ChangeTagsLogItem.php index 1827aab036..ce82b71fef 100644 --- a/includes/changetags/ChangeTagsLogItem.php +++ b/includes/changetags/ChangeTagsLogItem.php @@ -20,7 +20,7 @@ */ use MediaWiki\MediaWikiServices; -use MediaWiki\Storage\RevisionRecord; +use MediaWiki\Revision\RevisionRecord; /** * Item class for a logging table row with its associated change tags. diff --git a/includes/clientpool/SquidPurgeClient.php b/includes/clientpool/SquidPurgeClient.php index 6b5482cf8d..dffe6e14fa 100644 --- a/includes/clientpool/SquidPurgeClient.php +++ b/includes/clientpool/SquidPurgeClient.php @@ -191,11 +191,11 @@ class SquidPurgeClient { /** * Queue a purge operation * - * @param string $url + * @param string $url Fully expanded URL (with host and protocol) */ public function queuePurge( $url ) { global $wgSquidPurgeUseHostHeader; - $url = CdnCacheUpdate::expand( str_replace( "\n", '', $url ) ); + $url = str_replace( "\n", '', $url ); // sanity $request = []; if ( $wgSquidPurgeUseHostHeader ) { $url = wfParseUrl( $url ); diff --git a/includes/collation/CustomUppercaseCollation.php b/includes/collation/CustomUppercaseCollation.php index 170d5c2c69..8f4f058646 100644 --- a/includes/collation/CustomUppercaseCollation.php +++ b/includes/collation/CustomUppercaseCollation.php @@ -45,6 +45,9 @@ class CustomUppercaseCollation extends NumericUppercaseCollation { /** @var array $puaSubset List of private use area codes */ private $puaSubset; + /** @var array */ + private $firstLetters; + /** * @note This assumes $alphabet does not contain U+F3000-U+F3FFF * diff --git a/includes/composer/ComposerPhpunitXmlCoverageEdit.php b/includes/composer/ComposerPhpunitXmlCoverageEdit.php new file mode 100644 index 0000000000..7db4b11bb3 --- /dev/null +++ b/includes/composer/ComposerPhpunitXmlCoverageEdit.php @@ -0,0 +1,60 @@ +getArguments(); + if ( count( $args ) !== 1 ) { + throw new InvalidArgumentException( 'Pass extensions/$extensionName as an argument, ' . + 'e.g. "composer phpunit:coverage-edit -- extensions/BoilerPlate"' ); + } + $project = current( $args ); + $phpunitXml = \PHPUnit\Util\Xml::loadFile( $IP . '/phpunit.xml.dist' ); + $whitelist = iterator_to_array( $phpunitXml->getElementsByTagName( 'whitelist' ) ); + /** @var DOMNode $childNode */ + foreach ( $whitelist as $childNode ) { + $childNode->parentNode->removeChild( $childNode ); + } + $whitelistElement = $phpunitXml->createElement( 'whitelist' ); + $whitelistElement->setAttribute( 'addUncoveredFilesFromWhitelist', 'false' ); + // TODO: Use AutoloadClasses from extension.json to load the relevant directories + foreach ( [ 'includes', 'src', 'maintenance' ] as $dir ) { + $dirElement = $phpunitXml->createElement( 'directory', $project . '/' . $dir ); + $dirElement->setAttribute( 'suffix', '.php' ); + $whitelistElement->appendChild( $dirElement ); + + } + $phpunitXml->getElementsByTagName( 'filter' )->item( 0 ) + ->appendChild( $whitelistElement ); + $phpunitXml->formatOutput = true; + $phpunitXml->save( $IP . '/phpunit.xml' ); + } +} diff --git a/includes/config/ConfigFactory.php b/includes/config/ConfigFactory.php index 696bbf462f..bd174b2ed6 100644 --- a/includes/config/ConfigFactory.php +++ b/includes/config/ConfigFactory.php @@ -66,7 +66,8 @@ class ConfigFactory implements SalvageableService { public function salvage( SalvageableService $other ) { Assert::parameterType( self::class, $other, '$other' ); - /** @var ConfigFactory $other */ + /** @var self $other */ + '@phan-var self $other'; foreach ( $other->factoryFunctions as $name => $otherFunc ) { if ( !isset( $this->factoryFunctions[$name] ) ) { continue; diff --git a/includes/config/ConfigRepository.php b/includes/config/ConfigRepository.php index d48eb0ec43..ceb3944d17 100644 --- a/includes/config/ConfigRepository.php +++ b/includes/config/ConfigRepository.php @@ -188,6 +188,8 @@ class ConfigRepository implements SalvageableService { */ public function salvage( SalvageableService $other ) { Assert::parameterType( self::class, $other, '$other' ); + /** @var self $other */ + '@phan-var self $other'; foreach ( $other->configItems['public'] as $name => $otherConfig ) { if ( isset( $this->configItems['public'][$name] ) ) { diff --git a/includes/config/EtcdConfig.php b/includes/config/EtcdConfig.php index 09d0189a94..f3d3849476 100644 --- a/includes/config/EtcdConfig.php +++ b/includes/config/EtcdConfig.php @@ -161,6 +161,7 @@ class EtcdConfig implements Config, LoggerAwareInterface { if ( is_array( $etcdResponse['config'] ) ) { // Avoid having all servers expire cache keys at the same time $expiry = microtime( true ) + $this->baseCacheTTL; + // @phan-suppress-next-line PhanTypeMismatchArgumentInternal $expiry += mt_rand( 0, 1e6 ) / 1e6 * $this->skewCacheTTL; $data = [ 'config' => $etcdResponse['config'], diff --git a/includes/content/AbstractContent.php b/includes/content/AbstractContent.php index c82b4734ca..eb9ef850b4 100644 --- a/includes/content/AbstractContent.php +++ b/includes/content/AbstractContent.php @@ -529,7 +529,7 @@ abstract class AbstractContent implements Content { * @since 1.24 * * @param Title $title Context title for parsing - * @param int|null $revId Revision ID (for {{REVISIONID}}) + * @param int|null $revId Revision ID being rendered * @param ParserOptions|null $options * @param bool $generateHtml Whether or not to generate HTML * @@ -575,7 +575,8 @@ abstract class AbstractContent implements Content { * @since 1.24 * * @param Title $title Context title for parsing - * @param int|null $revId Revision ID (for {{REVISIONID}}) + * @param int|null $revId ID of the revision being rendered. + * See Parser::parse() for the ramifications. * @param ParserOptions $options * @param bool $generateHtml Whether or not to generate HTML * @param ParserOutput &$output The output object to fill (reference). diff --git a/includes/content/Content.php b/includes/content/Content.php index 2637aa6929..8596619d13 100644 --- a/includes/content/Content.php +++ b/includes/content/Content.php @@ -269,7 +269,8 @@ interface Content { * may call ParserOutput::recordOption() on the output object. * * @param Title $title The page title to use as a context for rendering. - * @param int|null $revId Optional revision ID being rendered. + * @param int|null $revId ID of the revision being rendered. + * See Parser::parse() for the ramifications. (default: null) * @param ParserOptions|null $options Any parser options. * @param bool $generateHtml Whether to generate HTML (default: true). If false, * the result of calling getText() on the ParserOutput object returned by diff --git a/includes/content/ContentHandler.php b/includes/content/ContentHandler.php index 48dfc70090..533f6397c5 100644 --- a/includes/content/ContentHandler.php +++ b/includes/content/ContentHandler.php @@ -26,7 +26,7 @@ * @author Daniel Kinzler */ -use MediaWiki\Storage\RevisionRecord; +use MediaWiki\Revision\RevisionRecord; use Wikimedia\Assert\Assert; use MediaWiki\Logger\LoggerFactory; use MediaWiki\MediaWikiServices; @@ -280,8 +280,10 @@ abstract class ContentHandler { } if ( !( $handler instanceof ContentHandler ) ) { - throw new MWException( "$classOrCallback from \$wgContentHandlers is not " . - "compatible with ContentHandler" ); + throw new MWException( + var_export( $classOrCallback, true ) . " from \$wgContentHandlers is not " . + "compatible with ContentHandler" + ); } } @@ -1077,7 +1079,8 @@ abstract class ContentHandler { } // Max content length = max comment length - length of the comment (excl. $1) - $text = $content ? $content->getTextForSummary( 255 - ( strlen( $reason ) - 2 ) ) : ''; + $maxLength = CommentStore::COMMENT_CHARACTER_LIMIT - ( strlen( $reason ) - 2 ); + $text = $content ? $content->getTextForSummary( $maxLength ) : ''; // Now replace the '$1' placeholder $reason = str_replace( '$1', $text, $reason ); @@ -1099,7 +1102,7 @@ abstract class ContentHandler { * @param Revision|Content $undoafter Must be from an earlier revision than $undo * @param bool $undoIsLatest Set true if $undo is from the current revision (since 1.32) * - * @return mixed Content on success, false on failure + * @return Content|false Content on success, false on failure */ public function getUndoContent( $current, $undo, $undoafter, $undoIsLatest = false ) { Assert::parameterType( Revision::class . '|' . Content::class, $current, '$current' ); @@ -1257,6 +1260,7 @@ abstract class ContentHandler { * @since 1.28 */ public function getFieldsForSearchIndex( SearchEngine $engine ) { + $fields = []; $fields['category'] = $engine->makeSearchFieldMapping( 'category', SearchIndexField::INDEX_TYPE_TEXT diff --git a/includes/content/FileContentHandler.php b/includes/content/FileContentHandler.php index 6a1cc62595..f3f9a97f52 100644 --- a/includes/content/FileContentHandler.php +++ b/includes/content/FileContentHandler.php @@ -11,6 +11,7 @@ use MediaWiki\MediaWikiServices; class FileContentHandler extends WikitextContentHandler { public function getFieldsForSearchIndex( SearchEngine $engine ) { + $fields = []; $fields['file_media_type'] = $engine->makeSearchFieldMapping( 'file_media_type', SearchIndexField::INDEX_TYPE_KEYWORD ); $fields['file_media_type']->setFlag( SearchIndexField::FLAG_CASEFOLD ); diff --git a/includes/content/TextContent.php b/includes/content/TextContent.php index 71dd35c84d..54a57a55e6 100644 --- a/includes/content/TextContent.php +++ b/includes/content/TextContent.php @@ -155,7 +155,9 @@ class TextContent extends AbstractContent { * @return string|bool The raw text, or false if the conversion failed. */ public function getWikitextForTransclusion() { + /** @var WikitextContent $wikitext */ $wikitext = $this->convert( CONTENT_MODEL_WIKITEXT, 'lossy' ); + '@phan-var WikitextContent $wikitext'; if ( $wikitext ) { return $wikitext->getText(); @@ -214,7 +216,8 @@ class TextContent extends AbstractContent { */ public function diff( Content $that, Language $lang = null ) { $this->checkModelID( $that->getModel() ); - + /** @var self $that */ + '@phan-var self $that'; // @todo could implement this in DifferenceEngine and just delegate here? if ( !$lang ) { diff --git a/includes/content/TextContentHandler.php b/includes/content/TextContentHandler.php index e3dc187ffe..e48dd511b8 100644 --- a/includes/content/TextContentHandler.php +++ b/includes/content/TextContentHandler.php @@ -45,6 +45,7 @@ class TextContentHandler extends ContentHandler { public function serializeContent( Content $content, $format = null ) { $this->checkFormat( $format ); + // @phan-suppress-next-line PhanUndeclaredMethod return $content->getText(); } diff --git a/includes/content/UnknownContent.php b/includes/content/UnknownContent.php new file mode 100644 index 0000000000..27199a0038 --- /dev/null +++ b/includes/content/UnknownContent.php @@ -0,0 +1,149 @@ +data = $data; + } + + /** + * @return Content $this + */ + public function copy() { + // UnknownContent is immutable, so no need to copy. + return $this; + } + + /** + * Returns an empty string. + * + * @param int $maxlength + * + * @return string + */ + public function getTextForSummary( $maxlength = 250 ) { + return ''; + } + + /** + * Returns the data size in bytes. + * + * @return int + */ + public function getSize() { + return strlen( $this->data ); + } + + /** + * Returns false. + * + * @param bool|null $hasLinks If it is known whether this content contains links, + * provide this information here, to avoid redundant parsing to find out. + * + * @return bool + */ + public function isCountable( $hasLinks = null ) { + return false; + } + + /** + * @return string data of unknown format and meaning + */ + public function getNativeData() { + return $this->getData(); + } + + /** + * @return string data of unknown format and meaning + */ + public function getData() { + return $this->data; + } + + /** + * Returns an empty string. + * + * @return string The raw text. + */ + public function getTextForSearchIndex() { + return ''; + } + + /** + * Returns false. + */ + public function getWikitextForTransclusion() { + return false; + } + + /** + * Fills the ParserOutput with an error message. + */ + protected function fillParserOutput( Title $title, $revId, + ParserOptions $options, $generateHtml, ParserOutput &$output + ) { + $msg = wfMessage( 'unsupported-content-model', [ $this->getModel() ] ); + $html = Html::rawElement( 'div', [ 'class' => 'error' ], $msg->inContentLanguage()->parse() ); + $output->setText( $html ); + } + + /** + * Returns false. + */ + public function convert( $toModel, $lossy = '' ) { + return false; + } + + protected function equalsInternal( Content $that ) { + if ( !$that instanceof UnknownContent ) { + return false; + } + + return $this->getData() == $that->getData(); + } + +} diff --git a/includes/content/UnknownContentHandler.php b/includes/content/UnknownContentHandler.php new file mode 100644 index 0000000000..a5be21c764 --- /dev/null +++ b/includes/content/UnknownContentHandler.php @@ -0,0 +1,115 @@ +getData(); + } + + /** + * Constructs an UnknownContent instance wrapping the given data. + * + * @since 1.21 + * + * @param string $blob serialized content in an unknown format + * @param string|null $format ignored + * + * @return Content The UnknownContent object wrapping $data + */ + public function unserializeContent( $blob, $format = null ) { + return new UnknownContent( $blob, $this->getModelID() ); + } + + /** + * Creates an empty UnknownContent object. + * + * @since 1.21 + * + * @return Content A new UnknownContent object with empty text. + */ + public function makeEmptyContent() { + return $this->unserializeContent( '' ); + } + + /** + * @return false + */ + public function supportsDirectEditing() { + return false; + } + + /** + * @param IContextSource $context + * + * @return SlotDiffRenderer + */ + protected function getSlotDiffRendererInternal( IContextSource $context ) { + return new UnsupportedSlotDiffRenderer( $context ); + } +} diff --git a/includes/content/WikitextContent.php b/includes/content/WikitextContent.php index 8e5e0a8305..a760a1b52f 100644 --- a/includes/content/WikitextContent.php +++ b/includes/content/WikitextContent.php @@ -89,6 +89,8 @@ class WikitextContent extends TextContent { "document uses $myModelId but " . "section uses $sectionModelId." ); } + /** @var self $with $oldtext */ + '@phan-var self $with'; $oldtext = $this->getText(); $text = $with->getText(); @@ -329,7 +331,8 @@ class WikitextContent extends TextContent { * using the global Parser service. * * @param Title $title - * @param int|null $revId Revision to pass to the parser (default: null) + * @param int|null $revId ID of the revision being rendered. + * See Parser::parse() for the ramifications. (default: null) * @param ParserOptions $options (default: null) * @param bool $generateHtml (default: true) * @param ParserOutput &$output ParserOutput representing the HTML form of the text, diff --git a/includes/context/ContextSource.php b/includes/context/ContextSource.php index 618253859d..a21f404b85 100644 --- a/includes/context/ContextSource.php +++ b/includes/context/ContextSource.php @@ -163,6 +163,7 @@ abstract class ContextSource implements IContextSource { * @param string|string[]|MessageSpecifier $key Message key, or array of keys, * or a MessageSpecifier. * @param mixed $args,... + * @suppress PhanCommentParamWithoutRealParam HHVM bug T228695#5450847 * @return Message */ public function msg( $key /* $args */ ) { diff --git a/includes/context/DerivativeContext.php b/includes/context/DerivativeContext.php index d32617e0c3..e4340ce2db 100644 --- a/includes/context/DerivativeContext.php +++ b/includes/context/DerivativeContext.php @@ -257,6 +257,7 @@ class DerivativeContext extends ContextSource implements MutableContext { * @param string|string[]|MessageSpecifier $key Message key, or array of keys, * or a MessageSpecifier. * @param mixed $args,... Arguments to wfMessage + * @suppress PhanCommentParamWithoutRealParam HHVM bug T228695#5450847 * @return Message */ public function msg( $key ) { diff --git a/includes/context/RequestContext.php b/includes/context/RequestContext.php index 6eeac1c72d..cbcaba1c9f 100644 --- a/includes/context/RequestContext.php +++ b/includes/context/RequestContext.php @@ -81,6 +81,12 @@ class RequestContext implements IContextSource, MutableContext { */ private static $instance = null; + /** + * Boolean flag to guard against recursion in getLanguage + * @var bool + */ + private $languageRecursion = false; + /** * @param Config $config */ @@ -318,7 +324,7 @@ class RequestContext implements IContextSource, MutableContext { * @since 1.19 */ public function getLanguage() { - if ( isset( $this->recursion ) ) { + if ( $this->languageRecursion === true ) { trigger_error( "Recursion detected in " . __METHOD__, E_USER_WARNING ); $e = new Exception; wfDebugLog( 'recursion-guard', "Recursion detected:\n" . $e->getTraceAsString() ); @@ -326,7 +332,7 @@ class RequestContext implements IContextSource, MutableContext { $code = $this->getConfig()->get( 'LanguageCode' ) ?: 'en'; $this->lang = Language::factory( $code ); } elseif ( $this->lang === null ) { - $this->recursion = true; + $this->languageRecursion = true; try { $request = $this->getRequest(); @@ -348,7 +354,7 @@ class RequestContext implements IContextSource, MutableContext { $this->lang = $obj; } } finally { - unset( $this->recursion ); + $this->languageRecursion = false; } } @@ -411,6 +417,7 @@ class RequestContext implements IContextSource, MutableContext { * @param string|string[]|MessageSpecifier $key Message key, or array of keys, * or a MessageSpecifier. * @param mixed $args,... + * @suppress PhanCommentParamWithoutRealParam HHVM bug T228695#5450847 * @return Message */ public function msg( $key ) { diff --git a/includes/dao/DBAccessBase.php b/includes/dao/DBAccessBase.php index e099b38f09..09314ed3ca 100644 --- a/includes/dao/DBAccessBase.php +++ b/includes/dao/DBAccessBase.php @@ -32,24 +32,24 @@ use Wikimedia\Rdbms\ILoadBalancer; * @author Daniel Kinzler */ abstract class DBAccessBase implements IDBAccessObject { - /** - * @var string|bool $wiki The target wiki's name. This must be an ID - * that LBFactory can understand. - */ - protected $wiki = false; + /** @var ILoadBalancer */ + private $lb; + + /** @var string|bool The target wiki's DB domain */ + protected $dbDomain = false; /** - * @param string|bool $wiki The target wiki's name. This must be an ID - * that LBFactory can understand. + * @param string|bool $dbDomain The target wiki's DB domain */ - public function __construct( $wiki = false ) { - $this->wiki = $wiki; + public function __construct( $dbDomain = false ) { + $this->dbDomain = $dbDomain; + $this->lb = MediaWikiServices::getInstance()->getDBLoadBalancerFactory() + ->getMainLB( $dbDomain ); } /** * Returns a database connection. * - * @see wfGetDB() * @see LoadBalancer::getConnection() * * @since 1.21 @@ -60,9 +60,7 @@ abstract class DBAccessBase implements IDBAccessObject { * @return IDatabase */ protected function getConnection( $id, array $groups = [] ) { - $loadBalancer = $this->getLoadBalancer(); - - return $loadBalancer->getConnection( $id, $groups, $this->wiki ); + return $this->getLoadBalancer()->getConnectionRef( $id, $groups, $this->dbDomain ); } /** @@ -73,12 +71,10 @@ abstract class DBAccessBase implements IDBAccessObject { * @since 1.21 * * @param IDatabase $db The database connection to release. + * @deprecated Since 1.34 */ protected function releaseConnection( IDatabase $db ) { - if ( $this->wiki !== false ) { - $loadBalancer = $this->getLoadBalancer(); - $loadBalancer->reuseConnection( $db ); - } + // no-op } /** @@ -90,8 +86,7 @@ abstract class DBAccessBase implements IDBAccessObject { * * @return ILoadBalancer The database load balancer object */ - public function getLoadBalancer() { - $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory(); - return $lbFactory->getMainLB( $this->wiki ); + protected function getLoadBalancer() { + return $this->lb; } } diff --git a/includes/db/CloneDatabase.php b/includes/db/CloneDatabase.php index cdf0f797df..df5f1151b3 100644 --- a/includes/db/CloneDatabase.php +++ b/includes/db/CloneDatabase.php @@ -93,9 +93,7 @@ class CloneDatabase { // Postgres: Temp tables are automatically deleted upon end of session // Same Temp table name hides existing table for current session - if ( $this->dropCurrentTables - && !in_array( $this->db->getType(), [ 'oracle' ] ) - ) { + if ( $this->dropCurrentTables ) { if ( $oldTableName === $newTableName ) { // Last ditch check to avoid data loss throw new LogicException( "Not dropping new table, as '$newTableName'" diff --git a/includes/db/MWLBFactory.php b/includes/db/MWLBFactory.php index 0c17840e4a..ad5708a2f2 100644 --- a/includes/db/MWLBFactory.php +++ b/includes/db/MWLBFactory.php @@ -66,7 +66,7 @@ abstract class MWLBFactory { * @param array $lbConf Config for LBFactory::__construct() * @param ServiceOptions $options * @param ConfiguredReadOnlyMode $readOnlyMode - * @param BagOStuff $srvCace + * @param BagOStuff $srvCache * @param BagOStuff $mainStash * @param WANObjectCache $wanCache * @return array @@ -76,7 +76,7 @@ abstract class MWLBFactory { array $lbConf, ServiceOptions $options, ConfiguredReadOnlyMode $readOnlyMode, - BagOStuff $srvCace, + BagOStuff $srvCache, BagOStuff $mainStash, WANObjectCache $wanCache ) { @@ -159,7 +159,7 @@ abstract class MWLBFactory { $options->get( 'DBprefix' ) ); - $lbConf = self::injectObjectCaches( $lbConf, $srvCace, $mainStash, $wanCache ); + $lbConf = self::injectObjectCaches( $lbConf, $srvCache, $mainStash, $wanCache ); return $lbConf; } @@ -168,7 +168,7 @@ abstract class MWLBFactory { * @return array */ private static function getDbTypesWithSchemas() { - return [ 'postgres', 'mssql' ]; + return [ 'postgres' ]; } /** @@ -193,16 +193,6 @@ abstract class MWLBFactory { // Work around the reserved word usage in MediaWiki schema 'keywordTableMap' => [ 'user' => 'mwuser', 'text' => 'pagecontent' ] ]; - } elseif ( $server['type'] === 'oracle' ) { - $server += [ - // Work around the reserved word usage in MediaWiki schema - 'keywordTableMap' => [ 'user' => 'mwuser', 'text' => 'pagecontent' ] - ]; - } elseif ( $server['type'] === 'mssql' ) { - $server += [ - 'port' => $options->get( 'DBport' ), - 'useWindowsAuth' => $options->get( 'DBWindowsAuthentication' ) - ]; } if ( in_array( $server['type'], self::getDbTypesWithSchemas(), true ) ) { @@ -232,6 +222,11 @@ abstract class MWLBFactory { private static function injectObjectCaches( array $lbConf, BagOStuff $sCache, BagOStuff $mStash, WANObjectCache $wCache ) { + // Fallback if APC style caching is not an option + if ( $sCache instanceof EmptyBagOStuff ) { + $sCache = new HashBagOStuff( [ 'maxKeys' => 100 ] ); + } + // Use APC/memcached style caching, but avoids loops with CACHE_DB (T141804) if ( $sCache->getQoS( $sCache::ATTR_EMULATION ) > $sCache::QOS_EMULATION_SQL ) { $lbConf['srvCache'] = $sCache; @@ -386,7 +381,6 @@ abstract class MWLBFactory { * T154872). */ $lbFactory->setIndexAliases( [ - 'ar_usertext_timestamp' => 'usertext_timestamp', 'un_user_id' => 'user_id', 'un_user_ip' => 'user_ip', ] ); diff --git a/includes/db/ORAField.php b/includes/db/ORAField.php deleted file mode 100644 index df31000364..0000000000 --- a/includes/db/ORAField.php +++ /dev/null @@ -1,53 +0,0 @@ -name = $info['column_name']; - $this->tablename = $info['table_name']; - $this->default = $info['data_default']; - $this->max_length = $info['data_length']; - $this->nullable = $info['not_null']; - $this->is_pk = isset( $info['prim'] ) && $info['prim'] == 1 ? 1 : 0; - $this->is_unique = isset( $info['uniq'] ) && $info['uniq'] == 1 ? 1 : 0; - $this->is_multiple = isset( $info['nonuniq'] ) && $info['nonuniq'] == 1 ? 1 : 0; - $this->is_key = ( $this->is_pk || $this->is_unique || $this->is_multiple ); - $this->type = $info['data_type']; - } - - function name() { - return $this->name; - } - - function tableName() { - return $this->tablename; - } - - function defaultValue() { - return $this->default; - } - - function maxLength() { - return $this->max_length; - } - - function isNullable() { - return $this->nullable; - } - - function isKey() { - return $this->is_key; - } - - function isMultipleKey() { - return $this->is_multiple; - } - - function type() { - return $this->type; - } -} diff --git a/includes/db/ORAResult.php b/includes/db/ORAResult.php deleted file mode 100644 index aafd386138..0000000000 --- a/includes/db/ORAResult.php +++ /dev/null @@ -1,110 +0,0 @@ -db =& $db; - - $this->nrows = oci_fetch_all( $stmt, $this->rows, 0, -1, OCI_FETCHSTATEMENT_BY_ROW | OCI_NUM ); - if ( $this->nrows === false ) { - $e = oci_error( $stmt ); - $db->reportQueryError( $e['message'], $e['code'], '', __METHOD__ ); - $this->free(); - - return; - } - - if ( $unique ) { - $this->rows = $this->array_unique_md( $this->rows ); - $this->nrows = count( $this->rows ); - } - - if ( $this->nrows > 0 ) { - foreach ( $this->rows[0] as $k => $v ) { - $this->columns[$k] = strtolower( oci_field_name( $stmt, $k + 1 ) ); - } - } - - $this->cursor = 0; - oci_free_statement( $stmt ); - } - - public function free() { - unset( $this->db ); - } - - public function seek( $row ) { - $this->cursor = min( $row, $this->nrows ); - } - - public function numRows() { - return $this->nrows; - } - - public function numFields() { - return count( $this->columns ); - } - - public function fetchObject() { - if ( $this->cursor >= $this->nrows ) { - return false; - } - $row = $this->rows[$this->cursor++]; - $ret = new stdClass(); - foreach ( $row as $k => $v ) { - $lc = $this->columns[$k]; - $ret->$lc = $v; - } - - return $ret; - } - - public function fetchRow() { - if ( $this->cursor >= $this->nrows ) { - return false; - } - - $row = $this->rows[$this->cursor++]; - $ret = []; - foreach ( $row as $k => $v ) { - $lc = $this->columns[$k]; - $ret[$lc] = $v; - $ret[$k] = $v; - } - - return $ret; - } -} diff --git a/includes/debug/MWDebug.php b/includes/debug/MWDebug.php index e8778362ec..6bcb0e6ff8 100644 --- a/includes/debug/MWDebug.php +++ b/includes/debug/MWDebug.php @@ -67,6 +67,30 @@ class MWDebug { */ protected static $deprecationWarnings = []; + /** + * @internal For use by Setup.php only. + */ + public static function setup() { + global $wgDebugToolbar, + $wgUseCdn, $wgUseFileCache, $wgCommandLineMode; + + if ( + // Easy to forget to falsify $wgDebugToolbar for static caches. + // If file cache or CDN cache is on, just disable this (DWIMD). + $wgUseCdn || + $wgUseFileCache || + // Keep MWDebug off on CLI. This prevents MWDebug from eating up + // all the memory for logging SQL queries in maintenance scripts. + $wgCommandLineMode + ) { + return; + } + + if ( $wgDebugToolbar ) { + self::init(); + } + } + /** * Enabled the debugger and load resource module. * This is called by Setup.php when $wgDebugToolbar is true. diff --git a/includes/debug/logger/ConsoleLogger.php b/includes/debug/logger/ConsoleLogger.php index 5a5e507195..56fc0b32bc 100644 --- a/includes/debug/logger/ConsoleLogger.php +++ b/includes/debug/logger/ConsoleLogger.php @@ -10,10 +10,19 @@ use Psr\Log\AbstractLogger; * goal. */ class ConsoleLogger extends AbstractLogger { + /** @var string */ + private $channel; + + /** + * @param string $channel + */ public function __construct( $channel ) { $this->channel = $channel; } + /** + * @inheritDoc + */ public function log( $level, $message, array $context = [] ) { fwrite( STDERR, "[$level] " . LegacyLogger::format( $this->channel, $message, $context ) ); diff --git a/includes/deferred/CdnCacheUpdate.php b/includes/deferred/CdnCacheUpdate.php index 66ce9a3ddf..b983e97406 100644 --- a/includes/deferred/CdnCacheUpdate.php +++ b/includes/deferred/CdnCacheUpdate.php @@ -24,12 +24,12 @@ use Wikimedia\Assert\Assert; use MediaWiki\MediaWikiServices; /** - * Handles purging appropriate CDN URLs given a title (or titles) + * Handles purging the appropriate CDN objects given a list of URLs or Title instances * @ingroup Cache */ class CdnCacheUpdate implements DeferrableUpdate, MergeableUpdate { /** @var string[] Collection of URLs to purge */ - protected $urls = []; + private $urls = []; /** * @param string[] $urlArr Collection of URLs to purge @@ -39,8 +39,9 @@ class CdnCacheUpdate implements DeferrableUpdate, MergeableUpdate { } public function merge( MergeableUpdate $update ) { - /** @var CdnCacheUpdate $update */ + /** @var self $update */ Assert::parameterType( __CLASS__, $update, '$update' ); + '@phan-var self $update'; $this->urls = array_merge( $this->urls, $update->urls ); } @@ -98,10 +99,9 @@ class CdnCacheUpdate implements DeferrableUpdate, MergeableUpdate { wfDebugLog( 'squid', __METHOD__ . ': ' . implode( ' ', $urlArr ) ); // Reliably broadcast the purge to all edge nodes - $relayer = MediaWikiServices::getInstance()->getEventRelayerGroup() - ->getRelayer( 'cdn-url-purges' ); $ts = microtime( true ); - $relayer->notifyMulti( + $relayerGroup = MediaWikiServices::getInstance()->getEventRelayerGroup(); + $relayerGroup->getRelayer( 'cdn-url-purges' )->notifyMulti( 'cdn-url-purges', array_map( function ( $url ) use ( $ts ) { @@ -137,7 +137,7 @@ class CdnCacheUpdate implements DeferrableUpdate, MergeableUpdate { foreach ( $chunks as $chunk ) { $client = new SquidPurgeClient( $server ); foreach ( $chunk as $url ) { - $client->queuePurge( $url ); + $client->queuePurge( self::expand( $url ) ); } $pool->addClient( $client ); } @@ -254,7 +254,7 @@ class CdnCacheUpdate implements DeferrableUpdate, MergeableUpdate { * @param string $url * @return string */ - public static function expand( $url ) { + private static function expand( $url ) { return wfExpandUrl( $url, PROTO_INTERNAL ); } diff --git a/includes/deferred/DeferrableCallback.php b/includes/deferred/DeferrableCallback.php index 2eb0d5dfa0..33961ed7bf 100644 --- a/includes/deferred/DeferrableCallback.php +++ b/includes/deferred/DeferrableCallback.php @@ -9,5 +9,5 @@ interface DeferrableCallback { /** * @return string Originating method name */ - function getOrigin(); + public function getOrigin(); } diff --git a/includes/deferred/DeferredUpdates.php b/includes/deferred/DeferredUpdates.php index d43ffbc975..3716971bca 100644 --- a/includes/deferred/DeferredUpdates.php +++ b/includes/deferred/DeferredUpdates.php @@ -161,6 +161,7 @@ class DeferredUpdates { if ( isset( $queue[$class] ) ) { /** @var MergeableUpdate $existingUpdate */ $existingUpdate = $queue[$class]; + '@phan-var MergeableUpdate $existingUpdate'; $existingUpdate->merge( $update ); // Move the update to the end to handle things like mergeable purge // updates that might depend on the prior updates in the queue running @@ -362,11 +363,16 @@ class DeferredUpdates { $update->setTransactionTicket( $ticket ); } - $fnameTrxOwner = get_class( $update ) . '::doUpdate'; + // Designate $update::doUpdate() as the write round owner + $fnameTrxOwner = ( $update instanceof DeferrableCallback ) + ? $update->getOrigin() + : get_class( $update ) . '::doUpdate'; + // Determine whether the write round will be explicit or implicit $useExplicitTrxRound = !( $update instanceof TransactionRoundAwareUpdate && $update->getTransactionRoundRequirement() == $update::TRX_ROUND_ABSENT ); + // Flush any pending changes left over from an implicit transaction round if ( $useExplicitTrxRound ) { $lbFactory->beginMasterChanges( $fnameTrxOwner ); // new explicit round diff --git a/includes/deferred/HTMLCacheUpdate.php b/includes/deferred/HTMLCacheUpdate.php index 29846bfb77..9e45241841 100644 --- a/includes/deferred/HTMLCacheUpdate.php +++ b/includes/deferred/HTMLCacheUpdate.php @@ -22,16 +22,15 @@ */ /** - * Class to invalidate the HTML cache of all the pages linking to a given title. + * Class to invalidate the HTML/file cache of all the pages linking to a given title * * @ingroup Cache */ class HTMLCacheUpdate extends DataUpdate { /** @var Title */ - public $mTitle; - + private $title; /** @var string */ - public $mTable; + private $table; /** * @param Title $titleTo @@ -42,16 +41,16 @@ class HTMLCacheUpdate extends DataUpdate { function __construct( Title $titleTo, $table, $causeAction = 'unknown', $causeAgent = 'unknown' ) { - $this->mTitle = $titleTo; - $this->mTable = $table; + $this->title = $titleTo; + $this->table = $table; $this->causeAction = $causeAction; $this->causeAgent = $causeAgent; } public function doUpdate() { $job = HTMLCacheUpdateJob::newForBacklinks( - $this->mTitle, - $this->mTable, + $this->title, + $this->table, [ 'causeAction' => $this->getCauseAction(), 'causeAgent' => $this->getCauseAgent() ] ); diff --git a/includes/deferred/JobQueueEnqueueUpdate.php b/includes/deferred/JobQueueEnqueueUpdate.php index 1691da216e..d1b592da9d 100644 --- a/includes/deferred/JobQueueEnqueueUpdate.php +++ b/includes/deferred/JobQueueEnqueueUpdate.php @@ -41,8 +41,9 @@ class JobQueueEnqueueUpdate implements DeferrableUpdate, MergeableUpdate { } public function merge( MergeableUpdate $update ) { - /** @var JobQueueEnqueueUpdate $update */ + /** @var self $update */ Assert::parameterType( __CLASS__, $update, '$update' ); + '@phan-var self $update'; foreach ( $update->jobsByDomain as $domain => $jobs ) { $this->jobsByDomain[$domain] = $this->jobsByDomain[$domain] ?? []; diff --git a/includes/deferred/LinksUpdate.php b/includes/deferred/LinksUpdate.php index 74e236fd4d..2bfdc0edc9 100644 --- a/includes/deferred/LinksUpdate.php +++ b/includes/deferred/LinksUpdate.php @@ -125,7 +125,7 @@ class LinksUpdate extends DataUpdate { if ( !$this->mId ) { // NOTE: subclasses may initialize mId before calling this constructor! - $this->mId = $title->getArticleID( Title::GAID_FOR_UPDATE ); + $this->mId = $title->getArticleID( Title::READ_LATEST ); } if ( !$this->mId ) { @@ -1066,6 +1066,7 @@ class LinksUpdate extends DataUpdate { private function invalidateProperties( $changed ) { global $wgPagePropLinkInvalidations; + $jobs = []; foreach ( $changed as $name => $value ) { if ( isset( $wgPagePropLinkInvalidations[$name] ) ) { $inv = $wgPagePropLinkInvalidations[$name]; @@ -1073,12 +1074,16 @@ class LinksUpdate extends DataUpdate { $inv = [ $inv ]; } foreach ( $inv as $table ) { - DeferredUpdates::addUpdate( - new HTMLCacheUpdate( $this->mTitle, $table, 'page-props' ) + $jobs[] = HTMLCacheUpdateJob::newForBacklinks( + $this->mTitle, + $table, + [ 'causeAction' => 'page-props' ] ); } } } + + JobQueueGroup::singleton()->lazyPush( $jobs ); } /** @@ -1192,4 +1197,14 @@ class LinksUpdate extends DataUpdate { return $this->db; } + + /** + * Whether or not this LinksUpdate will also update pages which transclude the + * current page or otherwise depend on it. + * + * @return bool + */ + public function isRecursive() { + return $this->mRecursive; + } } diff --git a/includes/deferred/MessageCacheUpdate.php b/includes/deferred/MessageCacheUpdate.php index c499d082f9..7f56a3693e 100644 --- a/includes/deferred/MessageCacheUpdate.php +++ b/includes/deferred/MessageCacheUpdate.php @@ -42,8 +42,9 @@ class MessageCacheUpdate implements DeferrableUpdate, MergeableUpdate { } public function merge( MergeableUpdate $update ) { - /** @var MessageCacheUpdate $update */ + /** @var self $update */ Assert::parameterType( __CLASS__, $update, '$update' ); + '@phan-var self $update'; foreach ( $update->replacements as $code => $messages ) { $this->replacements[$code] = array_merge( $this->replacements[$code] ?? [], $messages ); diff --git a/includes/deferred/SearchUpdate.php b/includes/deferred/SearchUpdate.php index a508746c55..84f6fc0eaf 100644 --- a/includes/deferred/SearchUpdate.php +++ b/includes/deferred/SearchUpdate.php @@ -50,7 +50,7 @@ class SearchUpdate implements DeferrableUpdate { */ public function __construct( $id, $title, $c = null ) { if ( is_string( $title ) ) { - wfDeprecated( __METHOD__ . " with a string for the title", 1.34 ); + wfDeprecated( __METHOD__ . " with a string for the title", '1.34' ); $this->title = Title::newFromText( $title ); if ( $this->title === null ) { throw new InvalidArgumentException( "Cannot construct the title: $title" ); @@ -62,10 +62,10 @@ class SearchUpdate implements DeferrableUpdate { $this->id = $id; // is_string() check is back-compat for ApprovedRevs if ( is_string( $c ) ) { - wfDeprecated( __METHOD__ . " with a string for the content", 1.34 ); + wfDeprecated( __METHOD__ . " with a string for the content", '1.34' ); $c = new TextContent( $c ); } elseif ( is_bool( $c ) ) { - wfDeprecated( __METHOD__ . " with a boolean for the content", 1.34 ); + wfDeprecated( __METHOD__ . " with a boolean for the content", '1.34' ); $c = null; } $this->content = $c; diff --git a/includes/deferred/SiteStatsUpdate.php b/includes/deferred/SiteStatsUpdate.php index 11e9337093..dbd7c50f0a 100644 --- a/includes/deferred/SiteStatsUpdate.php +++ b/includes/deferred/SiteStatsUpdate.php @@ -56,6 +56,7 @@ class SiteStatsUpdate implements DeferrableUpdate, MergeableUpdate { public function merge( MergeableUpdate $update ) { /** @var SiteStatsUpdate $update */ Assert::parameterType( __CLASS__, $update, '$update' ); + '@phan-var SiteStatsUpdate $update'; foreach ( self::$counters as $field ) { $this->$field += $update->$field; diff --git a/includes/deferred/UserEditCountUpdate.php b/includes/deferred/UserEditCountUpdate.php index 687dfbe907..4333c94042 100644 --- a/includes/deferred/UserEditCountUpdate.php +++ b/includes/deferred/UserEditCountUpdate.php @@ -46,6 +46,7 @@ class UserEditCountUpdate implements DeferrableUpdate, MergeableUpdate { public function merge( MergeableUpdate $update ) { /** @var UserEditCountUpdate $update */ Assert::parameterType( __CLASS__, $update, '$update' ); + '@phan-var UserEditCountUpdate $update'; foreach ( $update->infoByUser as $userId => $info ) { if ( !isset( $this->infoByUser[$userId] ) ) { diff --git a/includes/diff/ArrayDiffFormatter.php b/includes/diff/ArrayDiffFormatter.php index 70a963ba10..188135fcf9 100644 --- a/includes/diff/ArrayDiffFormatter.php +++ b/includes/diff/ArrayDiffFormatter.php @@ -34,6 +34,7 @@ class ArrayDiffFormatter extends DiffFormatter { * @param Diff $diff A Diff object. * * @return array[] List of associative arrays, each describing a difference. + * @suppress PhanParamSignatureMismatch */ public function format( $diff ) { $oldline = 1; diff --git a/includes/diff/DiffEngine.php b/includes/diff/DiffEngine.php index ce507d7a83..6fa40ea46c 100644 --- a/includes/diff/DiffEngine.php +++ b/includes/diff/DiffEngine.php @@ -47,7 +47,9 @@ use MediaWiki\Diff\ComplexityException; class DiffEngine { // Input variables + /** @var string[] */ private $from; + /** @var string[] */ private $to; private $m; private $n; diff --git a/includes/diff/DiffOp.php b/includes/diff/DiffOp.php index 2a1f3e18dc..df2792f2a8 100644 --- a/includes/diff/DiffOp.php +++ b/includes/diff/DiffOp.php @@ -42,12 +42,12 @@ abstract class DiffOp { public $type; /** - * @var string[] + * @var string[]|false */ public $orig; /** - * @var string[] + * @var string[]|false */ public $closing; diff --git a/includes/diff/DifferenceEngine.php b/includes/diff/DifferenceEngine.php index 841daea195..7e4e53e2b6 100644 --- a/includes/diff/DifferenceEngine.php +++ b/includes/diff/DifferenceEngine.php @@ -22,7 +22,6 @@ */ use MediaWiki\MediaWikiServices; -use MediaWiki\Permissions\PermissionManager; use MediaWiki\Revision\RevisionRecord; use MediaWiki\Revision\SlotRecord; use MediaWiki\Storage\NameTableAccessException; @@ -401,7 +400,8 @@ class DifferenceEngine extends ContextSource { * @return string|bool Link HTML or false */ public function deletedLink( $id ) { - if ( $this->getUser()->isAllowed( 'deletedhistory' ) ) { + $permissionManager = MediaWikiServices::getInstance()->getPermissionManager(); + if ( $permissionManager->userHasRight( $this->getUser(), 'deletedhistory' ) ) { $dbr = wfGetDB( DB_REPLICA ); $arQuery = Revision::getArchiveQueryInfo(); $row = $dbr->selectRow( @@ -541,10 +541,10 @@ class DifferenceEngine extends ContextSource { $permissionManager = MediaWikiServices::getInstance()->getPermissionManager(); - if ( $samePage && $this->mNewPage && $permissionManager->userCan( - 'edit', $user, $this->mNewPage, PermissionManager::RIGOR_QUICK + if ( $samePage && $this->mNewPage && $permissionManager->quickUserCan( + 'edit', $user, $this->mNewPage ) ) { - if ( $this->mNewRev->isCurrent() && $permissionManager->userCan( + if ( $this->mNewRev->isCurrent() && $permissionManager->quickUserCan( 'rollback', $user, $this->mNewPage ) ) { $rollbackLink = Linker::generateRollback( $this->mNewRev, $this->getContext(), @@ -555,8 +555,8 @@ class DifferenceEngine extends ContextSource { } } - if ( !$this->mOldRev->isDeleted( RevisionRecord::DELETED_TEXT ) && - !$this->mNewRev->isDeleted( RevisionRecord::DELETED_TEXT ) + if ( $this->userCanEdit( $this->mOldRev ) && + $this->userCanEdit( $this->mNewRev ) ) { $undoLink = Html::element( 'a', [ 'href' => $this->mNewPage->getLocalURL( [ @@ -765,12 +765,14 @@ class DifferenceEngine extends ContextSource { protected function getMarkPatrolledLinkInfo() { $user = $this->getUser(); $config = $this->getConfig(); + $permissionManager = MediaWikiServices::getInstance()->getPermissionManager(); // Prepare a change patrol link, if applicable if ( // Is patrolling enabled and the user allowed to? $config->get( 'UseRCPatrol' ) && - $this->mNewPage && $this->mNewPage->quickUserCan( 'patrol', $user ) && + $this->mNewPage && + $permissionManager->quickUserCan( 'patrol', $user, $this->mNewPage ) && // Only do this if the revision isn't more than 6 hours older // than the Max RC age (6h because the RC might not be cleaned out regularly) RecentChange::isInRCLifespan( $this->mNewRev->getTimestamp(), 21600 ) @@ -803,7 +805,7 @@ class DifferenceEngine extends ContextSource { // Build the link if ( $rcid ) { $this->getOutput()->preventClickjacking(); - if ( $user->isAllowed( 'writeapi' ) ) { + if ( $permissionManager->userHasRight( $user, 'writeapi' ) ) { $this->getOutput()->addModules( 'mediawiki.page.patrol.ajax' ); } @@ -898,7 +900,11 @@ class DifferenceEngine extends ContextSource { ) { $out->addParserOutput( $parserOutput, [ 'enableSectionEditLinks' => $this->mNewRev->isCurrent() - && $this->mNewRev->getTitle()->quickUserCan( 'edit', $this->getUser() ), + && MediaWikiServices::getInstance()->getPermissionManager()->quickUserCan( + 'edit', + $this->getUser(), + $this->mNewRev->getTitle() + ) ] ); } } @@ -1498,6 +1504,20 @@ class DifferenceEngine extends ContextSource { return wfMessage( $msg )->numParams( $numEdits, $numUsers )->parse(); } + /** + * @param Revision $rev + * @return bool whether the user can see and edit the revision. + */ + private function userCanEdit( Revision $rev ) { + $user = $this->getUser(); + + if ( !$rev->userCan( RevisionRecord::DELETED_TEXT, $user ) ) { + return false; + } + + return true; + } + /** * Get a header for a specified revision. * @@ -1531,13 +1551,14 @@ class DifferenceEngine extends ContextSource { $header = Linker::linkKnown( $title, $header, [], [ 'oldid' => $rev->getId() ] ); - if ( $rev->userCan( RevisionRecord::DELETED_TEXT, $user ) ) { + if ( $this->userCanEdit( $rev ) ) { $editQuery = [ 'action' => 'edit' ]; if ( !$rev->isCurrent() ) { $editQuery['oldid'] = $rev->getId(); } - $key = $title->quickUserCan( 'edit', $user ) ? 'editold' : 'viewsourceold'; + $key = MediaWikiServices::getInstance()->getPermissionManager() + ->quickUserCan( 'edit', $user, $title ) ? 'editold' : 'viewsourceold'; $msg = $this->msg( $key )->escaped(); $editLink = $this->msg( 'parentheses' )->rawParams( Linker::linkKnown( $title, $msg, [], $editQuery ) )->escaped(); @@ -1692,14 +1713,29 @@ class DifferenceEngine extends ContextSource { * false signifies that there is no previous/next revision ($old is the oldest/newest one). */ public function mapDiffPrevNext( $old, $new ) { + $rl = MediaWikiServices::getInstance()->getRevisionLookup(); if ( $new === 'prev' ) { // Show diff between revision $old and the previous one. Get previous one from DB. $newid = intval( $old ); - $oldid = $this->getTitle()->getPreviousRevisionID( $newid ); + $oldid = false; + $newRev = $rl->getRevisionById( $newid ); + if ( $newRev ) { + $oldRev = $rl->getPreviousRevision( $newRev ); + if ( $oldRev ) { + $oldid = $oldRev->getId(); + } + } } elseif ( $new === 'next' ) { // Show diff between revision $old and the next one. Get next one from DB. $oldid = intval( $old ); - $newid = $this->getTitle()->getNextRevisionID( $oldid ); + $newid = false; + $oldRev = $rl->getRevisionById( $oldid ); + if ( $oldRev ) { + $newRev = $rl->getNextRevision( $oldRev ); + if ( $newRev ) { + $newid = $newRev->getId(); + } + } } else { $oldid = intval( $old ); $newid = intval( $new ); diff --git a/includes/diff/SlotDiffRenderer.php b/includes/diff/SlotDiffRenderer.php index 969e0ba7a3..c58502b2b7 100644 --- a/includes/diff/SlotDiffRenderer.php +++ b/includes/diff/SlotDiffRenderer.php @@ -44,7 +44,7 @@ abstract class SlotDiffRenderer { * must have the same content model that was used to obtain this diff renderer. * @param Content|null $oldContent * @param Content|null $newContent - * @return string + * @return string HTML, one or more tags. */ abstract public function getDiff( Content $oldContent = null, Content $newContent = null ); diff --git a/includes/diff/TextSlotDiffRenderer.php b/includes/diff/TextSlotDiffRenderer.php index 510465bd96..ef8058cd4f 100644 --- a/includes/diff/TextSlotDiffRenderer.php +++ b/includes/diff/TextSlotDiffRenderer.php @@ -67,6 +67,7 @@ class TextSlotDiffRenderer extends SlotDiffRenderer { /** @var TextSlotDiffRenderer $slotDiffRenderer */ $slotDiffRenderer = ContentHandler::getForModelID( CONTENT_MODEL_TEXT ) ->getSlotDiffRenderer( RequestContext::getMain() ); + '@phan-var TextSlotDiffRenderer $slotDiffRenderer'; return $slotDiffRenderer->getTextDiff( $oldText, $newText ); } @@ -112,7 +113,7 @@ class TextSlotDiffRenderer extends SlotDiffRenderer { * Diff the text representations of two content objects (or just two pieces of text in general). * @param string $oldText * @param string $newText - * @return string + * @return string HTML, one or more tags. */ public function getTextDiff( $oldText, $newText ) { Assert::parameterType( 'string', $oldText, '$oldText' ); diff --git a/includes/diff/UnsupportedSlotDiffRenderer.php b/includes/diff/UnsupportedSlotDiffRenderer.php new file mode 100644 index 0000000000..db1b868238 --- /dev/null +++ b/includes/diff/UnsupportedSlotDiffRenderer.php @@ -0,0 +1,71 @@ +localizer = $localizer; + } + + /** @inheritDoc */ + public function getDiff( Content $oldContent = null, Content $newContent = null ) { + $this->normalizeContents( $oldContent, $newContent ); + + $oldModel = $oldContent->getModel(); + $newModel = $newContent->getModel(); + + if ( $oldModel !== $newModel ) { + $msg = $this->localizer->msg( 'unsupported-content-diff2', $oldModel, $newModel ); + } else { + $msg = $this->localizer->msg( 'unsupported-content-diff', $oldModel ); + } + + return Html::rawElement( + 'tr', + [], + Html::rawElement( + 'td', + [ 'colspan' => 4, 'class' => 'error' ], + $msg->parse() + ) + ); + } + +} diff --git a/includes/editpage/TextboxBuilder.php b/includes/editpage/TextboxBuilder.php index 103b3e5498..8161251d0b 100644 --- a/includes/editpage/TextboxBuilder.php +++ b/includes/editpage/TextboxBuilder.php @@ -75,8 +75,8 @@ class TextboxBuilder { public function getTextboxProtectionCSSClasses( Title $title ) { $classes = []; // Textarea CSS if ( $title->isProtected( 'edit' ) && - MediaWikiServices::getInstance()->getNamespaceInfo()-> - getRestrictionLevels( $title->getNamespace() ) !== [ '' ] + MediaWikiServices::getInstance()->getPermissionManager() + ->getNamespaceRestrictionLevels( $title->getNamespace() ) !== [ '' ] ) { # Is the title semi-protected? if ( $title->isSemiProtected() ) { diff --git a/includes/exception/BadRequestError.php b/includes/exception/BadRequestError.php index 5fcf0e6217..2448421afd 100644 --- a/includes/exception/BadRequestError.php +++ b/includes/exception/BadRequestError.php @@ -26,9 +26,9 @@ */ class BadRequestError extends ErrorPageError { - public function report() { + public function report( $action = self::SEND_OUTPUT ) { global $wgOut; $wgOut->setStatusCode( 400 ); - parent::report(); + parent::report( $action ); } } diff --git a/includes/exception/ErrorPageError.php b/includes/exception/ErrorPageError.php index 4b1812673f..64216a4f4c 100644 --- a/includes/exception/ErrorPageError.php +++ b/includes/exception/ErrorPageError.php @@ -25,6 +25,8 @@ * @ingroup Exception */ class ErrorPageError extends MWException implements ILocalizedException { + const SEND_OUTPUT = 0; + const STAGE_OUTPUT = 1; public $title, $msg, $params; /** @@ -60,13 +62,19 @@ class ErrorPageError extends MWException implements ILocalizedException { return wfMessage( $this->msg, $this->params ); } - public function report() { + public function report( $action = self::SEND_OUTPUT ) { if ( self::isCommandLine() || defined( 'MW_API' ) ) { parent::report(); } else { global $wgOut; $wgOut->showErrorPage( $this->title, $this->msg, $this->params ); - $wgOut->output(); + // Allow skipping of the final output step, so that web-based page views + // from MediaWiki.php, can inspect the staged OutputPage state, and perform + // graceful shutdown via doPreOutputCommit first, just like for regular + // output when there isn't an error page. + if ( $action === self::SEND_OUTPUT ) { + $wgOut->output(); + } } } } diff --git a/includes/exception/MWException.php b/includes/exception/MWException.php index c16d9f7f8f..29227c8f80 100644 --- a/includes/exception/MWException.php +++ b/includes/exception/MWException.php @@ -79,7 +79,7 @@ class MWException extends Exception { $res = false; if ( $this->useMessageCache() ) { try { - $res = wfMessage( $key, $params )->text(); + $res = wfMessage( $key, ...$params )->text(); } catch ( Exception $e ) { } } diff --git a/includes/exception/MWExceptionHandler.php b/includes/exception/MWExceptionHandler.php index b4e483bf53..4a72d72d6b 100644 --- a/includes/exception/MWExceptionHandler.php +++ b/includes/exception/MWExceptionHandler.php @@ -350,7 +350,7 @@ class MWExceptionHandler { // Look at message to see if this is a class not found failure // HHVM: Class undefined: foo - // PHP5: Class 'foo' not found + // PHP7: Class 'foo' not found if ( preg_match( "/Class (undefined: \w+|'\w+' not found)/", $message ) ) { // phpcs:disable Generic.Files.LineLength $msg = <<isLoggable() ) { $logger = LoggerFactory::getInstance( 'exception' ); + $context = self::getLogContext( $e, $catcher ); + if ( $extraData ) { + $context['extraData'] = $extraData; + } $logger->error( self::getLogNormalMessage( $e ), - self::getLogContext( $e, $catcher ) + $context ); $json = self::jsonSerializeException( $e, false, FormatJson::ALL_OK, $catcher ); diff --git a/includes/exception/MWExceptionRenderer.php b/includes/exception/MWExceptionRenderer.php index c52a867865..5515ef07d7 100644 --- a/includes/exception/MWExceptionRenderer.php +++ b/includes/exception/MWExceptionRenderer.php @@ -199,7 +199,7 @@ class MWExceptionRenderer { // FIXME: Keep logic in sync with MWException::msg. try { - $res = wfMessage( $key, $params )->text(); + $res = wfMessage( $key, ...$params )->text(); } catch ( Exception $e ) { $res = wfMsgReplaceArgs( $fallback, $params ); // If an exception happens inside message rendering, diff --git a/includes/exception/PermissionsError.php b/includes/exception/PermissionsError.php index cc69a762c1..9fa1c7ce29 100644 --- a/includes/exception/PermissionsError.php +++ b/includes/exception/PermissionsError.php @@ -18,6 +18,8 @@ * @file */ +use MediaWiki\MediaWikiServices; + /** * Show an error when a user tries to do something they do not have the necessary * permissions for. @@ -46,7 +48,9 @@ class PermissionsError extends ErrorPageError { if ( !count( $errors ) ) { $groups = []; - foreach ( User::getGroupsWithPermission( $this->permission ) as $group ) { + foreach ( MediaWikiServices::getInstance() + ->getPermissionManager() + ->getGroupsWithPermission( $this->permission ) as $group ) { $groups[] = UserGroupMembership::getLink( $group, RequestContext::getMain(), 'wiki' ); } @@ -63,10 +67,12 @@ class PermissionsError extends ErrorPageError { parent::__construct( 'permissionserrors', Message::newFromSpecifier( $errors[0] ) ); } - public function report() { + public function report( $action = self::SEND_OUTPUT ) { global $wgOut; $wgOut->showPermissionsErrorPage( $this->errors, $this->permission ); - $wgOut->output(); + if ( $action === self::SEND_OUTPUT ) { + $wgOut->output(); + } } } diff --git a/includes/exception/ThrottledError.php b/includes/exception/ThrottledError.php index bec0d904be..cdeb402a73 100644 --- a/includes/exception/ThrottledError.php +++ b/includes/exception/ThrottledError.php @@ -32,9 +32,9 @@ class ThrottledError extends ErrorPageError { ); } - public function report() { + public function report( $action = ErrorPageError::SEND_OUTPUT ) { global $wgOut; $wgOut->setStatusCode( 429 ); - parent::report(); + parent::report( $action ); } } diff --git a/includes/exception/UserNotLoggedIn.php b/includes/exception/UserNotLoggedIn.php index 7a99765e9c..ff992b07b7 100644 --- a/includes/exception/UserNotLoggedIn.php +++ b/includes/exception/UserNotLoggedIn.php @@ -75,14 +75,15 @@ class UserNotLoggedIn extends ErrorPageError { * Redirect to Special:Userlogin if the specified message is compatible. Otherwise, * show an error page as usual. */ - public function report() { + public function report( $action = self::SEND_OUTPUT ) { // If an unsupported message is used, don't try redirecting to Special:Userlogin, // since the message may not be compatible. if ( !in_array( $this->msg, LoginHelper::getValidErrorMessages() ) ) { - parent::report(); + parent::report( $action ); + return; } - // Message is valid. Redirec to Special:Userlogin + // Message is valid. Redirect to Special:Userlogin $context = RequestContext::getMain(); @@ -98,6 +99,8 @@ class UserNotLoggedIn extends ErrorPageError { 'warning' => $this->msg, ] ) ); - $output->output(); + if ( $action === self::SEND_OUTPUT ) { + $output->output(); + } } } diff --git a/includes/export/Dump7ZipOutput.php b/includes/export/Dump7ZipOutput.php index a50150e085..833d553696 100644 --- a/includes/export/Dump7ZipOutput.php +++ b/includes/export/Dump7ZipOutput.php @@ -60,8 +60,7 @@ class Dump7ZipOutput extends DumpPipeOutput { } /** - * @param string $newname - * @param bool $open + * @inheritDoc */ function closeAndRename( $newname, $open = false ) { $newname = $this->checkRenameArgCount( $newname ); diff --git a/includes/export/DumpFileOutput.php b/includes/export/DumpFileOutput.php index d0256fd877..ad68109999 100644 --- a/includes/export/DumpFileOutput.php +++ b/includes/export/DumpFileOutput.php @@ -59,7 +59,7 @@ class DumpFileOutput extends DumpOutput { } /** - * @param string $newname + * @inheritDoc */ function closeRenameAndReopen( $newname ) { $this->closeAndRename( $newname, true ); @@ -92,8 +92,7 @@ class DumpFileOutput extends DumpOutput { } /** - * @param string $newname - * @param bool $open + * @inheritDoc */ function closeAndRename( $newname, $open = false ) { $newname = $this->checkRenameArgCount( $newname ); diff --git a/includes/export/DumpFilter.php b/includes/export/DumpFilter.php index 75e149cea3..143fb8efe8 100644 --- a/includes/export/DumpFilter.php +++ b/includes/export/DumpFilter.php @@ -102,14 +102,16 @@ class DumpFilter { } /** - * @param string $newname + * @see DumpOutput::closeRenameAndReopen() + * @param string|string[] $newname */ function closeRenameAndReopen( $newname ) { $this->sink->closeRenameAndReopen( $newname ); } /** - * @param string $newname + * @see DumpOutput::closeAndRename() + * @param string|string[] $newname * @param bool $open */ function closeAndRename( $newname, $open = false ) { diff --git a/includes/export/DumpMultiWriter.php b/includes/export/DumpMultiWriter.php index 92118fe41d..2f5b3dc6df 100644 --- a/includes/export/DumpMultiWriter.php +++ b/includes/export/DumpMultiWriter.php @@ -27,6 +27,10 @@ * @ingroup Dump */ class DumpMultiWriter { + /** @var array */ + private $sinks; + /** @var int */ + private $count; /** * @param array $sinks diff --git a/includes/export/DumpNamespaceFilter.php b/includes/export/DumpNamespaceFilter.php index 0b8afa216f..f99746e3a0 100644 --- a/includes/export/DumpNamespaceFilter.php +++ b/includes/export/DumpNamespaceFilter.php @@ -35,7 +35,7 @@ class DumpNamespaceFilter extends DumpFilter { /** * @param DumpOutput &$sink - * @param array $param + * @param string $param * @throws MWException */ function __construct( &$sink, $param ) { @@ -61,7 +61,7 @@ class DumpNamespaceFilter extends DumpFilter { "NS_CATEGORY" => NS_CATEGORY, "NS_CATEGORY_TALK" => NS_CATEGORY_TALK ]; - if ( $param { 0 } == '!' ) { + if ( $param[0] == '!' ) { $this->invert = true; $param = substr( $param, 1 ); } diff --git a/includes/export/DumpOutput.php b/includes/export/DumpOutput.php index ab925b75ff..5c15c5f9c0 100644 --- a/includes/export/DumpOutput.php +++ b/includes/export/DumpOutput.php @@ -86,7 +86,7 @@ class DumpOutput { * and reopen new file with the old name. Use this * for writing out a file in multiple pieces * at specified checkpoints (e.g. every n hours). - * @param string|array $newname File name. May be a string or an array with one element + * @param string|string[] $newname File name. May be a string or an array with one element */ function closeRenameAndReopen( $newname ) { } @@ -95,7 +95,7 @@ class DumpOutput { * Close the old file, and move it to a specified name. * Use this for the last piece of a file written out * at specified checkpoints (e.g. every n hours). - * @param string|array $newname File name. May be a string or an array with one element + * @param string|string[] $newname File name. May be a string or an array with one element * @param bool $open If true, a new file with the old filename will be opened * again for writing (default: false) */ diff --git a/includes/export/DumpPipeOutput.php b/includes/export/DumpPipeOutput.php index a353c44761..4e61434f50 100644 --- a/includes/export/DumpPipeOutput.php +++ b/includes/export/DumpPipeOutput.php @@ -32,6 +32,7 @@ use MediaWiki\Shell\Shell; */ class DumpPipeOutput extends DumpFileOutput { protected $command, $filename; + /** @var resource|bool */ protected $procOpenResource = false; /** @@ -72,15 +73,14 @@ class DumpPipeOutput extends DumpFileOutput { } /** - * @param string $newname + * @inheritDoc */ function closeRenameAndReopen( $newname ) { $this->closeAndRename( $newname, true ); } /** - * @param string $newname - * @param bool $open + * @inheritDoc */ function closeAndRename( $newname, $open = false ) { $newname = $this->checkRenameArgCount( $newname ); diff --git a/includes/export/ExportProgressFilter.php b/includes/export/ExportProgressFilter.php index 9b1571f7de..5dd3e4c65b 100644 --- a/includes/export/ExportProgressFilter.php +++ b/includes/export/ExportProgressFilter.php @@ -30,6 +30,10 @@ class ExportProgressFilter extends DumpFilter { */ private $progress; + /** + * @param DumpOutput &$sink + * @param BackupDumper &$progress + */ function __construct( &$sink, &$progress ) { parent::__construct( $sink ); $this->progress = $progress; diff --git a/includes/export/WikiExporter.php b/includes/export/WikiExporter.php index 3ab88e2927..fd200d1fbb 100644 --- a/includes/export/WikiExporter.php +++ b/includes/export/WikiExporter.php @@ -28,7 +28,7 @@ */ use MediaWiki\MediaWikiServices as MediaWikiServicesAlias; -use MediaWiki\Storage\RevisionRecord; +use MediaWiki\Revision\RevisionRecord; use Wikimedia\Rdbms\IResultWrapper; use Wikimedia\Rdbms\IDatabase; @@ -304,29 +304,36 @@ class WikiExporter { if ( $cond ) { $where[] = $cond; } - # Get logging table name for logging.* clause - $logging = $this->db->tableName( 'logging' ); - $result = null; // Assuring $result is not undefined, if exception occurs early $commentQuery = CommentStore::getStore()->getJoin( 'log_comment' ); $actorQuery = ActorMigration::newMigration()->getJoin( 'log_user' ); + $tables = array_merge( + [ 'logging' ], $commentQuery['tables'], $actorQuery['tables'], [ 'user' ] + ); + $fields = [ + 'log_id', 'log_type', 'log_action', 'log_timestamp', 'log_namespace', + 'log_title', 'log_params', 'log_deleted', 'user_name' + ] + $commentQuery['fields'] + $actorQuery['fields']; + $options = [ + 'ORDER BY' => 'log_id', + 'USE INDEX' => [ 'logging' => 'PRIMARY' ], + 'LIMIT' => self::BATCH_SIZE, + ]; + $joins = [ + 'user' => [ 'JOIN', 'user_id = ' . $actorQuery['fields']['log_user'] ] + ] + $commentQuery['joins'] + $actorQuery['joins']; + $lastLogId = 0; while ( true ) { $result = $this->db->select( - array_merge( [ 'logging' ], $commentQuery['tables'], $actorQuery['tables'], [ 'user' ] ), - [ "{$logging}.*", 'user_name' ] + $commentQuery['fields'] + $actorQuery['fields'], + $tables, + $fields, array_merge( $where, [ 'log_id > ' . intval( $lastLogId ) ] ), __METHOD__, - [ - 'ORDER BY' => 'log_id', - 'USE INDEX' => [ 'logging' => 'PRIMARY' ], - 'LIMIT' => self::BATCH_SIZE, - ], - [ - 'user' => [ 'JOIN', 'user_id = ' . $actorQuery['fields']['log_user'] ] - ] + $commentQuery['joins'] + $actorQuery['joins'] + $options, + $joins ); if ( !$result->numRows() ) { diff --git a/includes/export/XmlDumpWriter.php b/includes/export/XmlDumpWriter.php index 000350662b..e697ef2197 100644 --- a/includes/export/XmlDumpWriter.php +++ b/includes/export/XmlDumpWriter.php @@ -658,6 +658,8 @@ class XmlDumpWriter { */ function writeUpload( $file, $dumpContents = false ) { if ( $file->isOld() ) { + /** @var OldLocalFile $file */ + '@phan-var OldLocalFile $file'; $archiveName = " " . Xml::element( 'archivename', null, $file->getArchiveName() ) . "\n"; } else { diff --git a/includes/externalstore/ExternalStoreDB.php b/includes/externalstore/ExternalStoreDB.php index 4db351b25b..264eabcd44 100644 --- a/includes/externalstore/ExternalStoreDB.php +++ b/includes/externalstore/ExternalStoreDB.php @@ -26,6 +26,7 @@ use Wikimedia\Rdbms\IDatabase; use Wikimedia\Rdbms\DBConnRef; use Wikimedia\Rdbms\MaintainableDBConnRef; use Wikimedia\Rdbms\DatabaseDomain; +use Wikimedia\Rdbms\DBUnexpectedError; /** * DB accessible external objects. @@ -113,7 +114,11 @@ class ExternalStoreDB extends ExternalStoreMedium { */ public function store( $location, $data ) { $dbw = $this->getMaster( $location ); - $dbw->insert( $this->getTable( $dbw ), [ 'blob_text' => $data ], __METHOD__ ); + $dbw->insert( + $this->getTable( $dbw, $location ), + [ 'blob_text' => $data ], + __METHOD__ + ); $id = $dbw->insertId(); if ( !$id ) { throw new MWException( __METHOD__ . ': no insert ID' ); @@ -149,10 +154,11 @@ class ExternalStoreDB extends ExternalStoreMedium { /** * Get a replica DB connection for the specified cluster * + * @since 1.34 * @param string $cluster Cluster name * @return DBConnRef */ - public function getSlave( $cluster ) { + public function getReplica( $cluster ) { $lb = $this->getLoadBalancer( $cluster ); return $lb->getConnectionRef( @@ -163,6 +169,17 @@ class ExternalStoreDB extends ExternalStoreMedium { ); } + /** + * Get a replica DB connection for the specified cluster + * + * @param string $cluster Cluster name + * @return DBConnRef + * @deprecated since 1.34 + */ + public function getSlave( $cluster ) { + return $this->getReplica( $cluster ); + } + /** * Get a master database connection for the specified cluster * @@ -211,15 +228,55 @@ class ExternalStoreDB extends ExternalStoreMedium { * Get the 'blobs' table name for this database * * @param IDatabase $db + * @param string|null $cluster Cluster name * @return string Table name ('blobs' by default) */ - public function getTable( $db ) { - $table = $db->getLBInfo( 'blobs table' ); - if ( is_null( $table ) ) { - $table = 'blobs'; + public function getTable( $db, $cluster = null ) { + if ( $cluster !== null ) { + $lb = $this->getLoadBalancer( $cluster ); + $info = $lb->getServerInfo( $lb->getWriterIndex() ); + if ( isset( $info['blobs table'] ) ) { + return $info['blobs table']; + } } - return $table; + return $db->getLBInfo( 'blobs table' ) ?? 'blobs'; // b/c + } + + /** + * Create the appropriate blobs table on this cluster + * + * @see getTable() + * @since 1.34 + * @param string $cluster + */ + public function initializeTable( $cluster ) { + global $IP; + + static $supportedTypes = [ 'mysql', 'sqlite' ]; + + $dbw = $this->getMaster( $cluster ); + if ( !in_array( $dbw->getType(), $supportedTypes, true ) ) { + throw new DBUnexpectedError( $dbw, "RDBMS type '{$dbw->getType()}' not supported." ); + } + + $sqlFilePath = "$IP/maintenance/storage/blobs.sql"; + $sql = file_get_contents( $sqlFilePath ); + if ( $sql === false ) { + throw new RuntimeException( "Failed to read '$sqlFilePath'." ); + } + + $rawTable = $this->getTable( $dbw, $cluster ); // e.g. "blobs_cluster23" + $encTable = $dbw->tableName( $rawTable ); + $dbw->query( + str_replace( + [ '/*$wgDBprefix*/blobs', '/*_*/blobs' ], + [ $encTable, $encTable ], + $sql + ), + __METHOD__, + $dbw::QUERY_IGNORE_DBO_TRX + ); } /** @@ -251,15 +308,23 @@ class ExternalStoreDB extends ExternalStoreMedium { $this->logger->debug( "ExternalStoreDB::fetchBlob cache miss on $cacheID" ); - $dbr = $this->getSlave( $cluster ); - $ret = $dbr->selectField( $this->getTable( $dbr ), - 'blob_text', [ 'blob_id' => $id ], __METHOD__ ); + $dbr = $this->getReplica( $cluster ); + $ret = $dbr->selectField( + $this->getTable( $dbr, $cluster ), + 'blob_text', + [ 'blob_id' => $id ], + __METHOD__ + ); if ( $ret === false ) { $this->logger->info( "ExternalStoreDB::fetchBlob master fallback on $cacheID" ); // Try the master $dbw = $this->getMaster( $cluster ); - $ret = $dbw->selectField( $this->getTable( $dbw ), - 'blob_text', [ 'blob_id' => $id ], __METHOD__ ); + $ret = $dbw->selectField( + $this->getTable( $dbw, $cluster ), + 'blob_text', + [ 'blob_id' => $id ], + __METHOD__ + ); if ( $ret === false ) { $this->logger->error( "ExternalStoreDB::fetchBlob master failed to find $cacheID" ); } @@ -283,9 +348,9 @@ class ExternalStoreDB extends ExternalStoreMedium { * Unlocated ids are not represented */ private function batchFetchBlobs( $cluster, array $ids ) { - $dbr = $this->getSlave( $cluster ); + $dbr = $this->getReplica( $cluster ); $res = $dbr->select( - $this->getTable( $dbr ), + $this->getTable( $dbr, $cluster ), [ 'blob_id', 'blob_text' ], [ 'blob_id' => array_keys( $ids ) ], __METHOD__ @@ -302,7 +367,8 @@ class ExternalStoreDB extends ExternalStoreMedium { ); // Try the master $dbw = $this->getMaster( $cluster ); - $res = $dbw->select( $this->getTable( $dbr ), + $res = $dbw->select( + $this->getTable( $dbr, $cluster ), [ 'blob_id', 'blob_text' ], [ 'blob_id' => array_keys( $ids ) ], __METHOD__ ); diff --git a/includes/filebackend/FileBackendGroup.php b/includes/filebackend/FileBackendGroup.php index 7ec2357e5b..9e04d09632 100644 --- a/includes/filebackend/FileBackendGroup.php +++ b/includes/filebackend/FileBackendGroup.php @@ -150,6 +150,7 @@ class FileBackendGroup { $class = $config['class']; if ( $class === FileBackendMultiWrite::class ) { + // @todo How can we test this? What's the intended use-case? foreach ( $config['backends'] as $index => $beConfig ) { if ( isset( $beConfig['template'] ) ) { // Config is just a modified version of a registered backend's. @@ -186,7 +187,7 @@ class FileBackendGroup { 'mimeCallback' => [ $this, 'guessMimeInternal' ], 'obResetFunc' => 'wfResetOutputBuffers', 'streamMimeFunc' => [ StreamFile::class, 'contentTypeFromPath' ], - 'tmpDirectory' => wfTempDir(), + 'tmpFileFactory' => $services->getTempFSFileFactory(), 'statusWrapper' => [ Status::class, 'wrap' ], 'wanCache' => $services->getMainWANObjectCache(), 'srvCache' => ObjectCache::getLocalServerInstance( 'hash' ), @@ -241,7 +242,8 @@ class FileBackendGroup { if ( !$type && $fsPath ) { $type = $magic->guessMimeType( $fsPath, false ); } elseif ( !$type && strlen( $content ) ) { - $tmpFile = TempFSFile::factory( 'mime_', '', wfTempDir() ); + $tmpFile = MediaWikiServices::getInstance()->getTempFSFileFactory() + ->newTempFSFile( 'mime_', '' ); file_put_contents( $tmpFile->getPath(), $content ); $type = $magic->guessMimeType( $tmpFile->getPath(), false ); } diff --git a/includes/filebackend/filejournal/DBFileJournal.php b/includes/filebackend/filejournal/DBFileJournal.php index 17cf8f0ca7..7ce2eddacc 100644 --- a/includes/filebackend/filejournal/DBFileJournal.php +++ b/includes/filebackend/filejournal/DBFileJournal.php @@ -24,6 +24,7 @@ use MediaWiki\MediaWikiServices; use Wikimedia\Rdbms\IDatabase; use Wikimedia\Rdbms\DBError; +use Wikimedia\Timestamp\ConvertibleTimestamp; /** * Version of FileJournal that logs to a DB table @@ -36,12 +37,12 @@ class DBFileJournal extends FileJournal { protected $domain; /** - * Construct a new instance from configuration. + * Construct a new instance from configuration. Do not call directly, use FileJournal::factory. * * @param array $config Includes: * domain: database domain ID of the wiki */ - protected function __construct( array $config ) { + public function __construct( array $config ) { parent::__construct( $config ); $this->domain = $config['domain'] ?? $config['wiki']; // b/c @@ -64,7 +65,7 @@ class DBFileJournal extends FileJournal { return $status; } - $now = wfTimestamp( TS_UNIX ); + $now = ConvertibleTimestamp::time(); $data = []; foreach ( $entries as $entry ) { @@ -80,8 +81,11 @@ class DBFileJournal extends FileJournal { try { $dbw->insert( 'filejournal', $data, __METHOD__ ); + // XXX Should we do this deterministically so it's testable? Maybe look at the last two + // digits of a hash of a bunch of the data? if ( mt_rand( 0, 99 ) == 0 ) { - $this->purgeOldLogs(); // occasionally delete old logs + // occasionally delete old logs + $this->purgeOldLogs(); // @codeCoverageIgnore } } catch ( DBError $e ) { $status->fatal( 'filejournal-fail-dbquery', $this->backend ); @@ -164,7 +168,7 @@ class DBFileJournal extends FileJournal { } $dbw = $this->getMasterDB(); - $dbCutoff = $dbw->timestamp( time() - 86400 * $this->ttlDays ); + $dbCutoff = $dbw->timestamp( ConvertibleTimestamp::time() - 86400 * $this->ttlDays ); $dbw->delete( 'filejournal', [ 'fj_timestamp < ' . $dbw->addQuotes( $dbCutoff ) ], diff --git a/includes/filebackend/lockmanager/LockManagerGroup.php b/includes/filebackend/lockmanager/LockManagerGroup.php index 957af3e4ae..9b43164a31 100644 --- a/includes/filebackend/lockmanager/LockManagerGroup.php +++ b/includes/filebackend/lockmanager/LockManagerGroup.php @@ -22,6 +22,7 @@ */ use MediaWiki\MediaWikiServices; use MediaWiki\Logger\LoggerFactory; +use Wikimedia\Rdbms\LBFactory; /** * Class to handle file lock manager registration @@ -30,62 +31,27 @@ use MediaWiki\Logger\LoggerFactory; * @since 1.19 */ class LockManagerGroup { - /** @var LockManagerGroup[] (domain => LockManagerGroup) */ - protected static $instances = []; + /** @var string domain (usually wiki ID) */ + protected $domain; - protected $domain; // string; domain (usually wiki ID) + /** @var LBFactory */ + protected $lbFactory; /** @var array Array of (name => ('class' => ..., 'config' => ..., 'instance' => ...)) */ protected $managers = []; /** + * Do not call this directly. Use LockManagerGroupFactory. + * * @param string $domain Domain (usually wiki ID) + * @param array[] $lockManagerConfigs In format of $wgLockManagers + * @param LBFactory $lbFactory */ - protected function __construct( $domain ) { + public function __construct( $domain, array $lockManagerConfigs, LBFactory $lbFactory ) { $this->domain = $domain; - } - - /** - * @param bool|string $domain Domain (usually wiki ID). Default: false. - * @return LockManagerGroup - */ - public static function singleton( $domain = false ) { - if ( $domain === false ) { - $domain = WikiMap::getCurrentWikiDbDomain()->getId(); - } - - if ( !isset( self::$instances[$domain] ) ) { - self::$instances[$domain] = new self( $domain ); - self::$instances[$domain]->initFromGlobals(); - } - - return self::$instances[$domain]; - } - - /** - * Destroy the singleton instances - */ - public static function destroySingletons() { - self::$instances = []; - } - - /** - * Register lock managers from the global variables - */ - protected function initFromGlobals() { - global $wgLockManagers; - - $this->register( $wgLockManagers ); - } + $this->lbFactory = $lbFactory; - /** - * Register an array of file lock manager configurations - * - * @param array $configs - * @throws Exception - */ - protected function register( array $configs ) { - foreach ( $configs as $config ) { + foreach ( $lockManagerConfigs as $config ) { $config['domain'] = $this->domain; if ( !isset( $config['name'] ) ) { throw new Exception( "Cannot register a lock manager with no name." ); @@ -104,6 +70,26 @@ class LockManagerGroup { } } + /** + * @deprecated since 1.34, use LockManagerGroupFactory + * + * @param bool|string $domain Domain (usually wiki ID). Default: false. + * @return LockManagerGroup + */ + public static function singleton( $domain = false ) { + return MediaWikiServices::getInstance()->getLockManagerGroupFactory() + ->getLockManagerGroup( $domain ); + } + + /** + * Destroy the singleton instances + * + * @deprecated since 1.34, use resetServiceForTesting() on LockManagerGroupFactory + */ + public static function destroySingletons() { + MediaWikiServices::getInstance()->resetServiceForTesting( 'LockManagerGroupFactory' ); + } + /** * Get the lock manager object with a given name * @@ -118,21 +104,10 @@ class LockManagerGroup { // Lazy-load the actual lock manager instance if ( !isset( $this->managers[$name]['instance'] ) ) { $class = $this->managers[$name]['class']; + '@phan-var string $class'; $config = $this->managers[$name]['config']; - if ( $class === DBLockManager::class ) { - $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory(); - $lb = $lbFactory->getMainLB( $config['domain'] ); - $config['dbServers']['localDBMaster'] = $lb->getLazyConnectionRef( - DB_MASTER, - [], - $config['domain'], - $lb::CONN_TRX_AUTOCOMMIT - ); - $config['srvCache'] = ObjectCache::getLocalServerInstance( 'hash' ); - } $config['logger'] = LoggerFactory::getInstance( 'LockManager' ); - // @phan-suppress-next-line PhanTypeInstantiateAbstract $this->managers[$name]['instance'] = new $class( $config ); } @@ -159,6 +134,8 @@ class LockManagerGroup { * Get the default lock manager configured for the site. * Returns NullLockManager if no lock manager could be found. * + * XXX This looks unused, should we just get rid of it? + * * @return LockManager */ public function getDefault() { @@ -172,6 +149,8 @@ class LockManagerGroup { * or at least some other effective configured lock manager. * Throws an exception if no lock manager could be found. * + * XXX This looks unused, should we just get rid of it? + * * @return LockManager * @throws Exception */ diff --git a/includes/filebackend/lockmanager/LockManagerGroupFactory.php b/includes/filebackend/lockmanager/LockManagerGroupFactory.php new file mode 100644 index 0000000000..9c8d4bb716 --- /dev/null +++ b/includes/filebackend/lockmanager/LockManagerGroupFactory.php @@ -0,0 +1,54 @@ + LockManagerGroup) */ + private $instances = []; + + /** + * Do not call directly, use MediaWikiServices. + * + * @param string $defaultDomain + * @param array $lockManagerConfigs In format of $wgLockManagers + * @param LBFactory $lbFactory + */ + public function __construct( $defaultDomain, array $lockManagerConfigs, LBFactory $lbFactory ) { + $this->defaultDomain = $defaultDomain; + $this->lockManagerConfigs = $lockManagerConfigs; + $this->lbFactory = $lbFactory; + } + + /** + * @param string|null|false $domain Domain (usually wiki ID). false for the default (normally + * the current wiki's domain). + * @return LockManagerGroup + */ + public function getLockManagerGroup( $domain = false ) : LockManagerGroup { + if ( $domain === false || $domain === null ) { + $domain = $this->defaultDomain; + } + + if ( !isset( $this->instances[$domain] ) ) { + $this->instances[$domain] = + new LockManagerGroup( $domain, $this->lockManagerConfigs, $this->lbFactory ); + } + + return $this->instances[$domain]; + } +} diff --git a/includes/filerepo/FileRepo.php b/includes/filerepo/FileRepo.php index 60f1607c7e..f095066806 100644 --- a/includes/filerepo/FileRepo.php +++ b/includes/filerepo/FileRepo.php @@ -142,6 +142,12 @@ class FileRepo { /** @var WANObjectCache */ protected $wanCache; + /** + * @var string + * @protected Use $this->getName(). Public for back-compat only + */ + public $name; + /** * @param array|null $info * @throws MWException @@ -832,7 +838,11 @@ class FileRepo { /** * Store a file to a given destination. * - * @param string $srcPath Source file system path, storage path, or virtual URL + * Using FSFile/TempFSFile can improve performance via caching. + * Using TempFSFile can further improve performance by signalling that it is safe + * to touch the source file or write extended attribute metadata to it directly. + * + * @param string|FSFile $srcPath Source file system path, storage path, or virtual URL * @param string $dstZone Destination zone * @param string $dstRel Destination relative path * @param int $flags Bitwise combination of the following flags: @@ -856,6 +866,8 @@ class FileRepo { /** * Store a batch of files * + * @see FileRepo::store() + * * @param array $triplets (src, dest zone, dest rel) triplets as per store() * @param int $flags Bitwise combination of the following flags: * self::OVERWRITE Overwrite an existing destination file instead of failing @@ -878,11 +890,18 @@ class FileRepo { $operations = []; // Validate each triplet and get the store operation... foreach ( $triplets as $triplet ) { - list( $srcPath, $dstZone, $dstRel ) = $triplet; + list( $src, $dstZone, $dstRel ) = $triplet; + $srcPath = ( $src instanceof FSFile ) ? $src->getPath() : $src; wfDebug( __METHOD__ . "( \$src='$srcPath', \$dstZone='$dstZone', \$dstRel='$dstRel' )\n" ); - + // Resolve source path + if ( $src instanceof FSFile ) { + $op = 'store'; + } else { + $src = $this->resolveToStoragePathIfVirtual( $src ); + $op = FileBackend::isStoragePath( $src ) ? 'copy' : 'store'; + } // Resolve destination path $root = $this->getZonePath( $dstZone ); if ( !$root ) { @@ -898,21 +917,13 @@ class FileRepo { return $this->newFatal( 'directorycreateerror', $dstDir ); } - // Resolve source to a storage path if virtual - $srcPath = $this->resolveToStoragePath( $srcPath ); - - // Get the appropriate file operation - if ( FileBackend::isStoragePath( $srcPath ) ) { - $opName = 'copy'; - } else { - $opName = 'store'; - } + // Copy the source file to the destination $operations[] = [ - 'op' => $opName, - 'src' => $srcPath, + 'op' => $op, + 'src' => $src, // storage path (copy) or local file path (store) 'dst' => $dstPath, - 'overwrite' => $flags & self::OVERWRITE, - 'overwriteSame' => $flags & self::OVERWRITE_SAME, + 'overwrite' => ( $flags & self::OVERWRITE ) ? true : false, + 'overwriteSame' => ( $flags & self::OVERWRITE_SAME ) ? true : false, ]; } @@ -949,7 +960,7 @@ class FileRepo { $path = $this->getZonePath( $zone ) . "/$rel"; } else { // Resolve source to a storage path if virtual - $path = $this->resolveToStoragePath( $path ); + $path = $this->resolveToStoragePathIfVirtual( $path ); } $operations[] = [ 'op' => 'delete', 'src' => $path ]; } @@ -969,6 +980,10 @@ class FileRepo { * This function can be used to write to otherwise read-only foreign repos. * This is intended for copying generated thumbnails into the repo. * + * Using FSFile/TempFSFile can improve performance via caching. + * Using TempFSFile can further improve performance by signalling that it is safe + * to touch the source file or write extended attribute metadata to it directly. + * * @param string|FSFile $src Source file system path, storage path, or virtual URL * @param string $dst Virtual URL or storage path * @param array|string|null $options An array consisting of a key named headers @@ -980,39 +995,14 @@ class FileRepo { return $this->quickImportBatch( [ [ $src, $dst, $options ] ] ); } - /** - * Purge a file from the repo. This does no locking nor journaling. - * This function can be used to write to otherwise read-only foreign repos. - * This is intended for purging thumbnails. - * - * @param string $path Virtual URL or storage path - * @return Status - */ - final public function quickPurge( $path ) { - return $this->quickPurgeBatch( [ $path ] ); - } - - /** - * Deletes a directory if empty. - * This function can be used to write to otherwise read-only foreign repos. - * - * @param string $dir Virtual URL (or storage path) of directory to clean - * @return Status - */ - public function quickCleanDir( $dir ) { - $status = $this->newGood(); - $status->merge( $this->backend->clean( - [ 'dir' => $this->resolveToStoragePath( $dir ) ] ) ); - - return $status; - } - /** * Import a batch of files from the local file system into the repo. * This does no locking nor journaling and overrides existing files. * This function can be used to write to otherwise read-only foreign repos. * This is intended for copying generated thumbnails into the repo. * + * @see FileRepo::quickImport() + * * All path parameters may be a file system path, storage path, or virtual URL. * When "headers" are given they are used as HTTP headers if supported. * @@ -1027,10 +1017,10 @@ class FileRepo { if ( $src instanceof FSFile ) { $op = 'store'; } else { - $src = $this->resolveToStoragePath( $src ); + $src = $this->resolveToStoragePathIfVirtual( $src ); $op = FileBackend::isStoragePath( $src ) ? 'copy' : 'store'; } - $dst = $this->resolveToStoragePath( $dst ); + $dst = $this->resolveToStoragePathIfVirtual( $dst ); if ( !isset( $triple[2] ) ) { $headers = []; @@ -1045,7 +1035,7 @@ class FileRepo { $operations[] = [ 'op' => $op, - 'src' => $src, + 'src' => $src, // storage path (copy) or local path/FSFile (store) 'dst' => $dst, 'headers' => $headers ]; @@ -1056,6 +1046,33 @@ class FileRepo { return $status; } + /** + * Purge a file from the repo. This does no locking nor journaling. + * This function can be used to write to otherwise read-only foreign repos. + * This is intended for purging thumbnails. + * + * @param string $path Virtual URL or storage path + * @return Status + */ + final public function quickPurge( $path ) { + return $this->quickPurgeBatch( [ $path ] ); + } + + /** + * Deletes a directory if empty. + * This function can be used to write to otherwise read-only foreign repos. + * + * @param string $dir Virtual URL (or storage path) of directory to clean + * @return Status + */ + public function quickCleanDir( $dir ) { + $status = $this->newGood(); + $status->merge( $this->backend->clean( + [ 'dir' => $this->resolveToStoragePathIfVirtual( $dir ) ] ) ); + + return $status; + } + /** * Purge a batch of files from the repo. * This function can be used to write to otherwise read-only foreign repos. @@ -1070,7 +1087,7 @@ class FileRepo { foreach ( $paths as $path ) { $operations[] = [ 'op' => 'delete', - 'src' => $this->resolveToStoragePath( $path ), + 'src' => $this->resolveToStoragePathIfVirtual( $path ), 'ignoreMissingSource' => true ]; } @@ -1139,7 +1156,7 @@ class FileRepo { $sources = []; foreach ( $srcPaths as $srcPath ) { // Resolve source to a storage path if virtual - $source = $this->resolveToStoragePath( $srcPath ); + $source = $this->resolveToStoragePathIfVirtual( $srcPath ); $sources[] = $source; // chunk to merge } @@ -1168,6 +1185,10 @@ class FileRepo { * Returns a Status object. On success, the value contains "new" or * "archived", to indicate whether the file was new with that name. * + * Using FSFile/TempFSFile can improve performance via caching. + * Using TempFSFile can further improve performance by signalling that it is safe + * to touch the source file or write extended attribute metadata to it directly. + * * Options to $options include: * - headers : name/value map of HTTP headers to use in response to GET/HEAD requests * @@ -1198,6 +1219,8 @@ class FileRepo { /** * Publish a batch of files * + * @see FileRepo::publish() + * * @param array $ntuples (source, dest, archive) triplets or * (source, dest, archive, options) 4-tuples as per publish(). * @param int $flags Bitfield, may be FileRepo::DELETE_SOURCE to indicate @@ -1226,7 +1249,7 @@ class FileRepo { $options = $ntuple[3] ?? []; // Resolve source to a storage path if virtual - $srcPath = $this->resolveToStoragePath( $srcPath ); + $srcPath = $this->resolveToStoragePathIfVirtual( $srcPath ); if ( !$this->validateFilename( $dstRel ) ) { throw new MWException( 'Validation error in $dstRel' ); } @@ -1266,27 +1289,17 @@ class FileRepo { // Copy (or move) the source file to the destination if ( FileBackend::isStoragePath( $srcPath ) ) { - if ( $flags & self::DELETE_SOURCE ) { - $operations[] = [ - 'op' => 'move', - 'src' => $srcPath, - 'dst' => $dstPath, - 'overwrite' => true, // replace current - 'headers' => $headers - ]; - } else { - $operations[] = [ - 'op' => 'copy', - 'src' => $srcPath, - 'dst' => $dstPath, - 'overwrite' => true, // replace current - 'headers' => $headers - ]; - } - } else { // FS source path + $operations[] = [ + 'op' => ( $flags & self::DELETE_SOURCE ) ? 'move' : 'copy', + 'src' => $srcPath, + 'dst' => $dstPath, + 'overwrite' => true, // replace current + 'headers' => $headers + ]; + } else { $operations[] = [ 'op' => 'store', - 'src' => $src, // prefer FSFile objects + 'src' => $src, // storage path (copy) or local path/FSFile (store) 'dst' => $dstPath, 'overwrite' => true, // replace current 'headers' => $headers @@ -1327,7 +1340,7 @@ class FileRepo { * @return Status */ protected function initDirectory( $dir ) { - $path = $this->resolveToStoragePath( $dir ); + $path = $this->resolveToStoragePathIfVirtual( $dir ); list( , $container, ) = FileBackend::splitStoragePath( $path ); $params = [ 'dir' => $path ]; @@ -1357,7 +1370,7 @@ class FileRepo { $status = $this->newGood(); $status->merge( $this->backend->clean( - [ 'dir' => $this->resolveToStoragePath( $dir ) ] ) ); + [ 'dir' => $this->resolveToStoragePathIfVirtual( $dir ) ] ) ); return $status; } @@ -1381,12 +1394,12 @@ class FileRepo { * @return array Map of files and existence flags, or false */ public function fileExistsBatch( array $files ) { - $paths = array_map( [ $this, 'resolveToStoragePath' ], $files ); + $paths = array_map( [ $this, 'resolveToStoragePathIfVirtual' ], $files ); $this->backend->preloadFileStat( [ 'srcs' => $paths ] ); $result = []; foreach ( $files as $key => $file ) { - $path = $this->resolveToStoragePath( $file ); + $path = $this->resolveToStoragePathIfVirtual( $file ); $result[$key] = $this->backend->fileExists( [ 'src' => $path ] ); } @@ -1517,7 +1530,7 @@ class FileRepo { * @return string * @throws MWException */ - protected function resolveToStoragePath( $path ) { + protected function resolveToStoragePathIfVirtual( $path ) { if ( self::isVirtualUrl( $path ) ) { return $this->resolveVirtualUrl( $path ); } @@ -1533,7 +1546,7 @@ class FileRepo { * @return TempFSFile|null Returns null on failure */ public function getLocalCopy( $virtualUrl ) { - $path = $this->resolveToStoragePath( $virtualUrl ); + $path = $this->resolveToStoragePathIfVirtual( $virtualUrl ); return $this->backend->getLocalCopy( [ 'src' => $path ] ); } @@ -1547,7 +1560,7 @@ class FileRepo { * @return FSFile|null Returns null on failure. */ public function getLocalReference( $virtualUrl ) { - $path = $this->resolveToStoragePath( $virtualUrl ); + $path = $this->resolveToStoragePathIfVirtual( $virtualUrl ); return $this->backend->getLocalReference( [ 'src' => $path ] ); } @@ -1578,7 +1591,7 @@ class FileRepo { * @return string|bool False on failure */ public function getFileTimestamp( $virtualUrl ) { - $path = $this->resolveToStoragePath( $virtualUrl ); + $path = $this->resolveToStoragePathIfVirtual( $virtualUrl ); return $this->backend->getFileTimestamp( [ 'src' => $path ] ); } @@ -1590,7 +1603,7 @@ class FileRepo { * @return int|bool False on failure */ public function getFileSize( $virtualUrl ) { - $path = $this->resolveToStoragePath( $virtualUrl ); + $path = $this->resolveToStoragePathIfVirtual( $virtualUrl ); return $this->backend->getFileSize( [ 'src' => $path ] ); } @@ -1602,7 +1615,7 @@ class FileRepo { * @return string|bool */ public function getFileSha1( $virtualUrl ) { - $path = $this->resolveToStoragePath( $virtualUrl ); + $path = $this->resolveToStoragePathIfVirtual( $virtualUrl ); return $this->backend->getFileSha1Base36( [ 'src' => $path ] ); } @@ -1617,7 +1630,7 @@ class FileRepo { * @since 1.27 */ public function streamFileWithStatus( $virtualUrl, $headers = [], $optHeaders = [] ) { - $path = $this->resolveToStoragePath( $virtualUrl ); + $path = $this->resolveToStoragePathIfVirtual( $virtualUrl ); $params = [ 'src' => $path, 'headers' => $headers, 'options' => $optHeaders ]; // T172851: HHVM does not flush the output properly, causing OOM diff --git a/includes/filerepo/ForeignAPIRepo.php b/includes/filerepo/ForeignAPIRepo.php index 314c4c30f0..655fd0de65 100644 --- a/includes/filerepo/ForeignAPIRepo.php +++ b/includes/filerepo/ForeignAPIRepo.php @@ -176,10 +176,10 @@ class ForeignAPIRepo extends FileRepo { /** * @param string $virtualUrl - * @return false + * @return array */ function getFileProps( $virtualUrl ) { - return false; + return []; } /** diff --git a/includes/filerepo/LocalRepo.php b/includes/filerepo/LocalRepo.php index bb65b0ad89..84c0a614d2 100644 --- a/includes/filerepo/LocalRepo.php +++ b/includes/filerepo/LocalRepo.php @@ -32,6 +32,7 @@ use Wikimedia\Rdbms\IDatabase; * in the wiki's own database. This is the most commonly used repository class. * * @ingroup FileRepo + * @method LocalFile|null newFile( $title, $time = false ) */ class LocalRepo extends FileRepo { /** @var callable */ @@ -180,7 +181,11 @@ class LocalRepo extends FileRepo { * @return string */ public static function getHashFromKey( $key ) { - return strtok( $key, '.' ); + $sha1 = strtok( $key, '.' ); + if ( is_string( $sha1 ) && strlen( $sha1 ) === 32 && $sha1[0] === '0' ) { + $sha1 = substr( $sha1, 1 ); + } + return $sha1; } /** @@ -209,20 +214,16 @@ class LocalRepo extends FileRepo { $setOpts += Database::getCacheSetOptions( $dbr ); - if ( $title instanceof Title ) { - $row = $dbr->selectRow( - [ 'page', 'redirect' ], - [ 'rd_namespace', 'rd_title' ], - [ - 'page_namespace' => $title->getNamespace(), - 'page_title' => $title->getDBkey(), - 'rd_from = page_id' - ], - $method - ); - } else { - $row = false; - } + $row = $dbr->selectRow( + [ 'page', 'redirect' ], + [ 'rd_namespace', 'rd_title' ], + [ + 'page_namespace' => $title->getNamespace(), + 'page_title' => $title->getDBkey(), + 'rd_from = page_id' + ], + $method + ); return ( $row && $row->rd_namespace == NS_FILE ) ? Title::makeTitle( $row->rd_namespace, $row->rd_title )->getDBkey() diff --git a/includes/filerepo/RepoGroup.php b/includes/filerepo/RepoGroup.php index e474ad3084..f61ca3b875 100644 --- a/includes/filerepo/RepoGroup.php +++ b/includes/filerepo/RepoGroup.php @@ -115,6 +115,7 @@ class RepoGroup { * user is allowed to view them. Otherwise, such files will not * be found. * latest: If true, load from the latest available data into File objects + * @phan-param array{time?:mixed,ignoreRedirect?:bool,private?:bool,latest?:bool} $options * @return File|bool False if title is not found */ function findFile( $title, $options = [] ) { diff --git a/includes/filerepo/file/ArchivedFile.php b/includes/filerepo/file/ArchivedFile.php index 6a3e819a33..a968831b08 100644 --- a/includes/filerepo/file/ArchivedFile.php +++ b/includes/filerepo/file/ArchivedFile.php @@ -94,6 +94,9 @@ class ArchivedFile { /** @var Title */ protected $title; # image title + /** @var bool */ + private $exists; + /** * @throws MWException * @param Title $title @@ -213,50 +216,6 @@ class ArchivedFile { return $file; } - /** - * Fields in the filearchive table - * @deprecated since 1.31, use self::getQueryInfo() instead. - * @return string[] - */ - static function selectFields() { - global $wgActorTableSchemaMigrationStage; - - if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_READ_NEW ) { - // If code is using this instead of self::getQueryInfo(), there's a - // decent chance it's going to try to directly access - // $row->fa_user or $row->fa_user_text and we can't give it - // useful values here once those aren't being used anymore. - throw new BadMethodCallException( - 'Cannot use ' . __METHOD__ - . ' when $wgActorTableSchemaMigrationStage has SCHEMA_COMPAT_READ_NEW' - ); - } - - wfDeprecated( __METHOD__, '1.31' ); - return [ - 'fa_id', - 'fa_name', - 'fa_archive_name', - 'fa_storage_key', - 'fa_storage_group', - 'fa_size', - 'fa_bits', - 'fa_width', - 'fa_height', - 'fa_metadata', - 'fa_media_type', - 'fa_major_mime', - 'fa_minor_mime', - 'fa_user', - 'fa_user_text', - 'fa_actor' => 'NULL', - 'fa_timestamp', - 'fa_deleted', - 'fa_deleted_timestamp', /* Used by LocalFileRestoreBatch */ - 'fa_sha1', - ] + MediaWikiServices::getInstance()->getCommentStore()->getFields( 'fa_description' ); - } - /** * Return the tables, fields, and join conditions to be selected to create * a new archivedfile object. @@ -479,7 +438,9 @@ class ArchivedFile { function pageCount() { if ( !isset( $this->pageCount ) ) { // @FIXME: callers expect File objects + // @phan-suppress-next-line PhanTypeMismatchArgument if ( $this->getHandler() && $this->handler->isMultiPage( $this ) ) { + // @phan-suppress-next-line PhanTypeMismatchArgument $this->pageCount = $this->handler->pageCount( $this ); } else { $this->pageCount = false; diff --git a/includes/filerepo/file/File.php b/includes/filerepo/file/File.php index ee7ee6f90d..73b08e6a34 100644 --- a/includes/filerepo/file/File.php +++ b/includes/filerepo/file/File.php @@ -305,7 +305,7 @@ abstract class File implements IDBAccessObject { * @return string */ public function getName() { - if ( !isset( $this->name ) ) { + if ( $this->name === null ) { $this->assertRepoDefined(); $this->name = $this->repo->getNameFromTitle( $this->title ); } @@ -1172,6 +1172,7 @@ abstract class File implements IDBAccessObject { $thumb = false; } elseif ( $thumb->isError() ) { // transform error /** @var MediaTransformError $thumb */ + '@phan-var MediaTransformError $thumb'; $this->lastError = $thumb->toText(); // Ignore errors if requested if ( $wgIgnoreImageErrors && !( $flags & self::RENDER_NOW ) ) { @@ -1352,7 +1353,8 @@ abstract class File implements IDBAccessObject { */ protected function makeTransformTmpFile( $thumbPath ) { $thumbExt = FileBackend::extensionFromPath( $thumbPath ); - return TempFSFile::factory( 'transform_', $thumbExt, wfTempDir() ); + return MediaWikiServices::getInstance()->getTempFSFileFactory() + ->newTempFSFile( 'transform_', $thumbExt ); } /** @@ -1465,13 +1467,15 @@ abstract class File implements IDBAccessObject { // Delete thumbnails and refresh file metadata cache $this->purgeCache(); $this->purgeDescription(); - // Purge cache of all pages using this file $title = $this->getTitle(); if ( $title ) { - DeferredUpdates::addUpdate( - new HTMLCacheUpdate( $title, 'imagelinks', 'file-purge' ) + $job = HTMLCacheUpdateJob::newForBacklinks( + $title, + 'imagelinks', + [ 'causeAction' => 'file-purge' ] ); + JobQueueGroup::singleton()->lazyPush( $job ); } } @@ -1520,7 +1524,7 @@ abstract class File implements IDBAccessObject { * @return string */ function getHashPath() { - if ( !isset( $this->hashPath ) ) { + if ( $this->hashPath === null ) { $this->assertRepoDefined(); $this->hashPath = $this->repo->getHashPath( $this->getName() ); } @@ -2038,7 +2042,7 @@ abstract class File implements IDBAccessObject { * Get the URL of the image description page. May return false if it is * unknown or not applicable. * - * @return string + * @return string|bool */ function getDescriptionUrl() { if ( $this->repo ) { diff --git a/includes/filerepo/file/ForeignAPIFile.php b/includes/filerepo/file/ForeignAPIFile.php index ab8ef2f8de..99ead166ed 100644 --- a/includes/filerepo/file/ForeignAPIFile.php +++ b/includes/filerepo/file/ForeignAPIFile.php @@ -75,6 +75,7 @@ class ForeignAPIFile extends File { ? count( $data['query']['redirects'] ) - 1 : -1; if ( $lastRedirect >= 0 ) { + // @phan-suppress-next-line PhanTypeArraySuspiciousNullable $newtitle = Title::newFromText( $data['query']['redirects'][$lastRedirect]['to'] ); $img = new self( $newtitle, $repo, $info, true ); $img->redirectedFrom( $title->getDBkey() ); diff --git a/includes/filerepo/file/LocalFile.php b/includes/filerepo/file/LocalFile.php index 54fc251f4b..6d2943381f 100644 --- a/includes/filerepo/file/LocalFile.php +++ b/includes/filerepo/file/LocalFile.php @@ -202,44 +202,6 @@ class LocalFile extends File { } } - /** - * Fields in the image table - * @deprecated since 1.31, use self::getQueryInfo() instead. - * @return string[] - */ - static function selectFields() { - global $wgActorTableSchemaMigrationStage; - - wfDeprecated( __METHOD__, '1.31' ); - if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_READ_NEW ) { - // If code is using this instead of self::getQueryInfo(), there's a - // decent chance it's going to try to directly access - // $row->img_user or $row->img_user_text and we can't give it - // useful values here once those aren't being used anymore. - throw new BadMethodCallException( - 'Cannot use ' . __METHOD__ - . ' when $wgActorTableSchemaMigrationStage has SCHEMA_COMPAT_READ_NEW' - ); - } - - return [ - 'img_name', - 'img_size', - 'img_width', - 'img_height', - 'img_metadata', - 'img_bits', - 'img_media_type', - 'img_major_mime', - 'img_minor_mime', - 'img_user', - 'img_user_text', - 'img_actor' => 'NULL', - 'img_timestamp', - 'img_sha1', - ] + MediaWikiServices::getInstance()->getCommentStore()->getFields( 'img_description' ); - } - /** * Return the tables, fields, and join conditions to be selected to create * a new localfile object. @@ -344,6 +306,7 @@ class LocalFile extends File { $this->loadFromDB( self::READ_NORMAL ); $fields = $this->getCacheFields( '' ); + $cacheVal = []; $cacheVal['fileExists'] = $this->fileExists; if ( $this->fileExists ) { foreach ( $fields as $field ) { @@ -489,6 +452,10 @@ class LocalFile extends File { * This covers fields that are sometimes not cached. */ protected function loadExtraFromDB() { + if ( !$this->title ) { + return; // Avoid hard failure when the file does not exist. T221812 + } + $fname = static::class . '::' . __FUNCTION__; # Unconditionally set loaded=true, we don't want the accessors constantly rechecking @@ -894,12 +861,24 @@ class LocalFile extends File { function getUser( $type = 'text' ) { $this->load(); - if ( $type === 'object' ) { - return $this->user; - } elseif ( $type === 'text' ) { - return $this->user->getName(); - } elseif ( $type === 'id' ) { - return $this->user->getId(); + if ( !$this->user ) { + // If the file does not exist, $this->user will be null, see T221812. + // Note: 'Unknown user' this is a reserved user name. + if ( $type === 'object' ) { + return User::newFromName( 'Unknown user', false ); + } elseif ( $type === 'text' ) { + return 'Unknown user'; + } elseif ( $type === 'id' ) { + return 0; + } + } else { + if ( $type === 'object' ) { + return $this->user; + } elseif ( $type === 'text' ) { + return $this->user->getName(); + } elseif ( $type === 'id' ) { + return $this->user->getId(); + } } throw new MWException( "Unknown type '$type'." ); @@ -913,9 +892,13 @@ class LocalFile extends File { * @since 1.27 */ public function getDescriptionShortUrl() { + if ( !$this->title ) { + return null; // Avoid hard failure when the file does not exist. T221812 + } + $pageId = $this->title->getArticleID(); - if ( $pageId !== null ) { + if ( $pageId ) { $url = $this->repo->makeUrl( [ 'curid' => $pageId ] ); if ( $url !== false ) { return $url; @@ -1079,6 +1062,7 @@ class LocalFile extends File { /** * Delete cached transformed files for the current version only. * @param array $options + * @phan-param array{forThumbRefresh?:bool} $options */ public function purgeThumbnails( $options = [] ) { $files = $this->getThumbnails(); @@ -1181,6 +1165,10 @@ class LocalFile extends File { * @return OldLocalFile[] */ function getHistory( $limit = null, $start = null, $end = null, $inc = true ) { + if ( !$this->exists() ) { + return []; // Avoid hard failure when the file does not exist. T221812 + } + $dbr = $this->repo->getReplicaDB(); $oldFileQuery = OldLocalFile::getQueryInfo(); @@ -1234,9 +1222,13 @@ class LocalFile extends File { * 0 return line for current version * 1 query for old versions, return first one * 2, ... return next old version from above query - * @return bool + * @return stdClass|bool */ public function nextHistoryLine() { + if ( !$this->exists() ) { + return false; // Avoid hard failure when the file does not exist. T221812 + } + # Polymorphic function name to distinguish foreign and local fetches $fname = static::class . '::' . __FUNCTION__; @@ -1447,8 +1439,6 @@ class LocalFile extends File { $oldver, $comment, $pageText, $props = false, $timestamp = false, $user = null, $tags = [], $createNullRevision = true, $revert = false ) { - global $wgActorTableSchemaMigrationStage; - if ( is_null( $user ) ) { global $wgUser; $user = $wgUser; @@ -1551,40 +1541,10 @@ class LocalFile extends File { 'oi_major_mime' => 'img_major_mime', 'oi_minor_mime' => 'img_minor_mime', 'oi_sha1' => 'img_sha1', + 'oi_actor' => 'img_actor', ]; $joins = []; - if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_OLD ) { - $fields['oi_user'] = 'img_user'; - $fields['oi_user_text'] = 'img_user_text'; - } - if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_NEW ) { - $fields['oi_actor'] = 'img_actor'; - } - - if ( - ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_BOTH ) === SCHEMA_COMPAT_WRITE_BOTH - ) { - // Upgrade any rows that are still old-style. Otherwise an upgrade - // might be missed if a deletion happens while the migration script - // is running. - $res = $dbw->select( - [ 'image' ], - [ 'img_name', 'img_user', 'img_user_text' ], - [ 'img_name' => $this->getName(), 'img_actor' => 0 ], - __METHOD__ - ); - foreach ( $res as $row ) { - $actorId = User::newFromAnyId( $row->img_user, $row->img_user_text, null )->getActorId( $dbw ); - $dbw->update( - 'image', - [ 'img_actor' => $actorId ], - [ 'img_name' => $row->img_name, 'img_actor' => 0 ], - __METHOD__ - ); - } - } - # (T36993) Note: $oldver can be empty here, if the previous # version of the file was broken. Allow registration of the new # version to continue anyway, because that's better than having @@ -1758,7 +1718,7 @@ class LocalFile extends File { # Add change tags, if any if ( $tags ) { - $logEntry->setTags( $tags ); + $logEntry->addTags( $tags ); } # Uploads can be patrolled @@ -1865,6 +1825,7 @@ class LocalFile extends File { : FSFile::getSha1Base36FromPath( $srcPath ); /** @var FileBackendDBRepoWrapper $wrapperBackend */ $wrapperBackend = $repo->getBackend(); + '@phan-var FileBackendDBRepoWrapper $wrapperBackend'; $dst = $wrapperBackend->getPathForSHA1( $sha1 ); $status = $repo->quickImport( $src, $dst ); if ( $flags & File::DELETE_SOURCE ) { @@ -1923,10 +1884,9 @@ class LocalFile extends File { wfDebugLog( 'imagemove', "Finished moving {$this->name}" ); - // Purge the source and target files... + // Purge the source and target files outside the transaction... $oldTitleFile = $localRepo->newFile( $this->title ); $newTitleFile = $localRepo->newFile( $target ); - // To avoid slow purges in the transaction, move them outside... DeferredUpdates::addUpdate( new AutoCommitUpdate( $this->getRepo()->getMasterDB(), @@ -1934,6 +1894,8 @@ class LocalFile extends File { function () use ( $oldTitleFile, $newTitleFile, $archiveNames ) { $oldTitleFile->purgeEverything(); foreach ( $archiveNames as $archiveName ) { + /** @var OldLocalFile $oldTitleFile */ + '@phan-var OldLocalFile $oldTitleFile'; $oldTitleFile->purgeOldThumbnails( $archiveName ); } $newTitleFile->purgeEverything(); @@ -1946,8 +1908,8 @@ class LocalFile extends File { // Now switch the object $this->title = $target; // Force regeneration of the name and hashpath - unset( $this->name ); - unset( $this->hashPath ); + $this->name = null; + $this->hashPath = null; } return $status; @@ -2092,9 +2054,13 @@ class LocalFile extends File { /** * Get the URL of the file description page. - * @return string + * @return string|bool */ function getDescriptionUrl() { + if ( !$this->title ) { + return false; // Avoid hard failure when the file does not exist. T221812 + } + return $this->title->getLocalURL(); } @@ -2107,6 +2073,10 @@ class LocalFile extends File { * @return string|false */ function getDescriptionText( Language $lang = null ) { + if ( !$this->title ) { + return false; // Avoid hard failure when the file does not exist. T221812 + } + $store = MediaWikiServices::getInstance()->getRevisionStore(); $revision = $store->getRevisionByTitle( $this->title, 0, Revision::READ_NORMAL ); if ( !$revision ) { @@ -2156,6 +2126,10 @@ class LocalFile extends File { * @return bool|string */ public function getDescriptionTouched() { + if ( !$this->exists() ) { + return false; // Avoid hard failure when the file does not exist. T221812 + } + // The DB lookup might return false, e.g. if the file was just deleted, or the shared DB repo // itself gets it from elsewhere. To avoid repeating the DB lookups in such a case, we // need to differentiate between null (uninitialized) and false (failed to load). diff --git a/includes/filerepo/file/LocalFileDeleteBatch.php b/includes/filerepo/file/LocalFileDeleteBatch.php index 61faa09535..f363beb3b9 100644 --- a/includes/filerepo/file/LocalFileDeleteBatch.php +++ b/includes/filerepo/file/LocalFileDeleteBatch.php @@ -22,7 +22,7 @@ */ use MediaWiki\MediaWikiServices; -use MediaWiki\Storage\RevisionRecord; +use MediaWiki\Revision\RevisionRecord; /** * Helper class for file deletion @@ -178,8 +178,6 @@ class LocalFileDeleteBatch { } protected function doDBInserts() { - global $wgActorTableSchemaMigrationStage; - $now = time(); $dbw = $this->file->repo->getMasterDB(); @@ -225,7 +223,8 @@ class LocalFileDeleteBatch { 'fa_minor_mime' => 'img_minor_mime', 'fa_description_id' => 'img_description_id', 'fa_timestamp' => 'img_timestamp', - 'fa_sha1' => 'img_sha1' + 'fa_sha1' => 'img_sha1', + 'fa_actor' => 'img_actor', ]; $joins = []; @@ -234,37 +233,6 @@ class LocalFileDeleteBatch { $commentStore->insert( $dbw, 'fa_deleted_reason', $this->reason ) ); - if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_OLD ) { - $fields['fa_user'] = 'img_user'; - $fields['fa_user_text'] = 'img_user_text'; - } - if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_NEW ) { - $fields['fa_actor'] = 'img_actor'; - } - - if ( - ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_BOTH ) === SCHEMA_COMPAT_WRITE_BOTH - ) { - // Upgrade any rows that are still old-style. Otherwise an upgrade - // might be missed if a deletion happens while the migration script - // is running. - $res = $dbw->select( - [ 'image' ], - [ 'img_name', 'img_user', 'img_user_text' ], - [ 'img_name' => $this->file->getName(), 'img_actor' => 0 ], - __METHOD__ - ); - foreach ( $res as $row ) { - $actorId = User::newFromAnyId( $row->img_user, $row->img_user_text, null )->getActorId( $dbw ); - $dbw->update( - 'image', - [ 'img_actor' => $actorId ], - [ 'img_name' => $row->img_name, 'img_actor' => 0 ], - __METHOD__ - ); - } - } - $dbw->insertSelect( 'filearchive', $tables, $fields, [ 'img_name' => $this->file->getName() ], __METHOD__, [], [], $joins ); } diff --git a/includes/filerepo/file/LocalFileLockError.php b/includes/filerepo/file/LocalFileLockError.php index 7cfc8c22c8..b76f3da2a0 100644 --- a/includes/filerepo/file/LocalFileLockError.php +++ b/includes/filerepo/file/LocalFileLockError.php @@ -29,9 +29,9 @@ class LocalFileLockError extends ErrorPageError { ); } - public function report() { + public function report( $action = self::SEND_OUTPUT ) { global $wgOut; $wgOut->setStatusCode( 429 ); - parent::report(); + parent::report( $action ); } } diff --git a/includes/filerepo/file/LocalFileMoveBatch.php b/includes/filerepo/file/LocalFileMoveBatch.php index 21980b90eb..0cdc2d5de1 100644 --- a/includes/filerepo/file/LocalFileMoveBatch.php +++ b/includes/filerepo/file/LocalFileMoveBatch.php @@ -46,6 +46,24 @@ class LocalFileMoveBatch { /** @var IDatabase */ protected $db; + /** @var string */ + protected $oldHash; + + /** @var string */ + protected $newHash; + + /** @var string */ + protected $oldName; + + /** @var string */ + protected $newName; + + /** @var string */ + protected $oldRel; + + /** @var string */ + protected $newRel; + /** * @param File $file * @param Title $target diff --git a/includes/filerepo/file/OldLocalFile.php b/includes/filerepo/file/OldLocalFile.php index 584e001211..f5b7d4384c 100644 --- a/includes/filerepo/file/OldLocalFile.php +++ b/includes/filerepo/file/OldLocalFile.php @@ -106,46 +106,6 @@ class OldLocalFile extends LocalFile { } } - /** - * Fields in the oldimage table - * @deprecated since 1.31, use self::getQueryInfo() instead. - * @return string[] - */ - static function selectFields() { - global $wgActorTableSchemaMigrationStage; - - wfDeprecated( __METHOD__, '1.31' ); - if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_READ_NEW ) { - // If code is using this instead of self::getQueryInfo(), there's a - // decent chance it's going to try to directly access - // $row->oi_user or $row->oi_user_text and we can't give it - // useful values here once those aren't being used anymore. - throw new BadMethodCallException( - 'Cannot use ' . __METHOD__ - . ' when $wgActorTableSchemaMigrationStage has SCHEMA_COMPAT_READ_NEW' - ); - } - - return [ - 'oi_name', - 'oi_archive_name', - 'oi_size', - 'oi_width', - 'oi_height', - 'oi_metadata', - 'oi_bits', - 'oi_media_type', - 'oi_major_mime', - 'oi_minor_mime', - 'oi_user', - 'oi_user_text', - 'oi_actor' => 'NULL', - 'oi_timestamp', - 'oi_deleted', - 'oi_sha1', - ] + MediaWikiServices::getInstance()->getCommentStore()->getFields( 'oi_description' ); - } - /** * Return the tables, fields, and join conditions to be selected to create * a new oldlocalfile object. diff --git a/includes/filerepo/file/UnregisteredLocalFile.php b/includes/filerepo/file/UnregisteredLocalFile.php index 2865ce5eff..4292ea0ace 100644 --- a/includes/filerepo/file/UnregisteredLocalFile.php +++ b/includes/filerepo/file/UnregisteredLocalFile.php @@ -43,7 +43,7 @@ class UnregisteredLocalFile extends File { /** @var bool|string */ protected $mime; - /** @var array Dimension data */ + /** @var array[]|bool[] Dimension data */ protected $dims; /** @var bool|string Handler-specific metadata which will be saved in the img_metadata field */ @@ -108,7 +108,7 @@ class UnregisteredLocalFile extends File { /** * @param int $page - * @return bool + * @return array|bool */ private function cachePageDimensions( $page = 1 ) { $page = (int)$page; diff --git a/includes/gallery/ImageGalleryBase.php b/includes/gallery/ImageGalleryBase.php index 06e12710b6..4781a489f5 100644 --- a/includes/gallery/ImageGalleryBase.php +++ b/includes/gallery/ImageGalleryBase.php @@ -75,21 +75,30 @@ abstract class ImageGalleryBase extends ContextSource { protected $mHideBadImages; /** - * @var Parser Registered parser object for output callbacks + * @var Parser|false Registered parser object for output callbacks */ public $mParser; /** - * @var Title Contextual title, used when images are being screened against + * @var Title|null Contextual title, used when images are being screened against * the bad image list */ - protected $contextTitle = false; + protected $contextTitle = null; /** @var array */ protected $mAttribs = []; - /** @var bool */ - private static $modeMapping = false; + /** @var int */ + protected $mPerRow; + + /** @var int */ + protected $mWidths; + + /** @var int */ + protected $mHeights; + + /** @var array */ + private static $modeMapping; /** * Get a new image gallery. This is the method other callers @@ -121,7 +130,7 @@ abstract class ImageGalleryBase extends ContextSource { } private static function loadModes() { - if ( self::$modeMapping === false ) { + if ( self::$modeMapping === null ) { self::$modeMapping = [ 'traditional' => TraditionalImageGallery::class, 'nolines' => NolinesImageGallery::class, @@ -363,7 +372,7 @@ abstract class ImageGalleryBase extends ContextSource { /** * Set the contextual title * - * @param Title $title Contextual title + * @param Title|null $title Contextual title */ public function setContextTitle( $title ) { $this->contextTitle = $title; @@ -372,12 +381,10 @@ abstract class ImageGalleryBase extends ContextSource { /** * Get the contextual title, if applicable * - * @return Title|bool Title or false + * @return Title|null */ public function getContextTitle() { - return is_object( $this->contextTitle ) && $this->contextTitle instanceof Title - ? $this->contextTitle - : false; + return $this->contextTitle; } /** diff --git a/includes/gallery/TraditionalImageGallery.php b/includes/gallery/TraditionalImageGallery.php index d25d9aa861..fadd5878ba 100644 --- a/includes/gallery/TraditionalImageGallery.php +++ b/includes/gallery/TraditionalImageGallery.php @@ -111,8 +111,8 @@ class TraditionalImageGallery extends ImageGalleryBase { if ( $this->mParser instanceof Parser ) { $this->mParser->addTrackingCategory( 'broken-file-category' ); } - } elseif ( $this->mHideBadImages - && wfIsBadImage( $nt->getDBkey(), $this->getContextTitle() ) + } elseif ( $this->mHideBadImages && MediaWikiServices::getInstance()->getBadFileLookup() + ->isBadFile( $nt->getDBkey(), $this->getContextTitle() ) ) { # The image is blacklisted, just show it as a text link. $thumbhtml = "\n\t\t\t" . '
false ]; /** diff --git a/includes/http/HttpRequestFactory.php b/includes/http/HttpRequestFactory.php index 8e5567b001..84e7b739bd 100644 --- a/includes/http/HttpRequestFactory.php +++ b/includes/http/HttpRequestFactory.php @@ -37,8 +37,8 @@ class HttpRequestFactory { * Generate a new MWHttpRequest object * @param string $url Url to use * @param array $options Possible keys for the array: - * - timeout Timeout length in seconds - * - connectTimeout Timeout for connection, in seconds (curl only) + * - timeout Timeout length in seconds or 'default' + * - connectTimeout Timeout for connection, in seconds (curl only) or 'default' * - postData An array of key-value pairs or a url-encoded form data * - proxy The proxy to use. * Otherwise it will use $wgHTTPProxy (if set) @@ -58,6 +58,9 @@ class HttpRequestFactory { * - password Password for HTTP Basic Authentication * - originalRequest Information about the original request (as a WebRequest object or * an associative array with 'ip' and 'userAgent'). + * @codingStandardsIgnoreStart + * @phan-param array{timeout?:int|string,connectTimeout?:int|string,postData?:array,proxy?:string,noProxy?:bool,sslVerifyHost?:bool,sslVerifyCert?:bool,caInfo?:string,maxRedirects?:int,followRedirects?:bool,userAgent?:string,method?:string,logger?:\Psr\Log\LoggerInterface,username?:string,password?:string,originalRequest?:\WebRequest|array{ip:string,userAgent:string}} $options + * @codingStandardsIgnoreEnd * @param string $caller The method making this request, for profiling * @throws RuntimeException * @return MWHttpRequest diff --git a/includes/http/MWCallbackStream.php b/includes/http/MWCallbackStream.php index a4120a3578..13ab9b7caf 100644 --- a/includes/http/MWCallbackStream.php +++ b/includes/http/MWCallbackStream.php @@ -29,6 +29,7 @@ use GuzzleHttp\Psr7\StreamDecoratorTrait; * * @private for use by GuzzleHttpRequest only * @since 1.33 + * @property StreamInterface $stream Defined in StreamDecoratorTrait via @property, not read by phan */ class MWCallbackStream implements StreamInterface { use StreamDecoratorTrait; diff --git a/includes/http/MWHttpRequest.php b/includes/http/MWHttpRequest.php index 41ea1dce35..0f3096ef96 100644 --- a/includes/http/MWHttpRequest.php +++ b/includes/http/MWHttpRequest.php @@ -46,6 +46,7 @@ abstract class MWHttpRequest implements LoggerAwareInterface { protected $sslVerifyCert = true; protected $caInfo = null; protected $method = "GET"; + /** @var array */ protected $reqHeaders = []; protected $url; protected $parsedUrl; @@ -63,6 +64,7 @@ abstract class MWHttpRequest implements LoggerAwareInterface { protected $headerList = []; protected $respVersion = "0.9"; protected $respStatus = "200 Ok"; + /** @var string[][] */ protected $respHeaders = []; /** @var StatusValue */ @@ -86,6 +88,9 @@ abstract class MWHttpRequest implements LoggerAwareInterface { /** * @param string $url Url to use. If protocol-relative, will be expanded to an http:// URL * @param array $options (optional) extra params to pass (see HttpRequestFactory::create()) + * @codingStandardsIgnoreStart + * @phan-param array{timeout?:int|string,connectTimeout?:int|string,postData?:array,proxy?:string,noProxy?:bool,sslVerifyHost?:bool,sslVerifyCert?:bool,caInfo?:string,maxRedirects?:int,followRedirects?:bool,userAgent?:string,logger?:LoggerInterface,username?:string,password?:string,originalRequest?:WebRequest|array{ip:string,userAgent:string},method?:string} $options + * @codingStandardsIgnoreEnd * @param string $caller The method making this request, for profiling * @param Profiler|null $profiler An instance of the profiler for profiling, or null * @throws Exception @@ -139,6 +144,7 @@ abstract class MWHttpRequest implements LoggerAwareInterface { // ensure that MWHttpRequest::method is always // uppercased. T38137 if ( $o == 'method' ) { + // @phan-suppress-next-line PhanTypeInvalidDimOffset $options[$o] = strtoupper( $options[$o] ); } $this->$o = $options[$o]; @@ -366,7 +372,7 @@ abstract class MWHttpRequest implements LoggerAwareInterface { /** * Take care of whatever is necessary to perform the URI request. * - * @return StatusValue + * @return Status * @note currently returns Status for B/C */ public function execute() { diff --git a/includes/import/ImportStreamSource.php b/includes/import/ImportStreamSource.php index e6936cb2e3..81e414e931 100644 --- a/includes/import/ImportStreamSource.php +++ b/includes/import/ImportStreamSource.php @@ -30,6 +30,12 @@ use MediaWiki\MediaWikiServices; * @ingroup SpecialPage */ class ImportStreamSource implements ImportSource { + /** @var resource */ + private $mHandle; + + /** + * @param resource $handle + */ function __construct( $handle ) { $this->mHandle = $handle; } diff --git a/includes/import/ImportStringSource.php b/includes/import/ImportStringSource.php index 85983b1a11..b75ea1a192 100644 --- a/includes/import/ImportStringSource.php +++ b/includes/import/ImportStringSource.php @@ -32,6 +32,15 @@ * @ingroup SpecialPage */ class ImportStringSource implements ImportSource { + /** @var string */ + private $mString; + + /** @var bool */ + private $mRead; + + /** + * @param string $string + */ function __construct( $string ) { $this->mString = $string; $this->mRead = false; diff --git a/includes/import/ImportableOldRevision.php b/includes/import/ImportableOldRevision.php index 6d1e24264c..8cfb6052d1 100644 --- a/includes/import/ImportableOldRevision.php +++ b/includes/import/ImportableOldRevision.php @@ -65,4 +65,10 @@ interface ImportableOldRevision { */ public function getSha1Base36(); + /** + * @since 1.34 + * @return string[] + */ + public function getTags(); + } diff --git a/includes/import/ImportableOldRevisionImporter.php b/includes/import/ImportableOldRevisionImporter.php index ad62e163f0..821d6f6d37 100644 --- a/includes/import/ImportableOldRevisionImporter.php +++ b/includes/import/ImportableOldRevisionImporter.php @@ -129,6 +129,11 @@ class ImportableOldRevisionImporter implements OldRevisionImporter { $revision->insertOn( $dbw ); $changed = $page->updateIfNewerOn( $dbw, $revision ); + $tags = $importableRevision->getTags(); + if ( $tags !== [] ) { + ChangeTags::addTags( $tags, null, $revision->getId() ); + } + if ( $changed !== false && $this->doUpdates ) { $this->logger->debug( __METHOD__ . ": running updates\n" ); // countable/oldcountable stuff is handled in WikiImporter::finishImportPage diff --git a/includes/import/WikiImporter.php b/includes/import/WikiImporter.php index 68f5b9b8bf..6eba4f3a3a 100644 --- a/includes/import/WikiImporter.php +++ b/includes/import/WikiImporter.php @@ -33,7 +33,8 @@ use MediaWiki\MediaWikiServices; * @ingroup SpecialPage */ class WikiImporter { - private $reader = null; + /** @var XMLReader */ + private $reader; private $foreignNamespaces = null; private $mLogItemCallback, $mUploadCallback, $mRevisionCallback, $mPageCallback; private $mSiteInfoCallback, $mPageOutCallback; @@ -738,6 +739,9 @@ class WikiImporter { return $this->logItemCallback( $revision ); } + /** + * @suppress PhanTypeInvalidDimOffset Phan not reading the reference inside the hook + */ private function handlePage() { // Handle page data. $this->debug( "Enter page handler." ); diff --git a/includes/import/WikiRevision.php b/includes/import/WikiRevision.php index e36d673499..e0799c0909 100644 --- a/includes/import/WikiRevision.php +++ b/includes/import/WikiRevision.php @@ -144,6 +144,12 @@ class WikiRevision implements ImportableUploadRevision, ImportableOldRevision { */ public $sha1base36 = false; + /** + * @since 1.34 + * @var string[] + */ + protected $tags = []; + /** * @since 1.17 * @var string @@ -179,9 +185,16 @@ class WikiRevision implements ImportableUploadRevision, ImportableOldRevision { /** @var bool */ private $mNoUpdates = false; - /** @var Config $config */ + /** + * @deprecated since 1.31, along with self::downloadSource() + * @var Config $config + */ private $config; + /** + * @param Config $config Deprecated since 1.31, along with self::downloadSource(). Just pass an + * empty HashConfig. + */ public function __construct( Config $config ) { $this->config = $config; } @@ -310,6 +323,14 @@ class WikiRevision implements ImportableUploadRevision, ImportableOldRevision { $this->sha1base36 = $sha1base36; } + /** + * @since 1.34 + * @param string[] $tags + */ + public function setTags( array $tags ) { + $this->tags = $tags; + } + /** * @since 1.12.2 * @param string $filename @@ -509,6 +530,14 @@ class WikiRevision implements ImportableUploadRevision, ImportableOldRevision { return false; } + /** + * @since 1.34 + * @return string[] + */ + public function getTags() { + return $this->tags; + } + /** * @since 1.17 * @return string diff --git a/includes/installer/CliInstaller.php b/includes/installer/CliInstaller.php index 99d594df97..0ff34b086f 100644 --- a/includes/installer/CliInstaller.php +++ b/includes/installer/CliInstaller.php @@ -120,7 +120,11 @@ class CliInstaller extends Installer { } $this->setVar( '_Extensions', $status->value ); } elseif ( isset( $options['with-extensions'] ) ) { - $this->setVar( '_Extensions', array_keys( $this->findExtensions() ) ); + $status = $this->findExtensions(); + if ( !$status->isOK() ) { + throw new InstallException( $status ); + } + $this->setVar( '_Extensions', array_keys( $status->value ) ); } // Set up the default skins @@ -131,7 +135,11 @@ class CliInstaller extends Installer { } $skins = $status->value; } else { - $skins = array_keys( $this->findExtensions( 'skins' ) ); + $status = $this->findExtensions( 'skins' ); + if ( !$status->isOK() ) { + throw new InstallException( $status ); + } + $skins = array_keys( $status->value ); } $this->setVar( '_Skins', $skins ); @@ -190,7 +198,7 @@ class CliInstaller extends Installer { // PerformInstallation bails on a fatal, so make sure the last item // completed before giving 'next.' Likewise, only provide back on failure $lastStepStatus = end( $result ); - if ( $lastStepStatus->isOk() ) { + if ( $lastStepStatus->isOK() ) { return Status::newGood(); } else { return $lastStepStatus; diff --git a/includes/installer/DatabaseInstaller.php b/includes/installer/DatabaseInstaller.php index ba5da6de50..ce7e29ddb5 100644 --- a/includes/installer/DatabaseInstaller.php +++ b/includes/installer/DatabaseInstaller.php @@ -177,6 +177,7 @@ abstract class DatabaseInstaller { * This will return a cached connection if one is available. * * @return Status + * @suppress PhanUndeclaredMethod */ public function getConnection() { if ( $this->db ) { @@ -341,6 +342,7 @@ abstract class DatabaseInstaller { public function setupSchemaVars() { $status = $this->getConnection(); if ( $status->isOK() ) { + // @phan-suppress-next-line PhanUndeclaredMethod $status->value->setSchemaVars( $this->getSchemaVars() ); } else { $msg = __METHOD__ . ': unexpected error while establishing' @@ -447,8 +449,7 @@ abstract class DatabaseInstaller { * @return string */ public function getReadableName() { - // Messages: config-type-mysql, config-type-postgres, config-type-sqlite, - // config-type-oracle + // Messages: config-type-mysql, config-type-postgres, config-type-sqlite return wfMessage( 'config-type-' . $this->getName() )->text(); } @@ -689,7 +690,7 @@ abstract class DatabaseInstaller { $this->getPasswordBox( 'wgDBpassword', 'config-db-password' ) . $this->parent->getHelpBox( 'config-db-web-help' ); if ( $noCreateMsg ) { - $s .= $this->parent->getWarningBox( wfMessage( $noCreateMsg )->plain() ); + $s .= Html::warningBox( wfMessage( $noCreateMsg )->plain(), 'config-warning-box' ); } else { $s .= $this->getCheckBox( '_CreateDBAccount', 'config-db-web-create' ); } diff --git a/includes/installer/DatabaseUpdater.php b/includes/installer/DatabaseUpdater.php index de7a347120..3412aef2ef 100644 --- a/includes/installer/DatabaseUpdater.php +++ b/includes/installer/DatabaseUpdater.php @@ -419,6 +419,7 @@ abstract class DatabaseUpdater { foreach ( $updates as $funcList ) { list( $func, $args, $origParams ) = $funcList; + // @phan-suppress-next-line PhanUndeclaredInvokeInCallable $func( ...$args ); flush(); $this->updatesSkipped[] = $origParams; @@ -1232,6 +1233,7 @@ abstract class DatabaseUpdater { $cl = $this->maintenance->runChild( RebuildLocalisationCache::class, 'rebuildLocalisationCache.php' ); + '@phan-var RebuildLocalisationCache $cl'; $this->output( "Rebuilding localisation cache...\n" ); $cl->setForce(); $cl->execute(); @@ -1291,6 +1293,7 @@ abstract class DatabaseUpdater { $task = $this->maintenance->runChild( MigrateImageCommentTemp::class, 'migrateImageCommentTemp.php' ); + // @phan-suppress-next-line PhanUndeclaredMethod $task->setForce(); $ok = $task->execute(); $this->output( $ok ? "done.\n" : "errors were encountered.\n" ); @@ -1305,10 +1308,7 @@ abstract class DatabaseUpdater { * @since 1.31 */ protected function migrateActors() { - global $wgActorTableSchemaMigrationStage; - if ( ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_NEW ) && - !$this->updateRowExists( 'MigrateActors' ) - ) { + if ( !$this->updateRowExists( 'MigrateActors' ) ) { $this->output( "Migrating actors to the 'actor' table, printing progress markers. For large\n" . "databases, you may want to hit Ctrl-C and do this manually with\n" . @@ -1328,6 +1328,7 @@ abstract class DatabaseUpdater { if ( $this->db->fieldExists( 'archive', 'ar_text', __METHOD__ ) ) { $this->output( "Migrating archive ar_text to modern storage.\n" ); $task = $this->maintenance->runChild( MigrateArchiveText::class, 'migrateArchiveText.php' ); + // @phan-suppress-next-line PhanUndeclaredMethod $task->setForce(); if ( $task->execute() ) { $this->applyPatch( 'patch-drop-ar_text.sql', false, @@ -1397,4 +1398,43 @@ abstract class DatabaseUpdater { } } } + + /** + * Only run a function if the `actor` table does not exist + * + * The transition to the actor table is dropping several indexes (and a few + * fields) that old upgrades want to add. This function is used to prevent + * those from running to re-add things when the `actor` table exists, while + * still allowing them to run if this really is an upgrade from an old MW + * version. + * + * @since 1.34 + * @param string|array|static $func Normally this is the string naming the method on $this to + * call. It may also be an array callable. If passed $this, it's assumed to be a call from + * runUpdates() with $passSelf = true: $params[0] is assumed to be the real $func and $this + * is prepended to the rest of $params. + * @param mixed ...$params Parameters for `$func` + * @return mixed Whatever $func returns, or null when skipped. + */ + protected function ifNoActorTable( $func, ...$params ) { + if ( $this->tableExists( 'actor' ) ) { + return null; + } + + // Handle $passSelf from runUpdates(). + $passSelf = false; + if ( $func === $this ) { + $passSelf = true; + $func = array_shift( $params ); + } + + if ( !is_array( $func ) && method_exists( $this, $func ) ) { + $func = [ $this, $func ]; + } elseif ( $passSelf ) { + array_unshift( $params, $this ); + } + + // @phan-suppress-next-line PhanUndeclaredInvokeInCallable Phan is confused + return $func( ...$params ); + } } diff --git a/includes/installer/InstallDocFormatter.php b/includes/installer/InstallDocFormatter.php index 08cfd8689a..eb96e05941 100644 --- a/includes/installer/InstallDocFormatter.php +++ b/includes/installer/InstallDocFormatter.php @@ -21,6 +21,9 @@ */ class InstallDocFormatter { + /** @var string */ + private $text; + public static function format( $text ) { $obj = new self( $text ); diff --git a/includes/installer/Installer.php b/includes/installer/Installer.php index 5dc3aeca92..b830b7006a 100644 --- a/includes/installer/Installer.php +++ b/includes/installer/Installer.php @@ -731,6 +731,7 @@ abstract class Installer { if ( !$status->isOK() ) { return $status; } + // @phan-suppress-next-line PhanUndeclaredMethod $status->value->insert( 'site_stats', [ @@ -759,8 +760,7 @@ abstract class Installer { $allNames = []; - // Messages: config-type-mysql, config-type-postgres, config-type-oracle, - // config-type-sqlite + // Messages: config-type-mysql, config-type-postgres, config-type-sqlite foreach ( self::getDBTypes() as $name ) { $allNames[] = wfMessage( "config-type-$name" )->text(); } @@ -1089,14 +1089,16 @@ abstract class Installer { /** * Checks if suhosin.get.max_value_length is set, and if so generate - * a warning because it decreases ResourceLoader performance. + * a warning because it is incompatible with ResourceLoader. * @return bool */ protected function envCheckSuhosinMaxValueLength() { - $maxValueLength = ini_get( 'suhosin.get.max_value_length' ); - if ( $maxValueLength > 0 && $maxValueLength < 1024 ) { - // Only warn if the value is below the sane 1024 - $this->showMessage( 'config-suhosin-max-value-length', $maxValueLength ); + $currentValue = ini_get( 'suhosin.get.max_value_length' ); + $minRequired = 2000; + $recommended = 5000; + if ( $currentValue > 0 && $currentValue < $minRequired ) { + $this->showError( 'config-suhosin-max-value-length', $currentValue, $minRequired, $recommended ); + return false; } return true; @@ -1271,7 +1273,8 @@ abstract class Installer { * * @param string $directory Directory to search in, relative to $IP, must be either "extensions" * or "skins" - * @return array [ $extName => [ 'screenshots' => [ '...' ] ] + * @return Status An object containing an error list. If there were no errors, an associative + * array of information about the extension can be found in $status->value. */ public function findExtensions( $directory = 'extensions' ) { switch ( $directory ) { @@ -1290,33 +1293,43 @@ abstract class Installer { * * @param string $type Either "extension" or "skin" * @param string $directory Directory to search in, relative to $IP - * @return array [ $extName => [ 'screenshots' => [ '...' ] ] + * @return Status An object containing an error list. If there were no errors, an associative + * array of information about the extension can be found in $status->value. */ protected function findExtensionsByType( $type = 'extension', $directory = 'extensions' ) { if ( $this->getVar( 'IP' ) === null ) { - return []; + return Status::newGood( [] ); } $extDir = $this->getVar( 'IP' ) . '/' . $directory; if ( !is_readable( $extDir ) || !is_dir( $extDir ) ) { - return []; + return Status::newGood( [] ); } $dh = opendir( $extDir ); $exts = []; + $status = new Status; while ( ( $file = readdir( $dh ) ) !== false ) { - if ( !is_dir( "$extDir/$file" ) ) { + // skip non-dirs and hidden directories + if ( !is_dir( "$extDir/$file" ) || $file[0] === '.' ) { continue; } - $status = $this->getExtensionInfo( $type, $directory, $file ); - if ( $status->isOK() ) { - $exts[$file] = $status->value; + $extStatus = $this->getExtensionInfo( $type, $directory, $file ); + if ( $extStatus->isOK() ) { + $exts[$file] = $extStatus->value; + } elseif ( $extStatus->hasMessage( 'config-extension-not-found' ) ) { + // (T225512) The directory is not actually an extension. Downgrade to warning. + $status->warning( 'config-extension-not-found', $file ); + } else { + $status->merge( $extStatus ); } } closedir( $dh ); uksort( $exts, 'strnatcasecmp' ); - return $exts; + $status->value = $exts; + + return $status; } /** @@ -1417,11 +1430,16 @@ abstract class Installer { } elseif ( $e->missingExtensions || $e->missingSkins ) { // There's an extension missing in the dependency tree, // so add those to the dependency list and try again - return $this->readExtension( + $status = $this->readExtension( $fullJsonFile, array_merge( $extDeps, $e->missingExtensions ), array_merge( $skinDeps, $e->missingSkins ) ); + if ( !$status->isOK() && !$status->hasMessage( 'config-extension-dependency' ) ) { + $status = Status::newFatal( 'config-extension-dependency', + basename( dirname( $fullJsonFile ) ), $status->getMessage() ); + } + return $status; } // Some other kind of dependency error? return Status::newFatal( 'config-extension-dependency', @@ -1608,11 +1626,11 @@ abstract class Installer { // If we've hit some sort of fatal, we need to bail. // Callback already had a chance to do output above. - if ( !$status->isOk() ) { + if ( !$status->isOK() ) { break; } } - if ( $status->isOk() ) { + if ( $status->isOK() ) { $this->showMessage( 'config-install-db-success' ); diff --git a/includes/installer/LocalSettingsGenerator.php b/includes/installer/LocalSettingsGenerator.php index a2179c6e7f..6921361423 100644 --- a/includes/installer/LocalSettingsGenerator.php +++ b/includes/installer/LocalSettingsGenerator.php @@ -30,6 +30,7 @@ class LocalSettingsGenerator { protected $extensions = []; + protected $skins = []; protected $values = []; protected $groupPermissions = []; protected $dbSettings = ''; diff --git a/includes/installer/MysqlInstaller.php b/includes/installer/MysqlInstaller.php index 69d03bde6c..383f8d8fc6 100644 --- a/includes/installer/MysqlInstaller.php +++ b/includes/installer/MysqlInstaller.php @@ -131,6 +131,7 @@ class MysqlInstaller extends DatabaseInstaller { * @var Database $conn */ $conn = $status->value; + '@phan-var Database $conn'; // Check version return static::meetsMinimumRequirement( $conn->getServerVersion() ); diff --git a/includes/installer/MysqlUpdater.php b/includes/installer/MysqlUpdater.php index c33d3ddc3c..7d41d04616 100644 --- a/includes/installer/MysqlUpdater.php +++ b/includes/installer/MysqlUpdater.php @@ -29,6 +29,7 @@ use MediaWiki\MediaWikiServices; * * @ingroup Deployment * @since 1.17 + * @property Wikimedia\Rdbms\DatabaseMysqlBase $db */ class MysqlUpdater extends DatabaseUpdater { protected function getCoreUpdateList() { @@ -100,8 +101,10 @@ class MysqlUpdater extends DatabaseUpdater { [ 'addTable', 'querycache_info', 'patch-querycacheinfo.sql' ], [ 'addTable', 'filearchive', 'patch-filearchive.sql' ], [ 'addField', 'ipblocks', 'ipb_anon_only', 'patch-ipb_anon_only.sql' ], - [ 'addIndex', 'recentchanges', 'rc_ns_usertext', 'patch-recentchanges-utindex.sql' ], - [ 'addIndex', 'recentchanges', 'rc_user_text', 'patch-rc_user_text-index.sql' ], + [ 'ifNoActorTable', 'addIndex', 'recentchanges', 'rc_ns_usertext', + 'patch-recentchanges-utindex.sql' ], + [ 'ifNoActorTable', 'addIndex', 'recentchanges', 'rc_user_text', + 'patch-rc_user_text-index.sql' ], // 1.9 [ 'addField', 'user', 'user_newpass_time', 'patch-user_newpass_time.sql' ], @@ -129,9 +132,12 @@ class MysqlUpdater extends DatabaseUpdater { [ 'addField', 'ipblocks', 'ipb_block_email', 'patch-ipb_emailban.sql' ], [ 'doCategorylinksIndicesUpdate' ], [ 'addField', 'oldimage', 'oi_metadata', 'patch-oi_metadata.sql' ], - [ 'addIndex', 'archive', 'usertext_timestamp', 'patch-archive-user-index.sql' ], - [ 'addIndex', 'image', 'img_usertext_timestamp', 'patch-image-user-index.sql' ], - [ 'addIndex', 'oldimage', 'oi_usertext_timestamp', 'patch-oldimage-user-index.sql' ], + [ 'ifNoActorTable', 'addIndex', 'archive', 'usertext_timestamp', + 'patch-archive-user-index.sql' ], + [ 'ifNoActorTable', 'addIndex', 'image', 'img_usertext_timestamp', + 'patch-image-user-index.sql' ], + [ 'ifNoActorTable', 'addIndex', 'oldimage', 'oi_usertext_timestamp', + 'patch-oldimage-user-index.sql' ], [ 'addField', 'archive', 'ar_page_id', 'patch-archive-page_id.sql' ], [ 'addField', 'image', 'img_sha1', 'patch-img_sha1.sql' ], @@ -139,7 +145,7 @@ class MysqlUpdater extends DatabaseUpdater { [ 'addTable', 'protected_titles', 'patch-protected_titles.sql' ], // 1.13 - [ 'addField', 'ipblocks', 'ipb_by_text', 'patch-ipb_by_text.sql' ], + [ 'ifNoActorTable', 'addField', 'ipblocks', 'ipb_by_text', 'patch-ipb_by_text.sql' ], [ 'addTable', 'page_props', 'patch-page_props.sql' ], [ 'addTable', 'updatelog', 'patch-updatelog.sql' ], [ 'addTable', 'category', 'patch-category.sql' ], @@ -149,7 +155,7 @@ class MysqlUpdater extends DatabaseUpdater { [ 'doPopulateParentId' ], [ 'checkBin', 'protected_titles', 'pt_title', 'patch-pt_title-encoding.sql', ], [ 'doMaybeProfilingMemoryUpdate' ], - [ 'doFilearchiveIndicesUpdate' ], + [ 'ifNoActorTable', 'doFilearchiveIndicesUpdate' ], // 1.14 [ 'addField', 'site_stats', 'ss_active_users', 'patch-ss_active_users.sql' ], @@ -162,9 +168,9 @@ class MysqlUpdater extends DatabaseUpdater { // 1.16 [ 'addTable', 'user_properties', 'patch-user_properties.sql' ], [ 'addTable', 'log_search', 'patch-log_search.sql' ], - [ 'addField', 'logging', 'log_user_text', 'patch-log_user_text.sql' ], + [ 'ifNoActorTable', 'addField', 'logging', 'log_user_text', 'patch-log_user_text.sql' ], # listed separately from the previous update because 1.16 was released without this update - [ 'doLogUsertextPopulation' ], + [ 'ifNoActorTable', 'doLogUsertextPopulation' ], [ 'doLogSearchPopulation' ], [ 'addTable', 'l10n_cache', 'patch-l10n_cache.sql' ], [ 'dropIndex', 'change_tag', 'ct_rc_id', 'patch-change_tag-indexes.sql' ], @@ -239,9 +245,10 @@ class MysqlUpdater extends DatabaseUpdater { // 1.23 [ 'addField', 'recentchanges', 'rc_source', 'patch-rc_source.sql' ], - [ 'addIndex', 'logging', 'log_user_text_type_time', + [ 'ifNoActorTable', 'addIndex', 'logging', 'log_user_text_type_time', 'patch-logging_user_text_type_time_index.sql' ], - [ 'addIndex', 'logging', 'log_user_text_time', 'patch-logging_user_text_time_index.sql' ], + [ 'ifNoActorTable', 'addIndex', 'logging', 'log_user_text_time', + 'patch-logging_user_text_time_index.sql' ], [ 'addField', 'page', 'page_links_updated', 'patch-page_links_updated.sql' ], [ 'addField', 'user', 'user_password_expires', 'patch-user_password_expire.sql' ], @@ -287,13 +294,14 @@ class MysqlUpdater extends DatabaseUpdater { [ 'doNonUniquePlTlIl' ], [ 'addField', 'change_tag', 'ct_id', 'patch-change_tag-ct_id.sql' ], [ 'modifyField', 'recentchanges', 'rc_ip', 'patch-rc_ip_modify.sql' ], - [ 'addIndex', 'archive', 'usertext_timestamp', 'patch-rename-ar_usertext_timestamp.sql' ], + [ 'ifNoActorTable', 'addIndex', 'archive', 'usertext_timestamp', + 'patch-rename-ar_usertext_timestamp.sql' ], // 1.29 [ 'addField', 'externallinks', 'el_index_60', 'patch-externallinks-el_index_60.sql' ], [ 'dropIndex', 'user_groups', 'ug_user_group', 'patch-user_groups-primary-key.sql' ], [ 'addField', 'user_groups', 'ug_expiry', 'patch-user_groups-ug_expiry.sql' ], - [ 'addIndex', 'image', 'img_user_timestamp', 'patch-image-user-index-2.sql' ], + [ 'ifNoActorTable', 'addIndex', 'image', 'img_user_timestamp', 'patch-image-user-index-2.sql' ], // 1.30 [ 'modifyField', 'image', 'img_media_type', 'patch-add-3d.sql' ], @@ -376,6 +384,12 @@ class MysqlUpdater extends DatabaseUpdater { [ 'dropTable', 'tag_summary' ], [ 'dropField', 'protected_titles', 'pt_reason', 'patch-drop-comment-fields.sql' ], [ 'modifyTable', 'job', 'patch-job-params-mediumblob.sql' ], + + // 1.34 + [ 'dropIndex', 'archive', 'ar_usertext_timestamp', + 'patch-drop-archive-ar_usertext_timestamp.sql' ], + [ 'dropIndex', 'archive', 'usertext_timestamp', 'patch-drop-archive-usertext_timestamp.sql' ], + [ 'dropField', 'logging', 'log_user', 'patch-drop-user-fields.sql' ], ]; } diff --git a/includes/installer/PostgresInstaller.php b/includes/installer/PostgresInstaller.php index d6a5145cda..e9b0c56059 100644 --- a/includes/installer/PostgresInstaller.php +++ b/includes/installer/PostgresInstaller.php @@ -22,6 +22,7 @@ */ use Wikimedia\Rdbms\Database; +use Wikimedia\Rdbms\DatabasePostgres; use Wikimedia\Rdbms\DBQueryError; use Wikimedia\Rdbms\DBConnectionError; @@ -353,6 +354,7 @@ class PostgresInstaller extends DatabaseInstaller { if ( !$status->isOK() ) { return $status; } + // @phan-suppress-next-line PhanUndeclaredMethod $exists = $status->value->roleExists( $this->getVar( 'wgDBuser' ) ); } @@ -507,6 +509,7 @@ class PostgresInstaller extends DatabaseInstaller { } /** @var DatabasePostgres $conn */ $conn = $status->value; + '@phan-var DatabasePostgres $conn'; // Create the schema if necessary $schema = $this->getVar( 'wgDBmwschema' ); @@ -542,7 +545,9 @@ class PostgresInstaller extends DatabaseInstaller { if ( !$status->isOK() ) { return $status; } + /** @var DatabasePostgres $conn */ $conn = $status->value; + '@phan-var DatabasePostgres $conn'; $safeuser = $conn->addIdentifierQuotes( $this->getVar( 'wgDBuser' ) ); $safepass = $conn->addQuotes( $this->getVar( 'wgDBpassword' ) ); @@ -599,6 +604,7 @@ class PostgresInstaller extends DatabaseInstaller { /** @var DatabasePostgres $conn */ $conn = $status->value; + '@phan-var DatabasePostgres $conn'; if ( $conn->tableExists( 'archive' ) ) { $status->warning( 'config-install-tables-exist' ); diff --git a/includes/installer/PostgresUpdater.php b/includes/installer/PostgresUpdater.php index 31827a1ab7..b2c7d66ae7 100644 --- a/includes/installer/PostgresUpdater.php +++ b/includes/installer/PostgresUpdater.php @@ -110,7 +110,7 @@ class PostgresUpdater extends DatabaseUpdater { [ 'addPgField', 'image', 'img_sha1', "TEXT NOT NULL DEFAULT ''" ], [ 'addPgField', 'ipblocks', 'ipb_allow_usertalk', 'SMALLINT NOT NULL DEFAULT 0' ], [ 'addPgField', 'ipblocks', 'ipb_anon_only', 'SMALLINT NOT NULL DEFAULT 0' ], - [ 'addPgField', 'ipblocks', 'ipb_by_text', "TEXT NOT NULL DEFAULT ''" ], + [ 'ifNoActorTable', 'addPgField', 'ipblocks', 'ipb_by_text', "TEXT NOT NULL DEFAULT ''" ], [ 'addPgField', 'ipblocks', 'ipb_block_email', 'SMALLINT NOT NULL DEFAULT 0' ], [ 'addPgField', 'ipblocks', 'ipb_create_account', 'SMALLINT NOT NULL DEFAULT 1' ], [ 'addPgField', 'ipblocks', 'ipb_deleted', 'SMALLINT NOT NULL DEFAULT 0' ], @@ -152,7 +152,7 @@ class PostgresUpdater extends DatabaseUpdater { [ 'addPgField', 'revision', 'rev_content_format', 'TEXT' ], [ 'addPgField', 'site_stats', 'ss_active_users', "INTEGER DEFAULT '-1'" ], [ 'addPgField', 'user_newtalk', 'user_last_timestamp', 'TIMESTAMPTZ' ], - [ 'addPgField', 'logging', 'log_user_text', "TEXT NOT NULL DEFAULT ''" ], + [ 'ifNoActorTable', 'addPgField', 'logging', 'log_user_text', "TEXT NOT NULL DEFAULT ''" ], [ 'addPgField', 'logging', 'log_page', 'INTEGER' ], [ 'addPgField', 'interwiki', 'iw_api', "TEXT NOT NULL DEFAULT ''" ], [ 'addPgField', 'interwiki', 'iw_wikiid', "TEXT NOT NULL DEFAULT ''" ], @@ -240,7 +240,7 @@ class PostgresUpdater extends DatabaseUpdater { [ 'checkOiDeleted' ], # New indexes - [ 'addPgIndex', 'archive', 'archive_user_text', '(ar_user_text)' ], + [ 'ifNoActorTable', 'addPgIndex', 'archive', 'archive_user_text', '(ar_user_text)' ], [ 'addPgIndex', 'image', 'img_sha1', '(img_sha1)' ], [ 'addPgIndex', 'ipblocks', 'ipb_parent_block_id', '(ipb_parent_block_id)' ], [ 'addPgIndex', 'oldimage', 'oi_sha1', '(oi_sha1)' ], @@ -253,7 +253,7 @@ class PostgresUpdater extends DatabaseUpdater { [ 'addPgIndex', 'watchlist', 'wl_user', '(wl_user)' ], [ 'addPgIndex', 'watchlist', 'wl_user_notificationtimestamp', '(wl_user, wl_notificationtimestamp)' ], - [ 'addPgIndex', 'logging', 'logging_user_type_time', + [ 'ifNoActorTable', 'addPgIndex', 'logging', 'logging_user_type_time', '(log_user, log_type, log_timestamp)' ], [ 'addPgIndex', 'logging', 'logging_page_id_time', '(log_page,log_timestamp)' ], [ 'addPgIndex', 'iwlinks', 'iwl_prefix_from_title', '(iwl_prefix, iwl_from, iwl_title)' ], @@ -263,9 +263,10 @@ class PostgresUpdater extends DatabaseUpdater { [ 'addPgIndex', 'job', 'job_cmd_token', '(job_cmd, job_token, job_random)' ], [ 'addPgIndex', 'job', 'job_cmd_token_id', '(job_cmd, job_token, job_id)' ], [ 'addPgIndex', 'filearchive', 'fa_sha1', '(fa_sha1)' ], - [ 'addPgIndex', 'logging', 'logging_user_text_type_time', + [ 'ifNoActorTable', 'addPgIndex', 'logging', 'logging_user_text_type_time', '(log_user_text, log_type, log_timestamp)' ], - [ 'addPgIndex', 'logging', 'logging_user_text_time', '(log_user_text, log_timestamp)' ], + [ 'ifNoActorTable', 'addPgIndex', 'logging', 'logging_user_text_time', + '(log_user_text, log_timestamp)' ], [ 'checkIndex', 'pagelink_unique', [ [ 'pl_from', 'int4_ops', 'btree', 0 ], @@ -363,30 +364,36 @@ class PostgresUpdater extends DatabaseUpdater { [ 'checkIwlPrefix' ], # All FK columns should be deferred - [ 'changeFkeyDeferrable', 'archive', 'ar_user', 'mwuser(user_id) ON DELETE SET NULL' ], + [ 'ifNoActorTable', 'changeFkeyDeferrable', 'archive', 'ar_user', + 'mwuser(user_id) ON DELETE SET NULL' ], [ 'changeFkeyDeferrable', 'categorylinks', 'cl_from', 'page(page_id) ON DELETE CASCADE' ], [ 'changeFkeyDeferrable', 'externallinks', 'el_from', 'page(page_id) ON DELETE CASCADE' ], [ 'changeFkeyDeferrable', 'filearchive', 'fa_deleted_user', 'mwuser(user_id) ON DELETE SET NULL' ], - [ 'changeFkeyDeferrable', 'filearchive', 'fa_user', 'mwuser(user_id) ON DELETE SET NULL' ], - [ 'changeFkeyDeferrable', 'image', 'img_user', 'mwuser(user_id) ON DELETE SET NULL' ], + [ 'ifNoActorTable', 'changeFkeyDeferrable', 'filearchive', 'fa_user', + 'mwuser(user_id) ON DELETE SET NULL' ], + [ 'ifNoActorTable', 'changeFkeyDeferrable', 'image', 'img_user', + 'mwuser(user_id) ON DELETE SET NULL' ], [ 'changeFkeyDeferrable', 'imagelinks', 'il_from', 'page(page_id) ON DELETE CASCADE' ], - [ 'changeFkeyDeferrable', 'ipblocks', 'ipb_by', 'mwuser(user_id) ON DELETE CASCADE' ], + [ 'ifNoActorTable', 'changeFkeyDeferrable', 'ipblocks', 'ipb_by', + 'mwuser(user_id) ON DELETE CASCADE' ], [ 'changeFkeyDeferrable', 'ipblocks', 'ipb_user', 'mwuser(user_id) ON DELETE SET NULL' ], [ 'changeFkeyDeferrable', 'ipblocks', 'ipb_parent_block_id', 'ipblocks(ipb_id) ON DELETE SET NULL' ], [ 'changeFkeyDeferrable', 'langlinks', 'll_from', 'page(page_id) ON DELETE CASCADE' ], - [ 'changeFkeyDeferrable', 'logging', 'log_user', 'mwuser(user_id) ON DELETE SET NULL' ], + [ 'ifNoActorTable', 'changeFkeyDeferrable', 'logging', 'log_user', + 'mwuser(user_id) ON DELETE SET NULL' ], [ 'changeFkeyDeferrable', 'oldimage', 'oi_name', 'image(img_name) ON DELETE CASCADE ON UPDATE CASCADE' ], - [ 'changeFkeyDeferrable', 'oldimage', 'oi_user', 'mwuser(user_id) ON DELETE SET NULL' ], + [ 'ifNoActorTable', 'changeFkeyDeferrable', 'oldimage', 'oi_user', + 'mwuser(user_id) ON DELETE SET NULL' ], [ 'changeFkeyDeferrable', 'pagelinks', 'pl_from', 'page(page_id) ON DELETE CASCADE' ], [ 'changeFkeyDeferrable', 'page_props', 'pp_page', 'page (page_id) ON DELETE CASCADE' ], [ 'changeFkeyDeferrable', 'page_restrictions', 'pr_page', 'page(page_id) ON DELETE CASCADE' ], [ 'changeFkeyDeferrable', 'protected_titles', 'pt_user', 'mwuser(user_id) ON DELETE SET NULL' ], - [ 'changeFkeyDeferrable', 'recentchanges', 'rc_user', + [ 'ifNoActorTable', 'changeFkeyDeferrable', 'recentchanges', 'rc_user', 'mwuser(user_id) ON DELETE SET NULL' ], [ 'changeFkeyDeferrable', 'redirect', 'rd_from', 'page(page_id) ON DELETE CASCADE' ], [ 'changeFkeyDeferrable', 'revision', 'rev_page', 'page (page_id) ON DELETE CASCADE' ], @@ -618,6 +625,34 @@ class PostgresUpdater extends DatabaseUpdater { [ 'dropDefault', 'logging', 'log_comment_id' ], [ 'dropPgField', 'protected_titles', 'pt_reason' ], [ 'dropDefault', 'protected_titles', 'pt_reason_id' ], + + // 1.34 + [ 'dropPgIndex', 'archive', 'archive_user_text' ], + [ 'dropPgField', 'archive', 'ar_user' ], + [ 'dropPgField', 'archive', 'ar_user_text' ], + [ 'dropDefault', 'archive', 'ar_actor' ], + [ 'dropPgField', 'ipblocks', 'ipb_by' ], + [ 'dropPgField', 'ipblocks', 'ipb_by_text' ], + [ 'dropDefault', 'ipblocks', 'ipb_by_actor' ], + [ 'dropPgField', 'image', 'img_user' ], + [ 'dropPgField', 'image', 'img_user_text' ], + [ 'dropDefault', 'image', 'img_actor' ], + [ 'dropPgField', 'oldimage', 'oi_user' ], + [ 'dropPgField', 'oldimage', 'oi_user_text' ], + [ 'dropDefault', 'oldimage', 'oi_actor' ], + [ 'dropPgField', 'filearchive', 'fa_user' ], + [ 'dropPgField', 'filearchive', 'fa_user_text' ], + [ 'dropDefault', 'filearchive', 'fa_actor' ], + [ 'dropPgField', 'recentchanges', 'rc_user' ], + [ 'dropPgField', 'recentchanges', 'rc_user_text' ], + [ 'dropDefault', 'recentchanges', 'rc_actor' ], + [ 'dropPgIndex', 'logging', 'logging_user_time' ], + [ 'dropPgIndex', 'logging', 'logging_user_type_time' ], + [ 'dropPgIndex', 'logging', 'logging_user_text_type_time' ], + [ 'dropPgIndex', 'logging', 'logging_user_text_time' ], + [ 'dropPgField', 'logging', 'log_user' ], + [ 'dropPgField', 'logging', 'log_user_text' ], + [ 'dropDefault', 'logging', 'log_actor' ], ]; } diff --git a/includes/installer/SqliteInstaller.php b/includes/installer/SqliteInstaller.php index 7c39dedae4..b21a177040 100644 --- a/includes/installer/SqliteInstaller.php +++ b/includes/installer/SqliteInstaller.php @@ -71,16 +71,19 @@ class SqliteInstaller extends DatabaseInstaller { } public function getGlobalDefaults() { + global $IP; $defaults = parent::getGlobalDefaults(); - if ( isset( $_SERVER['DOCUMENT_ROOT'] ) ) { - $path = str_replace( - [ '/', '\\' ], - DIRECTORY_SEPARATOR, - dirname( $_SERVER['DOCUMENT_ROOT'] ) . '/data' - ); - - $defaults['wgSQLiteDataDir'] = $path; + if ( !empty( $_SERVER['DOCUMENT_ROOT'] ) ) { + $path = dirname( $_SERVER['DOCUMENT_ROOT'] ); + } else { + // We use $IP when unable to get $_SERVER['DOCUMENT_ROOT'] + $path = $IP; } + $defaults['wgSQLiteDataDir'] = str_replace( + [ '/', '\\' ], + DIRECTORY_SEPARATOR, + $path . '/data' + ); return $defaults; } @@ -122,7 +125,7 @@ class SqliteInstaller extends DatabaseInstaller { # Try realpath() if the directory already exists $dir = self::realpath( $this->getVar( 'wgSQLiteDataDir' ) ); - $result = self::dataDirOKmaybeCreate( $dir, true /* create? */ ); + $result = self::checkDataDir( $dir ); if ( $result->isOK() ) { # Try expanding again in case we've just created it $dir = self::realpath( $dir ); @@ -135,12 +138,17 @@ class SqliteInstaller extends DatabaseInstaller { } /** - * @param string $dir - * @param bool $create - * @return Status + * Check if the data directory is writable or can be created + * @param string $dir Path to the data directory + * @return Status Return fatal Status if $dir un-writable or no permission to create a directory */ - private static function dataDirOKmaybeCreate( $dir, $create = false ) { - if ( !is_dir( $dir ) ) { + private static function checkDataDir( $dir ) : Status { + if ( is_dir( $dir ) ) { + if ( !is_readable( $dir ) ) { + return Status::newFatal( 'config-sqlite-dir-unwritable', $dir ); + } + } else { + // Check the parent directory if $dir not exists if ( !is_writable( dirname( $dir ) ) ) { $webserverGroup = Installer::maybeGetWebserverPrimaryGroup(); if ( $webserverGroup !== null ) { @@ -156,25 +164,25 @@ class SqliteInstaller extends DatabaseInstaller { ); } } + } + return Status::newGood(); + } - # Called early on in the installer, later we just want to sanity check - # if it's still writable - if ( $create ) { - Wikimedia\suppressWarnings(); - $ok = wfMkdirParents( $dir, 0700, __METHOD__ ); - Wikimedia\restoreWarnings(); - if ( !$ok ) { - return Status::newFatal( 'config-sqlite-mkdir-error', $dir ); - } - # Put a .htaccess file in in case the user didn't take our advice - file_put_contents( "$dir/.htaccess", "Deny from all\n" ); + /** + * @param string $dir Path to the data directory + * @return Status Return good Status if without error + */ + private static function createDataDir( $dir ) : Status { + if ( !is_dir( $dir ) ) { + Wikimedia\suppressWarnings(); + $ok = wfMkdirParents( $dir, 0700, __METHOD__ ); + Wikimedia\restoreWarnings(); + if ( !$ok ) { + return Status::newFatal( 'config-sqlite-mkdir-error', $dir ); } } - if ( !is_writable( $dir ) ) { - return Status::newFatal( 'config-sqlite-dir-unwritable', $dir ); - } - - # We haven't blown up yet, fall through + # Put a .htaccess file in in case the user didn't take our advice + file_put_contents( "$dir/.htaccess", "Deny from all\n" ); return Status::newGood(); } @@ -217,10 +225,15 @@ class SqliteInstaller extends DatabaseInstaller { public function setupDatabase() { $dir = $this->getVar( 'wgSQLiteDataDir' ); - # Sanity check. We checked this before but maybe someone deleted the - # data dir between then and now - $dir_status = self::dataDirOKmaybeCreate( $dir, false /* create? */ ); - if ( !$dir_status->isOK() ) { + # Sanity check (Only available in web installation). We checked this before but maybe someone + # deleted the data dir between then and now + $dir_status = self::checkDataDir( $dir ); + if ( $dir_status->isGood() ) { + $res = self::createDataDir( $dir ); + if ( !$res->isGood() ) { + return $res; + } + } else { return $dir_status; } @@ -242,27 +255,6 @@ class SqliteInstaller extends DatabaseInstaller { $this->setVar( 'wgDBpassword', '' ); $this->setupSchemaVars(); - # Create the global cache DB - try { - $conn = Database::factory( - 'sqlite', [ 'dbname' => 'wikicache', 'dbDirectory' => $dir ] ); - # @todo: don't duplicate objectcache definition, though it's very simple - $sql = -<<query( $sql ); - $conn->query( "CREATE INDEX IF NOT EXISTS exptime ON objectcache (exptime)" ); - $conn->query( "PRAGMA journal_mode=WAL" ); // this is permanent - $conn->close(); - } catch ( DBConnectionError $e ) { - return Status::newFatal( 'config-sqlite-connection-error', $e->getMessage() ); - } - # Create the l10n cache DB try { $conn = Database::factory( diff --git a/includes/installer/SqliteUpdater.php b/includes/installer/SqliteUpdater.php index 17ced507a2..7c3878ce44 100644 --- a/includes/installer/SqliteUpdater.php +++ b/includes/installer/SqliteUpdater.php @@ -47,9 +47,9 @@ class SqliteUpdater extends DatabaseUpdater { // 1.16 [ 'addTable', 'user_properties', 'patch-user_properties.sql' ], [ 'addTable', 'log_search', 'patch-log_search.sql' ], - [ 'addField', 'logging', 'log_user_text', 'patch-log_user_text.sql' ], + [ 'ifNoActorTable', 'addField', 'logging', 'log_user_text', 'patch-log_user_text.sql' ], # listed separately from the previous update because 1.16 was released without this update - [ 'doLogUsertextPopulation' ], + [ 'ifNoActorTable', 'doLogUsertextPopulation' ], [ 'doLogSearchPopulation' ], [ 'addTable', 'l10n_cache', 'patch-l10n_cache.sql' ], [ 'dropIndex', 'change_tag', 'ct_rc_id', 'patch-change_tag-indexes.sql' ], @@ -119,9 +119,10 @@ class SqliteUpdater extends DatabaseUpdater { // 1.23 [ 'addField', 'recentchanges', 'rc_source', 'patch-rc_source.sql' ], - [ 'addIndex', 'logging', 'log_user_text_type_time', + [ 'ifNoActorTable', 'addIndex', 'logging', 'log_user_text_type_time', 'patch-logging_user_text_type_time_index.sql' ], - [ 'addIndex', 'logging', 'log_user_text_time', 'patch-logging_user_text_time_index.sql' ], + [ 'ifNoActorTable', 'addIndex', 'logging', 'log_user_text_time', + 'patch-logging_user_text_time_index.sql' ], [ 'addField', 'page', 'page_links_updated', 'patch-page_links_updated.sql' ], [ 'addField', 'user', 'user_password_expires', 'patch-user_password_expire.sql' ], @@ -159,7 +160,7 @@ class SqliteUpdater extends DatabaseUpdater { // 1.29 [ 'addField', 'externallinks', 'el_index_60', 'patch-externallinks-el_index_60.sql' ], [ 'addField', 'user_groups', 'ug_expiry', 'patch-user_groups-ug_expiry.sql' ], - [ 'addIndex', 'image', 'img_user_timestamp', 'patch-image-user-index-2.sql' ], + [ 'ifNoActorTable', 'addIndex', 'image', 'img_user_timestamp', 'patch-image-user-index-2.sql' ], // 1.30 [ 'modifyField', 'image', 'img_media_type', 'patch-add-3d.sql' ], @@ -249,6 +250,15 @@ class SqliteUpdater extends DatabaseUpdater { [ 'dropField', 'recentchanges', 'rc_comment', 'patch-recentchanges-drop-rc_comment.sql' ], [ 'dropField', 'logging', 'log_comment', 'patch-logging-drop-log_comment.sql' ], [ 'dropField', 'protected_titles', 'pt_reason', 'patch-protected_titles-drop-pt_reason.sql' ], + + // 1.34 + [ 'dropField', 'archive', 'ar_user', 'patch-archive-drop-ar_user.sql' ], + [ 'dropField', 'ipblocks', 'ipb_by', 'patch-ipblocks-drop-ipb_by.sql' ], + [ 'dropField', 'image', 'img_user', 'patch-image-drop-img_user.sql' ], + [ 'dropField', 'oldimage', 'oi_user', 'patch-oldimage-drop-oi_user.sql' ], + [ 'dropField', 'filearchive', 'fa_user', 'patch-filearchive-drop-fa_user.sql' ], + [ 'dropField', 'recentchanges', 'rc_user', 'patch-recentchanges-drop-rc_user.sql' ], + [ 'dropField', 'logging', 'log_user', 'patch-logging-drop-log_user.sql' ], ]; } diff --git a/includes/installer/WebInstaller.php b/includes/installer/WebInstaller.php index db26c0b63d..d9cd6dea82 100644 --- a/includes/installer/WebInstaller.php +++ b/includes/installer/WebInstaller.php @@ -188,7 +188,9 @@ class WebInstaller extends Installer { # Special case for Creative Commons partner chooser box. if ( $this->request->getVal( 'SubmitCC' ) ) { + /** @var WebInstallerOptions $page */ $page = $this->getPageByName( 'Options' ); + '@phan-var WebInstallerOptions $page'; $this->output->useShortHeader(); $this->output->allowFrames(); $page->submitCC(); @@ -197,7 +199,9 @@ class WebInstaller extends Installer { } if ( $this->request->getVal( 'ShowCC' ) ) { + /** @var WebInstallerOptions $page */ $page = $this->getPageByName( 'Options' ); + '@phan-var WebInstallerOptions $page'; $this->output->useShortHeader(); $this->output->allowFrames(); $this->output->addHTML( $page->getCCDoneBox() ); @@ -386,7 +390,8 @@ class WebInstaller extends Installer { ); } $text = $msg->useDatabase( false )->plain(); - $this->output->addHTML( $this->getErrorBox( $text ) ); + $box = Html::errorBox( $text, '', 'config-error-box' ); + $this->output->addHTML( $box ); } /** @@ -633,34 +638,40 @@ class WebInstaller extends Installer { /** * Get HTML for an error box with an icon. * + * @deprecated since 1.34 Use Html::errorBox() instead. * @param string $text Wikitext, get this with wfMessage()->plain() * * @return string */ public function getErrorBox( $text ) { + wfDeprecated( __METHOD__, '1.34' ); return $this->getInfoBox( $text, 'critical-32.png', 'config-error-box' ); } /** * Get HTML for a warning box with an icon. * + * @deprecated since 1.34 Use Html::warningBox() instead. * @param string $text Wikitext, get this with wfMessage()->plain() * * @return string */ public function getWarningBox( $text ) { + wfDeprecated( __METHOD__, '1.34' ); return $this->getInfoBox( $text, 'warning-32.png', 'config-warning-box' ); } /** * Get HTML for an information message box with an icon. * + * @deprecated since 1.34. * @param string|HtmlArmor $text Wikitext to be parsed (from Message::plain) or raw HTML. * @param string|bool $icon Icon name, file in mw-config/images. Default: false * @param string|bool $class Additional class name to add to the wrapper div. Default: false. * @return string HTML */ public function getInfoBox( $text, $icon = false, $class = false ) { + wfDeprecated( __METHOD__, '1.34' ); $html = ( $text instanceof HtmlArmor ) ? HtmlArmor::getHtml( $text ) : $this->parse( $text, true ); @@ -1042,9 +1053,9 @@ class WebInstaller extends Installer { $text = $status->getWikiText(); if ( $status->isOK() ) { - $box = $this->getWarningBox( $text ); + $box = Html::warningBox( $text, 'config-warning-box' ); } else { - $box = $this->getErrorBox( $text ); + $box = Html::errorBox( $text, '', 'config-error-box' ); } $this->output->addHTML( $box ); diff --git a/includes/installer/WebInstallerDBConnect.php b/includes/installer/WebInstallerDBConnect.php index 7546bdf9d2..3bacb76e99 100644 --- a/includes/installer/WebInstallerDBConnect.php +++ b/includes/installer/WebInstallerDBConnect.php @@ -48,8 +48,7 @@ class WebInstallerDBConnect extends WebInstallerPage { $settings = ''; $defaultType = $this->getVar( 'wgDBtype' ); - // Messages: config-dbsupport-mysql, config-dbsupport-postgres, config-dbsupport-oracle, - // config-dbsupport-sqlite, config-dbsupport-mssql + // Messages: config-dbsupport-mysql, config-dbsupport-postgres, config-dbsupport-sqlite $dbSupport = ''; foreach ( Installer::getDBTypes() as $type ) { $dbSupport .= wfMessage( "config-dbsupport-$type" )->plain() . "\n"; @@ -78,8 +77,7 @@ class WebInstallerDBConnect extends WebInstallerPage { ) . "\n"; - // Messages: config-header-mysql, config-header-postgres, config-header-oracle, - // config-header-sqlite + // Messages: config-header-mysql, config-header-postgres, config-header-sqlite $settings .= Html::openElement( 'div', [ diff --git a/includes/installer/WebInstallerOptions.php b/includes/installer/WebInstallerOptions.php index 2412319ea3..3521fa188f 100644 --- a/includes/installer/WebInstallerOptions.php +++ b/includes/installer/WebInstallerOptions.php @@ -104,7 +104,8 @@ class WebInstallerOptions extends WebInstallerPage { $this->getFieldsetEnd() ); - $skins = $this->parent->findExtensions( 'skins' ); + $skins = $this->parent->findExtensions( 'skins' )->value; + '@phan-var array[] $skins'; $skinHtml = $this->getFieldsetStart( 'config-skins' ); $skinNames = array_map( 'strtolower', array_keys( $skins ) ); @@ -136,7 +137,7 @@ class WebInstallerOptions extends WebInstallerPage { } } else { $skinHtml .= - $this->parent->getWarningBox( wfMessage( 'config-skins-missing' )->plain() ) . + Html::warningBox( wfMessage( 'config-skins-missing' )->plain(), 'config-warning-box' ) . Html::hidden( 'config_wgDefaultSkin', $chosenSkinName ); } @@ -144,7 +145,8 @@ class WebInstallerOptions extends WebInstallerPage { $this->getFieldsetEnd(); $this->addHTML( $skinHtml ); - $extensions = $this->parent->findExtensions(); + $extensions = $this->parent->findExtensions()->value; + '@phan-var array[] $extensions'; $dependencyMap = []; if ( $extensions ) { @@ -324,11 +326,16 @@ class WebInstallerOptions extends WebInstallerPage { return null; } + /** + * @param string $name + * @param array $screenshots + */ private function makeScreenshotsLink( $name, $screenshots ) { global $wgLang; if ( count( $screenshots ) > 1 ) { $links = []; $counter = 1; + foreach ( $screenshots as $shot ) { $links[] = Html::element( 'a', @@ -448,7 +455,7 @@ class WebInstallerOptions extends WebInstallerPage { * @return bool */ public function submitSkins() { - $skins = array_keys( $this->parent->findExtensions( 'skins' ) ); + $skins = array_keys( $this->parent->findExtensions( 'skins' )->value ); $this->parent->setVar( '_Skins', $skins ); if ( $skins ) { @@ -498,7 +505,7 @@ class WebInstallerOptions extends WebInstallerPage { $this->setVar( 'wgRightsIcon', '' ); } - $skinsAvailable = array_keys( $this->parent->findExtensions( 'skins' ) ); + $skinsAvailable = array_keys( $this->parent->findExtensions( 'skins' )->value ); $skinsToInstall = []; foreach ( $skinsAvailable as $skin ) { $this->parent->setVarsFromRequest( [ "skin-$skin" ] ); @@ -519,7 +526,7 @@ class WebInstallerOptions extends WebInstallerPage { $retVal = false; } - $extsAvailable = array_keys( $this->parent->findExtensions() ); + $extsAvailable = array_keys( $this->parent->findExtensions()->value ); $extsToInstall = []; foreach ( $extsAvailable as $ext ) { $this->parent->setVarsFromRequest( [ "ext-$ext" ] ); diff --git a/includes/installer/WebInstallerOutput.php b/includes/installer/WebInstallerOutput.php index 991e484da7..51d4250b12 100644 --- a/includes/installer/WebInstallerOutput.php +++ b/includes/installer/WebInstallerOutput.php @@ -168,6 +168,7 @@ class WebInstallerOutput { foreach ( $moduleNames as $moduleName ) { /** @var ResourceLoaderFileModule $module */ $module = $resourceLoader->getModule( $moduleName ); + '@phan-var ResourceLoaderFileModule $module'; if ( !$module ) { // T98043: Don't fatal, but it won't look as pretty. continue; diff --git a/includes/installer/WebInstallerRestart.php b/includes/installer/WebInstallerRestart.php index be55c32fd0..07e2e7513f 100644 --- a/includes/installer/WebInstallerRestart.php +++ b/includes/installer/WebInstallerRestart.php @@ -36,7 +36,7 @@ class WebInstallerRestart extends WebInstallerPage { } $this->startForm(); - $s = $this->parent->getWarningBox( wfMessage( 'config-help-restart' )->plain() ); + $s = Html::warningBox( wfMessage( 'config-help-restart' )->plain(), '', 'config-warning-box' ); $this->addHTML( $s ); $this->endForm( 'restart' ); diff --git a/includes/installer/i18n/af.json b/includes/installer/i18n/af.json index 04aba37ad4..b3d25a9eee 100644 --- a/includes/installer/i18n/af.json +++ b/includes/installer/i18n/af.json @@ -46,10 +46,8 @@ "config-diff3-bad": "GNU diff3 nie gevind nie.", "config-db-type": "Databasistipe:", "config-db-host": "Databasisbediener:", - "config-db-host-oracle": "Databasis-TNS:", "config-db-wiki-settings": "Identifiseer hierdie wiki", "config-db-name": "Databasisnaam:", - "config-db-name-oracle": "Databasis-skema:", "config-db-install-account": "Gebruiker vir die installasie", "config-db-username": "Databasis gebruikersnaam:", "config-db-password": "Databasis wagwoord:", @@ -58,12 +56,9 @@ "config-db-port": "Databasispoort:", "config-db-schema": "Skema vir MediaWiki", "config-sqlite-dir": "Gids vir SQLite se data:", - "config-oracle-def-ts": "Standaard tabelruimte:", - "config-oracle-temp-ts": "Tydelike tabelruimte:", "config-header-mysql": "MySQL-instellings", "config-header-postgres": "PostgreSQL-instellings", "config-header-sqlite": "SQLite-instellings", - "config-header-oracle": "Oracle-instellings", "config-invalid-db-type": "Ongeldige databasistipe", "config-missing-db-name": "U moet 'n waarde vir \"Databasnaam\" verskaf", "config-sqlite-readonly": "Die lêer $1 kan nie geskryf word nie.", diff --git a/includes/installer/i18n/ar.json b/includes/installer/i18n/ar.json index 46b9c21fb4..ccb68b9b35 100644 --- a/includes/installer/i18n/ar.json +++ b/includes/installer/i18n/ar.json @@ -89,18 +89,14 @@ "config-uploads-not-safe": "تحذير: الدليل الافتراضي للمرفوعات $1 عرضة لتنفيذ سكريبتات عشوائية،\nعلى الرغم من أن ميدياويكي يتحقق من كل الملفات المرفوعة للتهديدات الأمنية، فمن المستحسن بشدة [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Security#Upload_security إغلاق هذه الثغرة الأمنية] قبل تمكين المرفوعات.", "config-no-cli-uploads-check": "تحذير: لم يتم تحديد الدليل الافتراضي للمرفوعات ($1) للقابلية للتأثر\nلتنفيذ برنامج تعسفي أثناء تثبيت CLI.", "config-brokenlibxml": "يحتوي نظامك على مجموعة من إصدارات PHP وlibxml2 ويمكن أن تسبب فسادا للبيانات في ميدياويكي وتطبيقات الويب الأخرى;\nقم بالترقية إلى libxml2 2.7.3 أو أحدث ([https://bugs.php.net/bug.php؟id=45996 تم تدقيم العلة مع PHP]); \nتم إحباط التثبيت.", - "config-suhosin-max-value-length": "تم تثبيت Suhosin وتقييد وسيط GET length إلى $1 بايت،\nسيعمل مكون ResourceLoader في ميدياويكي حول هذا الحد، لكن ذلك سيؤدي إلى انخفاض مستوى الأداء، \nإذا كان ذلك ممكنا، فيجب تعيين suhosin.get.max_value_length على 1024 أو أعلى في php.ini، وتعيين $wgResourceLoaderMaxQueryLength لنفس القيمة في LocalSettings.php.", + "config-suhosin-max-value-length": "تم تثبيت Suhosin وتقييد وسيط GET length إلى $1 بايت،\nيتطلب ميدياويكي أن يكون suhosin.get.max_value_length $2 على الأقل، قم بتعطيل هذا الإعداد أو بزيادة هذه القيمة إلى $3 في php.ini.", "config-using-32bit": "تحذير: يبدو أن نظامك يعمل مع الأعداد الصحيحة 32 بت، هذا [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:32-bit لا يُنصَح به].", "config-db-type": "نوع قاعدة البيانات:", "config-db-host": "مضيف قاعدة البيانات:", "config-db-host-help": "إذا كان خادم قاعدة البيانات موجودا في خادم مختلف، فأدخل اسم المضيف أو عنوان الآيبي هنا. \n\nإذا كنت تستخدم استضافة ويب مشتركة، فيجب أن يمنحك موفر الاستضافة اسم المضيف الصحيح في وثائقه. \n\nإذا كنت تستخدم MySQL، فإن استخدام \"localhost\" قد لا يعمل لاسم الخادم، إذا لم يتم ذلك، فجرب \"127.0.0.1\" لعنوان الآيبي المحلي. \n\nإذا كنت تستخدم PostgreSQL، فاترك هذا الحقل فارغا للاتصال عبر مقبس Unix.", - "config-db-host-oracle": "قاعدة بيانات TNS:", - "config-db-host-oracle-help": "أدخل [http://download.oracle.com/docs/cd/B28359_01/network.111/b28317/tnsnames.htm اسم اتصال محلي] صالحا، يجب أن يكون ملف tnsnames.ora مرئيا لهذا التثبيت.
إذا كنت تستخدم مكتبات العملاء 10g أو أحدث، فيمكنك أيضا استخدام طريقة التسمية [http://download.oracle.com/docs/cd/E11882_01/network.112/e10836/naming.اتصال htm السهل].", "config-db-wiki-settings": "حدِّد هذا الويكي", "config-db-name": "اسم قاعدة البيانات (لا شرط):", "config-db-name-help": "اختر الاسم الذي يعرف الويكي الخاص بك. لا يجب أن يحتوي على مسافات. إذا كنت تستخدم استضافة المواقع المشتركة، مزود الاستضافة إما سيعطيك اسم قاعدة بيانات محددة لاستخدامها أو سيتيح لك إنشاء قواعد بيانات عن طريق لوحة التحكم.", - "config-db-name-oracle": "سكيما قاعدة البيانات:", - "config-db-account-oracle-warn": "هناك ثلاثة سيناريوهات مدعومة لتثبيت Oracle كقاعدة بيانات خلفية: \n\nإذا كنت ترغب في إنشاء حساب قاعدة بيانات كجزء من عملية التثبيت، فيُرجَى تقديم حساب بدور SYSDBA كحساب قاعدة بيانات للتثبيت وتحديد بيانات الاعتماد المطلوبة لحساب الوصول إلى الإنترنت، وإلا يمكنك إما إنشاء حساب الوصول إلى الويب يدويا وتزويد الحساب فقط (إذا كان يتطلب صلاحيات لإنشاء كائنات المخطط) أو توفير حسابين مختلفين، أحدهما له امتيازات إنشاء وامتياز مقيد للدخول إلى الويب. \n\nيمكن العثور على البرنامج النصي لإنشاء حساب له امتيازات مطلوبة في دليل \"maintenance/oracle/\" لهذا التثبيت، ضع في اعتبارك أن استخدام حساب مقيد سيؤدي إلى تعطيل جميع إمكانات الصيانة باستخدام الحساب الافتراضي.", "config-db-install-account": "حساب المستخدم للتنصيب", "config-db-username": "اسم مستخدم قاعدة البيانات:", "config-db-password": "كلمة سر قاعدة البيانات:", @@ -119,37 +115,24 @@ "config-pg-test-error": "لا يمكن الاتصال بقاعدة البيانات $1: $2", "config-sqlite-dir": "دليل بيانات SQLite:", "config-sqlite-dir-help": "يخزن SQLite جميع البيانات في ملف واحد. \n\nيجب أن يكون الدليل الذي توفره قابلا للكتابة بواسطة خادم الويب أثناء التثبيت. \n\nيجب أن يكون غير متاحا عبر الويب؛ هذا هو سبب عدم وضعه في مكان ملفات PHP الخاصة بك. \n\nسيقوم المثبت بكتابة ملف .htaccess معه، ولكن إذا فشل ذلك، يمكن لشخص ما الوصول إلى قاعدة بياناتك الأولية، \nيتضمن ذلك بيانات المستخدم الأولية (عناوين البريد الإلكتروني وكلمات المرور المجزأة) بالإضافة إلى المراجعات المحذوفة والبيانات المحظورة الأخرى على الويكي. \n\nفكر في وضع قاعدة البيانات في مكان آخر تماما، على سبيل المثال في /var/lib/mediawiki/yourwiki$1; لأن الدليل الأصلي $2 غير قابل للكتابة بواسطة خادم الويب، \nحدد المثبت المستخدم الذي يعمل عليه خادم الويب الخاص بك،\nاجعل الدليل $3 قابلا للكتابة بواسطته للمتابعة، \nفي نظام يونكس/لينوكس قم بـ: \n
cd $2\nmkdir $3\nchgrp $4 $3\nchmod g+w $3
", "config-sqlite-parent-unwritable-nogroup": "لا يمكن إنشاء دليل البيانات $1; لأن الدليل الأصلي $2 غير قابل للكتابة بواسطة خادم الويب، \nتعذر على المثبت تحديد المستخدم الذي يعمل عليه خادم الويب الخاص بك،\nاجعل الدليل $3 قابلا للكتابة بواسطته للمتابعة، \nفي نظام يونكس/لينوكس قم بـ: \n\n
cd $2\nmkdir $3\nchmod a+w $3
", @@ -174,11 +157,6 @@ "config-mysql-engine": "محرك التخزين", "config-mysql-innodb": "InnoDB (مستحسن)", "config-mysql-engine-help": "InnoDB هو دائما الخيار الأفضل; لأنه يحتوي على دعم تزامن جيد.\n\nقد يكون MyISAM أسرع في تثبيت المستخدم الفردي أو للقراءة فقط،\nتميل قواعد بيانات MyISAM للتلف أكثر من قواعد بيانات InnoDB.", - "config-mssql-auth": "نوع الاستيثاق:", - "config-mssql-install-auth": "حدد نوع المصادقة الذي سيتم استخدامه للاتصال بقاعدة البيانات أثناء عملية التثبيت. \nإذا حددت \"{{int:config-mssql-windowsauth}}\"، فسيتم استخدام بيانات اعتماد أي مستخدم يعمل عليه خادم الويب.", - "config-mssql-web-auth": "حدد نوع المصادقة الذي سيستخدمه خادم الويب للاتصال بخادم قاعدة البيانات، أثناء التشغيل العادي للويكي. \nإذا حددت \"{{int:config-mssql-windowsauth}}\"، فسيتم استخدام بيانات اعتماد أي مستخدم يعمل عليه خادم الويب.", - "config-mssql-sqlauth": "مصادقة خادم SQL", - "config-mssql-windowsauth": "مصادقة ويندوز", "config-site-name": "اسم الويكي:", "config-site-name-help": "سيظهر هذا في شريط عنوان المتصفح وفي أماكن أخرى مختلفة.", "config-site-name-blank": "أدخل اسم موقع.", diff --git a/includes/installer/i18n/ast.json b/includes/installer/i18n/ast.json index 26d2ea7cce..55a1606230 100644 --- a/includes/installer/i18n/ast.json +++ b/includes/installer/i18n/ast.json @@ -87,13 +87,9 @@ "config-db-type": "Tipu de base de datos:", "config-db-host": "Servidor de la base de datos:", "config-db-host-help": "Si'l sirvidor de bases de datos ta nun sirvidor diferente, introduz equí la IP o'l nome del agospiu.\n\nSi tas usando un agospiu web compartíu, el so fornidor debió especificar el nome del agospiu na so documentación.\n\nSi tas usando MySQL, usar «localhost» pue nun funcionar pal nome del sirvidor. Si non, prueba «120.0.0.1» pa la direición IP llocal.\n\nSi tas usando PostgreSQL, dexa esti campu baleru pa coneutate per una ralura d'Unix.", - "config-db-host-oracle": "TNS de la base de datos:", - "config-db-host-oracle-help": "Escribe un [http://download.oracle.com/docs/cd/B28359_01/network.111/b28317/tnsnames.htm nome de conexón local] válidu; un archivu tnsnames.ora ten de ser visible pa esta instalación.
Si tas utilizando biblioteques de veceru 10g o más recién tamién puedes utilizar el métodu de asignación de nomes [http://download.oracle.com/docs/cd/Y11882_01/network.112/y10836/naming.htm Easy Connect].", "config-db-wiki-settings": "Identifica esta wiki", "config-db-name": "Nome de la base de datos (ensin guiones):", "config-db-name-help": "Escueye un nome qu'identifique la to wiki. Nun tien de contener espacios. \nSi tas utilizando agospiamientu web compartíu, el to provisor va date un nome específicu de base de datos por que lu utilices, o bien va dexate crear bases de datos al traviés d'un panel de control.", - "config-db-name-oracle": "Esquema de la base de datos:", - "config-db-account-oracle-warn": "Hai tres escenarios compatibles pa la instalación de Oracle como motor de base de datos:\n\nSi desees crear una cuenta de base de datos como parte del procesu d'instalación, por favor apurre una cuenta con rol SYSDBA como cuenta de base de datos pa la instalación y especifica les credenciales que quies tener pal accesu a la web a la cuenta; d'otra miente, puedes crear manualmente la cuenta d'accesu a la web y suministrar namái esa cuenta (si tien los permisos necesarios pa crear los oxetos d'esquema) o dar dos cuentes distintos, una con privilexos de creación y otra con accesu acutáu a la web\n\nLa secuencia de comandos (script) pa crear una cuenta colos privilexos necesarios puede atopase nel direutoriu \"maintenance/oracle/\" d'esta instalación. Ten en cuenta qu'utilizar una cuenta acutada va desactivar toles capacidaes de caltenimientu cola cuenta predeterminada.", "config-db-install-account": "Cuenta d'usuariu pa la instalación", "config-db-username": "Nome d'usuariu de base de datos:", "config-db-password": "Contraseña de base de datos:", @@ -112,37 +108,24 @@ "config-pg-test-error": "Nun puede coneutase cola base de datos $1: $2", "config-sqlite-dir": "Direutoriu de datos SQLite:", "config-sqlite-dir-help": "SQLite almacena tolos datos nun ficheru únicu.\n\nEl direutoriu que proporciones tien de poder escribise pol servidor web mientres la instalación.\n\nNun tendría de tener accesu pela web, por eso nun se pon nel sitiu onde tán los ficheros PHP.\n\nL'instalador escribirá un ficheru .htaccess xunto con él, pero si esto falla dalguién podría tener accesu a la base de datos completa.\nEso incluye los datos d'usuariu completos (direcciones de corréu electrónicu, contraseñes con hash) lo mesmo que les revisiones desaniciaes y otros datos acutaos de la wiki.\n\nConsidera poner la base de datos en dalgún otru sitiu, por casu en /var/lib/mediawiki/miowiki.", - "config-oracle-def-ts": "Espaciu de tables predetermináu:", - "config-oracle-temp-ts": "Espaciu de tables temporal:", "config-type-mysql": "MariaDB, MySQL o compatible", "config-type-postgres": "PostgreSQL", "config-type-sqlite": "SQLite", - "config-type-oracle": "Oracle", - "config-type-mssql": "Microsoft SQL Server", "config-support-info": "MediaWiki ye compatible colos siguientes sistemes de bases de datos:\n\n$1\n\nSi nun atopes na llista el sistema de base de datos que tas intentando utilizar, sigue les instrucciones enllazaes enriba p'activar la compatibilidá.", "config-dbsupport-mysql": "* [{{int:version-db-mariadb-url}} MariaDB] ye la base de datos primaria pa MediaWiki y la que tien mayor encontu. MediaWiki tamién funciona con [{{int:version-db-myslql-url}} MySQL] y [{{int:version-db-percona-url}} Percona Server], que son compatibles con MariaDB. ([https://www.php.net/manual/en/mysqli.installation.php Cómo compilar PHP con compatibilidá MySQL])", "config-dbsupport-postgres": "* [{{int:version-db-postgres-url}} PostgreSQL] ye un sistema popular de base de datos de códigu abiertu, como alternativa a MySQL. ([https://www.php.net/manual/en/pgsql.installation.php Cómo compilar PHP con compatibilidá PostgreSQL]).", "config-dbsupport-sqlite": "* [{{int:version-db-sqlite-url}} SQLite] ye un sistema de base de datos llixeru que tien encontu perbonu. ([https://https://www.php.net/manual/en/pdo.installation.php Cómo compilar PHP con encontu pa SQLite], usa PDO)", - "config-dbsupport-oracle": "* [{{int:version-db-oracle-url}} Oracle] ye una base de datos comercial a nivel empresarial. ([https://www.php.net/manual/en/oci8.installation.php Cómo compilar PHP con encontu pa OCI8])", - "config-dbsupport-mssql": "* [{{int:version-db-mssql-url}} Microsoft SQL Server] ye un sistema comercial de base de datos empresariales pa Windows. ([https://www.php.net/manual/en/sqlsrv.installation.php Cómo compilar PHP con compatibilidá pa SQLSRV])", "config-header-mysql": "Axustes de MariaDB/MySQL", "config-header-postgres": "Axustes de PostgreSQL", "config-header-sqlite": "Axustes de SQLite", - "config-header-oracle": "Axustes d'Oracle", - "config-header-mssql": "Axustes de Microsoft SQL Server", "config-invalid-db-type": "Triba non válida de base de datos.", "config-missing-db-name": "Has introducir un valor pa «{{int:config-db-name}}».", "config-missing-db-host": "Has introducir un valor pa «{{int:config-db-host}}».", - "config-missing-db-server-oracle": "Has introducir un valor pa «{{int:config-db-host-oracle}}».", - "config-invalid-db-server-oracle": "TNS inválidu pa la base de datos «$1».\nUsa una cadena «TNS Name» o «Easy Connect» ([http://docs.oracle.com/cd/E11882_01/network.112/e10836/naming.htm Métodos de nomenclatura d'Oracle]).", "config-invalid-db-name": "Nome inválidu de la base de datos «$1».\nUsa sólo lletres ASCII (a-z, A-Z), númberos (0-9), guiones baxos (_) y guiones (-).", "config-invalid-db-prefix": "Prefixu inválidu pa la base de datos «$1».\nUsa sólo lletres ASCII (a-z, A-Z), númberos (0-9), guiones baxos (_) y guiones (-).", "config-connection-error": "$1.\n\nComprueba'l sirvidor, el nome d'usuariu y la contraseña, y tenta nuevamente.Si uses \"localhost\" como sirvidor de base de datos, tenta usando \"127.0.0.1\" nel so llugar (o viceversa).", "config-invalid-schema": "Esquema inválidu «$1» pa MediaWiki.\nUsa sólo lletres ASCII (a-z, A-Z), númberos (0-9) y guiones baxos (_).", - "config-db-sys-create-oracle": "L'instalador namái sofita l'usu d'una cuenta SYSDBA pa la creación d'otra cuenta nueva.", - "config-db-sys-user-exists-oracle": "La cuenta d'usuariu «$1» yá esiste. ¡SYSDBA sólo puede utilizase pa crear una nueva cuenta!", "config-postgres-old": "Ríquese PostgreSQL $1 o posterior. Tienes la versión $2.", - "config-mssql-old": "Ríquese Microsoft SQL Server $1 o posterior. Tienes la versión $2.", "config-sqlite-name-help": "Escueye'l nome qu'identifica la to wiki.\nNun uses espacios o guiones.\nEsti va usase como nome del ficheru de datos pa SQLite.", "config-sqlite-parent-unwritable-group": "Nun puede crease el direutoriu de datos $1 porque'l servidor web nun tien permisu d'escritura nel direutoriu padre $2.\n\nL'instalador determinó l'usuariu col que s'executa'l sirvidor web.\nDa-y permisos d'escritura nel direutoriu $3 pa siguir.\nNun sistema Unix/Linux fai:\n\n
cd $2\nmkdir $3\nchgrp $4 $3\nchmod g+w $3
", "config-sqlite-parent-unwritable-nogroup": "Nun puede crease'l direutoriu de datos $1, porque'l sirvidor web nun tien permisu d'escritura nel direutoriu padre $2.\n\nL'instalador nun pudo determinar l'usuariu col que s'executa'l sirvidor web.\nDa permisos d'escritura universal pa él (¡y pa otros!) nel direutoriu $3 pa siguir.\nNun sistema Unix/Linux fai:\n\n
cd $2\nmkdir $3\nchmod a+w $3
", @@ -166,9 +149,6 @@ "config-db-web-no-create-privs": "La cuenta qu'especificasti pa la instalación nun tien permisos abondo pa crear una cuenta.\nLa cuenta qu'especifiques equí yá tien d'esistir.", "config-mysql-engine": "Motor d'almacenamientu:", "config-mysql-innodb": "InnoDB (aconséyase)", - "config-mssql-auth": "Triba d'autenticación:", - "config-mssql-sqlauth": "Autenticación de SQL Server", - "config-mssql-windowsauth": "Autenticación de Windows", "config-site-name": "Nome de la wiki:", "config-site-name-help": "Esto apaecerá na barra de títulos del navegador y en dellos sitios más.", "config-site-name-blank": "Escriba un nome pal sitiu.", diff --git a/includes/installer/i18n/ba.json b/includes/installer/i18n/ba.json index 23e53880b5..65bfc4f250 100644 --- a/includes/installer/i18n/ba.json +++ b/includes/installer/i18n/ba.json @@ -88,13 +88,9 @@ "config-db-type": "Мәғлүмәт базаһы төрө:", "config-db-host": "Мәғлүмәт базаһы хосты:", "config-db-host-help": "Әгәр ҙә серверҙың база мәғлүмәттәре икенсе серверҙа урынлашһа, бында уның исемен йәки IP-адресын индерегеҙ.\nӘгәр ҙә һеҙ виртуаль хостингты ҡулланһағыҙ, һеҙҙең провайдерығыҙ хостың дөрөҫ исемен үҙенең документацияһында күрһәтергә тейеш.\nӘгәр ҙә һеҙ системаны Windows аҫтына ҡуяһығыҙ һәм MySQL - ды ҡулланаһығыҙ икән, «localhost» исемле сервер эшләй алмаясаҡ. Был осраҡта 127.0.0.1 локаль IP-адресығыҙҙы күрһәтергә тырышығыҙ.\nӘгәр ҙә һеҙ PostgreSQL-ды ҡулланаһығыҙ икән, был шаҡмаҡты сокет Unix аша инеү өсөн буш ҡалдырығыҙ.", - "config-db-host-oracle": "TNS мәғлүмәт базаһы:", - "config-db-host-oracle-help": "Ғәмәлдәге [http://download.oracle.com/docs/cd/B28359_01/network.111/b28317/tnsnames.htm Local Connect Name] индерегеҙ; tnsnames.ora файлы был инсталляция өсөн күренергә тейеш.
Клиенттарҙың 10g версияһындағы һәм юғарыраҡ китапханаһын ҡулланғанда шулай уҡ атама биреү ысулын файҙаланыу мөмкинлеге бар [http://download.oracle.com/docs/cd/E11882_01/network.112/e10836/naming.htm Easy Connect].", "config-db-wiki-settings": "Был викиҙың идентификацияһы", "config-db-name": "Мәғлүмәт базаһы исеме:", "config-db-name-help": "Үҙегеҙҙең вики өсөн исем - идентификатор һайлағыҙ.\nИсемдә тултырылмаған урын булмаҫҡа тейеш.\nӘгәр һеҙ виртуаль хостингты ҡулланаһығыҙ икән, провайдер һеҙгә мәғлүмәттәр базаһының конкрет исемен бирер йәки идара итеү панеле ярҙамы менән мәғлүмәттәр базаһын булдырырға мөмкинлек бирер.", - "config-db-name-oracle": "Мәғлүмәт базаһы схемаһы", - "config-db-account-oracle-warn": "Oracle мәғлүмәттәр базаһы итеп ҡуйыуҙың өс юлы бар:\nӘгәр иҫәп яҙмаһын ҡуйыу процесында булдырырға теләһәгеҙ, зинһар, SYSDBA ҡуйыу өсөн иҫәп алыу ролен һәм веб-күҙәтеү мөмкинлеге булған иҫәп алыуҙың теләгән вәкәләттәрен күрһәтегеҙ. Шулай уҡ веб-күҙәтеү мөмкинлеге булған иҫәпте ҡулдан эшләргә һәм уны (әгәр схема объекттарын төҙөүгә кәрәкле рөхсәте бар икән) йәки ике иҫәп яҙмаһын, береһен - объекттар төҙөү хоҡуғы менән, икенсеһен веб-күҙәтеүҙе сикләүсе, күрһәтәһегеҙ. \nТейешле өҫтөнлөктәр менән иҫәп яҙмаһын булдырыу сценарийын ошо ҡоролма программаһының «maintenance/oracle/» папкаһында табырға мөмкин. Сикләнгән иҫәп яҙмаһын файҫаланыу килешеү буйынса иҫәп яҙмаларының барлыҡ мөмкинлектәрен һүндереүгә килтереү ихтималлығын күҙ уңында тотоғоҙ.", "config-db-install-account": "Көйләү өсөн иҫәп яҙмаһы", "config-db-username": "Мәғлүмәт базаһын ҡулланыусы исеме", "config-db-password": "Мәғлүмәт базаһының серһүҙе", @@ -113,34 +109,22 @@ "config-pg-test-error": "Мәғлүмәт базаһына инеп булманы$1: $2", "config-sqlite-dir": "SQLite мәғлүмәттәре директориһы:", "config-sqlite-dir-help": "SQLite бөтә мәғлүмәттәрҙе бер файлда һаҡлай. \nҠуйған ваҡытта веб-сервер һеҙ күрһәткән директорияны уҡый алырға тейеш. \n\nУға Интернет аша инеү '''мөмкин түгел''', шуға ул PHP файлдар һаҡланған файл менән тап килмәҫкә тейеш.\nҠуйыусы бал директорияны .htaccess файлына яҙасаҡ, әгәр ул эшләмәһә, кемдер бөтөн мәғлүмәт базаһына инә аласаҡ. Был базала шулай уҡ ҡулланыусылар тураһында мәғлүмәт тә (электрон почта адрестары, серһүҙ хештары), шулай уҡ юйылған биттәр һәм вики тураһында башҡа йәшерен мәғлүмәттәр һаҡлана. \n\nБыл базаны, мөмкин булһа, ситтәрәк, мәҫәлән, /var/lib/mediawiki/yourwiki һаҡлағыҙ.", - "config-oracle-def-ts": "Килешеү буйынса таблица арауығы:", - "config-oracle-temp-ts": "Таблицаларҙың ваҡытлы киңлеге:", "config-type-mysql": "MySQL (йәки тура килгән)", - "config-type-mssql": "Microsoft SQL Server", "config-support-info": "MediaWiki -ла түбәндәге СУБД бар:\n\n$1\n\nӘгәр мәғлүмәт һаҡлау системаһын исемлектә күрмәһәгеҙ, рөхсәт алыу өсөн өҫтәге һылтанмалағы инструкция буйынса эш итегеҙ.", "config-dbsupport-mysql": "* [{{int:version-db-mysql-url}} MySQL] — MediaWiki-ҙың иҫ яҡшы эшләгән төп мәғлүмәттәр базаһы. MediaWiki шулай уҡ MySQL-тап килгән [{{int:version-db-mariadb-url}} MariaDB] һәм [{{int:version-db-percona-url}} Percona Server] менән эшләй. ([https://www.php.net/manual/ru/mysql.installation.php MySQL-ярҙамында PHP туплау инструкцияһы])", "config-dbsupport-postgres": "* [{{int:version-db-postgres-url}} PostgreSQL] — СУБД-ның популяр открыткаһы, MySQL өсөн альтернатива.\nТөҙәтелмәгән хаталар булыуы мөмкин, эш схемаһында ҡулланыу тәҡдим ителмәй. ([https://www.php.net/manual/en/pgsql.installation.php PostgreSQL рөхсәт ителгән РНР йыйыу инструкцияһы]).", "config-dbsupport-sqlite": "* [{{int:version-db-sqlite-url}} SQLite] — яҡшы һәм еңел мәғлүмәт базаһы системаһы. ([http://www.php.net/manual/ru/pdo.installation.php собрать PHP SQLite] PDO менән эшләй торған инструкция)", - "config-dbsupport-oracle": "* [{{int:version-db-oracle-url}} Oracle] — предприятие масштабындаға коммерция базыһы. ([http://www.php.net/manual/ru/oci8.installation.php OCI8 ярҙамындағы РНР нисек йыйырға])", - "config-dbsupport-mssql": "* [{{int:version-db-oracle-url}} Oracle] — предприятие масштабындаға Windows өсөн коммерция базыһы. ([https://www.php.net/manual/en/sqlsrv.installation.php OCI8 ярҙамындағы РНР нисек йыйырға])", "config-header-mysql": "MySQL көйләү", "config-header-postgres": "PostgreSQL көйләү", "config-header-sqlite": "SQLite көйләү", - "config-header-oracle": "Оракул көйләү", - "config-header-mssql": "Microsoft SQL Серверенең билдәле дәүмәлдәре", "config-invalid-db-type": "Нигеҙ тибтарының дөрөҫ булмаған күрһәткестәре", "config-missing-db-name": "Һеҙ мәғәнәне индерергә тейешһегеҙ «{{int:config-db-name}}».", "config-missing-db-host": "Параметр мәғәнәһен индереү мотлаҡ «{{int:config-db-host}}».", - "config-missing-db-server-oracle": "Һеҙ бында мәғәнәне индерергә тейешһегеҙ «{{int:config-db-host-oracle}}».", - "config-invalid-db-server-oracle": "«$1» мәғлүмәттәр базаһының дөрөҫ булмаған TNS.\nЙә «TNS Name», йә «Easy Connect» ([http://docs.oracle.com/cd/E11882_01/network.112/e10836/naming.htm Oracle атамалары ысулы]) ҡулланығыҙ.", "config-invalid-db-name": "«$1» мәғлүмәттәр базаһының дөрөҫ булмаған префиксы. Тик ASCII символдарын: (a-z, A-Z) хәрефтәрен, (0-9) һандарын, (_) аҫтына һыҙыу билдәһен һәм (-)дефисты ҡулланығыҙ.", "config-invalid-db-prefix": "«$1» мәғлүмәттәр базаһының дөрөҫ булмаған префиксы. Тик ASCII (a-z, A-Z) хәрефтәрен, (0-9) һандарын, (_) аҫтына һыҙыу билдәһен һәм (-)дефисты ҡулланығыҙ.", "config-connection-error": "$1.\n\nХостығыҙҙы, ҡулланыусы исемен һәм паролде тикшерегеҙ ҙә яңынан инеп ҡарағыҙ.", "config-invalid-schema": "MediaWiki «$1» өсөн схема дөрөҫ түгел.\nБары тик ASCII символдарын (a-z, A-Z), цифрҙарҙы (0-9) һәм аҫҡы һыҙыҡты (_) ғына ҡулланығыҙ.", - "config-db-sys-create-oracle": "Яңы иҫәп-хисап яҙмаһын булдырыу өсөн урынлаштырыу программаһы тик SYSDBA ҡулланыу хуплана", - "config-db-sys-user-exists-oracle": "Иҫәп яҙмаһы \"$1\". SYSDBA яңы иҫәп-хисап яҙмаһын булдырыу өсөн генә ҡулланыла", "config-postgres-old": "PostgreSQL $1 йәки тағы ла һуңыраҡ булған версия кәрәк. Һеҙҙә PostgreSQL $2 ҡуйылған.", - "config-mssql-old": "$1 йә һуңыраҡ версиянан Microsoft SQL Server кәрәк. Һеҙҙә $2 версияһы ҡуйылған.", "config-sqlite-name-help": "Үҙегеҙҙең вики өсөн исем-идентификатор һайлағыҙ.\nДефисы һәм буш урын ҡалдырмағыҙ.\nЬыл юл SQLite файлының исемендә ҡулланыласаҡ.", "config-sqlite-parent-unwritable-group": "$1 мәғлүмәт директорияһын эшләп булманы, веб-серверҙың төп директорияны яҙырға хоҡуғы юҡ $2.\n\nУрынлаштырыусы ҡатнашыусының веб-серверын билдәләне.\n$3 яҙма мөмкин булған директория эшләгеҙ һәм дауам итегеҙ.\nUnix/Linux системаһында түбәндәгене башҡарығыҙ:\n\n
cd $2\nmkdir $3\nchgrp $4 $3\nchmod g+w $3
", "config-sqlite-parent-unwritable-nogroup": "$1 мәғлүмәт директорияһын эшләп булманы, веб-серверҙың төп директорияны яҙырға хоҡуғы юҡ $2.\n\nУрынлаштырыусы ҡатнашыусының веб-серверын билдәләй алманы.\n$3 яҙма мөмкин булған директория эшләгеҙ һәм дауам итегеҙ.\nUnix/Linux системаһында түбәндәгене башҡарығыҙ:\n\n
cd $2 mkdir $3 chmod a+w $3
", @@ -164,11 +148,6 @@ "config-mysql-engine": "Мәғлүмәт базаһы шыуҙырмаһы", "config-mysql-innodb": "InnoDB", "config-mysql-engine-help": "Параллель рәүештә яҡшыраҡ эшләгәне өсөн '''InnoDB''' өҫтөнлөрәк.\n\nБер ҡулланыусы йәки төҙәтеүҙәр әҙ булғанда вики өсөн '''MyISAM'''тың тиҙлеге шәберәк, әммә унда мәғлүмәт базаһы InnoDB-ҡа ҡарағанда йышыраҡ сафтан сыға.", - "config-mssql-auth": "Аутентификация төрө :", - "config-mssql-install-auth": "Ҡуйыу процесында мәғлүмәт базаһына инеү өсөн файҙаланылған төп нөсхәне тикшереү тибын һайлағыҙ. \n\nӘгәр «{{int:config-mssql-windowsauth}}» һайлаһығыҙ, ҡулланыусының веб-сервер эшләгән иҫәп яҙмаһы файҙаланыласаҡ.", - "config-mssql-web-auth": "Викиҙың ғәҙәттәге эше ваҡытында мәғлүмәттәр базаһы серверына инеү өсөн веб-сервер файҙаланған төп нөсхәне тикшереү тибын һайлағыҙ. \n\nӘгәр «{{int:config-mssql-windowsauth}}» һайлаһығыҙ, ҡулланыусының веб-сервер эшләгән иҫәп яҙмаһы файҙаланыласаҡ.", - "config-mssql-sqlauth": "SQL Server ысынлығын тикшереү", - "config-mssql-windowsauth": "Windows нөсхәһен тикшереү", "config-site-name": "Вики атамаһы:", "config-site-name-help": "Исеме браузерҙың баш һүҙендә һәм башҡа урындарҙа күрәнәсәк.", "config-site-name-blank": "Сайт исемен яҙығыҙ", diff --git a/includes/installer/i18n/be-tarask.json b/includes/installer/i18n/be-tarask.json index c29f5f7ada..dfea0a5740 100644 --- a/includes/installer/i18n/be-tarask.json +++ b/includes/installer/i18n/be-tarask.json @@ -7,7 +7,8 @@ "Zedlik", "아라", "Red Winged Duck", - "Macofe" + "Macofe", + "Renessaince" ] }, "config-desc": "Праграма ўсталяваньня MediaWiki", @@ -85,18 +86,14 @@ "config-uploads-not-safe": "'''Папярэджаньне:''' дырэкторыя для загрузак па змоўчваньні $1 уразьлівая да выкананьня адвольнага коду.\nХоць MediaWiki і правярае ўсе файлы перад захаваньнем, вельмі рэкамэндуецца [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Security#Upload_security закрыць гэтую ўразьлівасьць] перад уключэньнем магчымасьці загрузкі файлаў.", "config-no-cli-uploads-check": "'''Папярэджаньне:''' Вашая дырэкторыя для загрузак па змоўчваньні ($1), не правераная на ўразьлівасьць да выкананьня адвольных скрыптоў падчас усталяваньня CLI.\n.", "config-brokenlibxml": "У Вашай сыстэме ўсталяваныя PHP і libxml2 зь несумяшчальнымі вэрсіямі, што можа прывесьці да пашкоджаньня зьвестак MediaWiki і іншых вэб-дастасаваньняў.\nАбнавіце libxml2 да вэрсіі 2.7.3 ці больш позьняй ([https://bugs.php.net/bug.php?id=45996 паведамленьне пра памылку на сайце PHP]).\nУсталяваньне перарванае.", - "config-suhosin-max-value-length": "Suhosin усталяваны і абмяжоўвае даўжыню парамэтру GET да $1 {{PLURAL:$1|1=байта|байтаў}}.\nResourceLoader, складнік MediaWiki, будзе абходзіць гэтае абмежаваньне, што адаб’ецца на прадукцыйнасьці.\nКалі магчыма, варта ўсталяваць у php.ini значэньне suhosin.get.max_value_length роўным 1024 ці больш, а таксама вызначыць тое ж значэньне для $wgResourceLoaderMaxQueryLength у LocalSettings.php.", + "config-suhosin-max-value-length": "Suhosin усталяваны і абмяжоўвае даўжыню парамэтру GET да $1 {{PLURAL:$1|1=байта|байтаў}}.\nMediaWiki патрабуе, каб suhosin.get.max_value_length складаў прынамсі $2. Адключыце гэтую наладу ці павялічце гэтае значэньня да $3 ў php.ini.", "config-using-32bit": "Папярэджаньне: падобна, што вашая сыстэма выкарыстоўвае 32-бітавыя цэлыя лікі. Гэта [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:32-bit не рэкамэндуецца].", "config-db-type": "Тып базы зьвестак:", "config-db-host": "Хост базы зьвестак:", "config-db-host-help": "Калі сэрвэр вашай базы зьвестак знаходзіцца на іншым сэрвэры, увядзіце тут імя хоста ці IP-адрас.\n\nКалі вы карыстаецеся shared-хостынгам, ваш хостынг-правайдэр мусіць даць вам слушнае імя хоста базы зьвестак у сваёй дакумэнтацыі.\n\nКалі вы ўжываеце MySQL, выкарыстаньне «localhost» можа не працаваць для назвы сэрвэра. У гэтым выпадку паспрабуйце пазначыць «127.0.0.1» для лякальнага IP-адрасу.\n\nКалі вы выкарыстоўваеце PostgreSQL, пакіньце поле пустым, каб далучыцца праз Unix-сокет.", - "config-db-host-oracle": "TNS базы зьвестак:", - "config-db-host-oracle-help": "Увядзіце слушнае [http://download.oracle.com/docs/cd/B28359_01/network.111/b28317/tnsnames.htm лякальнае імя злучэньня]; файл tnsnames.ora павінен быць бачным для гэтага ўсталяваньня.
Калі Вы выкарыстоўваеце кліенцкія бібліятэкі 10g ці больш новыя, Вы можаце таксама выкарыстоўваць мэтад наданьня назваў [http://download.oracle.com/docs/cd/E11882_01/network.112/e10836/naming.htm лёгкае злучэньне].", "config-db-wiki-settings": "Ідэнтыфікацыя гэтай вікі", "config-db-name": "Назва базы зьвестак:", "config-db-name-help": "Выберыце імя для вызначэньня Вашай вікі.\nЯно ня мусіць зьмяшчаць прагалаў.\n\nКалі Вы набываеце shared-хостынг, Ваш хостынг-правайдэр мусіць надаць Вам ці пэўнае імя базы зьвестак для выкарыстаньня, ці магчымасьць ствараць базы зьвестак праз кантрольную панэль.", - "config-db-name-oracle": "Схема базы зьвестак:", - "config-db-account-oracle-warn": "Існуюць тры сцэнары ўсталяваньня Oracle як базы зьвестак для MediaWiki:\n\nКалі Вы жадаеце стварыць рахунак базы зьвестак як частку працэсу ўсталяваньня, калі ласка, падайце рахунак з роляй SYSDBA як рахунак базы зьвестак для ўсталяваньня і пазначце пажаданыя правы рахунку з доступам да Інтэрнэту, у адваротным выпадку Вы можаце таксама стварыць рахунак з доступам да Інтэрнэту ўручную і падаць толькі гэты рахунак (калі патрабуюцца правы для стварэньня схемы аб’ектаў) ці падайце два розных рахункі, адзін з правамі на стварэньне і адзін з абмежаваньнямі для доступу да Інтэрнэту.\n\nСкрыпт для стварэньня рахунку з патрабуемымі правамі можна знайсьці ў дырэкторыі гэтага ўсталяваньня «maintenance/oracle/». Памятайце, што выкарыстаньне рахунку з абмежаваньнямі адключыць усе падтрымліваемыя магчымасьці даступныя па змоўчваньні.", "config-db-install-account": "Імя карыстальніка для ўсталяваньня", "config-db-username": "Імя карыстальніка базы зьвестак:", "config-db-password": "Пароль базы зьвестак:", @@ -115,37 +112,24 @@ "config-pg-test-error": "Немагчыма далучыцца да базы зьвестак '''$1''': $2", "config-sqlite-dir": "Дырэкторыя зьвестак SQLite:", "config-sqlite-dir-help": "SQLite захоўвае ўсе зьвесткі ў адзіным файле.\n\nПададзеная Вамі дырэкторыя павінна быць даступнай да запісу вэб-сэрвэрам падчас усталяваньня.\n\nЯна '''ня''' мусіць быць даступнай праз Сеціва, вось чаму мы не захоўваем яе ў адным месцы з файламі PHP.\n\nПраграма ўсталяваньня дадаткова створыць файл .htaccess, але калі ён не выкарыстоўваецца, хто заўгодна зможа атрымаць зьвесткі з базы зьвестак.\nГэта ўключае як прыватныя зьвесткі ўдзельнікаў (адрасы электроннай пошты, хэшы пароляў), гэтак і выдаленыя вэрсіі старонак і іншыя зьвесткі, доступ да якіх маецца абмежаваны.\n\nПадумайце над тым, каб зьмяшчаць базу зьвестак у іншым месцы, напрыклад у /var/lib/mediawiki/yourwiki.", - "config-oracle-def-ts": "Прастора табліцаў па змоўчваньні:", - "config-oracle-temp-ts": "Часовая прастора табліцаў:", "config-type-mysql": "MariaDB, MySQL, або сумяшчальная", "config-type-postgres": "PostgreSQL", "config-type-sqlite": "SQLite", - "config-type-oracle": "Oracle", - "config-type-mssql": "Microsoft SQL Server", "config-support-info": "MediaWiki падтрымлівае наступныя сыстэмы базаў зьвестак:\n\n$1\n\nКалі Вы ня бачыце сыстэму базаў зьвестак, якую Вы спрабуеце выкарыстоўваць ў сьпісе ніжэй, перайдзіце па спасылцы інструкцыі, якая знаходзіцца ніжэй, каб уключыць падтрымку.", "config-dbsupport-mysql": "* [{{int:version-db-mariadb-url}} MariaDB] зьяўляецца галоўнай мэтай MediaWiki і падтрымліваецца найлепш. MediaWiki таксама працуе з [{{int:version-db-mysql-url}} MySQL] і [{{int:version-db-percona-url}} Percona Server], якія сумяшчальныя з MariaDB. ([https://www.php.net/manual/en/mysqli.installation.php Як скампіляваць PHP з падтрымкай MySQL])", "config-dbsupport-postgres": "* [{{int:version-db-postgres-url}} PostgreSQL] — папулярная сыстэма базы зьвестак з адкрытым кодам, якая зьяўляецца альтэрнатывай MySQL. ([https://www.php.net/manual/en/pgsql.installation.php Як кампіляваць PHP з падтрымкай PostgreSQL])", "config-dbsupport-sqlite": "* [{{int:version-db-sqlite-url}} SQLite] — невялікая сыстэма базы зьвестак, якая мае вельмі добрую падтрымку. ([https://www.php.net/manual/en/pdo.installation.php Як кампіляваць PHP з падтрымкай SQLite], выкарыстоўвае PDO)", - "config-dbsupport-oracle": "* [{{int:version-db-oracle-url}} Oracle] зьяўляецца камэрцыйнай прафэсійнай базай зьвестак. ([https://www.php.net/manual/en/oci8.installation.php Як скампіляваць PHP з падтрымкай OCI8])", - "config-dbsupport-mssql": "* [{{int:version-db-mssql-url}} Microsoft SQL Server] — камэрцыйная база зьвестак для Windows. ([https://www.php.net/manual/en/sqlsrv.installation.php Як скампіляваць PHP з падтрымкай SQLSRV])", "config-header-mysql": "Налады MariaDB/MySQL", "config-header-postgres": "Налады PostgreSQL", "config-header-sqlite": "Налады SQLite", - "config-header-oracle": "Налады Oracle", - "config-header-mssql": "Налады Microsoft SQL Server", "config-invalid-db-type": "Няслушны тып базы зьвестак", "config-missing-db-name": "Вы мусіце ўвесьці значэньне парамэтру «{{int:config-db-name}}».", "config-missing-db-host": "Вы мусіце ўвесьці значэньне парамэтру «{{int:config-db-host}}».", - "config-missing-db-server-oracle": "Вы мусіце ўвесьці значэньне парамэтру «{{int:config-db-host-oracle}}».", - "config-invalid-db-server-oracle": "Няслушнае TNS базы зьвестак «$1».\nВыкарыстоўвайце або «TNS Name», або радок «Easy Connect» ([http://docs.oracle.com/cd/E11882_01/network.112/e10836/naming.htm Мэтады найменьня Oracle])", "config-invalid-db-name": "Няслушная назва базы зьвестак «$1».\nНазва можа ўтрымліваць толькі ASCII-літары (a-z, A-Z), лічбы (0-9), сымбалі падкрэсьліваньня(_) і працяжнікі (-).", "config-invalid-db-prefix": "Няслушны прэфікс базы зьвестак «$1».\nЁн можа зьмяшчаць толькі ASCII-літары (a-z, A-Z), лічбы (0-9), сымбалі падкрэсьліваньня (_) і працяжнікі (-).", "config-connection-error": "$1.\n\nПраверце хост, імя карыстальніка і пароль і паспрабуйце зноў. Калі вы ўжываеце «localhost» у якасьці хосту базы зьвестак, паспрабуйце «127.0.0.1» замест (ці наадварот).", "config-invalid-schema": "Няслушная схема для MediaWiki «$1».\nВыкарыстоўвайце толькі ASCII-літары (a-z, A-Z), лічбы (0-9) і сымбалі падкрэсьліваньня (_).", - "config-db-sys-create-oracle": "Праграма ўсталяваньня падтрымлівае толькі выкарыстаньне рахунку SYSDBA для стварэньня новага рахунку.", - "config-db-sys-user-exists-oracle": "Рахунак карыстальніка «$1» ужо існуе. SYSDBA можа выкарыстоўвацца толькі для стварэньня новых рахункаў!", "config-postgres-old": "Патрабуецца PostgreSQL $1 ці навейшая, усталяваная вэрсія $2.", - "config-mssql-old": "Патрабуецца Microsoft SQL Server вэрсіі $1 ці больш позьняй. У вас усталяваная вэрсія $2.", "config-sqlite-name-help": "Выберыце назву, якая будзе ідэнтыфікаваць Вашую вікі.\nНе выкарыстоўвайце прагалы ці злучкі.\nНазва будзе выкарыстоўвацца ў назьве файла зьвестак SQLite.", "config-sqlite-parent-unwritable-group": "Немагчыма стварыць дырэкторыю зьвестак $1, таму што бацькоўская дырэкторыя $2 абароненая ад запісаў вэб-сэрвэра.\n\nПраграма ўсталяваньня вызначыла карыстальніка, які запусьціў вэб-сэрвэр.\nДазвольце запісы ў дырэкторыю $3 для працягу.\nУ сыстэме Unix/Linux зрабіце:\n\n
cd $2\nmkdir $3\nchgrp $4 $3\nchmod g+w $3
", "config-sqlite-parent-unwritable-nogroup": "Немагчыма стварыць дырэкторыю зьвестак $1, таму што бацькоўская дырэкторыя $2 абароненая ад запісаў вэб-сэрвэра.\n\nПраграма ўсталяваньня вызначыла карыстальніка, які запусьціў вэб-сэрвэр.\nДазвольце яму (і іншым) запісы ў дырэкторыю $3 для працягу.\nУ сыстэме Unix/Linux зрабіце:\n\n
cd $2\nmkdir $3\nchmod a+w $3
", @@ -170,11 +154,6 @@ "config-mysql-engine": "Рухавік сховішча:", "config-mysql-innodb": "InnoDB (рэкамэндавана)", "config-mysql-engine-help": "'''InnoDB''' — звычайна найбольш слушны варыянт, таму што добра падтрымлівае паралелізм.\n\n'''MyISAM''' можа быць хутчэйшай у вікі з адным удзельнікам, ці толькі для чытаньня.\nБазы зьвестак на MyISAM вядомыя тым, што ў іх зьвесткі шкодзяцца нашмат часьцей за InnoDB.", - "config-mssql-auth": "Тып аўтэнтыфікацыі:", - "config-mssql-install-auth": "Абярыце тып аўтэнтыфікацыі, які будзе выкарыстаны для злучэньня з базай зьвестак падчас працэсу ўсталяваньня.\nКалі вы абярэце «{{int:config-mssql-windowsauth}}», будуць выкарыстаныя ўліковыя зьвесткі карыстальніка, пад якім працуе вэб-сэрвэр.", - "config-mssql-web-auth": "Абярыце тып аўтэнтыфікацыі, які вэб-сэрвэр будзе выкарыстоўваць для злучэньня з базай зьвестак падчас звычайнага функцыянаваньня вікі.\nКалі вы абярэце «{{int:config-mssql-windowsauth}}», будуць выкарыстаныя ўліковыя зьвесткі карыстальніка, пад якім працуе вэб-сэрвэр.", - "config-mssql-sqlauth": "Аўтэнтыфікацыя SQL-сэрвэра", - "config-mssql-windowsauth": "Windows-аўтэнтыфікацыя", "config-site-name": "Назва вікі:", "config-site-name-help": "Назва будзе паказвацца ў загалоўку браўзэра і ў некаторых іншых месцах.", "config-site-name-blank": "Увядзіце назву сайта.", diff --git a/includes/installer/i18n/bg.json b/includes/installer/i18n/bg.json index de2f0cb047..ffc534ce8b 100644 --- a/includes/installer/i18n/bg.json +++ b/includes/installer/i18n/bg.json @@ -90,13 +90,9 @@ "config-db-type": "Тип на базата от данни:", "config-db-host": "Сървър на базата от данни:", "config-db-host-help": "Ако базата от данни е на друг сървър, в кутията се въвежда името на хоста или IP адреса.\n\nАко се използва споделен уеб хостинг, доставчикът на услугата би трябвало да е предоставил в документацията си коректния хост.\n\nАко се използва MySQL, използването на „localhost“ може да е неприемливо. В такива случаи се използва „127.0.0.1“ за локален IP адрес.\n\nПри използване на PostgreSQL, това поле се оставя празно, за свързване чрез Unix socket.", - "config-db-host-oracle": "TNS на базата от данни:", - "config-db-host-oracle-help": "Въведете валидно [http://download.oracle.com/docs/cd/B28359_01/network.111/b28317/tnsnames.htm Local Connect Name]. Файлът tnsnames.ora трябва да бъде видим за инсталацията.
Ако използвате клиентска библиотека версия 10g или по-нова можете да използвате метода [http://download.oracle.com/docs/cd/E11882_01/network.112/e10836/naming.htm Easy Connect].", "config-db-wiki-settings": "Идентифициране на това уики", "config-db-name": "Име на базата от данни (без тирета):", "config-db-name-help": "Избира се име, което да идентифицира уикито.\nТо не трябва да съдържа интервали.\n\nАко се използва споделен хостинг, доставчикът на услугата би трябвало да е предоставил или име на базата от данни, която да бъде използвана, или да позволява създаването на бази от данни чрез контролния панел.", - "config-db-name-oracle": "Схема на базата от данни:", - "config-db-account-oracle-warn": "Има три поддържани сценария за инсталиране на Oracle като бекенд база данни:\n\nАко искате да създадете профил в базата данни като част от процеса на инсталиране, моля, посочете профил със SYSDBA като профил в базата данни за инсталиране и посочете желаните данни за влизане (име и парола) за профил с уеб достъп; в противен случай можете да създадете профил с уеб достъп ръчно и предоставите само него (ако той има необходимите права за създаване на схематични обекти), или да предоставите два различни профила - един с привилегии за създаване на обекти, и друг - с ограничения за уеб достъп.\n\nСкрипт за създаването на профил с необходимите привилегии може да се намери в папката „maintenance/oracle/“ на тази инсталация. Имайте в предвид, че използването на ограничен профил ще деактивира всички възможности за обслужване на профила по подразбиране.", "config-db-install-account": "Потребителска сметка за инсталацията", "config-db-username": "Потребителско име за базата от данни:", "config-db-password": "Парола за базата от данни:", @@ -115,34 +111,22 @@ "config-pg-test-error": "Невъзможно свързване с базата данни $1: $2", "config-sqlite-dir": "Директория за данни на SQLite:", "config-sqlite-dir-help": "SQLite съхранява всички данни в един файл.\n\nПо време на инсталацията уеб сървърът трябва да има права за писане в посочената директория.\n\nТя не трябва да е достъпна през уеб, затова не е там, където са PHP файловете.\n\nИнсталаторът ще съхрани заедно с нея файл .htaccess, но ако този метод пропадне, някой може да придобие достъп до суровите данни от базата от данни.\nТова включва сурови данни за потребителите (адреси за е-поща, хеширани пароли), както и изтрити версии на страници и друга чувствителна и с ограничен достъп информация от и за уикито.\n\nБазата от данни е препоръчително да се разположи на друго място, например в /var/lib/mediawiki/yourwiki.", - "config-oracle-def-ts": "Таблично пространство по подразбиране:", - "config-oracle-temp-ts": "Временно таблично пространство:", "config-type-mysql": "MariaDB, MySQL (или съвместима)", - "config-type-mssql": "Microsoft SQL сървър", "config-support-info": "МедияУики поддържа следните системи за бази от данни:\n\n$1\n\nАко не виждате желаната за използване система в списъка по-долу, следвайте инструкциите за активиране на поддръжка по-горе.", "config-dbsupport-mysql": "* [{{int:version-db-mariadb-url}} MariaDB] е най-важна за МедияУики и се поддържа най-добре. МедияУики работи също така с [{{int:version-db-mysql-url}} MySQL] и [{{int:version-db-percona-url}} Percona Server], които са съвместими с MariaDB.\n([https://www.php.net/manual/en/mysqli.installation.php Как се компилира PHP с поддръжка на MySQL])", "config-dbsupport-postgres": "* [{{int:version-db-postgres-url}} PostgreSQL] е популярна система за управление на бази от данни, алтернатива на MySQL. ([https://www.php.net/manual/bg/pgsql.installation.php Как се компилира PHP с поддръжка на PostgreSQL])", "config-dbsupport-sqlite": "* [{{int:version-db-sqlite-url}} SQLite] е олекотена система за бази от данни, която е много добре поддържана. ([https://www.php.net/manual/en/pdo.installation.php Как се компилира PHP с поддръжка на SQLite], използва PDO)", - "config-dbsupport-oracle": "* [{{int:version-db-oracle-url}} Oracle] е комерсиална корпоративна база от данни. ([https://www.php.net/manual/en/oci8.installation.php Как се компилира PHP с поддръжка на OCI8])", - "config-dbsupport-mssql": "* [{{int:version-db-mssql-url}} Microsoft SQL Server] е комерсиална корпоративна база от данни за Windows. ([https://www.php.net/manual/en/sqlsrv.installation.php Как да се компилира PHP с поддръжка на SQLSRV])", "config-header-mysql": "Настройки на MariaDB/MySQL", "config-header-postgres": "Настройки за PostgreSQL", "config-header-sqlite": "Настройки за SQLite", - "config-header-oracle": "Настройки за Oracle", - "config-header-mssql": "Настройки за Microsoft SQL сървър", "config-invalid-db-type": "Невалиден тип база от данни", "config-missing-db-name": "Необходимо е да се въведе стойност за „{{int:config-db-name}}“.", "config-missing-db-host": "Необходимо е да се въведе стойност за „{{int:config-db-host}}“.", - "config-missing-db-server-oracle": "Необходимо е да се въведе стойност за „{{int:config-db-host-oracle}}“.", - "config-invalid-db-server-oracle": "Невалиден TNS на базата от данни „$1“.\nИзползвайте „TNS Name“ или „Easy Connect“ ([http://docs.oracle.com/cd/E11882_01/network.112/e10836/naming.htm Методи за именуване на Oracle])", "config-invalid-db-name": "Невалидно име на базата от данни „$1“.\nИзползват се само ASCII букви (a-z, A-Z), цифри (0-9), долни черти (_) и тирета (-).", "config-invalid-db-prefix": "Невалидна представка за базата от данни „$1“.\nПозволени са само ASCII букви (a-z, A-Z), цифри (0-9), долни черти (_) и тирета (-).", "config-connection-error": "$1.\n\nНеобходимо е да се проверят хостът, потребителското име и паролата, след което да се опита отново. Ако използвате „localhost“ като хост на базата от данни, опитайте с „127.0.0.1“ вместо него (и обратно).", "config-invalid-schema": "Невалидна схема за МедияУики „$1“.\nДопустими са само ASCII букви (a-z, A-Z), цифри (0-9) и долни черти (_).", - "config-db-sys-create-oracle": "Инсталаторът поддържа само сметка SYSDBA за създаване на нова сметка.", - "config-db-sys-user-exists-oracle": "Потребителската сметка „$1“ вече съществува. SYSDBA може да се използва само за създаване на нова сметка!", "config-postgres-old": "Изисква се PostgreSQL $1 или по-нова версия, наличната версия е $2.", - "config-mssql-old": "Изисква се Microsoft SQL Server версия $1 или по-нова. Вашата версия е $2.", "config-sqlite-name-help": "Избира се име, което да идентифицира уикито.\nНе се използват интервали или тирета.\nТова име ще се използва за име на файла за данни на SQLite.", "config-sqlite-parent-unwritable-group": "Директорията за данни $1 не може да бъде създадена, тъй като уеб сървърът няма права за писане в родителската директория $2.\n\nИнсталаторът разпознава потребителското име, с което работи уеб сървърът.\nУверете се, че той притежава права за писане в директорията $3 преди да продължите.\nВ Unix/Линукс системи можете да използвате:\n\n
cd $2\nmkdir $3\nchgrp $4 $3\nchmod g+w $3
", "config-sqlite-parent-unwritable-nogroup": "Директорията за данни $1 не може да бъде създадена, тъй като уеб сървърът няма права за писане в родителската директория $2.\n\nИнсталаторът не може да определи потребителското име, с което работи уеб сървърът.\nУверете се, че в директория $3 може да бъде писано от уеб сървъра (или от други потребители!) преди да продължите.\nНа Unix/Линукс системи можете да използвате:\n\n
cd $2\nmkdir $3\nchmod a+w $3
", @@ -167,11 +151,6 @@ "config-mysql-engine": "Хранилище на данни:", "config-mysql-innodb": "InnoDB (препоръчително)", "config-mysql-engine-help": "InnoDB почти винаги е най-добрата възможност заради навременната си поддръжка.\n\nMyISAM може да е по-бърза при инсталации с един потребител или само за четене.\nБазите от данни MyISAM се повреждат по-често от InnoDB.", - "config-mssql-auth": "Тип на удостоверяването:", - "config-mssql-install-auth": "Изберете начин за удостоверяване, който ще бъде използван за връзка с базата от данни по време на инсталацията.\nАко изберете \"{{int:config-mssql-windowsauth}}\", ще се използват идентификационните данни на потребителя под който работи уеб сървъра.", - "config-mssql-web-auth": "Изберете начина за удостоверяване, който ще се използва от уеб сървъра за връзка със сървъра за бази от данни по време на нормалните операции на уикито.\nАко изберете \"{{int:config-mssql-windowsauth}}\", ще се използват идентификационните данни на потребителя под който работи уеб сървъра.", - "config-mssql-sqlauth": "Удостоверяване чрез SQL Server", - "config-mssql-windowsauth": "Удостоверяване чрез Windows", "config-site-name": "Име на уикито:", "config-site-name-help": "Това име ще се показва в заглавната лента на браузъра и на различни други места.", "config-site-name-blank": "Необходимо е да се въведе име на уикито.", diff --git a/includes/installer/i18n/bn.json b/includes/installer/i18n/bn.json index 0debe4a0ed..b8a467c3c1 100644 --- a/includes/installer/i18n/bn.json +++ b/includes/installer/i18n/bn.json @@ -68,26 +68,20 @@ "config-db-schema": "মিডিয়াউইকির জন্য স্কিমা (হাইফেন ছাড়া):", "config-pg-test-error": "উপাত্তশালা $1-এর সাথে সংযোগ দেয়া সম্ভব হয়নি। কারন:$2", "config-sqlite-dir": "SQLite উপাত্ত ডিরেক্টরি:", - "config-oracle-def-ts": "পূর্বনির্ধারিত টেবিলস্পেস", - "config-oracle-temp-ts": "সাময়কি টেবিলস্পেস:", "config-type-mysql": "MariaDB, MySQL, বা উপযুক্তগুলি", - "config-type-mssql": "মাইক্রোসফট SQL সার্ভার", "config-dbsupport-postgres": "* MySQL-এর বিকল্প হিসেবে [{{int:version-db-postgres-url}} PostgreSQL] হচ্ছে একটি জনপ্রিয় মুক্ত উৎসের ডাটাবেস ব্যবস্থা। ([https://www.php.net/manual/en/pgsql.installation.php PostgreSQL সমর্থনসহ কিভাবে PHP সঙ্কলন করবেন])", "config-header-mysql": "MariaDB/MySQL সেটিং", "config-header-postgres": "PostgreSQL সেটিংস", "config-header-sqlite": "SQLite সেটিংস", - "config-header-oracle": "ওরাকল সেটিংস", "config-invalid-db-type": "ডেটাবেজের ধরন অগ্রহযোগ্য", "config-missing-db-name": "আপনাকে অবশ্যই \"{{int:config-db-name}}\"-এর জন্য একটি মান প্রবেশ করাতে হবে।", "config-missing-db-host": "আপনাকে অবশ্যই \"{{int:config-db-host}}\"-এর জন্য একটি মান প্রবেশ করাতে হবে।", - "config-missing-db-server-oracle": "আপনাকে অবশ্যই \"{{int:config-db-host-oracle}}\"-এর জন্য একটি মান প্রবেশ করাতে হবে।", "config-connection-error": "$1।\n\n\nদয়া করে প্রস্তাবকারী, ব্যবহারকারী নাম ও পাসওয়ার্ড দেখুন এবং পুনরায় চেষ্টা করুন।", "config-sqlite-readonly": "ফাইল $1 লিখনযোগ্য নয়।", "config-sqlite-cant-create-db": "ডাটাবেজ ফাইল $1 তৈরি করা যায়নি।", "config-regenerate": "LocalSettings.php পুনরূত্পাদিত করুন →", "config-mysql-engine": "সংগ্রহস্থল ইঞ্জিন:", "config-mysql-innodb": "InnoDB (সুপারিশকৃত)", - "config-mssql-windowsauth": "উইন্ডোজ প্রমাণীকরণ", "config-site-name": "উইকির নাম:", "config-site-name-blank": "একটি সাইটের নাম প্রবেশ করান।", "config-project-namespace": "প্রকল্প নামস্থান:", diff --git a/includes/installer/i18n/br.json b/includes/installer/i18n/br.json index de6655b3e7..b9fb7b84e0 100644 --- a/includes/installer/i18n/br.json +++ b/includes/installer/i18n/br.json @@ -84,13 +84,9 @@ "config-db-type": "Doare an diaz roadennoù :", "config-db-host": "Anv implijer an diaz roadennoù :", "config-db-host-help": "M'emañ ho servijer roadennoù war ur servijer disheñvel, merkit amañ anv an ostiz pe ar chomlec'h IP.\n\nMa rit gant un herberc'hiañ kenrannet, e tlefe ho herberc'hier bezañ pourchaset deoc'h an anv ostiz reizh en teulioù titouriñ.\n\nM'emaoc'h o staliañ ur servijer Windows ha ma rit gant MySQL, marteze ne'z aio ket en-dro \"localhost\" evel anv servijer. Ma ne dro ket, klaskit ober gant \"127.0.0.1\" da chomlec'h IP lechel.", - "config-db-host-oracle": "TNS an diaz roadennoù :", - "config-db-host-oracle-help": "Merkit un [http://download.oracle.com/docs/cd/B28359_01/network.111/b28317/tnsnames.htm anv kevreañ lec'hel] reizh; dleout a ra ur restr tnsnames.ora bezañ hewel e-pad ar staliadur.
Ma rit gant al levraouegoù arval 10g pe nevesoc'h e c'hallit ivez ober gant an hentenn envel [http://download.oracle.com/docs/cd/E11882_01/network.112/e10836/naming.htm Easy Connect].", "config-db-wiki-settings": "Anavezout ar wiki-mañ", "config-db-name": "Anv an diaz roadennoù :", "config-db-name-help": "Dibabit un anv evit ho wiki.\nNa lakait ket a esaouennoù ennañ.\n\nMa ri gant un herberc'hiañ kenrannet e vo pourchaset deoc'h un anv diaz roadennoù dibar da vezañ graet gantañ gant ho herberc'hier pe e lezo ac'hanoc'h da grouiñ diazoù roadennoù dre ur banell gontrolliñ.", - "config-db-name-oracle": "Brastres diaz roadennoù :", - "config-db-account-oracle-warn": "Skoret ez eus tri doare evit staliañ Oracle da v/backend diaz roadennoù :\n\nMar fell deoc'h krouiñ ur gont diaz roadennoù e-ser an argerzh staliañ eo rekis pourchas ur gont gant ur roll SYSDBA evel kont diaz roadennoù evit ar staliañ, ha spisaat an titouroù anaout a fell deoc'h evit ar gont moned ouzh ar web. A-hend-all, e c'hallit krouiñ ar gont moned ouzh ar web gant an dorn ha pourchas hepken ar gont-se (ma'z eus bet ranket diskouez aotreoù ret evit krouiñ traezoù ar brastres) pe pourveziñ div gont disheñvel, unan gant dreistwirioù krouiñ hag eben, gant gwirioù strishaet, evit moned ouzh ar web.\n\nGallout a reer kaout ar skript evit kouiñ ur gont a zo rekis dreistwirioù eviti e kavlec'h \"trezalc'h/oracle/\" ar staliadur-mañ. Na zisoñjit ket e vo diweredekaet holl varregezhioù trezalc'h ar gont dre ziouer ma rit gant ur gont strishaet he gwirioù.", "config-db-install-account": "Kont implijer evit ar staliadur", "config-db-username": "Anv implijer an diaz roadennoù :", "config-db-password": "Ger-tremen an diaz roadennoù :", @@ -109,37 +105,24 @@ "config-pg-test-error": "N'haller ket kevreañ ouzh an diaz-titouroù '''$1''' : $2", "config-sqlite-dir": "Kavlec'h roadennoù SQLite :", "config-sqlite-dir-help": "Stokañ a ra SQLite an holl roadennoù en ur restr nemetken.\n\nE-pad ar staliañ, rankout a ra ar servijer web gallout skrivañ er c'havlec'h pourchaset ganeoc'h.\n\nNe zlefe ket bezañ tizhadus dre ar web; setu perak ne lakaomp ket anezhañ el lec'h m'emañ ho restroù PHP.\n\nSkivañ a raio ar stalier ur restr .htaccess war un dro gantañ met ma c'hoarvez ur fazi e c'hallfe unan bennak tapout krog en ho roadennoù.\nKement-se a sell ouzh ar roadennoù implijer (chomlec'hioù postel, gerioù-tremen hachet) hag ouzh an adweladennoù diverket ha takadoù gwarzeet all eus ar wiki.\n\nEn em soñjit ha ne vefe ket gwelloc'h lakaat an diaz roadennoù en un tu bennak all, da skouer e /var/lib/mediawiki/yourwiki.", - "config-oracle-def-ts": "Esaouenn stokañ (\"tablespace\") dre ziouer :", - "config-oracle-temp-ts": "Esaouenn stokañ (''tablespace'') da c'hortoz :", "config-type-mysql": "MySQL (pe kenglotus)", "config-type-postgres": "PostgreSQL", "config-type-sqlite": "SQLite", - "config-type-oracle": "Oracle", - "config-type-mssql": "Microsoft SQL Server", "config-support-info": "Skoret eo ar reizhiadoù diaz titouroù da-heul gant MediaWiki :\n\n$1\n\nMa ne welit ket amañ dindan ar reizhiad diaz titouroù a fell deoc'h ober ganti, heuilhit an titouroù a-us (s.o. al liammoù) evit gweredekaat ar skorañ.", "config-dbsupport-mysql": "* [{{int:version-db-mysql-url}} MySQL] eo an dibab kentañ evit MediaWiki hag an hini skoret ar gwellañ. Mont a ra MediaWiki en-dro gant [{{int:version-db-mariadb-url}} MariaDB] ha [{{int:version-db-percona-url}} Percona Server] ivez, kenglotus o-daou gant MySQL. ([https://www.php.net/manual/en/mysqli.installation.php Penaos kempunañ PHP gant skor MySQL])", "config-dbsupport-postgres": "* [{{int:version-db-postgres-url}} PostgreSQL] zo anezhi ur reizhiad diaz roadennoù frank a wirioù brudet-mat a c'haller ober gantañ e plas MySQL. ([https://www.php.net/manual/en/pgsql.installation.php Penaos kempunañ PHP gant skor PostgreSQL])", "config-dbsupport-sqlite": "* [{{int:version-db-sqlite-url}} SQLite] zo anezhi ur reizhiad diaz roadennoù skañv skoret eus ar c'hentañ. ([http://www.php.net/manual/en/pdo.installation.php Penaos kempunañ PHP gant skor SQLite], implijout a ra PDO)", - "config-dbsupport-oracle": "* Un embregerezh kenwerzhel diaz roadennoù eo [{{int:version-db-oracle-url}} Oracle]. ([http://www.php.net/manual/en/oci8.installation.php Penaos kempunañ PHP gant skor OCI8])", - "config-dbsupport-mssql": "* Un embregerezh kenwerzhel diaz roadennoù evit Windows eo [{{int:version-db-mssql-url}} Microsoft SQL Server]. ([https://www.php.net/manual/en/sqlsrv.installation.php Penaos kempunañ PHP gant skor SQLSRV])", "config-header-mysql": "Arventennoù MySQL", "config-header-postgres": "Arventennoù PostgreSQL", "config-header-sqlite": "Arventennoù SQLite", - "config-header-oracle": "Arventennoù Oracle", - "config-header-mssql": "Arventennoù Microsoft SQL Server", "config-invalid-db-type": "Direizh eo ar seurt diaz roadennoù", "config-missing-db-name": "Ret eo deoc'h merkañ un dalvoudenn evit \"{{int:config-db-name}}\".", "config-missing-db-host": "Ret eo deoc'h merkañ un dalvoudenn evit \"{{int:config-db-host}}\"", - "config-missing-db-server-oracle": "Ret eo deoc'h merkañ un dalvoudenn evit \"{{int:config-db-host-oracle}}\".", - "config-invalid-db-server-oracle": "Direizh eo anv TNS an diaz roadennoù \"$1\".\nOber gant an neudennad \"TNS Name\" pe c'hoazh gant \"Easy Connect ([http://docs.oracle.com/cd/E11882_01/network.112/e10836/naming.htm Hentennoù envel Oracle]).", "config-invalid-db-name": "Direizh eo anv an diaz titouroù \"$1\".\nOber hepken gant lizherennoù ASCII (a-z, A-Z), sifroù (0-9), arouezennoù islinennañ (_) ha tiredoù (-).", "config-invalid-db-prefix": "Direizh eo rakger an diaz titouroù \"$1\".\nOber hepken gant lizherennoù ASCII (a-z, A-Z), sifroù (0-9), arouezennoù islinennañ (_) ha tiredoù (-).", "config-connection-error": "$1.\n\nGwiriit anv an ostiz, an anv implijer, ar ger-tremen ha klaskit en-dro.", "config-invalid-schema": "Chema direizh evit MediaWiki \"$1\".\nGrit hepken gant lizherennoù ASCII (a-z, A-Z), sifroù (0-9) hag arouezennoù islinennañ (_).", - "config-db-sys-create-oracle": "N'anavez ar stalier nemet ar c'hontoù SYSDBA evit krouiñ kontoù nevez.", - "config-db-sys-user-exists-oracle": "Bez' ez eus eus ar gont \"$1\" c'hoazh. N'haller ober gant SYSDBA nemet evit krouiñ kontoù nevez !", "config-postgres-old": "Rekis eo PostgreSQL $1 pe ur stumm nevesoc'h; ober a rit gant $2.", - "config-mssql-old": "Stumm $1 Microsoft SQL Server, pe unan nevesoc'h, zo rekis. Ganeoc'h emañ ar stumm $2.", "config-sqlite-name-help": "Dibabit un anv dibar d'ho wiki.\nArabat ober gant esaouennoù pe barrennigoù-stagañ.\nImplijet e vo evit ar restr roadennoù SQLite.", "config-sqlite-parent-unwritable-group": "N'haller ket krouiñ ar c'havlec'h roadennoù $1 peogwir n'hall ket ar servijer Web skrivañ war ar c'havlec'h kar $2.\n\nKavet eo bet gant ar stalier an anv implijer m'eo oberiant ar servijer drezañ. Evit gallout kenderc'hel, lakait ar c'havlec'h $3 da vezañ tizhus evit ar skrivañ.\nWar ur reizhiad Unix/Linux system ober :\n\n
cd $2\nmkdir $3\nchgrp $4 $3\nchmod g+w $3
", "config-sqlite-parent-unwritable-nogroup": "N'haller ket krouiñ ar c'havlec'h roadennoù $1 peogwir n'hall ket ar servijer Web skrivañ war ar c'havlec'h kar $2.\n\nN'eo ket bet ar servijer evit kavout anv an implijer ma tro ar servijer. Evit kenderc'hel, lakaat ar c'havlec'h $3 da vezañ tizhus evit ar skrivañ dre vras.\nWar ur reizhiad Unix/Linux merkañ :\n\n
cd $2\nmkdir $3\nchmod a+w $3
", @@ -163,11 +146,6 @@ "config-mysql-engine": "Lusker stokañ :", "config-mysql-innodb": "InnoDB", "config-mysql-engine-help": "InnoDB eo an dibab gwellañ koulz lavaret atav, kemer a ra e kont ar monedoù kevezus.\n\nMyISAM a c'hall bezañ fonnusoc'h evit ar staliadurioù unpost pe ar re lenn hepken.\nDiazoù roadennoù MyISAM zo techet da vezañ gwastet aliesoc'h eget re InnoDB.", - "config-mssql-auth": "Seut anaoudadur :", - "config-mssql-install-auth": "Diuzañ ar seurt dilesa a vo implijet evit kevreañ ouzh an diaz roadennoù e-pad ar staliañ.\nMa tibabit \"{{int:config-mssql-windowsauth}}\", e vo implijet titouroù anaout an implijer a laka ar servijer da dreiñ.", - "config-mssql-web-auth": "Diuzañ ar seurt dilesa a vo implijet gant ar servijer web evit kevreañ ouzh diaz roadennoù ar servijer e-pad oberiadennoù boas ar wiki.\nMa tibabit \"{{int:config-mssql-windowsauth}}\", e vo implijet titouroù anaout an implijer a laka ar servijer da dreiñ.", - "config-mssql-sqlauth": "Anaoudadur SQL Server", - "config-mssql-windowsauth": "Anaoudadur Windows", "config-site-name": "Anv ar wiki :", "config-site-name-help": "Dont a raio war wel e barrenn ditl ar merdeer hag e meur a lec'h all c'hoazh.", "config-site-name-blank": "Lakait anv ul lec'hienn .", diff --git a/includes/installer/i18n/bs.json b/includes/installer/i18n/bs.json index 50421abb9c..5228e59e0d 100644 --- a/includes/installer/i18n/bs.json +++ b/includes/installer/i18n/bs.json @@ -64,7 +64,6 @@ "config-db-host": "Domaćin baze podataka:", "config-db-wiki-settings": "Identificiraj ovu wiki", "config-db-name": "Naziv baze podataka:", - "config-db-name-oracle": "Šema baze podataka:", "config-db-install-account": "Korisnički račun za instalaciju", "config-db-username": "Korisničko ime baze podataka:", "config-db-password": "Lozinka baze podataka:", @@ -75,22 +74,14 @@ "config-db-schema": "Šema za MediaWiki:", "config-pg-test-error": "Ne mogu se povezati na bazu podataka $1: $2", "config-sqlite-dir": "Folder za SQLite-podatke:", - "config-oracle-def-ts": "Predodređeni tabelarni prostor:", - "config-oracle-temp-ts": "Privremeni tabelarni prostor:", "config-type-mysql": "MySQL (ili kompaktibilan)", - "config-type-mssql": "Microsoft SQL Server", "config-header-mysql": "Postavke MySQL-a", "config-header-postgres": "Postavke PostgreSQL-a", "config-header-sqlite": "Postavke SQLite-a", - "config-header-oracle": "Postavke Oraclea", - "config-header-mssql": "Postavke Microsoft SQL Servera", "config-invalid-db-type": "Nevažeća vrsta baze podataka.", "config-missing-db-name": "Morate unijeti vrijednost za \"{{int:config-db-name}}\".", "config-missing-db-host": "Morate unijeti vrijednost za \"{{int:config-db-host}}\".", - "config-missing-db-server-oracle": "Morate unijeti vrijednost za \"{{int:config-db-host-oracle}}\".", - "config-db-sys-create-oracle": "Program za instalaciju podržava samo upotrebu SYSDBA-računa za pravljenje novih računa.", "config-postgres-old": "Zahtijeva se PostgreSQL $1 ili noviji. Vi imate $2.", - "config-mssql-old": "Zahtijeva se Microsoft SQL Server $1 ili noviji. Vi imate $2.", "config-sqlite-name-help": "Izaberite ime koje će predstavljati Vaš wiki.\nNemojte koristiti razmake i crte.\nOvo će se koristiti za ime datoteke SQLite-podataka.", "config-sqlite-readonly": "Datoteka $1 nije zapisiva.", "config-sqlite-cant-create-db": "Ne mogu napraviti datoteku $1 za bazu podataka.", @@ -104,7 +95,6 @@ "config-db-web-create": "Napravi račun ako već ne postoji", "config-mysql-engine": "Skladišni pogon:", "config-mysql-innodb": "InnoDB", - "config-mssql-auth": "Vrsta autentifikacije:", "config-site-name": "Ime wikija:", "config-site-name-blank": "Upišite ime sajta.", "config-project-namespace": "Imenski prostor projekta:", diff --git a/includes/installer/i18n/bto.json b/includes/installer/i18n/bto.json index 19d6e45b10..50a6040523 100644 --- a/includes/installer/i18n/bto.json +++ b/includes/installer/i18n/bto.json @@ -22,21 +22,15 @@ "config-diff3-bad": "Diri nataurakan a GNU diff3.", "config-db-type": "Klase ka database:", "config-db-host": "Host ka database:", - "config-db-host-oracle": "Database ka TNS:", "config-db-wiki-settings": "Mibdiron adin wiki", "config-db-name": "Ngaran ka database:", "config-db-port": "Port ka database:", "config-db-schema": "Skema para sa MediaWiki:", "config-sqlite-dir": "Direktoryo ka data sa SQLite:", - "config-oracle-def-ts": "Dating tablescape:", - "config-oracle-temp-ts": "Temporaryong tablescape:", "config-type-mysql": "MySQL (o compatible)", - "config-type-mssql": "Microsoft SQL Server", "config-header-mysql": "MySQL settings", "config-header-postgres": "PostgreSQL settings", "config-header-sqlite": "SQLite settings", - "config-header-oracle": "Oracle settings", - "config-header-mssql": "Microsoft SQL Server settings", "config-mysql-innodb": "InnoDB", "config-site-name": "Ngaran ka wiki", "config-site-name-blank": "Ibutang a ngaran ka site.", diff --git a/includes/installer/i18n/ca.json b/includes/installer/i18n/ca.json index d56295ba6b..04d99355b7 100644 --- a/includes/installer/i18n/ca.json +++ b/includes/installer/i18n/ca.json @@ -92,11 +92,9 @@ "config-db-type": "Tipus de base de dades:", "config-db-host": "Servidor de la base de dades:", "config-db-host-help": "Si el servidor de base de dades és en un servidor diferent, introduïu el nom del servidor o l'adreça IP a continuació.\n\nSi feu servir un hostatge web compartit, el vostre proveïdor us hauria de proporcionar el nom del servidor a la documentació.\n\nSi feu servir MySQL, «localhost» podria no funcionar com a nom de servidor. Si no funciona, proveu «127.0.0.1» com a adreça IP local.\n\nSi feu servir PostgreSQL, deixeu aquest camp en blanc per a connectar-vos a través d'un sòcol Unix.", - "config-db-host-oracle": "TNS de la base de dades:", "config-db-wiki-settings": "Identifica aquest wiki", "config-db-name": "Nom de la base de dades (sense guionets):", "config-db-name-help": "Trieu un nom que identifiqui el wiki.\nNo ha de contenir espais.\n\nSi esteu fent servir un hostatge web compartit, el vostre proveïdor us proporcionarà un nom específic per a la base de dades o us permetrà crear base de dades des d'un tauler de control.", - "config-db-name-oracle": "Esquema de la base de dades:", "config-db-install-account": "Compte d'usuari per a la instal·lació", "config-db-username": "Nom d'usuari de la base de dades:", "config-db-password": "Contrasenya de la base de dades:", @@ -115,28 +113,19 @@ "config-pg-test-error": "No es pot connectar a la base de dades '''$1''': $2", "config-sqlite-dir": "Directori de dades de l'SQLite", "config-sqlite-dir-help": "L'SQLite emmagatzema totes les dades en un únic fitxer.\n\nEl directori que proporcioneu ha de ser escrivible pel servidor durant la instal·lació.\n\nNo hauria de ser accessible des del web. Aquest és el motiu perquè no el posem on són els fitxers PHP.\n\nL'instal·lador escriurà un fitxer .htaccess al mateix temps, però si això fallés, seria possible que s'accedís a la base de dades crua.\nAixò inclou dades d'usuari crues (adreces electròniques, contrasenyes en resum) així com revisions eliminades i altres dades restringida en el wiki.\n\nConsidereu posar la base de dades en algun altre lloc tot plegat, per exemple a /var/lib/mediawiki/yourwiki.", - "config-oracle-def-ts": "Espai de taules per defecte:", - "config-oracle-temp-ts": "Espai de taules temporal:", "config-type-mysql": "MariaDB, MySQL o compatible", - "config-type-mssql": "Microsoft SQL Server", "config-support-info": "MediaWiki és compatible amb els següents sistemes de bases de dades:\n$1\nSi el sistema de bases de dades que intenteu utilitzar no apareix a la llista, seguiu les instruccions enllaçades més amunt per habilitar el suport.", "config-header-mysql": "Paràmetres de MariaDB/MySQL", "config-header-postgres": "Paràmetres del PostgreSQL", "config-header-sqlite": "Paràmetres de l'SQLite", - "config-header-oracle": "Paràmetres de l'Oracle", - "config-header-mssql": "Paràmetres del Microsoft SQL Server", "config-invalid-db-type": "Tipus de base de dades no vàlid", "config-missing-db-name": "Heu d'introduir un valor per a «{{int:config-db-name}}».", "config-missing-db-host": "Heu d'introduir un valor per a «{{int:config-db-host}}».", - "config-missing-db-server-oracle": "Heu d’introduir un valor per a «{{int:config-db-host-oracle}}».", "config-invalid-db-name": "El nom de la base de dades, «$1», no és vàlid.\nUtilitzeu només lletres de l’ASCII (a-z, A-Z), xifres (0-9), guions baixos (_) i guionets (-).", "config-invalid-db-prefix": "El prefix de la base de dades, «$1», no és vàlid.\nUtilitzeu només lletres de l’ASCII (a-z, A-Z), xifres (0-9), guions baixos (_) i guionets (-).", "config-connection-error": "$1.\n\nComproveu el servidor central, el nom d'usuari i la contrasenya i torneu-ho a provar. Si feu servir «localhost» com a servidor de base de dades, proveu llavors d'utilitzar «127.0.0.1» (o a l'inrevés).", "config-invalid-schema": "L’esquema «$1» no és vàlid per al MediaWiki.\nUtilitzeu només lletres de l’ASCII (a-z, A-Z), xifres (0-9), guions baixos (_) i guionets (-).", - "config-db-sys-create-oracle": "L'instal·lador només accepta emprar un compte SYSDBA per a la creació d'un nou compte.", - "config-db-sys-user-exists-oracle": "El compte d’usuari «$1» ja existeix. SYSDBA només es pot fer servir per crear comptes nous.", "config-postgres-old": "Cal el PostgreSQL $1 o posterior. Teniu el $2.", - "config-mssql-old": "Cal utilitzar el Microsoft SQL Server $1 o posterior. Teniu la versió $2.", "config-sqlite-name-help": "Trieu un nom per identificar el wiki.\nNo feu servir espais ni guionets.\nAquest nom s’utilitzarà per a denominar el fitxer de les dades de l’SQLite.", "config-sqlite-parent-unwritable-group": "No es pot crear el directori de dades $1, perquè el directori pare $2 no el pot escriure el servidor web.\n\nL'instal·lador no pot determinar l'usuari amb què s'executa el servidor web.\nFeu el directori $3 escrivible globalment per l'usuari del servidor web (i altres) per continuar.\nEn un sistema Unix/Linux feu:\n\n
cd $2\nmkdir $3\nchgrp $4 $3\nchmod g+w $3
", "config-sqlite-parent-unwritable-nogroup": "No es pot crear el directori de dades $1, perquè el directori pare $2 no el pot escriure el servidor web.\n\nL'instal·lador no pot determinar l'usuari amb què s'executa el servidor web.\nFeu el directori $3 escrivible globalment per l'usuari del servidor web (i altres) per continuar.\nEn un sistema Unix/Linux feu:\n\n
cd $2\nmkdir $3\nchmod a+w $3
", @@ -161,11 +150,6 @@ "config-mysql-engine": "Motor d'emmagatzemament:", "config-mysql-innodb": "InnoDB (recomanat)", "config-mysql-engine-help": "InnoDB és gairebé sempre la millor opció perquè té una bona implementació de concurrència.\n\nMyISAM pot ser més ràpid en instal·lacions d'un únic usuari o de només lectura.\nLes bases de dades MyISAM tendeixen a corrompre's més sovint que les InnoDB.", - "config-mssql-auth": "Tipus d'autenticació:", - "config-mssql-install-auth": "Seleccioneu el tipus d'autenticació que s'utilitzarà per connectar-se amb el servidor de base de dades durant el procés d'instal·lació.\nSi seleccioneu «{{int:config-mssql-windowsauth}}», s'utilitzaran les credencials de l'usuari amb què s'executa el servidor web.", - "config-mssql-web-auth": "Seleccioneu el tipus d'autenticació que utilitzarà el servidor web per connectar-se amb el servidor de base de dades durant les operacions rutinàries del wiki.\nSi seleccioneu «{{int:config-mssql-windowsauth}}», s'utilitzaran les credencials de l'usuari amb què s'executa el servidor web.", - "config-mssql-sqlauth": "Autenticació de l’SQL Server", - "config-mssql-windowsauth": "Autenticació del Windows", "config-site-name": "Nom del wiki:", "config-site-name-help": "Això apareixerà en la barra de títol del navegador i en altres llocs diferents.", "config-site-name-blank": "Introduïu un nom per al lloc.", diff --git a/includes/installer/i18n/ce.json b/includes/installer/i18n/ce.json index baba3e2ec5..5e778fa293 100644 --- a/includes/installer/i18n/ce.json +++ b/includes/installer/i18n/ce.json @@ -31,15 +31,10 @@ "config-no-fts3": "'''Тергам бе''': SQLite гулйина хуттург йоцуш [//sqlite.org/fts3.html FTS3] — лахар болхбеш хир дац оцу бухца.", "config-no-cli-uri": "'''ДӀахьедар''': --scriptpath параметр язйина яц, иза Ӏадйитаран кепаца лелош ю: $1 .", "config-db-name": "Хаамийн базан цӀе:", - "config-type-mssql": "Microsoft SQL Server", - "config-header-mssql": "Microsoft SQL Server параметраш", "config-invalid-db-type": "Хаамийн базан нийса йоцу тайп", "config-missing-db-name": "Ахьа «{{int:config-db-name}}» маьӀна даздан дезаш ду.", "config-missing-db-host": "Ахьа «{{int:config-db-host}}» параметран маьӀна даздан дезаш ду.", - "config-missing-db-server-oracle": "Ахьа тӀеюза езаш ю «{{int:config-db-host-oracle}}»", - "config-invalid-db-server-oracle": "Хаамийн базан «$1» нийса йоцу TNS.\nЛелае «TNS Name», я могӀа «Easy Connect» ([http://docs.oracle.com/cd/E11882_01/network.112/e10836/naming.htm ЦӀерш техкаран кеп Oracle])", "config-sqlite-fts3-downgrade": "PHPн гӀо до FTS3 яц — кхуссу таблицаш", - "config-mssql-auth": "Аутентификацин тайп:", "config-site-name": "Викин цӀе:", "config-site-name-blank": "Язъе сайтан цӀе.", "config-project-namespace": "Проектан цӀерийн меттиг:", diff --git a/includes/installer/i18n/ckb.json b/includes/installer/i18n/ckb.json index 1b814a2f97..a7a9d5327e 100644 --- a/includes/installer/i18n/ckb.json +++ b/includes/installer/i18n/ckb.json @@ -35,7 +35,7 @@ "config-wincache": "[https://www.iis.net/downloads/microsoft/wincache-extension WinCache] دامەزراوە", "config-db-type": "جۆری داتابەیس:", "config-db-host": "خانەخوێی داتابەیس:", - "config-db-name": "ناوی بنکەدراوە:", + "config-db-name": "ناوی بنکەدراوە (بێ دابڕین (-)):", "config-db-install-account": "ھەژماری بەکارھێنەری بۆ دامەزراندن", "config-db-username": "ناوی بەکارھێنەری بنکەدراوە:", "config-db-password": "تێپەڕوشەی بنکەدراوە", @@ -56,5 +56,5 @@ "config-install-step-done": "کرا", "config-help": "یارمەتی", "mainpagetext": "میدیاویکی بە سەرکەوتوویی دامەزرا.", - "mainpagedocfooter": "لە [https://meta.wikimedia.org/wiki/Help:Contents ڕێنوێنیی بەکارھێنەران] بۆ زانیاری سەبارەت بە بەکارھێنانی نەرمامێری ویکی کەڵک وەربگرە.\n\n== دەستپێکردن ==\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings پێرستی ڕێکخستنەکانی شێوەپێدان]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ پرسیارە دووپاتکراوەکانی میدیاویکی (MediaWiki FAQ)]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce پێرستی ئیمەیلی وەشانەکانی میدیاویکی]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation#Translation_resources خۆماڵیکردنی ویکیمیدیا بۆ زمانەکەت]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Combating_spam فێربە چۆن ڕووبەڕووى ئیمەیڵە بێزارکەرەکانی ویکییەکەت دەبیتەوە]" + "mainpagedocfooter": "لە [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents ڕێنوێنیی بەکارھێنەران] بۆ زانیاری سەبارەت بە بەکارھێنانی نەرمامێری ویکی کەڵک وەربگرە.\n\n== دەستپێکردن ==\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings پێرستی ڕێکخستنەکانی شێوەپێدان]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ پرسیارە دووپاتکراوەکانی میدیاویکی (MediaWiki FAQ)]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce پێرستی ئیمەیلی وەشانەکانی میدیاویکی]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation#Translation_resources خۆماڵیکردنی ویکیمیدیا بۆ زمانەکەت]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Combating_spam فێربە چۆن ڕووبەڕووى ئیمەیڵە بێزارکەرەکانی ویکییەکەت دەبیتەوە]" } diff --git a/includes/installer/i18n/cs.json b/includes/installer/i18n/cs.json index aef912c462..e13c506891 100644 --- a/includes/installer/i18n/cs.json +++ b/includes/installer/i18n/cs.json @@ -96,13 +96,9 @@ "config-db-type": "Typ databáze:", "config-db-host": "Databázový server:", "config-db-host-help": "Pokud je váš databázový server na jiném počítači, zadejte zde jméno stroje nebo IP adresu.\n\nPokud používáte sdílený webový hosting, váš poskytovatel by vám měl v dokumentaci sdělit správné jméno stroje.\n\nPokud používáte MySQL, jméno „localhost“ nemusí fungovat. V takovém případě zkuste jako místní IP adresu zadat „127.0.0.1“.\n\nPokud používáte PostgreSQL, můžete se připojit Unixovými sockety tak, že toto pole necháte prázdné.", - "config-db-host-oracle": "Databázové TNS:", - "config-db-host-oracle-help": "Zadejte platné [http://download.oracle.com/docs/cd/B28359_01/network.111/b28317/tnsnames.htm Local Connect Name]; tato instalace musí vidět soubor tnsnames.ora.
Pokud používáte klientské knihovny verze 10g nebo novější, můžete také používat názvy [http://download.oracle.com/docs/cd/E11882_01/network.112/e10836/naming.htm Easy Connect].", "config-db-wiki-settings": "Identifikace této wiki", "config-db-name": "Jméno databáze (bez spojovníků):", "config-db-name-help": "Zvolte jméno, které označuje vaši wiki.\nNemělo by obsahovat mezery.\n\nPokud používáte sdílený webový hosting, váš poskytovatel vám buď sdělí konkrétní jméno databáze, nebo vás nechá vytvářet databáze pomocí nějakého ovládacího panelu.", - "config-db-name-oracle": "Databázové schéma:", - "config-db-account-oracle-warn": "Existují tři podporované možnosti pro instalaci s použitím databáze Oracle.\n\nPokud chcete v rámci instalace založit databázový účet, zadejte jako databázový účet pro instalaci účet s rolí SYSDBA a uveďte požadované údaje pro účet pro webový přístup, jinak můžete vytvořit účet pro webový přístup ručně a zadat pouze tento účet (pokud má dostatečná oprávnění k zakládání objektů schématu) nebo poskytnout dva různé účty, jeden s oprávněními k zakládání, druhý omezený pro webový přístup.\n\nSkript pro založení účtu s potřebnými privilegii můžete v této instalaci nalézt v adresáři „maintenance/oracle/“. Nezapomeňte, že použití omezeného účtu znepřístupní veškeré možnosti údržby přes implicitní účet.", "config-db-install-account": "Uživatelský účet pro instalaci", "config-db-username": "Databázové uživatelské jméno:", "config-db-password": "Databázové heslo:", @@ -121,37 +117,24 @@ "config-pg-test-error": "Nelze se připojit k databázi '''$1''': $2", "config-sqlite-dir": "Adresář pro data SQLite:", "config-sqlite-dir-help": "SQLite ukládá veškerá data v jediném souboru.\n\nZadaný adresář musí být v průběhu instalace být přístupný pro zápis.\n\n'''Neměl by''' být dostupný z webu, proto ho nedáváme tam, kde jsou vaše PHP soubory.\n\nInstalátor do adresáře přidá soubor .htaccess, ale pokud to selže, mohl by někdo získat přístup k vaší holé databázi.\nTo zahrnuje syrová uživatelská data (e-mailové adresy, hašovaná hesla), jako i smazané revize a další data s omezeným přístupem z vaší wiki.\n\nZvažte umístění databáze někam zcela jinam, například do /var/lib/mediawiki/mojewiki.", - "config-oracle-def-ts": "Implicitní tabulkový prostor:", - "config-oracle-temp-ts": "Dočasný tabulkový prostor:", "config-type-mysql": "MariaDB, MySQL nebo kompatibilní", "config-type-postgres": "PostgreSQL", "config-type-sqlite": "SQLite", - "config-type-oracle": "Oracle", - "config-type-mssql": "Microsoft SQL Server", "config-support-info": "MediaWiki podporuje následující databázové systémy:\n\n$1\n\nPokud v nabídce níže nevidíte databázový systém, který chcete použít, musíte pro zapnutí podpory následovat instrukce odkázané výše.", "config-dbsupport-mysql": "* [{{int:version-db-mariadb-url}} MariaDB] je pro MediaWiki hlavní platformou a je podporováno nejlépe. MediaWiki pracuje také s [{{int:version-db-mysql-url}} MySQL] a [{{int:version-db-percona-url}} Percona Server], které jsou s MariaDB kompatibilní. ([https://www.php.net/manual/en/mysqli.installation.php Jak zkompilovat PHP s podporou MySQL])", "config-dbsupport-postgres": "* [{{int:version-db-postgres-url}} PostgreSQL] je populární otevřený databázový systém používaný jako alternativa k MySQL. ([https://www.php.net/manual/en/pgsql.installation.php Jak přeložit PHP s podporou PostgreSQL])", "config-dbsupport-sqlite": "* [{{int:version-db-sqlite-url}} SQLite] je velmi dobře podporovaný odlehčený databázový systém. ([https://www.php.net/manual/en/pdo.installation.php Jak přeložit PHP s podporou SQLite], používá PDO)", - "config-dbsupport-oracle": "* [{{int:version-db-oracle-url}} Oracle] je komerční podniková databáze. ([https://www.php.net/manual/en/oci8.installation.php Jak přeložit PHP s podporou OCI8])", - "config-dbsupport-mssql": "* [{{int:version-db-mssql-url}} Microsoft SQL Server] je komerční podniková databáze pro Windows. ([https://www.php.net/manual/en/sqlsrv.installation.php Jak přeložit PHP s podporou SQLSRV])", "config-header-mysql": "Nastavení MariaDB/MySQL", "config-header-postgres": "Nastavení PostgreSQL", "config-header-sqlite": "Nastavení SQLite", - "config-header-oracle": "Nastavení Oracle", - "config-header-mssql": "Nastavení Microsoft SQL Serveru", "config-invalid-db-type": "Chybný typ databáze", "config-missing-db-name": "Musíte zadat hodnotu pro „{{int:config-db-name}}“.", "config-missing-db-host": "Musíte zadat hodnotu pro „{{int:config-db-host}}“.", - "config-missing-db-server-oracle": "Musíte zadat hodnotu pro „{{int:config-db-host-oracle}}“.", - "config-invalid-db-server-oracle": "Chybné databázové TNS „$1“.\nPoužívejte buď „TNS Name“ nebo „Easy Connect“ (vizte [http://docs.oracle.com/cd/E11882_01/network.112/e10836/naming.htm Oracle Naming Methods]).", "config-invalid-db-name": "Chybné jméno databáze „$1“.\nPoužívejte pouze ASCII písmena (a-z, A-Z), čísla (0-9), podtržítko (_) a spojovník (-).", "config-invalid-db-prefix": "Chybný databázový prefix „$1“.\nPoužívejte pouze ASCII písmena (a-z, A-Z), čísla (0-9), podtržítko (_) a spojovník (-).", "config-connection-error": "$1.\n\nZkontrolujte server, uživatelské jméno a heslo a zkuste to znovu. Pokud jako adresu databázového serveru používáte „localhost“, zkuste použít „127.0.0.1“ (a naopak).", "config-invalid-schema": "Neplatné schéma pro MediaWiki „$1“.\nPoužívejte pouze ASCII písmena (a-z, A-Z), čísla (0-9) a podtržítko (_).", - "config-db-sys-create-oracle": "Instalátor podporuje zakládání nového účtu pouze prostřednictvím účtu SYSDBA.", - "config-db-sys-user-exists-oracle": "Uživatelský účet „$1“ již existuje. SYSDBA lze použít pouze pro založení nového účtu!", "config-postgres-old": "Je vyžadován PostgreSQL $1 nebo novější, vy máte $2.", - "config-mssql-old": "Je vyžadován Microsoft SQL Server $1 nebo novější. Vy máte $2.", "config-sqlite-name-help": "Zvolte jméno, které označuje vaši wiki.\nNepoužívejte mezery a spojovníky.\nPoužije se jako název souboru s daty SQLite.", "config-sqlite-parent-unwritable-group": "Nelze vytvořit datový adresář $1, protože do nadřazeného adresáře $2 nemá webový server právo zapisovat.\n\nInstalátor zjistil uživatele, pod kterým váš webový server běží.\nAbyste mohli pokračovat, umožněte mu zapisovat do adresáře $3.\nNa systémech Unix/Linux proveďte:\n\n
cd $2\nmkdir $3\nchgrp $4 $3\nchmod g+w $3
", "config-sqlite-parent-unwritable-nogroup": "Nelze vytvořit datový adresář $1, protože do nadřazeného adresáře $2 nemá webový server právo zapisovat.\n\nInstalátoru se nepodařilo zjistit uživatele, pod kterým váš webový server běží.\nAbyste mohli pokračovat, umožněte zápis do $3 všem uživatelům.\nNa systémech Unix/Linux proveďte:\n\n
cd $2\nmkdir $3\nchmod a+w $3
", @@ -176,11 +159,6 @@ "config-mysql-engine": "Typ úložiště:", "config-mysql-innodb": "InnoDB (doporučeno)", "config-mysql-engine-help": "'''InnoDB''' je téměř vždy nejlepší volba, neboť má dobrou podporu současného přístupu.\n\n'''MyISAM''' může být rychlejší u instalací pro jednoho uživatele nebo jen pro čtení.\nDatabáze MyISAM bývají poškozeny častěji než databáze InnoDB.", - "config-mssql-auth": "Typ autentizace:", - "config-mssql-install-auth": "Zvolte si typ autentizace, který se bude používat pro připojení k databázi v průběhu instalace.\nPokud zvolíte možnost „{{int:config-mssql-windowsauth}}“, použijí se přihlašovací údaje uživatele, pod kterým běží webový server.", - "config-mssql-web-auth": "Zvolte si typ autentizace, který se bude používat pro připojení k databázi za běžného provozu wiki.\nPokud zvolíte možnost „{{int:config-mssql-windowsauth}}“, použijí se přihlašovací údaje uživatele, pod kterým běží webový server.", - "config-mssql-sqlauth": "Autentizace SQL serveru", - "config-mssql-windowsauth": "Windows autentizace", "config-site-name": "Název wiki:", "config-site-name-help": "Bude se zobrazovat v titulku prohlížeče a na dalších místech.", "config-site-name-blank": "Zadejte název serveru.", diff --git a/includes/installer/i18n/da.json b/includes/installer/i18n/da.json index 8924964251..498fd7cb35 100644 --- a/includes/installer/i18n/da.json +++ b/includes/installer/i18n/da.json @@ -59,17 +59,14 @@ "config-mysql-old": "MySQL $1 eller nyere kræves. Du har $2.", "config-db-port": "Databaseport:", "config-type-mysql": "MariaDB, MySQL eller kompatibel", - "config-type-mssql": "Microsoft SQL-server", "config-header-mysql": "MariaDB/MySQL-indstillinger", "config-header-postgres": "PostgreSQL-indstillinger", "config-header-sqlite": "SQLite-indstillinger", - "config-header-oracle": "Oracle-indstillinger", "config-invalid-db-type": "Ugyldig databasetype", "config-sqlite-readonly": "Filen $1 er ikke skrivbar.", "config-sqlite-cant-create-db": "Kunne ikke oprette databasefilen $1.", "config-db-web-create": "Opret kontoen hvis den ikke allerede findes", "config-mysql-innodb": "InnoDB (anbefalet)", - "config-mssql-windowsauth": "Windows-godkendelse", "config-site-name": "Navn på wiki:", "config-site-name-blank": "Indtast et hjemmesidenavn.", "config-project-namespace": "Projektnavnerum:", @@ -100,12 +97,14 @@ "config-install-step-failed": "mislykkedes", "config-install-extensions": "Inkluderer udvidelser", "config-install-database": "Opsætter database", + "config-install-user": "Opretter databasebruger", "config-install-user-alreadyexists": "Brugeren \"$1\" findes allerede", "config-install-user-create-failed": "Oprettelse af brugeren \"$1\" mislykkedes: $2", "config-install-tables": "Opretter tabeller", "config-install-keys": "Genererer hemmelige nøgler", "config-install-mainpage-exists": "Forsiden findes allerede, springer over", "config-install-mainpage-failed": "Kunne ikke indsætte forside: $1", + "config-install-db-success": "Databasen blev sat op", "config-help": "hjælp", "config-help-tooltip": "klik for at udvide", "config-nofile": "Filen \"$1\" kunne ikke blive fundet. Er den blevet slettet?", diff --git a/includes/installer/i18n/de.json b/includes/installer/i18n/de.json index bdf1c6f639..403939157d 100644 --- a/includes/installer/i18n/de.json +++ b/includes/installer/i18n/de.json @@ -16,7 +16,8 @@ "Das Schäfchen", "Florian", "Macofe", - "ThePiscin" + "ThePiscin", + "Tobi 406" ] }, "config-desc": "Das MediaWiki-Installationsprogramm", @@ -58,15 +59,19 @@ "config-welcome": "=== Prüfung der Installationsumgebung ===\nDie Basisprüfungen werden jetzt durchgeführt, um festzustellen, ob die Installationsumgebung für MediaWiki geeignet ist.\nNotiere diese Informationen und gib sie an, sofern du Hilfe beim Installieren benötigst.", "config-welcome-section-copyright": "=== Lizenz und Nutzungsbedingungen ===\n\n$1\n\nDieses Programm ist freie Software, d. h. es kann, gemäß den Bedingungen der von der Free Software Foundation veröffentlichten ''GNU General Public License'', weiterverteilt und/oder modifiziert werden. Dabei kann die Version 2, oder nach eigenem Ermessen, jede neuere Version der Lizenz verwendet werden.\n\nDieses Programm wird in der Hoffnung verteilt, dass es nützlich sein wird, allerdings '''ohne jegliche Garantie''' und sogar ohne die implizierte Garantie einer '''Marktgängigkeit''' oder '''Eignung für einen bestimmten Zweck'''. Hierzu sind weitere Hinweise in der ''GNU General Public License'' enthalten.\n\nEine [$2 Kopie der GNU General Public License] sollte zusammen mit diesem Programm verteilt worden sein. Sofern dies nicht der Fall war, kann eine Kopie bei der Free Software Foundation Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA, schriftlich angefordert oder auf deren Website [https://www.gnu.org/copyleft/gpl.html online gelesen] werden.", "config-sidebar": "* [https://www.mediawiki.org/wiki/MediaWiki/de Website von MediaWiki]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents/de Benutzer­anleitung]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Contents/de Administratoren­anleitung]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ/de Häufig gestellte Fragen]\n----\n* Lies mich\n* Versions­informationen\n* Lizenz­bestimmungen\n* Aktualisierung", + "config-sidebar-readme": "Lies mich", + "config-sidebar-relnotes": "Veröffentlichungsinformationen", + "config-sidebar-license": "Kopieren", + "config-sidebar-upgrade": "Aktualisieren", "config-env-good": "Die Installationsumgebung wurde geprüft.\nMediaWiki kann installiert werden.", "config-env-bad": "Die Installationsumgebung wurde geprüft.\nMediaWiki kann nicht installiert werden.", "config-env-php": "Die Skriptsprache „PHP“ ($1) ist installiert.", "config-env-hhvm": "HHVM $1 ist installiert.", - "config-unicode-using-intl": "Zur Unicode-Normalisierung wird die [https://pecl.php.net/intl PECL-Erweiterung intl] eingesetzt.", - "config-unicode-pure-php-warning": "'''Warnung:''' Die [https://pecl.php.net/intl PECL-Erweiterung intl] ist für die Unicode-Normalisierung nicht verfügbar, so dass stattdessen die langsame pure-PHP-Implementierung genutzt wird.\nSofern eine Website mit großer Benutzeranzahl betrieben wird, sollten weitere Informationen auf der Webseite [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations Unicode-Normalisierung (en)] gelesen werden.", + "config-unicode-using-intl": "Zur Unicode-Normalisierung wird die [https://www.php.net/manual/de/book.intl.php PHP-Erweiterung intl] eingesetzt.", + "config-unicode-pure-php-warning": "'''Warnung:''' Die [https://www.php.net/manual/de/book.intl.php PHP-Erweiterung intl] ist für die Unicode-Normalisierung nicht verfügbar, so dass stattdessen die langsame pure-PHP-Implementierung genutzt wird.\nSofern eine Website mit großer Benutzeranzahl betrieben wird, sollten weitere Informationen auf der Seite [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations Unicode-Normalisierung] gelesen werden.", "config-unicode-update-warning": "'''Warnung:''' Die installierte Version des Unicode-Normalisierungswrappers nutzt einer ältere Version der Bibliothek des [http://site.icu-project.org/ ICU-Projekts].\nDiese sollte [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations aktualisiert] werden, sofern auf die Verwendung von Unicode Wert gelegt wird.", "config-no-db": "Es konnte kein adäquater Datenbanktreiber gefunden werden. Es muss daher ein Datenbanktreiber für PHP installiert werden.\n{{PLURAL:$2|Das folgende Datenbanksystem wird|Die folgenden Datenbanksysteme werden}} unterstützt: $1\n\nWenn du PHP selbst kompiliert hast, konfiguriere es erneut mit einem aktivierten Datenbankclient, zum Beispiel durch Verwendung von ./configure --with-mysqli.\nWenn du PHP von einem Debian- oder Ubuntu-Paket installiert hast, dann musst du auch beispielsweise das php-mysql-Paket installieren.", - "config-outdated-sqlite": "'''Warnung:''' SQLite $1 ist installiert. Allerdings benötigt MediaWiki SQLite $2 oder höher. SQLite wird daher nicht verfügbar sein.", + "config-outdated-sqlite": "Warnung: du hast SQLite $2, was unter der benötigten Version $1 liegt. SQLite wird daher nicht verfügar sein.", "config-no-fts3": "'''Warnung:''' SQLite wurde ohne das [//sqlite.org/fts3.html FTS3-Modul] kompiliert, sodass keine Suchfunktionen für dieses Datenbanksystem zur Verfügung stehen werden.", "config-pcre-old": "Fataler Fehler: PCRE $1 oder neuer ist erforderlich!\nDie vorhandene PHP-Binärdatei ist mit PCRE $2 verknüpft.\n[https://www.mediawiki.org/wiki/Manual:Errors_and_symptoms/PCRE Weitere Informationen].", "config-pcre-no-utf8": "'''Fataler Fehler:''' Das PHP-Modul PCRE scheint ohne PCRE_UTF8-Unterstützung kompiliert worden zu sein.\nMediaWiki benötigt die UTF-8-Unterstützung, um fehlerfrei lauffähig zu sein.", @@ -95,13 +100,9 @@ "config-db-type": "Datenbanksystem:", "config-db-host": "Datenbankserver:", "config-db-host-help": "Sofern sich die Datenbank auf einem anderen Server befindet, ist hier der Servername oder die entsprechende IP-Adresse anzugeben.\n\nSofern ein gemeinschaftlich genutzter Server verwendet wird, sollte der Hoster den zutreffenden Servernamen in seiner Dokumentation angegeben haben.\n\nSofern MySQL genutzt wird, funktioniert der Servername „localhost“ voraussichtlich nicht. Wenn nicht, sollte „127.0.0.1“ oder die lokale IP-Adresse angegeben werden.\n\nSofern PostgresQL genutzt wird, muss dieses Feld leer gelassen werden, um über ein Unix-Socket zu verbinden.", - "config-db-host-oracle": "Datenbank-TNS:", - "config-db-host-oracle-help": "Einen gültigen [http://download.oracle.com/docs/cd/B28359_01/network.111/b28317/tnsnames.htm „Local Connect“-Namen] angeben. Die „tnsnames.ora“-Datei muss von dieser Installation erkannt werden können.
Sofern die Client-Bibliotheken für Version 10g oder neuer verwendet werden, kann auch [http://download.oracle.com/docs/cd/E11882_01/network.112/e10836/naming.htm „Easy Connect“] zur Namensgebung genutzt werden.", "config-db-wiki-settings": "Bitte Daten zur eindeutigen Identifikation dieses Wikis angeben", "config-db-name": "Name der Datenbank (ohne Bindestriche):", "config-db-name-help": "Bitte einen Namen angeben, mit dem das Wiki identifiziert werden kann.\nDabei sollten keine Leerzeichen verwendet werden.\n\nSofern ein gemeinschaftlich genutzter Server verwendet wird, sollte der Hoster den Datenbanknamen angegeben oder aber die Erstellung einer Datenbank über ein entsprechendes Interface gestattet haben.", - "config-db-name-oracle": "Datenbankschema:", - "config-db-account-oracle-warn": "Es gibt drei von MediaWiki unterstützte Möglichkeiten, Oracle als Datenbank einzurichten:\n\nSofern das Datenbankbenutzerkonto während des Installationsvorgangs erstellt werden soll, muss ein Datenbankbenutzerkonto mit der SYSDBA-Berechtigung zusammen mit den entsprechenden Anmeldeinformationen angegeben werden, mit dem dann über das Web auf die Datenbank zugegriffen werden kann. Alternativ kann man auch lediglich ein einzelnes manuell angelegtes Datenbankbenutzerkonto angeben, mit dem über das Web auf die Datenbank zugegriffen werden kann, sofern dieses über die Berechtigung zur Erstellung von Datenbankschemen verfügt. Zudem ist es möglich, zwei Datenbankbenutzerkonten anzugeben, von denen eines die Berechtigung zur Erstellung von Datenbankschemen hat und das andere, um mit ihm über das Web auf die Datenbank zuzugreifen.\n\nEin Skript zum Anlegen eines Datenbankbenutzerkontos mit den notwendigen Berechtigungen findet man unter dem Pfad „…/maintenance/oracle/“ dieser MediaWiki-Installation. Es ist dabei zu bedenken, dass die Verwendung eines Datenbankbenutzerkontos mit beschränkten Berechtigungen die Nutzung der Wartungsfunktionen für das Standarddatenbankbenutzerkonto deaktiviert.", "config-db-install-account": "Benutzerkonto für die Installation", "config-db-username": "Name des Datenbankbenutzers:", "config-db-password": "Passwort des Datenbankbenutzers:", @@ -120,37 +121,24 @@ "config-pg-test-error": "Es kann keine Verbindung zur Datenbank '''$1''' hergestellt werden: $2", "config-sqlite-dir": "SQLite-Datenverzeichnis:", "config-sqlite-dir-help": "SQLite speichert alle Daten in einer einzigen Datei.\n\nDas für sie vorgesehene Verzeichnis muss während des Installationsvorgangs beschreibbar sein.\n\nEs sollte '''nicht''' über das Web zugänglich sein, was der Grund ist, warum die Datei nicht dort abgelegt wird, wo sich die PHP-Dateien befinden.\n\nDas Installationsprogramm wird mit der Datei zusammen eine zusätzliche .htaccess-Datei erstellen. Sofern dies scheitert, können Dritte auf die Datendatei zugreifen.\nDies umfasst die Nutzerdaten (E-Mail-Adressen, Passwörter, etc.) wie auch gelöschte Seitenversionen und andere vertrauliche Daten, die im Wiki gespeichert sind.\n\nEs ist daher zu erwägen, die Datendatei an gänzlich anderer Stelle abzulegen, beispielsweise im Verzeichnis ./var/lib/mediawiki/yourwiki.", - "config-oracle-def-ts": "Standardtabellenraum:", - "config-oracle-temp-ts": "Temporärer Tabellenraum:", "config-type-mysql": "MariaDB, MySQL (oder kompatible Datenbanksysteme)", "config-type-postgres": "PostgreSQL", "config-type-sqlite": "SQLite", - "config-type-oracle": "Oracle", - "config-type-mssql": "Microsoft SQL Server", "config-support-info": "MediaWiki unterstützt die folgenden Datenbanksysteme:\n\n$1\n\nSofern unterhalb nicht das Datenbanksystem angezeigt wird, das verwendet werden soll, muss dieses noch verfügbar gemacht werden. Oben ist zu jedem unterstützten Datenbanksystem ein Link zur entsprechenden Anleitung vorhanden.", "config-dbsupport-mysql": "* [{{int:version-db-mariadb-url}} MariaDB] ist das von MediaWiki primär unterstützte Datenbanksystem. MediaWiki funktioniert auch mit [{{int:version-db-mysql-url}} MySQL] und [{{int:version-db-percona-url}} Percona Server], die MariaDB-kompatibel sind. ([https://www.php.net/manual/en/mysqli.installation.php Anleitung zur Kompilierung von PHP mit MySQL-Unterstützung])", "config-dbsupport-postgres": "* [{{int:version-db-postgres-url}} PostgreSQL] ist ein beliebtes Open-Source-Datenbanksystem und eine Alternative zu MySQL. ([https://www.php.net/manual/en/pgsql.installation.php Anleitung zur Kompilierung von PHP mit PostgreSQL-Unterstützung])", "config-dbsupport-sqlite": "* [{{int:version-db-sqlite-url}} SQLite] ist ein verschlanktes Datenbanksystem, das auch gut unterstützt wird ([https://www.php.net/manual/de/pdo.installation.php Anleitung zur Kompilierung von PHP mit SQLite-Unterstützung], verwendet PHP Data Objects (PDO))", - "config-dbsupport-oracle": "* [{{int:version-db-oracle-url}} Oracle] ist eine kommerzielle Unternehmensdatenbank ([https://www.php.net/manual/en/oci8.installation.php Anleitung zur Kompilierung von PHP mit OCI8-Unterstützung])", - "config-dbsupport-mssql": "* [{{int:version-db-mssql-url}} Microsoft SQL Server] ist eine gewerbliche Unternehmensdatenbank für Windows. ([https://www.php.net/manual/de/sqlsrv.installation.php Anleitung zur Kompilierung von PHP mit SQLSRV-Unterstützung])", "config-header-mysql": "MariaDB/MySQL-Einstellungen", "config-header-postgres": "PostgreSQL-Einstellungen", "config-header-sqlite": "SQLite-Einstellungen", - "config-header-oracle": "Oracle-Einstellungen", - "config-header-mssql": "Einstellungen von Microsoft SQL Server", "config-invalid-db-type": "Unzulässiges Datenbanksystem", "config-missing-db-name": "Bei „{{int:config-db-name}}“ muss ein Wert angegeben werden.", "config-missing-db-host": "Bei „{{int:config-db-host}}“ muss ein Wert angegeben werden.", - "config-missing-db-server-oracle": "Für „{{int:config-db-host-oracle}}“ muss ein Wert eingegeben werden.", - "config-invalid-db-server-oracle": "Ungültiges Datenbank-TNS „$1“.\nEntweder „TNS Name“ oder eine „Easy Connect“-Zeichenfolge verwenden ([http://docs.oracle.com/cd/E11882_01/network.112/e10836/naming.htm Oracle-Benennungsmethoden])", "config-invalid-db-name": "Ungültiger Datenbankname „$1“.\nEs dürfen nur ASCII-codierte Buchstaben (a-z, A-Z), Zahlen (0-9), Unter- (_) sowie Bindestriche (-) verwendet werden.", "config-invalid-db-prefix": "Ungültiger Datenbanktabellenpräfix „$1“.\nEs dürfen nur ASCII-codierte Buchstaben (a-z, A-Z), Zahlen (0-9), Unter- (_) sowie Bindestriche (-) verwendet werden.", "config-connection-error": "$1.\n\nBitte unten angegebenen Servernamen, Benutzernamen sowie das Passwort überprüfen und es danach erneut versuchen. Falls „localhost“ als Datenbankhost verwendet wird, versuche stattdessen „127.0.0.1“ (oder umgekehrt).", "config-invalid-schema": "Ungültiges Datenschema für MediaWiki „$1“.\nEs dürfen nur ASCII-codierte Buchstaben (a-z, A-Z), Zahlen (0-9) und Unterstriche (_) verwendet werden.", - "config-db-sys-create-oracle": "Das Installationsprogramm unterstützt nur die Verwendung eines Datenbankbenutzerkontos mit SYSDBA-Berechtigung zum Anlegen eines neuen Datenbankbenutzerkontos.", - "config-db-sys-user-exists-oracle": "Das Datenbankbenutzerkonto „$1“ ist bereits vorhanden. Ein Datenbankbenutzerkontos mit SYSDBA-Berechtigung kann nur zum Anlegen eines neuen Datenbankbenutzerkontos genutzt werden.", "config-postgres-old": "PostgreSQL $1 oder höher wird benötigt. PostgreSQL $2 ist momentan vorhanden.", - "config-mssql-old": "Es wird Microsoft SQL Server $1 oder später benötigt. Deine Version ist $2.", "config-sqlite-name-help": "Bitten einen Namen angeben, mit dem das Wiki identifiziert werden kann.\nDabei bitte keine Leerzeichen oder Bindestriche verwenden.\nDieser Name wird für die SQLite-Datendateinamen genutzt.", "config-sqlite-parent-unwritable-group": "Das Datenverzeichnis $1 kann nicht erzeugt werden, da das übergeordnete Verzeichnis $2 nicht für den Webserver beschreibbar ist.\n\nDas Installationsprogramm konnte den Benutzer bestimmen, mit dem Webserver ausgeführt wird.\nSchreibzugriff auf das $3-Verzeichnis muss für diesen ermöglicht werden, um den Installationsvorgang fortsetzen zu können.\n\nAuf einem Unix- oder Linux-System:\n
cd $2\nmkdir $3\nchgrp $4 $3\nchmod g+w $3
", "config-sqlite-parent-unwritable-nogroup": "Das Datenverzeichnis $1 kann nicht erzeugt werden, da das übergeordnete Verzeichnis $2 nicht für den Webserver beschreibbar ist.\n\nDas Installationsprogramm konnte den Benutzer bestimmen, mit dem Webserver ausgeführt wird.\nSchreibzugriff auf das $3-Verzeichnis muss global für diesen und andere Benutzer ermöglicht werden, um den Installationsvorgang fortsetzen zu können.\n\nAuf einem Unix- oder Linux-System:\n
cd $2\nmkdir $3\nchmod a+w $3
", @@ -175,11 +163,6 @@ "config-mysql-engine": "Datenbanksystem:", "config-mysql-innodb": "InnoDB (empfohlen)", "config-mysql-engine-help": "InnoDB als Speichersubsystem für das Datenbanksystem MySQL ist fast immer die bessere Wahl, da es gleichzeitige Zugriffe gut unterstützt.\n\nMyISAM als Speichersubsystem für das Datenbanksystem MySQL ist hingegen in Einzelnutzerumgebungen oder bei schreibgeschützten Wikis schneller.\nDatenbanken, die MyISAM verwenden, sind indes tendenziell fehleranfälliger als solche, die InnoDB verwenden.", - "config-mssql-auth": "Authentifikationstyp:", - "config-mssql-install-auth": "Wähle den Authentifikationstyp aus, der zur Verbindung mit der Datenbank während des Installationsprozesses verwendet wird.\nFalls du „{{int:config-mssql-windowsauth}}“ auswählst, werden die Anmeldeinformationen eines beliebigen Benutzers verwendet, der den Webserver ausführt.", - "config-mssql-web-auth": "Wähle den Authentifikationstyp aus, der vom Webserver zur Verbindung mit dem Datenbankserver während des gewöhnlichen Betriebs des Wikis verwendet wird.\nFalls du „{{int:config-mssql-windowsauth}}“ auswählst, werden die Anmeldeinformationen eines beliebigen Benutzers verwendet, der den Webserver ausführt.", - "config-mssql-sqlauth": "SQL-Server-Authentifikation", - "config-mssql-windowsauth": "Windows-Authentifikation", "config-site-name": "Name des Wikis:", "config-site-name-help": "Er wird in der Titelleiste des Browsers, wie auch verschiedenen anderen Stellen, genutzt.", "config-site-name-blank": "Den Namen des Wikis angeben.", diff --git a/includes/installer/i18n/diq.json b/includes/installer/i18n/diq.json index d5967849ae..3617e18b93 100644 --- a/includes/installer/i18n/diq.json +++ b/includes/installer/i18n/diq.json @@ -27,7 +27,7 @@ "config-page-install": "Bar ke", "config-page-complete": "Temam!", "config-page-restart": "Barkerdışi fına ser kı", - "config-page-readme": "Mı bıwan", + "config-page-readme": "Mı bıwane", "config-page-releasenotes": "Notë versiyoni", "config-page-copying": "Kopyayeno", "config-page-upgradedoc": "Berzkerdış", @@ -38,28 +38,18 @@ "config-env-hhvm": "HHVM $1 saz bi ya.", "config-db-type": "Database tipe:", "config-db-host": "Database host:", - "config-db-host-oracle": "Database TNS:", "config-db-wiki-settings": "Ena wikiyer akernë", "config-db-name": "Database name:", - "config-db-name-oracle": "Şemaya hardata:", "config-db-username": "Database nameykarberi:", "config-db-password": "Database parola :", "config-db-port": "Portê database:", - "config-oracle-def-ts": "Hesıbyaye caytabloy:", - "config-oracle-temp-ts": "İdareten caytabloy:", "config-type-mysql": "MySQL (yana hewlın)", - "config-type-mssql": "Microsoft SQL Server", "config-header-mysql": "Eyarê MySQL", "config-header-sqlite": "SQLite sazi", - "config-header-oracle": "Orqcle sazi", - "config-header-mssql": "Sazë Microsoft SQL Serveri", "config-missing-db-name": "\"{{int:config-db-name}}\"nrë jew erc dekerdış gerek keno.", "config-missing-db-host": "\"{{int:config-db-host}}\" rë jew erc gerek keno", - "config-missing-db-server-oracle": "\"{{int:config-db-host-oracle}}\" rë jew erc gerek keno", "config-mysql-engine": "Motorë depok kerdışi", "config-mysql-innodb": "InnoDB", - "config-mssql-sqlauth": "SQL Server araştnayış", - "config-mssql-windowsauth": "Windows kamiye araştnayış", "config-site-name": "Namey wiki:", "config-site-name-blank": "Yew nameyê sita cıkewe.", "config-project-namespace": "Wareyê nameyê proceyi:", diff --git a/includes/installer/i18n/el.json b/includes/installer/i18n/el.json index 97b4fd35a8..d0a386f014 100644 --- a/includes/installer/i18n/el.json +++ b/includes/installer/i18n/el.json @@ -85,11 +85,9 @@ "config-db-type": "Τύπος βάσης δεδομένων:", "config-db-host": "Φιλοξενία βάσης δεδομένων:", "config-db-host-help": "Εάν ο διακομιστής βάσης δεδομένων σας βρίσκεται σε διαφορετικό διακομιστή, εισαγάγετε εδώ το όνομα του κεντρικού υπολογιστή ή τη διεύθυνση IP.\n\nΕάν χρησιμοποιείτε μοιραζόμενη φιλοξενία του ιστοτόπου σας, ο πάροχος φιλοξενίας σας θα πρέπει να σας δίνει το σωστό όνομα κεντρικού υπολογιστή στην τεκμηρίωση του.\n\nΕάν εγκαθιστάτε σε διακομιστή Windows και χρησιμοποιείτε MySQL, το «localhost» μπορεί να μην λειτουργεί ως όνομα διακομιστή. Εάν δεν λειτουργεί, δοκιμάστε «127.0.0.1» ως τοπική διεύθυνση IP.\n\nΕάν χρησιμοποιείτε PostgreSQL, αφήστε αυτό το πεδίο κενό για να συνδεθείτε μέσω υποδοχής Unix.", - "config-db-host-oracle": "Βάση δεδομένων TNS:", "config-db-wiki-settings": "Αναγνώριση αυτού του wiki", "config-db-name": "Όνομα βάσης δεδομένων (χωρίς υφέν):", "config-db-name-help": "Επιλέξτε όνομα που να χαρακτηρίζει το wiki σας. Δεν πρέπει να περιέχει κενά διαστήματα.\n\nΕάν χρησιμοποιείτε μοιραζόμενη φιλοξενία του ιστοτόπου σας, ο πάροχος φιλοξενίας σας είτε θα σας δίνει να χρησιμοποιήσετε ένα συγκεκριμένο όνομα βάσης δεδομένων ή θα σας δίνει τη δυνατότητα να δημιουργείτε βάσεις δεδομένων μέσω κάποιου πίνακα ελέγχου.", - "config-db-name-oracle": "Σχήμα βάσης δεδομένων:", "config-db-install-account": "Λογαριασμός χρήστη για την εγκατάσταση", "config-db-username": "Όνομα χρήστη βάσης δεδομένων:", "config-db-password": "Συνθηματικό βάσης δεδομένων:", @@ -107,31 +105,21 @@ "config-db-schema-help": "Αυτό το σχήμα συνήθως αρκεί.\nΑλλάξτε το μόνο αν είστε βέβαιοι ότι χρειάζεται.", "config-pg-test-error": "Δεν μπορεί να συνδεθεί στη βάση δεδομένων $1: $2", "config-sqlite-dir": "SQLite κατάλογος δεδομένων:", - "config-oracle-temp-ts": "Προσωρινό tablespace:", "config-type-mysql": "MySQL (ή συμβατό)", "config-type-postgres": "PostgreSQL", "config-type-sqlite": "SQLite", - "config-type-oracle": "Oracle", - "config-type-mssql": "Microsoft SQL Server", "config-support-info": "To MediaWiki υποστηρίζει τα ακόλουθα συστήματα βάσεων δεδομένων:\n\n$1\n\nΑν δεν εμφανίζεται παρακάτω το σύστημα βάσης δεδομένων που θέλετε να χρησιμοποιήσετε, τότε ακολουθήστε τις οδηγίες στον παραπάνω σύνδεσμο για να ενεργοποιήσετε την υποστήριξη.", "config-dbsupport-mysql": "* Η [{{int:version-db-mysql-url}} MySQL] είναι ο πρωταρχικός στόχος για το MediaWiki και υποστηρίζεται καλύτερα. Το MediaWiki συνεργάζεται επίσης με τη [{{int:version-db-mariadb-url}} MariaDB] και το [{{int:version-db-percona-url}} διακομιστή Percona], που είναι όλα συμβατά με MySQL. ([https://www.php.net/manual/en/mysqli.installation.php Πώς να μεταγλωττίσετε την PHP με υποστήριξη MySQL])", "config-dbsupport-postgres": "* Η [{{int:version-db-postgres-url}} PostgreSQL] είναι δημοφιλές σύστημα βάσης δεδομένων ανοικτού κώδικα ως εναλλακτική της MySQL. ([https://www.php.net/manual/en/pgsql.installation.php Πώς να μεταγλωττίσετε την PHP με υποστήριξη PostgreSQL])", "config-dbsupport-sqlite": "* Η [{{int:version-db-sqlite-url}} SQLite] είναι ένα ελαφρύ σύστημα βάσης δεδομένων που υποστηρίζεται πολύ καλά. ([http://www.php.net/manual/en/pdo.installation.php Πώς να μεταγλωττίσετε την PHP με υποστήριξη SQLite], χρησιμοποιεί PDO)", - "config-dbsupport-oracle": "* Η [{{int:version-db-oracle-url}} Oracle] είναι εμπορική βάση δεδομένων για επιχειρήσεις. ([http://www.php.net/manual/en/oci8.installation.php Πώς να μεταγλωττίσετε την PHP με υποστήριξη OCI8])", - "config-dbsupport-mssql": "* Ο [{{int:version-db-mssql-url}} Microsoft SQL Server] είναι εμπορική βάση δεδομένων για επιχειρήσεις που λειτουργεί σε Windows. ([https://www.php.net/manual/en/sqlsrv.installation.php Πώς να μεταγλωττίσετε την PHP με υποστήριξη SQLSRV])", "config-header-mysql": "Ρυθμίσεις MySQL", "config-header-postgres": "Ρυθμίσεις PostgreSQL", "config-header-sqlite": "Ρυθμίσεις SQLite", - "config-header-oracle": "Ρυθμίσεις Oracle", - "config-header-mssql": "Ρυθμίσεις του Microsoft SQL Server", "config-invalid-db-type": "Μη έγκυρος τύπος βάσης δεδομένων", "config-missing-db-name": "Πρέπει να εισαγάγετε μια τιμή για \"{{int:config-db-name}}\".", "config-missing-db-host": "Πρέπει να εισαγάγετε μια τιμή για \"{{int:config-db-host}}\".", - "config-missing-db-server-oracle": "Πρέπει να εισαγάγετε μια τιμή για \"{{int:config-db-host-oracle}}\".", "config-connection-error": "$1.\n\nΕλέγξτε τη διεύθυνση της βάσης δεδομένων, το όνομα χρήστη και το συνθηματικό και προσπαθήστε ξανά.", - "config-db-sys-user-exists-oracle": "Ο λογαριασμός χρήστη \"$1\" υπάρχει ήδη. Το SYSDBA μπορεί να χρησιμοποιηθεί μόνο για τη δημιουργία ενός νέου λογαριασμού!", "config-postgres-old": "Απαιτείται PostgreSQL $1 ή νεότερο. Εσείς έχετε $2.", - "config-mssql-old": "Απαιτείται Microsoft SQL Server $1 ή νεότερο. Εσείς έχετε $2.", "config-sqlite-readonly": "Το αρχείο $1 δεν είναι εγγράψιμο.", "config-sqlite-cant-create-db": "Δεν ήταν δυνατή η δημιουργία του αρχείου βάσης δεδομένων $1.", "config-upgrade-error": "Υπήρξε σφάλμα κατά την αναβάθμιση της λίστας MediaWiki στην βάση δεδομένων σας.\n\nΓια περισσότερες πληροφορίες δέστε την σύνδεση σας πιο πάνω και ξανακάνετε κλικ στο Continue.", @@ -143,9 +131,6 @@ "config-mysql-engine": "Μηχανή αποθήκευσης:", "config-mysql-innodb": "InnoDB", "config-mysql-engine-help": "Το InnoDB είναι σχεδόν πάντα η καλύτερη επιλογή, αφού έχει καλή υποστήριξη ταυτόχρονης λειτουργίας.\n\nΤο MyISAM μπορεί να είναι ταχύτερο σε εγκαταστάσεις του ενός χρήστη ή μόνο ανάγνωσης. \nΟι βάσεις δεδομένων MyISAM τείνουν να φθείρονται συχνότερα από τις βάσεις δεδομένων InnoDB.", - "config-mssql-auth": "Τύπος ελέγχου ταυτότητας:", - "config-mssql-sqlauth": "Έλεγχος ταυτότητας του SQL Server", - "config-mssql-windowsauth": "Έλεγχος ταυτότητας των Windows", "config-site-name": "Όνομα του wiki:", "config-site-name-help": "Αυτό θα εμφανίζεται στη γραμμή τίτλου του προγράμματος περιήγησης και σε διάφορα άλλα μέρη.", "config-site-name-blank": "Εισαγάγετε όνομα ιστοχώρου.", diff --git a/includes/installer/i18n/en.json b/includes/installer/i18n/en.json index a9da56dc43..ba525caac3 100644 --- a/includes/installer/i18n/en.json +++ b/includes/installer/i18n/en.json @@ -78,18 +78,14 @@ "config-uploads-not-safe": "Warning: Your default directory for uploads $1 is vulnerable to arbitrary scripts execution.\nAlthough MediaWiki checks all uploaded files for security threats, it is highly recommended to [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Security#Upload_security close this security vulnerability] before enabling uploads.", "config-no-cli-uploads-check": "Warning: Your default directory for uploads ($1) is not checked for vulnerability\nto arbitrary script execution during the CLI install.", "config-brokenlibxml": "Your system has a combination of PHP and libxml2 versions that is buggy and can cause hidden data corruption in MediaWiki and other web applications.\nUpgrade to libxml2 2.7.3 or later ([https://bugs.php.net/bug.php?id=45996 bug filed with PHP]).\nInstallation aborted.", - "config-suhosin-max-value-length": "Suhosin is installed and limits the GET parameter length to $1 bytes.\nMediaWiki's ResourceLoader component will work around this limit, but that will degrade performance.\nIf at all possible, you should set suhosin.get.max_value_length to 1024 or higher in php.ini, and set $wgResourceLoaderMaxQueryLength to the same value in LocalSettings.php.", + "config-suhosin-max-value-length": "Suhosin is installed and limits the GET parameter length to $1 bytes.\nMediaWiki requires suhosin.get.max_value_length to be at least $2. Disable this setting, or increase this value to $3 in php.ini.", "config-using-32bit": "Warning: your system appears to be running with 32-bit integers. This is [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:32-bit not advised].", "config-db-type": "Database type:", "config-db-host": "Database host:", "config-db-host-help": "If your database server is on a different server, enter the host name or IP address here.\n\nIf you are using shared web hosting, your hosting provider should give you the correct host name in their documentation.\n\nIf you are using MySQL, using \"localhost\" may not work for the server name. If it does not, try \"127.0.0.1\" for the local IP address.\n\nIf you are using PostgreSQL, leave this field blank to connect via a Unix socket.", - "config-db-host-oracle": "Database TNS:", - "config-db-host-oracle-help": "Enter a valid [http://download.oracle.com/docs/cd/B28359_01/network.111/b28317/tnsnames.htm Local Connect Name]; a tnsnames.ora file must be visible to this installation.
If you are using client libraries 10g or newer you can also use the [http://download.oracle.com/docs/cd/E11882_01/network.112/e10836/naming.htm Easy Connect] naming method.", "config-db-wiki-settings": "Identify this wiki", "config-db-name": "Database name (no hyphens):", "config-db-name-help": "Choose a name that identifies your wiki.\nIt should not contain spaces.\n\nIf you are using shared web hosting, your hosting provider will either give you a specific database name to use or let you create databases via a control panel.", - "config-db-name-oracle": "Database schema:", - "config-db-account-oracle-warn": "There are three supported scenarios for installing Oracle as database backend:\n\nIf you wish to create database account as part of the installation process, please supply an account with SYSDBA role as database account for installation and specify the desired credentials for the web-access account, otherwise you can either create the web-access account manually and supply only that account (if it has required permissions to create the schema objects) or supply two different accounts, one with create privileges and a restricted one for web access.\n\nScript for creating an account with required privileges can be found in \"maintenance/oracle/\" directory of this installation. Keep in mind that using a restricted account will disable all maintenance capabilities with the default account.", "config-db-install-account": "User account for installation", "config-db-username": "Database username:", "config-db-password": "Database password:", @@ -108,37 +104,24 @@ "config-pg-test-error": "Cannot connect to database $1: $2", "config-sqlite-dir": "SQLite data directory:", "config-sqlite-dir-help": "SQLite stores all data in a single file.\n\nThe directory you provide must be writable by the webserver during installation.\n\nIt should not be accessible via the web; this is why we're not putting it where your PHP files are.\n\nThe installer will write a .htaccess file along with it, but if that fails someone can gain access to your raw database.\nThat includes raw user data (email addresses, hashed passwords) as well as deleted revisions and other restricted data on the wiki.\n\nConsider putting the database somewhere else altogether, for example in /var/lib/mediawiki/yourwiki.", - "config-oracle-def-ts": "Default tablespace:", - "config-oracle-temp-ts": "Temporary tablespace:", "config-type-mysql": "MariaDB, MySQL, or compatible", "config-type-postgres": "PostgreSQL", "config-type-sqlite": "SQLite", - "config-type-oracle": "Oracle", - "config-type-mssql": "Microsoft SQL Server", "config-support-info": "MediaWiki supports the following database systems:\n\n$1\n\nIf you do not see the database system you are trying to use listed below, then follow the instructions linked above to enable support.", "config-dbsupport-mysql": "* [{{int:version-db-mariadb-url}} MariaDB] is the primary target for MediaWiki and is best supported. MediaWiki also works with [{{int:version-db-mysql-url}} MySQL] and [{{int:version-db-percona-url}} Percona Server], which are MariaDB compatible. ([https://www.php.net/manual/en/mysqli.installation.php How to compile PHP with MySQL support])", "config-dbsupport-postgres": "* [{{int:version-db-postgres-url}} PostgreSQL] is a popular open source database system as an alternative to MySQL. ([https://www.php.net/manual/en/pgsql.installation.php How to compile PHP with PostgreSQL support])", "config-dbsupport-sqlite": "* [{{int:version-db-sqlite-url}} SQLite] is a lightweight database system that is very well supported. ([https://www.php.net/manual/en/pdo.installation.php How to compile PHP with SQLite support], uses PDO)", - "config-dbsupport-oracle": "* [{{int:version-db-oracle-url}} Oracle] is a commercial enterprise database. ([https://www.php.net/manual/en/oci8.installation.php How to compile PHP with OCI8 support])", - "config-dbsupport-mssql": "* [{{int:version-db-mssql-url}} Microsoft SQL Server] is a commercial enterprise database for Windows. ([https://www.php.net/manual/en/sqlsrv.installation.php How to compile PHP with SQLSRV support])", "config-header-mysql": "MariaDB/MySQL settings", "config-header-postgres": "PostgreSQL settings", "config-header-sqlite": "SQLite settings", - "config-header-oracle": "Oracle settings", - "config-header-mssql": "Microsoft SQL Server settings", "config-invalid-db-type": "Invalid database type.", "config-missing-db-name": "You must enter a value for \"{{int:config-db-name}}\".", "config-missing-db-host": "You must enter a value for \"{{int:config-db-host}}\".", - "config-missing-db-server-oracle": "You must enter a value for \"{{int:config-db-host-oracle}}\".", - "config-invalid-db-server-oracle": "Invalid database TNS \"$1\".\nUse either \"TNS Name\" or an \"Easy Connect\" string ([http://docs.oracle.com/cd/E11882_01/network.112/e10836/naming.htm Oracle Naming Methods]).", "config-invalid-db-name": "Invalid database name \"$1\".\nUse only ASCII letters (a-z, A-Z), numbers (0-9), underscores (_) and hyphens (-).", "config-invalid-db-prefix": "Invalid database prefix \"$1\".\nUse only ASCII letters (a-z, A-Z), numbers (0-9), underscores (_) and hyphens (-).", "config-connection-error": "$1.\n\nCheck the host, username and password and try again. If using \"localhost\" as the database host, try using \"127.0.0.1\" instead (or vice versa).", "config-invalid-schema": "Invalid schema for MediaWiki \"$1\".\nUse only ASCII letters (a-z, A-Z), numbers (0-9) and underscores (_).", - "config-db-sys-create-oracle": "Installer only supports using a SYSDBA account for creating a new account.", - "config-db-sys-user-exists-oracle": "User account \"$1\" already exists. SYSDBA can only be used for creating of a new account!", "config-postgres-old": "PostgreSQL $1 or later is required. You have $2.", - "config-mssql-old": "Microsoft SQL Server $1 or later is required. You have $2.", "config-sqlite-name-help": "Choose a name that identifies your wiki.\nDo not use spaces or hyphens.\nThis will be used for the SQLite data filename.", "config-sqlite-parent-unwritable-group": "Cannot create the data directory $1, because the parent directory $2 is not writable by the webserver.\n\nThe installer has determined the user your webserver is running as.\nMake the $3 directory writable by it to continue.\nOn a Unix/Linux system do:\n\n
cd $2\nmkdir $3\nchgrp $4 $3\nchmod g+w $3
", "config-sqlite-parent-unwritable-nogroup": "Cannot create the data directory $1, because the parent directory $2 is not writable by the webserver.\n\nThe installer could not determine the user your webserver is running as.\nMake the $3 directory globally writable by it (and others!) to continue.\nOn a Unix/Linux system do:\n\n
cd $2\nmkdir $3\nchmod a+w $3
", @@ -163,11 +146,6 @@ "config-mysql-engine": "Storage engine:", "config-mysql-innodb": "InnoDB (recommended)", "config-mysql-engine-help": "InnoDB is almost always the best option, since it has good concurrency support.\n\nMyISAM may be faster in single-user or read-only installations.\nMyISAM databases tend to get corrupted more often than InnoDB databases.", - "config-mssql-auth": "Authentication type:", - "config-mssql-install-auth": "Select the authentication type that will be used to connect to the database during the installation process.\nIf you select \"{{int:config-mssql-windowsauth}}\", the credentials of whatever user the webserver is running as will be used.", - "config-mssql-web-auth": "Select the authentication type that the web server will use to connect to the database server, during ordinary operation of the wiki.\nIf you select \"{{int:config-mssql-windowsauth}}\", the credentials of whatever user the webserver is running as will be used.", - "config-mssql-sqlauth": "SQL Server Authentication", - "config-mssql-windowsauth": "Windows Authentication", "config-site-name": "Name of wiki:", "config-site-name-help": "This will appear in the title bar of the browser and in various other places.", "config-site-name-blank": "Enter a site name.", diff --git a/includes/installer/i18n/eo.json b/includes/installer/i18n/eo.json index 3f83612474..d0759889a8 100644 --- a/includes/installer/i18n/eo.json +++ b/includes/installer/i18n/eo.json @@ -54,30 +54,21 @@ "config-db-host": "Datenbanka gastigilo:", "config-db-wiki-settings": "Identigu ĉi tiun vikion", "config-db-name": "Nomo de datumbazo:", - "config-db-name-oracle": "Datenbanka skemo:", "config-db-username": "Datenbanka uzantnomo:", "config-db-password": "Datenbanka pasvorto:", "config-db-port": "Datenbanka pordo:", "config-db-schema": "Skemo por MediaVikio (sen streketo):", "config-sqlite-dir": "Datena dosierujo por SQLite:", - "config-oracle-def-ts": "Implicita tabelspaco:", - "config-oracle-temp-ts": "Portempa tabelspaco:", "config-type-mysql": "MariaDB, MySQL, aŭ kongrua", - "config-type-mssql": "Microsoft SQL Server", "config-header-mysql": "MairaDB/MySQL-agordoj", "config-header-postgres": "PostgreSQL-agordoj", "config-header-sqlite": "SQLite-agordoj", - "config-header-oracle": "Oracle-agordoj", - "config-header-mssql": "Microsoft SQL Server-agordoj", "config-invalid-db-type": "Nevalida speco de datenbanko.", "config-sqlite-readonly": "La dosiero $1 ne estas surskribebla.", "config-sqlite-cant-create-db": "Ne povis krei datenbankan dosieron $1.", "config-regenerate": "Refari dosieron LocalSettings.php →", "config-mysql-engine": "Konservada modulo:", "config-mysql-innodb": "InnoDB (rekomendata)", - "config-mssql-auth": "Speco de aŭtentokontrolo:", - "config-mssql-sqlauth": "Aŭtentokontrolo de Microsoft SQL-Servilo", - "config-mssql-windowsauth": "Aŭtentokontrolo de Windows", "config-site-name": "Nomo de vikio:", "config-site-name-blank": "Enigu nomon de retejo.", "config-project-namespace": "Projekta nomspaco:", diff --git a/includes/installer/i18n/es.json b/includes/installer/i18n/es.json index 5e3d3a1034..aa0d5ee400 100644 --- a/includes/installer/i18n/es.json +++ b/includes/installer/i18n/es.json @@ -104,7 +104,7 @@ "config-apc": "[https://www.php.net/apc APC] está instalado", "config-apcu": "[https://www.php.net/apcu APCu] está instalado", "config-wincache": "[https://www.iis.net/downloads/microsoft/wincache-extension WinCache] está instalado", - "config-no-cache-apcu": "Atención: no se pudo encontrar [https://www.php.net/apcu APCu] o [https://www.iis.net/downloads/microsoft/wincache-extension WinCache].\nEl almacenamiento en caché de objetos no está activado.", + "config-no-cache-apcu": "Atención: no se pudo encontrar [https://www.php.net/apcu APCu] o [https://www.iis.net/downloads/microsoft/wincache-extension WinCache].\nEl almacenamiento en antememoria de objetos no está activado.", "config-mod-security": "Advertencia: tu servidor web tiene activado [https://modsecurity.org/ mod_security]/mod_security2. Muchas de sus configuraciones comunes pueden causar problemas a MediaWiki u otro software que permita a los usuarios publicar contenido arbitrario. De ser posible, deberías desactivarlo. Si no, consulta la [https://modsecurity.org/documentation/ documentación de mod_security] o contacta con el administrador de tu servidor si encuentras errores aleatorios.", "config-diff3-bad": "GNU diff3 no se encuentra.", "config-git": "Se encontró el software de control de versiones Git: $1.", @@ -124,13 +124,9 @@ "config-db-type": "Tipo de base de datos:", "config-db-host": "Servidor de la base de datos:", "config-db-host-help": "Si tu servidor de base de datos está en otro servidor, escribe el nombre del equipo o su dirección IP aquí.\n\nSi estás utilizando alojamiento web compartido, tu proveedor debería darte el nombre correcto del servidor en su documentación.\n\nSi vas a instalar en un servidor Windows y a utilizar MySQL, el uso de \"localhost\" como nombre del servidor puede no funcionar. Si es así, intenta poner \"127.0.0.1\" como dirección IP local.\n\nSi utilizas PostgreSQL, deja este campo vacío para conectarse a través de un socket de Unix.", - "config-db-host-oracle": "TNS de la base de datos:", - "config-db-host-oracle-help": "Escribe un [http://download.oracle.com/docs/cd/B28359_01/network.111/b28317/tnsnames.htm nombre de conexión local] válido; un archivo tnsnames.ora debe ser visible para esta instalación.
Si estás utilizando bibliotecas de cliente 10g o más recientes también puedes utilizar el método de asignación de nombres [http://download.oracle.com/docs/cd/E11882_01/network.112/e10836/naming.htm Easy Connect].", "config-db-wiki-settings": "Identifica este wiki", "config-db-name": "Nombre de base de datos (ningún guion):", "config-db-name-help": "Elige un nombre que identifique tu wiki.\nNo debe contener espacios.\n\nSi estás utilizando alojamiento web compartido, tu proveedor te dará un nombre específico de base de datos para que lo utilices, o bien te permitirá crear bases de datos a través de un panel de control.", - "config-db-name-oracle": "Esquema de la base de datos:", - "config-db-account-oracle-warn": "Hay tres escenarios compatibles para la instalación de Oracle como base de datos back-end:\n\nSi desea crear una cuenta de base de datos como parte del proceso de instalación, por favor suministre una cuenta con función SYSDBA como cuenta de base de datos para la instalación y especifique las credenciales deseadas de la cuenta de acceso al web, de lo contrario puede crear manualmente la cuenta de acceso al web y suministrar sólo esa cuenta (si tiene los permisos necesarios para crear los objetos de esquema) o suministrar dos cuentas diferentes, una con privilegios de creación y otra con acceso restringido a la web\n\nLa secuencia de comandos (script) para crear una cuenta con los privilegios necesarios puede encontrarse en el directorio \"maintenance/oracle/\" de esta instalación. Tenga en cuenta que utilizando una cuenta restringida desactivará todas las capacidades de mantenimiento con la cuenta predeterminada.", "config-db-install-account": "Cuenta de usuario para instalación", "config-db-username": "Nombre de usuario de la base de datos:", "config-db-password": "Contraseña de la base de datos:", @@ -149,37 +145,24 @@ "config-pg-test-error": "No se puede conectar con la base de datos $1: $2", "config-sqlite-dir": "Directorio de datos SQLite:", "config-sqlite-dir-help": "SQLite almacena todos los datos en un único archivo.\n\nEl directorio que proporciones debe poder escribirse por el servidor web durante la instalación.\n\n'''No''' debería ser accesible a través de Internet. Por eso no vamos a ponerlo en el sitio donde están los archivos PHP.\n\nEl instalador escribirá un archivo .htaccess junto con él, pero si falla alguien podría tener acceso a la base de datos en bloque.\nEso incluye los datos de usuario en bloque (direcciones de correo electrónico, las contraseñas con hash) así como revisiones eliminadas y otros datos restringidos del wiki.\n\nConsidera poner la base de datos en algún otro sitio, por ejemplo en /var/lib/mediawiki/tuwiki .", - "config-oracle-def-ts": "Espacio de tablas predeterminado:", - "config-oracle-temp-ts": "Espacio de tablas temporal:", "config-type-mysql": "MariaDB, MySQL o un sistema compatible", "config-type-postgres": "PostgreSQL", "config-type-sqlite": "SQLite", - "config-type-oracle": "Oracle", - "config-type-mssql": "Microsoft SQL Server", "config-support-info": "MediaWiki es compatible con los siguientes sistemas de bases de datos:\n\n$1\n\nSi no encuentras en el listado el sistema de base de datos que estás intentando utilizar, sigue las instrucciones enlazadas arriba para activar la compatibilidad.", "config-dbsupport-mysql": "* [{{int:version-db-mariadb-url}} MariaDB] es la base de datos mayoritaria para MediaWiki y la que goza de mayor compatibilidad. MediaWiki también funciona con [{{int:version-db-myslql-url}} MySQL] y [{{int:version-db-percona-url}} Percona Server], que son compatibles con MariaDB. ([https://www.php.net/manual/es/mysql.installation.php Cómo compilar PHP con compatibilidad MySQL])", "config-dbsupport-postgres": "* [{{int:version-db-postgres-url}} PostgreSQL] es un sistema de base de datos popular de código abierto, alternativa a MySQL. ([https://www.php.net/manual/es/pgsql.installation.php Cómo compilar PHP con compatibilidad PostgreSQL]).", "config-dbsupport-sqlite": "* [{{int:version-db-sqlite-url}} SQLite] es un sistema de base de datos ligero con gran compatibilidad con MediaWiki. ([https://www.php.net/manual/en/pdo.installation.php Cómo compilar PHP con compatibilidad SQLite], usando PDO)", - "config-dbsupport-oracle": "* [{{int:version-db-oracle-url}} Oracle] es una base de datos comercial a nivel empresarial. ([https://www.php.net/manual/en/oci8.installation.php Cómo compilar PHP con compatibilidad con OCI8])", - "config-dbsupport-mssql": "* [{{int:version-db-mssql-url}} Microsoft SQL Server] es un sistema comercial de base de datos empresariales para Windows. ([https://www.php.net/manual/en/sqlsrv.installation.php Cómo compilar PHP con compatibilidad con SQLSRV])", "config-header-mysql": "Configuración de MariaDB/MySQL", "config-header-postgres": "Configuración de PostgreSQL", "config-header-sqlite": "Configuración de SQLite", - "config-header-oracle": "Configuración de Oracle", - "config-header-mssql": "Configuración de Microsoft SQL Server", "config-invalid-db-type": "El tipo de base de datos no es válido", "config-missing-db-name": "Debes escribir un valor para \"{{int:config-db-nombre}}\".", "config-missing-db-host": "Debes escribir un valor para \"{{int:config-db-host}}\".", - "config-missing-db-server-oracle": "Debes escribir un valor para \"{{int:config-db-host-oracle}}\".", - "config-invalid-db-server-oracle": "El TNS de la base de datos «$1» es inválido.\nDebes usar un \"TNS Name\" o una cadena \"Easy Connect\" ([http://docs.oracle.com/cd/E11882_01/network.112/e10836/naming.htm Nomenclatura de Oracle]).", "config-invalid-db-name": "El nombre de la base de datos \"$1\" no es válido.\nUsa sólo caracteres ASCII: letras (a-z, A-Z), números (0-9), guiones bajos (_) y guiones (-).", "config-invalid-db-prefix": "El prefijo de la base de datos \"$1\" no es válido.\nUsa sólo caracteres ASCII: letras (a-z, A-Z), números (0-9), guiones bajos (_) y guiones (-).", "config-connection-error": "$1.\n\nControl el anfitrión, username y contraseña y probar otra vez. Si utilizando \"localhost\" como el anfitrión de base de datos, prueba utilizar \"127.0.0.1\" en cambio (o viceversa).", "config-invalid-schema": "El esquema de la base de datos \"$1\" es inválido.\nUse sólo carateres ASCII: letras (a-z, A-Z), guarismos (0-9) y guiones bajos (_).", - "config-db-sys-create-oracle": "El instalador sólo admite el empleo de cuentas SYSDBA como método para crear una cuenta nueva.", - "config-db-sys-user-exists-oracle": "La cuenta de usuario «$1» ya existe. SYSDBA solo puede utilizarse para crear cuentas nuevas.", "config-postgres-old": "Se requiere PostgreSQL $1 o posterior. Tienes la versión $2.", - "config-mssql-old": "Se requiere Microsoft SQL Server $1 o posterior. Tienes la versión $2.", "config-sqlite-name-help": "Elige el nombre que identificará a tu wiki.\nNo uses espacios o guiones.\nEste nombre se usará como nombre del archivo de datos de SQLite.", "config-sqlite-parent-unwritable-group": "No se puede crear el directorio de datos $1, porque el servidor web no tiene permiso de escribir en el directorio padre $2.\n\nEl instalador ha determinado el usuario con el que se ejecuta tu servidor web.\nConcede permisos de escritura a él en el directorio $3 para continuar.\nEn un sistema Unix/Linux haz:\n\n
cd $2\nmkdir $3\nchgrp $4 $3\nchmod g+w $3
", "config-sqlite-parent-unwritable-nogroup": "No se puede crear el directorio de datos $1, porque el servidor web no tiene permiso de escribir en el directorio padre $2.\n\nEl instalador no pudo determinar el usuario con el que se ejecuta tu servidor web.\nConcede permisos de escritura a él (¡y a otros!) en el directorio $3 para continuar.\nEn un sistema Unix/Linux haz:\n\n
cd $2\nmkdir $3\nchmod a+w $3
", @@ -203,11 +186,6 @@ "config-mysql-engine": "Motor de almacenamiento:", "config-mysql-innodb": "InnoDB", "config-mysql-engine-help": "InnoDB es casi siempre la mejor opción, dado que soporta bien los accesos simultáneos.\n\nMyISAM puede ser más rápido en instalaciones con usuario único o de sólo lectura.\nLas bases de datos MyISAM tienden a corromperse más a menudo que las bases de datos InnoDB.", - "config-mssql-auth": "Tipo de autenticación:", - "config-mssql-install-auth": "Selecciona el tipo de autenticación que se utilizará para conectarse a la base de datos durante el proceso de instalación.\nSi seleccionas \"{{int:config-mssql-windowsauth}}\", se usarán las credenciales del usuario con el que se ejecuta el servidor web.", - "config-mssql-web-auth": "Selecciona el tipo de autenticación que utilizará el servidor web para conectarse al servidor de base de datos, durante el funcionamiento normal de la wiki.\nSi seleccionas \"{{int:config-mssql-windowsauth}}\", se usarán las credenciales del usuario con el cual se ejecuta el servidor web.", - "config-mssql-sqlauth": "Autenticación de SQL Server", - "config-mssql-windowsauth": "Autenticación de Windows", "config-site-name": "Nombre del wiki:", "config-site-name-help": "Esto aparecerá en la barra de título del navegador y en varios otros lugares.", "config-site-name-blank": "Escribe un nombre de sitio.", @@ -283,10 +261,10 @@ "config-cc-again": "Elegir otra vez...", "config-cc-not-chosen": "Elige la licencia Creative Commons que desees y haz clic en \"proceed\".", "config-advanced-settings": "Configuración avanzada", - "config-cache-options": "Configuración de la caché de objetos:", + "config-cache-options": "Configuración de la antememoria de objetos:", "config-cache-help": "El almacenamiento en caché de objetos se utiliza para mejorar la velocidad de MediaWiki mediante el almacenamiento en caché los datos usados más frecuentemente.\nA los sitios medianos y grandes se les recomienda que permitirlo. También es beneficioso para los sitios pequeños.", "config-cache-none": "Sin almacenamiento en caché (no se pierde ninguna funcionalidad, pero la velocidad puede resentirse en sitios grandes)", - "config-cache-accel": "Almacenamiento en caché de objetos PHP (APC, APCu o WinCache)", + "config-cache-accel": "Almacenamiento en antememoria de objetos PHP (APC, APCu o WinCache)", "config-cache-memcached": "Utilizar Memcached (necesita ser instalado y configurado aparte)", "config-memcached-servers": "Servidores Memcached:", "config-memcached-help": "Lista de direcciones IP que serán usadas por Memcached.\nDeben especificarse una por cada línea y especificar el puerto a utilizar. Por ejemplo:\n127.0.0.1:11211\n192.168.1.25:1234", diff --git a/includes/installer/i18n/et.json b/includes/installer/i18n/et.json index 2cd5dd2049..a981d201e2 100644 --- a/includes/installer/i18n/et.json +++ b/includes/installer/i18n/et.json @@ -39,7 +39,6 @@ "config-diff3-bad": "Tekstivõrdluse vahendit GNU diff3 ei leitud. Saad seda eirata, aga võid sattuda edaspidi sagedamini redigeerimiskonfliktidesse.", "config-db-type": "Andmebaasi tüüp:", "config-db-name": "Andmebaasi nimi (sidekriipsudeta):", - "config-db-name-oracle": "Andmebaasi skeem:", "config-db-username": "Andmebaasi kasutajanimi:", "config-db-password": "Andmebaasi parool:", "config-db-port": "Andmebaasi port:", diff --git a/includes/installer/i18n/eu.json b/includes/installer/i18n/eu.json index 4e004f9301..cd6c110e2a 100644 --- a/includes/installer/i18n/eu.json +++ b/includes/installer/i18n/eu.json @@ -85,13 +85,9 @@ "config-db-type": "Datu-base mota:", "config-db-host": "Datu-basearen zerbitzaria:", "config-db-host-help": "Zure datu-basearen zerbitzaria beste zerbitzari batean badago, sartu ostalariaren izena edo IP helbidea hemen.\n\nPartekatutako web-ostatua erabiltzen ari bazara, zure ostalaritza-hornitzaileak dokumentazio-ostalariaren izen egokia eman beharko lizuke.\n\nWindows zerbitzari batean instalatzen bazara eta MySQL erabiliz, \"localhost\" agian ez du zerbitzariaren izenerako funtzionatuko. Ez badago, saiatu \"127.0.0.1\" tokiko IP helbideetarako.\n\nPostgreSQL erabiltzen ari bazara, utzi eremu hau hutsik Unix socket bidez konektatzeko.", - "config-db-host-oracle": "Datu-baseko TNS:", - "config-db-host-oracle-help": "Sartu baliozko [http://download.oracle.com/docs/cd/B28359_01/network.111/b28317/tnsnames.htm Konekzio izan lokala]; instalazio honetarako tnsnames.ora fitxategia ikusgai egon behar da.
Bezeroen 10g liburutegiak edo berriagoak erabiltzen ari bazara, [http://download.oracle.com/docs/cd/E11882_01/network.112 ere erabil dezakezu. /e10836/naming.htm Konektatzeko erraza] izendatzeko metodoa.", "config-db-wiki-settings": "Wiki hau identifikatu", "config-db-name": "Datu-base izena:", "config-db-name-help": "Aukeratu zure Wikia identifikatzen duen izena.\nEzin dira espazioak eabili.\n\nErabiltzen ari bazara web hosting partekatua, hostin-eko hornitzaileak emango dizu datu-basearen izen espezifikoa edo kontrol panel baten bitzrtez zure datu-basea sortzea utziko dizu.", - "config-db-name-oracle": "Datu-baseko eskema:", - "config-db-account-oracle-warn": "Hiru euskarri onartzen dira Oracle datu-basearen euskarri gisa instalatzeko:\n\nInstalazio-prozesuaren zati gisa datu-basearen kontua sortu nahi baduzu, hornitu kontu bat SYSDBA rol datu-baseko kontu gisa instalatzeko eta webgunerako sarbide konturako nahi dituzun kredentzialak zehazteko; bestela, web-sarbideen kontua eskuz sortu eta hornitu kontu hori bakarrik (eskemaren objektuak sortzeko baimenak behar baditu) edo bi kontu ezberdin, bi pribilegio sortu eta sarbide mugatua eskaintzen dutenak.\n\nBeharrezko baimenak dituen kontu bat sortzeko gidoia instalazio honen \"mantentze/orakulu/\" direktorioan aurki daiteke. Kontuan izan kontu mugatu bat erabiliz kontu lehenetsiarekin mantentze-gaitasun guztiak desgaituko dituela.", "config-db-install-account": "Instalazio prozesuan erabili erabiltzaile kontua.", "config-db-username": "Datu-base lankide izena:", "config-db-password": "Datu-base pasahitza:", @@ -110,37 +106,24 @@ "config-pg-test-error": "Ezin da datu-basearekin konektatu $1: $2", "config-sqlite-dir": "SQLite -eko informazioaren direktorioa:", "config-sqlite-dir-help": "SQLite-k datu guztiak fitxategi bakarrean gordetzen ditu.\n\nHornitu duzun direktorioa web zerbitzariaren bidez idatzia izateko aukera eman beharko duu instalazioan zehar.\n\nEz da webgunearen bidez eskuragarri egon behar; horregatik zure PHP fitxategiak non dauden ez dugu erakutsi.\n\nInstalatzaileak .htaccess fitxategi bat idatziko du bertan, baina horrek huts egiten badu zure datu base gordinera norbait sar daiteke.\nErabiltzaileen datu gordinak (helbide elektronikoak, pasahitzak), ezabatutako berrikusketa eta gainontzeko datu mugatuak ere barnean hartuz.\n\nDatu-basea beste nonbait jartzearen inguruan hausnartu, adibidez, /var/lib/mediawiki/yourwiki-n.", - "config-oracle-def-ts": "Taula-toki lehenetsia:", - "config-oracle-temp-ts": "Aldi baterako taula:", "config-type-mysql": "MariaDB, MySQL edo bateragarria", "config-type-postgres": "PostgreSQL", "config-type-sqlite": "SQLite", - "config-type-oracle": "Oracle", - "config-type-mssql": "Microsoft SQL Server", "config-support-info": "MediaWiki-k onartzen du hurrengo datu-base sistemak:\n\n$1\n\nListan ez baduzu ikusten erabili nahi duzun sistema, jarraitu goiko argibideak aktibatzeko.", "config-dbsupport-mysql": "* [{{int:version-db-mysql-url}} MySQL] MediaWikiren lehenengoko helburua da eta primeran babesturik dago. MediaWikik ere [{{int:version-db-mariadb-url}} MariaDB]-rekin egiten du lan baita [{{int:version-db-percona-url}} Percona Server]-kin, MySQL-rekin balio dutenak. ([https://www.php.net/manual/en/mysqli.installation.php How to compile PHP with MySQL support])", "config-dbsupport-postgres": "* [{{int:version-db-postgres-url}} PostgreSQL] iturburu irekiko datu basea sistema famatua da MySQL-rako alternatiba bezala. ([https://www.php.net/manual/en/pgsql.installation.php How to compile PHP with PostgreSQL support])", "config-dbsupport-sqlite": "* [{{int:version-db-sqlite-url}} SQLite] oso ondo onartzen duen datu-basearen sistema arina da.\n ([http://www.php.net/manual/en/pdo.installation.php How to compile PHP with SQLite support], uses PDO)", - "config-dbsupport-oracle": "* [{{int:version-db-oracle-url}} Oracle] enpresa komertzial baten datu-basea da. ([http://www.php.net/manual/en/oci8.installation.php How to compile PHP with OCI8 support])", - "config-dbsupport-mssql": "* [{{int:version-db-mssql-url}} Microsoft SQL Server] Windows-entzako enpresa komertzial baten datu-basea da. ([https://www.php.net/manual/en/sqlsrv.installation.php How to compile PHP with SQLSRV support])", "config-header-mysql": "MySQL hobespenak", "config-header-postgres": "PostgreSQL hobespenak", "config-header-sqlite": "SQLite hobespenak", - "config-header-oracle": "Oracle hobespenak", - "config-header-mssql": "Microsoft SQL Server-en ezarpenak", "config-invalid-db-type": "Datu-base mota baliogabea.", "config-missing-db-name": "\"{{int:config-db-name}}\"-rentzako balioa sartu behar duzu.", "config-missing-db-host": "\"{{int:config-db-host}}\"-rentzako balioa sartu behar duzu.", - "config-missing-db-server-oracle": "\"{{int:config-db-host-oracle}}\"-rentzako balioa sartu behar duzu.", - "config-invalid-db-server-oracle": "\"$1\" TNS datu basea baliogabea.\nErabili \"TNS izena\" edo \"Konektagarritasun erraza\" katea ([http://docs.oracle.com/cd/E11882_01/network.112/e10836/naming.htm Oracle Naming Methods]).", "config-invalid-db-name": "Datu-basearen izen okerra \"$1\"\nErabil ezazu ASCII letrak bakarrik (a-z, A-Z), zenbakiak (09), behe-gidoiak (_) eta gidoiak (-)", "config-invalid-db-prefix": "Datu-basearen aurrizki okerra \"$1\"\nErabil ezazu ASCII letrak bakarrik (a-z, A-Z) behe-gidoiak (_) eta gidoiak (-)", "config-connection-error": "$1\n\nHost-a, erabiltzaile izena eta pasahitza egiaztatu eta saiatu berriro.", "config-invalid-schema": "MediaWikiko eskema okerra \"$1\"\nErabil ezazu ASCII letrak bakarrik (a-z, A-Z) behe-gidoiak (_).", - "config-db-sys-create-oracle": "Instalatzaileak bakarrik jasaten du SYSBDA kontu bat erabiltzaile kontu berri bat sortzeko.", - "config-db-sys-user-exists-oracle": "$1 erabiltzaile kontua dagoeneko existitzen da. SYSDBA kontu berri bat sortzeko erabili daiteke soilik!", "config-postgres-old": "PostgreSQL $1 edo berriagoa behar da. Zuk $2 badaukazu.", - "config-mssql-old": "Microsoft SQL Server $1 edo berriagoa behar da. Zuk $2 badaukazu.", "config-sqlite-name-help": "Aukeratu zure wikia identifikatzen duen izen bat.\nEz erabili zuriunerik edo gidoirik.\nHau erabiliko da SQLite datuen artxiborako.", "config-sqlite-parent-unwritable-group": "Ezin da datu-direktorioa sortu $1, web zerbitzariak ezin baitu $2 guraso direktorioan idatzi.\n\nInstalatzaileak webgunea exekutatzen ari den bitartean zure erabiltzailea zehaztu du.\nEgin $3 direktorioan idazteko gai izatea jarraitzeko.\nUnix/Linux sistema batean:\n\n
cd $2\nmkdir $3\nchgrp $4 $3\nchmod g+w $3
", "config-sqlite-parent-unwritable-nogroup": "Ezin da datu-direktorioa sortu $1, web zerbitzariak ezin baitu $2 guraso direktorioan idatzi.\n\nInstalatzaileak webgunea exekutatzen ari den bitartean zure erabiltzailea zehaztu dezake.\nEgin $3 direktorioa globalean idazteko gai izatea (horretarako eta besteentzako!) jarraitzeko.\nUnix/Linux sistema batean:\n\n
cd $2\nmkdir $3\nchmod a+w $3
", @@ -164,11 +147,6 @@ "config-mysql-engine": "Biltegiratze motorea:", "config-mysql-innodb": "InnoDB", "config-mysql-engine-help": "InnoDB ia beti aukerarik onena da, konkurrentzia-laguntza ona duelako.\n\nMyISAM erabiltzaile bakarreko edo irakurketa bakarreko instalazioetan azkarragoa izan daiteke.\nMyISAM datu-basea gehiagokotan hondatuta ageri da InnoDB datu-baseareakin baino.", - "config-mssql-auth": "Autentifikazio mota:", - "config-mssql-install-auth": "Aukeratu instalazio prozesuan zehar datu-basera konektatzeko erabiliko den autentifikazio mota.\n\"{{Int: config-mssql-windowsauth}}\" hautatzen baduzu, web zerbitzariak duen edozein erabiltzailek erabiliko duen kredentziala erabiliko da.", - "config-mssql-web-auth": "Aukeratu instalazio prozesuan zehar datu-base zerbitzariari konektatzeko erabiliko den autentifikazio mota.\n\"{{Int: config-mssql-windowsauth}}\" hautatzen baduzu, web zerbitzariak duen edozein erabiltzailek erabiliko duen kredentziala erabiliko da.", - "config-mssql-sqlauth": "SQL Serbidorearen Autentifikazioa", - "config-mssql-windowsauth": "Windows-eko Autentifikazioa.", "config-site-name": "Wikiaren izena:", "config-site-name-help": "Hau nabigatzailearen tituluaren lerroan agertuko da eta pare bat leku gehiagotan.", "config-site-name-blank": "Aukeratu webgunearen izena.", diff --git a/includes/installer/i18n/fa.json b/includes/installer/i18n/fa.json index 7cac681f2c..158e43d55f 100644 --- a/includes/installer/i18n/fa.json +++ b/includes/installer/i18n/fa.json @@ -97,13 +97,9 @@ "config-db-type": "نوع پایگاه اطلاعات:", "config-db-host": "میزبان پایگاه اطلاعات:", "config-db-host-help": "اگر سرور پایگاه اطلاعاتی شما در سرور دیگری است، نام گروه و آدرس آی‌پی را اینجا وارد کنید.\nاگر از میزبان شبکهٔ به اشتراک گذاشته‌شده استفاده می‌کنید، تهیه‌کنندهٔ خدمات میزبانی شما باید نام میزبان صحیح در اسناد و مدارک را به شما بدهد.\nاگر از مای‌اس‌کیو‌ال استفاده می‌کنید، ممکن است استفاده از «میزبان‌محلی» برای نام سرور کار نکند.اگر کار نکرد، «۱۲۷.۰.۰.۱» را برای آدرس آی‌پی محلی امتحان کنید.\nاگر از پستگرس‌کیوال استفاده می‌کنید، برای اتصال از طریق یک سوکت یونیکس این قسمت را خالی رها کنید.", - "config-db-host-oracle": "پایگاه اطلاعاتی تی‌ان‌اس:", - "config-db-host-oracle-help": "یک [http://download.oracle.com/docs/cd/B28359_01/network.111/b28317/tnsnames.htm Local Connect Name] معتبر وارد کنید؛ پوشهٔ tnsnames.ora باید برای این نصب نمایان باشد.
اگر از کتابخانه‌های پردازشگر ۱۰جی یا جدیدتر استفاده می‌کنید،همچنین می‌توانید از روش نامبردهٔ [http://download.oracle.com/docs/cd/E11882_01/network.112/e10836/naming.htm Easy Connect] استفاده کنید.", "config-db-wiki-settings": "این ویکی را شناسایی کنید.", "config-db-name": "نام پایگاه داده (بدون خط پیوند):", "config-db-name-help": "نامی را انتخاب کنید که ویکی شما را شناسایی کند.\nنباید شامل فاصله باشد.\nاگر از گروه شبکهٔ اشتراک‌گذاری استفاده می‌کنید، تهیه‌کنندهٔ گروهتان یا باید به شما نام یک پایگاه اطلاعاتی مشخص برای استفاده بدهد یا برای ایجاد پایگاه‌های اطلاعاتی از طریق یک کنترل پنل به شما اجازه بدهد.", - "config-db-name-oracle": "طرح کلی پایگاه اطلاعاتی:", - "config-db-account-oracle-warn": "برای نصب برنامهٔ اوراکل به عنوان پایگاه اطلاعاتی در بخش گذشته،سه سناریو پشتیبانی شده است:\nاگر مایل به ایجاد حساب پایگاه اطلاعاتی به عنوان بخشی از روند نصب هستید، لطفاً یک حساب با نقش اس‌وای‌اس‌دی‌بی‌ای به عنوان حساب پایگاه اطلاعاتی برای نصب تهیه کنید و اعتبارنامه‌های مطلوبی را برای حساب دردسترس شبکه تعیین کنید، به عبارتی دیگر یا می‌توانید حساب دردسترس شبکه را به طور دستی ایجاد کنید و تنها آن حساب را تهیه کنید (اگر مستلزم مجوزهایی برای ایجاد موضوعات طرح کلی باشد) یا دو حساب دیگر تهیه کنید،یکی با ایجاد مزایا و یک حساب محدود برای دسترسی شبکه.\nمتنی برای ایجاد یک حساب با مزایای لازم بنویسید که می‌تواند در فهرست\"نگهداری/برنامهٔ اوراکل\" این نصب یافت شود. به یاد داشته باشید که استفاده از یک حساب محدود،همهٔ قابلیت‌های نگهداری با حساب پیش‌فرض را غیرفعال خواهد کرد.", "config-db-install-account": "حساب کاربری برای نصب", "config-db-username": "نام کاربری پایگاه اطلاعات:", "config-db-password": "گذرواژه پایگاه‌های داده:", @@ -122,34 +118,22 @@ "config-pg-test-error": "نمی‌توان به پایگاه اطلاعاتی '''$1''' وصل شد: $2", "config-sqlite-dir": "فهرست اطلاعات اس‌کیو‌لایت:", "config-sqlite-dir-help": "اس‌کیولایت همهٔ اطلاعات را در یک پوشهٔ جداگانه ذخیره می‌کند.\nفهرستی را که به وجود‌ آوردید باید در طی نصب به‌ وسیلهٔ وب‌سرور قابل نوشتن باشد.\nنباید از طریق وب در دسترس باشد، به همین دلیل ما آن را در جایی که پوشه‌های پی‌اچ‌پی شما هست، قرار نمی‌دهیم.\nنصب کننده یک پوشهٔ .htaccess همراه آن خواهدآورد،اما اگر این کار را انجام ندهد،کسی می‌تواند به پایگاه اطلاعاتی شما دسترسی پیدا کند.\nاطلاعات خام کاربر شامل (آدرس‌های ایمیل، علامت‌‌ها با شماره‌های رمز عبور) به خوبی پاک کردن تغییرات و دیگر اطلاعات محرمانه در ویکی.\nقرار دادن پایگاه اطلاعاتی باهم را در جایی دیگر در نظر بگیرید، برای مثال در /var/lib/mediawiki/yourwiki.", - "config-oracle-def-ts": "جدول پیش فرض:", - "config-oracle-temp-ts": "جدول موقت:", "config-type-mysql": "MariaDB، مای‌اس‌کیو‌ال (یا سازگار)", - "config-type-mssql": "سرور مایکروسافت اس‌کیو‌ال", "config-support-info": "مدیاویکی سامانه‌های پایگاه اطلاعاتی زیر را حمایت می‌کند:\n$1\nاگر متوجه سامانه پایگاه اطلاعاتی که سعی دارید از فهرست زیر استفاده کنید، نمی‌شوید، بنابراین دستورالعمل‌های مرتبط در بالا را برای فعال کردن پشتیبانی دنبال کنید.", "config-dbsupport-mysql": "*[{{int:version-db-mariadb-url}} MariaDB] مهم‌ترین هدف برای مدیاویکی است و بهترین پشتیبانی. مدیاویکی همچنین کار می‌کند با [{{int:version-db-mysql-url}} MariaDB] و [{{int:version-db-percona-url}} Percona Server] که با MariaDB سازگار هستند.([https://www.php.net/manual/en/mysqli.installation.php چگونه php را با MariaDB کامپایل کنیم])", "config-dbsupport-postgres": "* [{{int:version-db-postgres-url}} پستگرس‌کیوال] یک سامانه پایگاه اطلاعات متن‌باز پر‌طرفدار است که جایگزینی برای مای‌اس‌کیوال است. ([https://www.php.net/manual/en/pgsql.installation.php راهنمای تنظیم کردن پی‌اچ‌پی به همراه پستگرس‌کیوال])", "config-dbsupport-sqlite": "*[{{int:version-db-sqlite-url}} اس‌کیولایت] یک سامانه پایگاه اطلاعاتی کم حجمی است که بسیار خوب پشتیبانی شده‌است.\n([https://www.php.net/manual/en/pdo.installation.php چگونگی کامپایل پی‌اچ‌پی با اس‌کیولایت]، از PDO استفاده می‌کند)", - "config-dbsupport-oracle": "* [{{int:version-db-oracle-url}} Oracle] یک پایگاه اطلاعاتی کار تبلیغاتی است.\n([https://www.php.net/manual/en/oci8.installation.php How to compile PHP with OCI8 support])", - "config-dbsupport-mssql": "* [{{int:version-db-mssql-url}} Microsoft SQL Server] یک پایگاه اطلاعاتی موسسهٔ تبلیغاتی برای وینذوز است. ([https://www.php.net/manual/en/sqlsrv.installation.php How to compile PHP with SQLSRV support])", "config-header-mysql": "تنظیمات MariaDB/مای‌اس‌کیو‌ال", "config-header-postgres": "تنظیمات پست‌گر‌اس‌کیو‌ال", "config-header-sqlite": "تنظیمات اس‌کیو‌لایت", - "config-header-oracle": "تنظیمات اوراکل", - "config-header-mssql": "تنظیمات سرور مایکرپسافت اس‌کیو‌ال", "config-invalid-db-type": "نوع پایگاه اطلاعاتی نامعتبر", "config-missing-db-name": "شما باید یک مقدار برای \"نام {{int:config-db-name}}\" وارد کنید", "config-missing-db-host": "شما باید یک مقدار برای \"گروه {{int:config-db-host}}\" وارد کنید", - "config-missing-db-server-oracle": "شما باید یک مقدار برای \"تی‌ان‌اس {{int:config-db-host-oracle}}\" وارد کنید", - "config-invalid-db-server-oracle": "تی‌ان‌اس پایگاه اطلاعاتی $1 نامعتبر.\nیا از \"نام تی‌ان‌اس\" یا یک سلسله \"ارتباط آسان\" ([http://docs.oracle.com/cd/E11882_01/network.112/e10836/naming.htm Oracle Naming Methods]) استفاده کنید.", "config-invalid-db-name": "نام پایگاه اطلاعاتی نامعتبر \"$1\".\nفقط حروف اِی‌اس‌سی‌آی‌آی بزرگ (اِی-زدکوچک،اِی-زد بزرگ)،اعداد (۰-۹)،آندرلاین (_) و خط تیره کوتاه (-) استفاده کنید.", "config-invalid-db-prefix": "پیشوند پایگاه اطلاعاتی نامعتبر \"$1\".\nفقط حروف اِی‌اس‌سی‌آی‌آی بزرگ (اِی-زدکوچک،اِی-زد بزرگ)،اعداد (۰-۹)،آندرلاین (_) و خط تیره کوتاه (-) استفاده کنید.", "config-connection-error": "$1.\n\nمیزبان، نام کاربری و گذرواژه را بررسی کرده و دوباره امتحان کنید. اگر از «میزبان محلی» به عنوان میزبان پایگاه داده استفاده می‌کنید، استفاده از «۱۲۷.۰.۰.۱» را امتحان کنید (یا برعکس).", "config-invalid-schema": "طرح‌کلی برای مدیاویکی نامعتبر \"$1\".\nفقط حروف اِی‌اس‌سی‌آی‌آی بزرگ (اِی-زدکوچک،اِی-زد بزرگ)،اعداد (۰-۹)،آندرلاین (_) و خط تیره کوتاه (-) استفاده کنید.", - "config-db-sys-create-oracle": "نصب‌کننده تنها از استفادهٔ یک حساب اس‌وای‌اس‌دی‌بی‌اِی برای ایجاد یک حساب جدید حمایت می‌کند.", - "config-db-sys-user-exists-oracle": "حساب کاربری \"$1\" در‌حال‌حاضر وجود دارد.تنها اس‌وای‌اس‌دی‌بی‌اِی می‌تواند برای ایجاد یک حساب جدید استفاده شود!", "config-postgres-old": "پستگِرِاس‌کیو‌ال نسخهٔ $1 یا بالاتر لازم است. شما نسخهٔ $2 را دارید.", - "config-mssql-old": "سرور مایکروسافت اس‌کیو‌ال $1 یا اخیر آن لازم است. شما $2 را دارید.", "config-sqlite-name-help": "نامی را انتخاب کنید که ویکی شما را شناسایی می‌کند.\nاز فاصله‌ها یا خط‌های تیره کوتاه استفاده نکنید.\nاین برای نام پوشهٔ اطلاعات اس‌کیولایت استفاده خواهد‌شد.", "config-sqlite-parent-unwritable-group": "فهرست اطلاعات $1 نمی‌تواند ایجاد شود، چون فهرست منشأ $2 توسط سرور شبکه قابل نوشتن نیست.\nنصب کننده، کاربری را که سرور شبکه شما را اجرا می‌کند، مشخص کرده‌است.\nبرای ادامه دادن،فهرستی قابل نوشتن $3 توسط آن ایجاد کنید.\nدر یک سامانه یونیکس/لینوکس انجام می‌دهد:\n
cd $2\nmkdir $3\nchgrp $4 $3\nchmod g+w $3
", "config-sqlite-parent-unwritable-nogroup": "فهرست اطلاعات $1 نمی‌تواند ایجاد شود، چون فهرست منشأ $2 توسط کارساز شبکه قابل نوشتن نیست.\nنصب کننده، کاربری را که سرور شبکه شما را اجرا می‌کند، نتوانست مشخص کند.\nفهرست کلی قابل نوشتن $3 توسط آن (و دیگران!) برای ادامه دادن،ایجاد کنید.\nدر یک سامانه یونیکس/لینوکس انجام می‌دهد:\n
cd $2\nmkdir $3\nchmod a+w $3
", @@ -174,11 +158,6 @@ "config-mysql-engine": "موتور ذخیره سازی:", "config-mysql-innodb": "اینودی‌بی (پیشنهاد می‌شود)", "config-mysql-engine-help": "'''اینودی‌بی''' تقریباً همیشه بهترین گزینه است،زیرا پشتیبانی همزمان خوبی دارد.\n'''مای‌آی‌اس‌ای‌ام''' ممکن است در نصب‌های کاربر جداگانه یا فقط خواندنی سریع‌تر باشد.\nپایگاه‌های اطلاعاتی مای‌آی‌اس‌ای‌ام اغلب بیشتر از پایگاه‌های اطلاعاتی اینودی‌بی مستعد ازبین رفتن هستند.", - "config-mssql-auth": "نوع تأیید:", - "config-mssql-install-auth": "نوع تأییدی را که برای اتصال به پایگاه اطلاعاتی حین فرآیند نصب مورد استفاده قرار گیرد را انتخاب کنید.\nاگر \"{{int:config-mssql-windowsauth}}\" را انتخاب می‌کنید، اعتبارات هرآنچه کاربر وب سرور به عنوان آن مورد استفاده قرار می‌دهد مورد استفاده قرار خواهد گرفت.", - "config-mssql-web-auth": "نوع تأییدی را که کارساز وب به‌وسیلهٔ آن برای کارهای معمولی به پایگاه اطلاعاتی متصل خواهد شد را انتخاب کنید.\nاگر «{{int:config-mssql-windowsauth}}» را انتخاب می‌کنید، اعتبارات هرآنچه کاربر وب سرور به عنوان آن مورد استفاده قرار می‌دهد مورد استفاده قرار خواهد گرفت.", - "config-mssql-sqlauth": "تأیید سرور اس‌کیوال", - "config-mssql-windowsauth": "تأیید ویندوز", "config-site-name": "نام ویکی:", "config-site-name-help": "این در نوار عنوان مرورگر و در دیگر جاهای مختلف ظاهر خواهد‌شد.", "config-site-name-blank": "نام تارنما را وارد کنید.", diff --git a/includes/installer/i18n/fi.json b/includes/installer/i18n/fi.json index 8c338f85c7..b9ecf984a7 100644 --- a/includes/installer/i18n/fi.json +++ b/includes/installer/i18n/fi.json @@ -97,11 +97,9 @@ "config-db-type": "Tietokannan tyyppi:", "config-db-host": "Tietokantapalvelin:", "config-db-host-help": "Jos tietokantapalvelimesi sijaitsee eri palvelimella, syötä palvelimen nimi tai ip-osoite tähän.\n\nJos käytössäsi on ulkoinen palveluntarjoaja, pitäisi palvelimen nimen löytyä yrityksen ohjesivuilta.\n\nJos käytät MySQL:ää, ei palvelimen nimi \"localhost\" välttämättä toimi. Tässä tapauksessa koita käyttää osoitetta 127.0.0.1.\n\nJos käytät PostgreSQL:ää jätä tämä kenttä tyhjäksi.", - "config-db-host-oracle": "Tietokannan TNS:", "config-db-wiki-settings": "Identifioi tämä wiki", "config-db-name": "Tietokannan nimi (ei väliviivoja):", "config-db-name-help": "Valitse wikiäsi kuvaava nimi.\nNimessä ei saa olla välilyöntejä.\n\nMikäli et pysty itse hallitsemaan tietokantojasi, pyydä palveluntarjoajaasi luomaan tietokanta tai tee se palveluntarjoajasi hallintapaneelissa.", - "config-db-name-oracle": "Tietokannan rakenne:", "config-db-install-account": "Asennuksessa käytettävä käyttäjätili", "config-db-username": "Tietokannan käyttäjätunnus:", "config-db-password": "Tietokannan salasana:", @@ -120,37 +118,24 @@ "config-pg-test-error": "Tietokantaan $1 ei voida muodostaa yhteyttä: $2", "config-sqlite-dir": "SQLiten datahakemisto:", "config-sqlite-dir-help": "SQLite tallentaa kaiken sisällön yhteen tiedostoon.\n\nPalvelimen pitää pystyä kirjoittamaan tietoa hakemistoon asennuksen aikana.\n\nHakemiston ei tulisi olla nähtävissä www-selaimella. Siksi hakemisto on eri kuin missä PHP-tiedostot sijaitsevat.\n\nAsennusohjelma luo .htaccess-tiedoston, mutta jos sen luomisessa ilmenee ongelmia joku voi päästä käsiksi tietokantaasi. \nTietokannassa on kaikki sähköpostiosoitteet, salasanat, poistetut versiot ja kaikki muu tieto, joka ei näy wikissä.\n\nSuosittelemme tallentamaan tietokannan eri hakemistoon, esimerkiksi /var/lib/mediawiki/yourwiki.", - "config-oracle-def-ts": "Oletus taulukkotila:", - "config-oracle-temp-ts": "Väliaikainen taulukkotila:", "config-type-mysql": "MariaDB, MySQL tai yhteensopiva", "config-type-postgres": "PostgreSQL", "config-type-sqlite": "SQLite", - "config-type-oracle": "Oracle", - "config-type-mssql": "Microsoft SQL Server", "config-support-info": "MediaWiki tukee seuraavia tietokantajärjestelmiä:\n\n$1\n\nJos et näe tietokantajärjestelmää, jota yrität käyttää, listattuna alhaalla, seuraa yläpuolella olevia ohjeita tuen aktivoimiseksi.", "config-dbsupport-mysql": "* [{{int:version-db-mariadb-url}} MariaDB] on MediaWikin ensisijainen kohde ja se on myös parhaiten tuettu. MediaWiki voi myös käyttää [{{int:version-db-mysql-url}} MySQL]- sekä [{{int:version-db-percona-url}} Percona Server]-järjestelmiä, jotka ovat MariaDB-yhteensopivia. ([https://www.php.net/manual/en/mysqli.installation.php Miten käännetään PHP MySQL-tuella])", "config-dbsupport-postgres": "* [{{int:version-db-postgres-url}} PostgreSQL] on suosittu avoimen lähdekoodin tietokantajärjestelmä vaihtoehtona MySQL:lle. ([https://www.php.net/manual/en/pgsql.installation.php Kuinka käännetään PHP PostgreSQL-tuella])", "config-dbsupport-sqlite": "* [{{int:version-db-sqlite-url}} SQLite] on kevyt tietokantajärjestelmä, jota tuetaan hyvin. ([https://www.php.net/manual/en/pdo.installation.php Miten käännetään PHP SQLite-tuella], käyttää PDO:ta)", - "config-dbsupport-oracle": "* [{{int:version-db-oracle-url}} Oracle] on kaupallinen yritystietokanta. ([https://www.php.net/manual/en/oci8.installation.php Kuinka käännetään PHP OCI8-tuella])", - "config-dbsupport-mssql": "* [{{int:version-db-mssql-url}} Microsoft SQL Server] on kaupallinen yritystietokanta Windowsille. ([https://www.php.net/manual/en/sqlsrv.installation.php Miten käännetään PHP SQLSRV-tuella])", "config-header-mysql": "MariaDB/MySQL-asetukset", "config-header-postgres": "PostgreSQL-asetukset", "config-header-sqlite": "SQLite-asetukset", - "config-header-oracle": "Oracle-asetukset", - "config-header-mssql": "Microsoft SQL Server asetukset", "config-invalid-db-type": "Virheellinen tietokantatyyppi", "config-missing-db-name": "\"{{int:config-db-name}}\" on pakollinen.", "config-missing-db-host": "\"{{int:config-db-host}}\" on pakollinen.", - "config-missing-db-server-oracle": "\"{{int:config-db-host-oracle}}\" on pakollinen.", - "config-invalid-db-server-oracle": "Virheellinen tietokanta TNS \"$1\".\nKäytä joko \"TNS Name\"- tai \"Easy Connect\" -tekstiä\n([http://docs.oracle.com/cd/E11882_01/network.112/e10836/naming.htm Oracle metodien nimeäminen]).", "config-invalid-db-name": "”$1” ei kelpaa tietokannan nimeksi.\nKäytä ainoastaan kirjaimia (a-z, A-Z), numeroita (0-9), alaviivoja (_) ja tavuviivoja (-).", "config-invalid-db-prefix": "”$1” ei kelpaa tietokannan etuliitteeksi.\nKäytä ainoastaan kirjaimia (a-z, A-Z), numeroita (0-9), alaviivoja (_) ja tavuviivoja (-).", "config-connection-error": "$1.\n\nTarkista isäntä, käyttäjänimi, salasana ja yritä uudestaan. Jos käytät \"localhost\" tietokannan isäntänä, kokeile käyttää \"127.0.0.1\" sen sijaan (tai toisinpäin).", "config-invalid-schema": "Virheellinen skeema MediaWikille \"$1\".\nKäytä pelkkiä ASCII-kirjaimia (a-z, A-Z), numeroita (0-9) ja alaviivoja (_).", - "config-db-sys-create-oracle": "Asennusohjelma tukee ainoastaan SYSDBA-tunnuksen käyttämistä uuden tunnuksen luonnissa.", - "config-db-sys-user-exists-oracle": "Käyttäjätunnus \"$1\" on jo olemassa. SYSDBA:ta voidaan käyttää vain uuden tunnuksen luontiin!", "config-postgres-old": "MediaWiki tarvitsee PostgreSQL:n version $1 tai uudemman. Nykyinen versio on $2.", - "config-mssql-old": "Vaaditaan Microsoft SQL Server $1 tai uudempi. Sinulla on käytössä $2.", "config-sqlite-name-help": "Valitse nimi, joka yksilöi tämän wikin.\nÄlä käytä välilyöntejä tai viivoja.\nNimeä käytetään SQLite-tietokannan tiedostonimessä.", "config-sqlite-mkdir-error": "Virhe luodessa datakansiota \"$1\".\nTarkista sijainti ja yritä uudelleen", "config-sqlite-dir-unwritable": "Hakemistoon ”$1” kirjoittaminen epäonnistui.\nMuuta hakemiston käyttöoikeuksia siten, että palvelinohjelmisto voi kirjoittaa siihen ja yritä uudelleen.", @@ -171,11 +156,6 @@ "config-db-web-no-create-privs": "Tilillä jota käytetään asennuksessa ei ole oikeuksia luoda uutta tiliä.\nTähän määriteltävä tili täytyy olla jo olemassa.", "config-mysql-engine": "Tallennusmoottori", "config-mysql-innodb": "InnoDB (suositeltu)", - "config-mssql-auth": "Varmennuksen tyyppi:", - "config-mssql-install-auth": "Valitse varmennuksen tyyppi, jota käytetään yhdistäessä tietokantaan asennuksen aikana.\nJos valitset \"{{int:config-mssql-windowsauth}}\", käytetään verkkopalvelimen käyttäjän kirjautumistietoja.", - "config-mssql-web-auth": "Valitse varmennuksen tyyppi, jota verkkopalvelin käyttää yhdistäessään tietokantapalvelimeen wikin tavallisen toiminnan aikana.\nJos valitset \"{{int:config-mssql-windowsauth}}\", käytetään verkkopalvelimen käyttäjän kirjautumistietoja.", - "config-mssql-sqlauth": "SQL Server varmennus", - "config-mssql-windowsauth": "Windows-varmennus", "config-site-name": "Wikin nimi:", "config-site-name-help": "Tämä näkyy selaimen otsikkona ja muissa kohdissa.", "config-site-name-blank": "Kirjoita sivuston nimi.", diff --git a/includes/installer/i18n/fr.json b/includes/installer/i18n/fr.json index b07c83e493..b57ed3d2a4 100644 --- a/includes/installer/i18n/fr.json +++ b/includes/installer/i18n/fr.json @@ -73,20 +73,20 @@ "config-welcome": "=== Vérifications liées à l’environnement ===\nDes vérifications de base vont maintenant être effectuées pour voir si cet environnement est adapté à l’installation de MediaWiki.\nRappelez-vous d’inclure ces informations si vous recherchez de l’aide sur la manière de terminer l’installation.", "config-welcome-section-copyright": "=== Droit d’auteur et conditions ===\n\n$1\n\nCe programme est un logiciel libre : vous pouvez le redistribuer ou le modifier selon les termes de la Licence Publique Générale GNU telle que publiée par la Free Software Foundation (version 2 de la Licence, ou, à votre choix, toute version ultérieure).\n\nCe programme est distribué dans l’espoir qu’il sera utile, mais '''sans aucune garantie''' : sans même les garanties implicites de '''commercialisabilité''' ou d’'''adéquation à un usage particulier'''.\nVoir la Licence Publique Générale GNU pour plus de détails.\n\nVous devriez avoir reçu [$2 une copie de la Licence Publique Générale GNU] avec ce programme ; dans le cas contraire, écrivez à la Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. ou [https://www.gnu.org/copyleft/gpl.html lisez-la en ligne].", "config-sidebar": "* [https://www.mediawiki.org Accueil MediaWiki]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents Guide de l’utilisateur]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Contents Guide de l’administrateur]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ FAQ]", - "config-sidebar-readme": "Me lire", + "config-sidebar-readme": "Lisez-moi", "config-sidebar-relnotes": "Notes de version", - "config-sidebar-license": "Copie", + "config-sidebar-license": "Droit de copie", "config-sidebar-upgrade": "Mise à jour", "config-env-good": "L’environnement a été vérifié.\nVous pouvez installer MediaWiki.", "config-env-bad": "L’environnement a été vérifié.\nVous ne pouvez pas installer MediaWiki.", "config-env-php": "PHP $1 est installé.", "config-env-hhvm": "HHVM $1 est installé.", - "config-unicode-using-intl": "Utilisation de [https://php.net/manual/en/book.intl.php extension intl de PHP] pour la normalisation Unicode.", - "config-unicode-pure-php-warning": "Attention : L’[https://php.net/manual/en/book.intl.php extension intl de PHP] n’est pas disponible pour la normalisation d’Unicode, retour à la version lente implémentée en PHP seulement.\nSi votre site web sera très fréquenté, vous devriez lire ceci : [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations ''Unicode normalization''] (en anglais).", + "config-unicode-using-intl": "Utilisation de l’[https://php.net/manual/en/book.intl.php extension intl de PHP] pour la normalisation Unicode.", + "config-unicode-pure-php-warning": "Attention : l’[https://php.net/manual/en/book.intl.php extension intl de PHP] n’est pas disponible pour la normalisation d’Unicode, retour à la version lente implémentée en PHP seulement.\nSi votre site web sera très fréquenté, vous devriez lire ceci : [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations ''Unicode normalization''] (en anglais).", "config-unicode-update-warning": "Attention : la version installée du normalisateur Unicode utilise une ancienne version de la bibliothèque logicielle du [http://site.icu-project.org/ ''Projet ICU''].\nVous devriez faire une [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations mise à jour] si vous êtes concerné par l’usage d’Unicode.", "config-no-db": "Impossible de trouver un pilote de base de données approprié ! Vous devez installer un pilote de base de données pour PHP. {{PLURAL:$2|Le type suivant|Les types suivants}} de bases de données {{PLURAL:$2|est reconnu|sont reconnus}} : $1.\n\nSi vous avez compilé PHP vous-même, reconfigurez-le avec un client de base de données activé, par exemple en utilisant ./configure --with-mysqli. \nSi vous avez installé PHP depuis un paquet Debian ou Ubuntu, alors vous devrez aussi installer, par exemple, le paquet php-mysql.", - "config-outdated-sqlite": "Attention : vous avez SQLite $2, qui est inférieur à la version minimale requise $1. SQLite sera indisponible.", - "config-no-fts3": "Attention : SQLite est compilé sans le [//sqlite.org/fts3.html module FTS3] ; les fonctions de recherche ne seront pas disponibles sur ce moteur.", + "config-outdated-sqlite": "Attention : vous avez SQLite $2, qui est inférieur à la version minimale requise $1. SQLite sera indisponible.", + "config-no-fts3": "Attention : SQLite est compilé sans le [//sqlite.org/fts3.html module FTS3] ; les fonctions de recherche ne seront pas disponibles sur ce moteur.", "config-pcre-old": "Erreur fatale : PCRE $1 ou ultérieur est nécessaire.\nVotre binaire PHP est lié avec PCRE $2.\n[https://www.mediawiki.org/wiki/Manual:Errors_and_symptoms/Plus d’information sur PCRE].", "config-pcre-no-utf8": "Erreur fatale : le module PCRE de PHP semble être compilé sans la prise en charge de PCRE_UTF8.\nMediaWiki a besoin de la gestion d’UTF-8 pour fonctionner correctement.", "config-memory-raised": "Le paramètre memory_limit de PHP était à $1, porté à $2.", @@ -94,82 +94,65 @@ "config-apc": "[https://www.php.net/apc APC] est installé", "config-apcu": "[https://www.php.net/apcu APCu] est installé", "config-wincache": "[https://www.iis.net/downloads/microsoft/wincache-extension WinCache] est installé", - "config-no-cache-apcu": "Attention : impossible de trouver [https://www.php.net/apcu APCu] ou [https://www.iis.net/downloads/microsoft/wincache-extension WinCache].\nLa mise en cache des objets n’est pas activée.", - "config-mod-security": "Attention : votre serveur web a activé [https://modsecurity.org/ mod_security]/mod_security2 . Dans plusieurs configurations communes cela pose des problèmes à MediaWiki ou à d’autres applications qui permettent aux utilisateurs de publier un contenu quelconque. \nSi possible, ceci devrait être désactivé. Sinon, reportez-vous à [https://modsecurity.org/documentation/ la documentation de mod_security] ou contactez l’assistance de votre hébergeur si vous rencontrez des erreurs aléatoires.", + "config-no-cache-apcu": "Attention : impossible de trouver [https://www.php.net/apcu APCu] ou [https://www.iis.net/downloads/microsoft/wincache-extension WinCache].\nLa mise en cache des objets n’est pas activée.", + "config-mod-security": "Attention : votre serveur web a activé [https://modsecurity.org/ mod_security] ou mod_security2. Dans plusieurs configurations communes cela pose des problèmes à MediaWiki ou à d’autres applications qui permettent aux utilisateurs de publier un contenu quelconque. \nSi possible, ceci devrait être désactivé. Sinon, reportez-vous à la [https://modsecurity.org/documentation/ documentation de mod_security] ou contactez l’assistance de votre hébergeur si vous rencontrez des erreurs aléatoires.", "config-diff3-bad": "L’utilitaire de comparaison de texte GNU diff3 est introuvable. Vous pouvez l’ignorer pour le moment, mais cela peut provoquer des conflits de modification plus souvent.", "config-git": "Logiciel de contrôle de version Git trouvé : $1.", "config-git-bad": "Logiciel de contrôle de version Git non trouvé. Vous pouvez l’ignorer pour le moment. Notez que Special:Version n’affichera pas les hachages de validation.", "config-imagemagick": "ImageMagick trouvé : $1.\nLa génération de vignettes d’images sera activée si vous activez les téléversements.", - "config-gd": "La bibliothèque graphique GD intégrée a été trouvée.\nLa miniaturisation d'images sera activée si vous activez le téléversement de fichiers.", + "config-gd": "La bibliothèque graphique GD intégrée a été trouvée.\nLa miniaturisation d’images sera activée si vous activez le téléversement de fichiers.", "config-no-scaling": "Impossible de trouver la bibliothèque GD ou ImageMagick.\nLa miniaturisation d’images sera désactivée.", "config-no-uri": "Erreur : impossible de déterminer l’URI du script actuel.\nInstallation interrompue.", - "config-no-cli-uri": "Attention : Aucun --scriptpath n’a été spécifié ; $1 sera utilisé par défaut.", + "config-no-cli-uri": "Attention : aucun --scriptpath n’a été spécifié ; $1 sera utilisé par défaut.", "config-using-server": "Utilisation du nom de serveur « $1 ».", - "config-using-uri": "Utilisation de l'URL de serveur \"$1$2\".", - "config-uploads-not-safe": "Attention : Votre répertoire par défaut pour les téléversements, $1, est vulnérable, car il peut exécuter n’importe quel script.\nBien que MediaWiki vérifie tous les fichiers téléversés, il est fortement recommandé de [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Security#Upload_security fermer cette faille de sécurité] (texte en anglais) avant d’activer les téléversements.", - "config-no-cli-uploads-check": "'''Attention:''' Votre répertoire par défaut pour les imports($1) n'est pas contrôlé concernant la vulnérabilité d'exécution de scripts arbitraires lors de l'installation CLI.", - "config-brokenlibxml": "Votre système utilise une combinaison de versions de PHP et libxml2 qui est boguée et peut engendrer des corruptions cachées de données dans MediaWiki et d’autres applications web.\nVeuillez mettre à jour votre système vers libxml2 2.7.3 ou plus récent ([https://bugs.php.net/bug.php?id=45996 bogue déposé auprès de PHP]).\nInstallation interrompue.", - "config-suhosin-max-value-length": "Suhosin est installé et limite la longueur du paramètre GET à $1 octets.\nLe composant ResourceLoader de MediaWiki va répondre en respectant cette limite, mais ses performances seront dégradées. Si possible, vous devriez définir suhosin.get.max_value_length à 1024 ou plus dans le fichier php.ini, et fixer $wgResourceLoaderMaxQueryLength à la même valeur dans LocalSettings.php.", - "config-using-32bit": "Attention: votre système semble utiliser les entiers sur 32 bits. Ceci n'est [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:32-bit pas recommandé].", - "config-db-type": "Type de base de données :", - "config-db-host": "Nom d’hôte de la base de données :", - "config-db-host-help": "Si votre serveur de base de données est sur un serveur différent, saisissez ici son nom d’hôte ou son adresse IP.\n\nSi vous utilisez un hébergement mutualisé, votre hébergeur doit vous avoir fourni le nom d’hôte correct dans sa documentation.\n\nSi vous utilisez MySQL, « localhost » peut ne pas fonctionner comme nom de serveur. S’il ne fonctionne pas, essayez « 127.0.0.1 » comme adresse IP locale.\n\nSi vous utilisez PostgreSQL, laissez ce champ vide pour vous connecter via un socket Unix.", - "config-db-host-oracle": "Nom TNS de la base de données :", - "config-db-host-oracle-help": "Entrez un [http://download.oracle.com/docs/cd/B28359_01/network.111/b28317/tnsnames.htm nom de connexion locale] valide ; un fichier tnsnames.ora doit être visible par cette installation.
Si vous utilisez les bibliothèques clientes version 10g ou plus récentes, vous pouvez également utiliser la méthode de nommage [http://download.oracle.com/docs/cd/E11882_01/network.112/e10836/naming.htm Easy Connect].", + "config-using-uri": "Utilisation de l’URL de serveur « $1$2 ».", + "config-uploads-not-safe": "Attention : votre répertoire par défaut pour les téléversements, $1, est vulnérable, car il peut exécuter n’importe quel script.\nBien que MediaWiki vérifie tous les fichiers téléversés, il est fortement recommandé de [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Security#Upload_security fermer cette faille de sécurité] (texte en anglais) avant d’activer les téléversements.", + "config-no-cli-uploads-check": "'''Attention :''' votre répertoire par défaut pour les imports ($1) n’est pas contrôlé concernant la vulnérabilité d’exécution de scripts arbitraires lors de l’installation CLI.", + "config-brokenlibxml": "Votre système utilise une combinaison de versions de PHP et libxml2 qui est boguée et peut engendrer des corruptions cachées de données dans MediaWiki et d’autres applications web.\nVeuillez mettre à jour votre système vers libxml2 2.7.3 ou plus récent ([https://bugs.php.net/bug.php?id=45996 anomalie signalée auprès de PHP]).\nInstallation interrompue.", + "config-suhosin-max-value-length": "Suhosin est installé et limite la longueur de paramètre GET à $1 octets.\nMediaWiki exige que suhosin.get.max_value_length vaille au moins $2. Désactiver ce paramètre, ou augmenter sa valeur à $3 dans php.ini.", + "config-using-32bit": "Attention : votre système semble utiliser les entiers sur 32 bits. Ceci n’est [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:32-bit pas recommandé].", + "config-db-type": "Type de base de données :", + "config-db-host": "Nom d’hôte de la base de données :", + "config-db-host-help": "Si votre serveur de base de données est sur un serveur différent, saisissez ici son nom d’hôte ou son adresse IP.\n\nSi vous utilisez un hébergement mutualisé, votre hébergeur doit vous avoir fourni le nom d’hôte correct dans sa documentation.\n\nSi vous utilisez MySQL, « localhost » peut ne pas fonctionner comme nom de serveur. S’il ne fonctionne pas, essayez « 127.0.0.1 » comme adresse IP locale.\n\nSi vous utilisez PostgreSQL, laissez ce champ vide pour vous connecter via un socket Unix.", "config-db-wiki-settings": "Identifier ce wiki", - "config-db-name": "Nom de la base de données (sans tirets):", - "config-db-name-help": "Choisissez un nom qui identifie votre wiki.\nIl ne doit pas contenir d'espaces.\n\nSi vous utilisez un hébergement web partagé, votre hébergeur vous fournira un nom spécifique de base de données à utiliser, ou bien vous permet de créer des bases de données via un panneau de contrôle.", - "config-db-name-oracle": "Schéma de base de données :", - "config-db-account-oracle-warn": "Il existe trois scénarios pris en charge pour l’installation d'Oracle comme backend de base de données:\n\nSi vous souhaitez créer un compte de base de données dans le cadre de la procédure d’installation, veuillez fournir un compte avec le rôle de SYSDBA comme compte de base de données pour l’installation et spécifier les informations d’identification souhaitées pour le compte d'accès au web, sinon vous pouvez créer le compte d’accès web manuellement et fournir uniquement ce compte (si elle a exigé des autorisations nécessaires pour créer les objets de schéma) ou fournir deux comptes différents, l’un avec les privilèges pour créer et l'autre restreint, pour l’accès web.\n\nUn script pour créer un compte avec des privilèges requis peut être trouvé dans le répertoire « entretien/oracle/ » de cette installation. N’oubliez pas que le fait de l’utilisation d’un compte limité désactive toutes les fonctionnalités d’entretien avec le compte par défaut.", - "config-db-install-account": "Compte d'utilisateur pour l'installation", - "config-db-username": "Nom d’utilisateur de la base de données :", - "config-db-password": "Mot de passe de la base de données :", - "config-db-install-username": "Entrez le nom d’utilisateur qui sera utilisé pour se connecter à la base de données pendant le processus d'installation. Il ne s’agit pas du nom d’utilisateur du compte MediaWiki, mais du nom d’utilisateur pour votre base de données.", - "config-db-install-password": "Entrez le mot de passe qui sera utilisé pour se connecter à la base de données pendant le processus d'installation. Il ne s’agit pas du mot de passe du compte MediaWiki, mais du mot de passe pour votre base de données.", - "config-db-install-help": "Entrez le nom d'utilisateur et le mot de passe qui seront utilisés pour se connecter à la base de données pendant le processus d'installation.", - "config-db-account-lock": "Utiliser le même nom d'utilisateur et le même mot de passe pendant le fonctionnement habituel", - "config-db-wiki-account": "Compte d'utilisateur pour le fonctionnement habituel", - "config-db-wiki-help": "Entrez le nom d'utilisateur et le mot de passe qui seront utilisés pour se connecter à la base de données pendant le fonctionnement habituel du wiki.\nSi le compte n'existe pas, et le compte d'installation dispose de privilèges suffisants, ce compte d'utilisateur sera créé avec les privilèges minimum requis pour faire fonctionner le wiki.", - "config-db-prefix": "Préfixe des tables de la base de données (sans tirets) :", - "config-db-prefix-help": "Si vous avez besoin de partager une base de données entre plusieurs wikis, ou entre MediaWiki et une autre application Web, vous pouvez choisir d'ajouter un préfixe à tous les noms de table pour éviter les conflits.\nNe pas utiliser d'espaces.\n\nCe champ est généralement laissé vide.", + "config-db-name": "Nom de la base de données (sans tirets) :", + "config-db-name-help": "Choisissez un nom qui identifie votre wiki.\nIl ne doit pas contenir d’espaces.\n\nSi vous utilisez un hébergement web partagé, votre hébergeur vous fournira un nom spécifique de base de données à utiliser, ou bien vous permet de créer des bases de données via un panneau de contrôle.", + "config-db-install-account": "Compte d’utilisateur pour l’installation", + "config-db-username": "Nom d’utilisateur de la base de données :", + "config-db-password": "Mot de passe de la base de données :", + "config-db-install-username": "Entrez le nom d’utilisateur qui sera utilisé pour se connecter à la base de données pendant le processus d’installation. Il ne s’agit pas du nom d’utilisateur du compte MediaWiki (serveur web, PHP) sur le système, mais du nom d’utilisateur dans votre base de données SQL.", + "config-db-install-password": "Entrez le mot de passe qui sera utilisé pour se connecter à la base de données pendant le processus d’installation. Il ne s’agit pas du mot de passe du compte MediaWiki (serveur web, PHP) sur le système, mais du mot de passe dans votre base de données SQL.", + "config-db-install-help": "Entrez le nom d’utilisateur et le mot de passe qui seront utilisés pour se connecter à la base de données pendant le processus d’installation.", + "config-db-account-lock": "Utiliser le même nom d’utilisateur et le même mot de passe pour les opérations communes", + "config-db-wiki-account": "Compte d’utilisateur pour les opérations communes", + "config-db-wiki-help": "Entrez le nom d’utilisateur et le mot de passe qui seront utilisés pour se connecter à la base de données pendant le fonctionnement habituel du wiki.\nSi le compte n’existe pas, et le compte d’installation dispose de privilèges suffisants, ce compte d’utilisateur sera créé avec les privilèges minimum requis pour faire fonctionner le wiki.", + "config-db-prefix": "Préfixe des tables de la base de données (sans tirets) :", + "config-db-prefix-help": "Si vous avez besoin de partager une base de données entre plusieurs wikis, ou entre MediaWiki et une autre application Web, vous pouvez choisir d’ajouter un préfixe à tous les noms de table pour éviter les conflits.\nNe pas utiliser d’espaces.\n\nCe champ est généralement laissé vide.", "config-mysql-old": "MySQL $1 ou version ultérieure est requis. Vous avez $2.", - "config-db-port": "Port de la base de données :", - "config-db-schema": "Schéma pour MediaWiki (sans tirets) :", - "config-db-schema-help": "Ce schéma est généralement correct.\nNe le changez que si vous êtes sûr que c'est nécessaire.", - "config-pg-test-error": "Impossible de se connecter à la base de données '''$1''' : $2", - "config-sqlite-dir": "Dossier des données SQLite :", - "config-sqlite-dir-help": "SQLite stocke toutes les données dans un fichier unique.\n\nLe répertoire que vous fournissez doit être accessible en écriture par le serveur lors de l'installation.\n\nIl '''ne faut pas''' qu'il soit accessible via le web, c'est pourquoi il n'est pas à l'endroit où sont vos fichiers PHP.\n\nL'installateur écrira un fichier .htaccess en même temps, mais s'il y a échec, quelqu'un peut accéder à votre base de données.\nCela comprend les données des utilisateurs (adresses de courriel, mots de passe hachés) ainsi que des révisions supprimées et d'autres données confidentielles du wiki.\n\nEnvisagez de placer la base de données ailleurs, par exemple dans /var/lib/mediawiki/yourwiki.", - "config-oracle-def-ts": "Espace de stockage (''tablespace'') par défaut :", - "config-oracle-temp-ts": "Espace de stockage (''tablespace'') temporaire :", + "config-db-port": "Port de la base de données :", + "config-db-schema": "Schéma pour MediaWiki (sans tirets) :", + "config-db-schema-help": "Ce schéma est généralement correct.\nNe le changez que si vous êtes sûr que c’est nécessaire.", + "config-pg-test-error": "Impossible de se connecter à la base de données '''$1''' : $2", + "config-sqlite-dir": "Dossier des données SQLite :", + "config-sqlite-dir-help": "SQLite stocke toutes les données dans un fichier unique.\n\nLe répertoire que vous fournissez doit être accessible en écriture par le serveur lors de l’installation.\n\nIl '''ne faut pas''' qu’il soit accessible via le web, c’est pourquoi il n’est pas à l’endroit où sont vos fichiers PHP.\n\nL’installateur écrira un fichier .htaccess en même temps, mais s’il y a échec, quelqu’un peut accéder à votre base de données.\nCela comprend les données des utilisateurs (adresses de courriel, mots de passe hachés) ainsi que des révisions supprimées et d’autres données confidentielles du wiki.\n\nEnvisagez de placer la base de données ailleurs, par exemple dans /var/lib/mediawiki/yourwiki.", "config-type-mysql": "MariaDB, MySQL , ou compatible", "config-type-postgres": "PostgreSQL", "config-type-sqlite": "SQLite", - "config-type-oracle": "Oracle", - "config-type-mssql": "Microsoft SQL Server", "config-support-info": "MediaWiki prend en charge ces systèmes de bases de données :\n\n$1\n\nSi vous ne voyez pas le système de base de données que vous essayez d’utiliser ci-dessous, alors suivez les instructions ci-dessus (voir liens) pour activer la prise en charge.", "config-dbsupport-mysql": "* [{{int:version-db-mariadb-url}} MariaDB] est le premier choix pour MediaWiki et est le mieux pris en charge. MediaWiki fonctionne aussi avec [{{int:version-db-mysql-url}} MySQL] et [{{int:version-db-percona-url}} Percona Server], qui sont compatibles avec MariaDB. ([https://www.php.net/manual/en/mysqli.installation.php Comment compiler PHP avec la prise en charge de MySQL])", "config-dbsupport-postgres": "* [{{int:version-db-postgres-url}} PostgreSQL] est un système de base de données populaire en ''source ouverte'' qui peut être une alternative à MySQL ([https://www.php.net/manual/en/pgsql.installation.php Comment compiler PHP avec la prise en charge de PostgreSQL]).", "config-dbsupport-sqlite": "* [{{int:version-db-sqlite-url}} SQLite] est un système de base de données léger bien pris en charge ([https://www.php.net/manual/en/pdo.installation.php Comment compiler PHP avec la prise en charge de SQLite], en utilisant PDO)", - "config-dbsupport-oracle": "* [{{int:version-db-oracle-url}} Oracle] est un système commercial de gestion de base de données d’entreprise. ([https://www.php.net/manual/en/oci8.installation.php Comment compiler PHP avec la prise en charge d’OCI8])", - "config-dbsupport-mssql": "* [{{int:version-db-mssql-url}} Microsoft SQL Server] est une base de données commerciale d’entreprise pour Windows. ([https://www.php.net/manual/en/sqlsrv.installation.php Comment compiler PHP avec la prise en charge de SQLSRV])", "config-header-mysql": "Paramètres de MariaDB/MySQL", "config-header-postgres": "Paramètres de PostgreSQL", "config-header-sqlite": "Paramètres de SQLite", - "config-header-oracle": "Paramètres d’Oracle", - "config-header-mssql": "Paramètres de Microsoft SQL Server", "config-invalid-db-type": "Type de base de données non valide", "config-missing-db-name": "Vous devez entrer une valeur pour « {{int:config-db-name}} ».", "config-missing-db-host": "Vous devez entrer une valeur pour « {{int:config-db-host}} ».", - "config-missing-db-server-oracle": "Vous devez entrer une valeur pour « {{int:config-db-host-oracle}} ».", - "config-invalid-db-server-oracle": "Le nom TNS de la base de données (« $1 ») est invalide.\nUtilisez uniquement la chaîne \"TNS Name\" ou \"Easy Connect\" ([http://docs.oracle.com/cd/E11882_01/network.112/e10836/naming.htm Méthodes de nommage Oracle])", "config-invalid-db-name": "Nom de la base de données invalide (« $1 »).\nUtiliser seulement les lettres ASCII (a-z, A-Z), les chiffres (0-9), les caractères de soulignement (_) et les tirets (-).", "config-invalid-db-prefix": "Préfixe de la base de données non valide « $1 ».\nUtiliser seulement les lettres ASCII (a-z, A-Z), les chiffres (0-9), les caractères de soulignement (_) et les tirets (-).", - "config-connection-error": "$1.\n\nVérifier le nom d’hôte, le nom d’utilisateur et le mot de passe ci-dessous puis réessayer. Si vous utilisez « localhost » comme hôte de base de données, essayez d’utiliser « 127.0.0.1 » à la place (ou inversement).", + "config-connection-error": "$1.\n\nVérifier le nom d’hôte, le nom d’utilisateur et le mot de passe ci-dessous puis réessayer. Si vous utilisez « localhost » comme hôte de base de données, essayez d’utiliser « 127.0.0.1 » à la place (ou inversement).", "config-invalid-schema": "Schéma invalide pour MediaWiki « $1 ».\nUtiliser seulement les lettres ASCII (a-z, A-Z), les chiffres (0-9) et les caractères de soulignement (_).", - "config-db-sys-create-oracle": "L'installateur ne reconnaît que le compte SYSDBA lors de la création d'un nouveau compte.", - "config-db-sys-user-exists-oracle": "Le compte « $1 » existe déjà. Seul SYSDBA peut être utilisé pour créer un nouveau compte.", "config-postgres-old": "PostgreSQL $1 ou version ultérieure est requis. Vous avez $2.", - "config-mssql-old": "Microsoft SQL Server version $1 ou supérieur est requis. Vous avez la version $2.", "config-sqlite-name-help": "Choisir un nom qui identifie votre wiki.\nNe pas utiliser d'espaces ni de traits d'union.\nIl sera utilisé pour le fichier de données SQLite.", "config-sqlite-parent-unwritable-group": "Impossible de créer le répertoire de données $1, parce que le répertoire parent $2 n'est pas accessible en écriture par le serveur Web.\n\nL'installateur a détecté sous quel nom d'utilisateur, le serveur web est actif.\nRendre le répertoire $3 accessible en écriture pour continuer.\nSur un système UNIX/Linux, saisir :\n\n
cd $2\nmkdir $3\nchgrp $4 $3\nchmod g+w $3
", "config-sqlite-parent-unwritable-nogroup": "Impossible de créer le répertoire de données $1, parce que le répertoire parent $2 n'est pas accessible en écriture par le serveur Web.\n\nL'installateur n'a pas pu déterminer le nom de l'utilisateur sous lequel le serveur s'exécute.\nRendre le répertoire $3 globalement accessible en écriture pour continuer.\nSur un système UNIX/Linux, saisir :\n\n
cd $2\nmkdir $3\nchmod a+w $3
", @@ -180,7 +163,7 @@ "config-sqlite-cant-create-db": "Impossible de créer le fichier de base de données $1.", "config-sqlite-fts3-downgrade": "PHP n’a pas trouvé la prise en charge FTS3, les tables sont restreintes.", "config-can-upgrade": "Il y a des tables MediaWiki dans cette base de données.\nPour les mettre au niveau de MediaWiki $1, cliquez sur '''Continuer'''.", - "config-upgrade-error": "Une erreur est survenue durant la mise à jour des tables MédiaWiki de votre base de données.\n\nPour plus d'informations voir le journal ci-dessus, pour réessayer, cliquez sur Continuer.", + "config-upgrade-error": "Une erreur est survenue durant la mise à jour des tables MediaWiki de votre base de données.\n\nPour plus d'informations voir le journal ci-dessus, pour réessayer, cliquez sur Continuer.", "config-upgrade-done": "Mise à jour terminée.\n\nVous pouvez maintenant [$1 commencer à utiliser votre wiki].\n\nSi vous souhaitez régénérer votre fichier LocalSettings.php, cliquez sur le bouton ci-dessous.\nCeci '''n'est pas recommandé''' sauf si vous rencontrez des problèmes avec votre wiki.", "config-upgrade-done-no-regenerate": "Mise à jour terminée.\n\nVous pouvez maintenant [$1 commencer à utiliser votre wiki].", "config-regenerate": "Regénérer LocalSettings.php →", @@ -194,11 +177,6 @@ "config-mysql-engine": "Moteur de stockage :", "config-mysql-innodb": "InnoDB (recommandé)", "config-mysql-engine-help": "InnoDB est presque toujours la meilleure option, car il prend bien en charge les accès concurrents.\n\nMyISAM peut être plus rapide dans les installations monoposte ou en lecture seule.\nLes bases de données MyISAM ont tendance à se corrompre plus souvent que les bases d’InnoDB.", - "config-mssql-auth": "Type d’authentification :", - "config-mssql-install-auth": "Sélectionner le type d’authentification qui sera utilisé pour se connecter à la base de données pendant le processus d’installation.\nSi vous sélectionnez « {{int:config-mssql-windowsauth}} », les informations d’identification de l’utilisateur faisant tourner le serveur seront utilisées.", - "config-mssql-web-auth": "Sélectionner le type d’authentification que le serveur web utilisera pour se connecter au serveur de base de données lors des opérations habituelles du wiki.\nSi vous sélectionnez « {{int:config-mssql-windowsauth}} », les informations d’identification de l’utilisateur sous lequel tourne le serveur web seront utilisées.", - "config-mssql-sqlauth": "Authentification de SQL Server", - "config-mssql-windowsauth": "Authentification Windows", "config-site-name": "Nom du wiki :", "config-site-name-help": "Ceci apparaîtra dans la barre de titre du navigateur et en divers autres endroits.", "config-site-name-blank": "Entrez un nom de site.", @@ -207,7 +185,7 @@ "config-ns-site-name": "Même nom que le wiki : $1", "config-ns-other": "Autre (préciser)", "config-ns-other-default": "MonWiki", - "config-project-namespace-help": "Suivant l’exemple de Wikipédia, plusieurs wikis gardent leurs pages de politique séparées de leurs pages de contenu, dans un '''espace de noms de niveau projet''' propre.\nTous les titres de page de cet espace de noms commence par un préfixe défini, que vous pouvez spécifier ici.\nTraditionnellement, ce préfixe est dérivé du nom du wiki, et ne peut contenir de caractères de ponctuation tels que « # » ou « : ».", + "config-project-namespace-help": "Suivant l’exemple de Wikipédia, plusieurs wikis gardent leurs pages de politique séparées de leurs pages de contenu, dans un '''espace de noms de niveau projet''' propre.\nTous les titres de page de cet espace de noms commence par un préfixe défini, que vous pouvez spécifier ici.\nTraditionnellement, ce préfixe est dérivé du nom du wiki, et ne peut contenir de caractères de ponctuation tels que « # » ou « : ».", "config-ns-invalid": "L'espace de noms spécifié « $1 » n'est pas valide.\nSpécifiez un espace de noms différent pour le projet.", "config-ns-conflict": "L'espace de noms spécifié « $1 » est en conflit avec un espace de noms par défaut de MediaWiki.\nChoisir un autre espace de noms pour le projet.", "config-admin-box": "Compte administrateur", @@ -290,7 +268,7 @@ "config-skins": "Habillages", "config-skins-help": "Les habillages listés ci-dessous ont été détectés dans votre répertoire ./skins. Vous devez en activer au moins un, et choisir celui par défaut.", "config-skins-use-as-default": "Utiliser cet habillage par défaut", - "config-skins-missing": "Aucun habillage trouvé ; MédiaWiki utilisera un habillage de secours jusqu’à ce que vous en installiez un approprié.", + "config-skins-missing": "Aucun habillage trouvé ; MediaWiki utilisera un habillage de secours jusqu’à ce que vous en installiez un approprié.", "config-skins-must-enable-some": "Vous devez choisir au moins un habillage à activer.", "config-skins-must-enable-default": "L’habillage choisi par défaut doit être activé.", "config-install-alreadydone": "'''Attention''': Vous semblez avoir déjà installé MediaWiki et tentez de l'installer à nouveau.\nS'il vous plaît, allez à la page suivante.", @@ -331,8 +309,8 @@ "config-install-extension-tables": "Création de tables pour les extensions activées", "config-install-mainpage-failed": "Impossible d’insérer la page principale : $1", "config-install-done": "Félicitations!\nVous avez installé MediaWiki.\n\nLe programme d'installation a généré un fichier LocalSettings.php. Il contient tous les paramètres de votre configuration.\n\nVous devrez le télécharger et le mettre à la racine de votre installation wiki (dans le même répertoire que index.php). Le téléchargement devrait démarrer automatiquement.\n\nSi le téléchargement n'a pas été proposé, ou que vous l'avez annulé, vous pouvez redémarrer le téléchargement en cliquant ce lien :\n\n$3\n\nNote : Si vous ne le faites pas maintenant, ce fichier de configuration généré ne sera pas disponible plus tard si vous quittez l'installation sans le télécharger.\n\nLorsque c'est fait, vous pouvez [$2 accéder à votre wiki] .", - "config-install-done-path": "Félicitations !\nVous avez installé MédiaWiki.\n\nL’installeur a généré un fichier LocalSettings.php.\nIl contient toute votre configuration.\n\nVous devez le télécharger et le mettre dans $4. Le téléchargement devrait avoir démarré automatiquement.\n\nSi le téléchargement n’a pas été proposé ou si vous l’avez annulé, vous pouvez le redémarrer en cliquant sur le lien ci-dessous :\n\n$3\n\nNote : Si vous ne le faites pas maintenant, ce fichier de configuration généré ne sera plus disponible ultérieurement si vous quittez l’installation sans le télécharger.\n\nUne fois ceci fait, vous pouvez [$2 entrer dans votre wiki].", - "config-install-success": "MédiaWiki a été installé correctement. Vous pouvez maintenant visiter <$1$2> pour voir votre wiki.\nSi vous avez des questions, consultez notre foire aux questions :\n ou utilisez un des\nforums de support liés sur cette page.", + "config-install-done-path": "Félicitations !\nVous avez installé MediaWiki.\n\nL’installeur a généré un fichier LocalSettings.php.\nIl contient toute votre configuration.\n\nVous devez le télécharger et le mettre dans $4. Le téléchargement devrait avoir démarré automatiquement.\n\nSi le téléchargement n’a pas été proposé ou si vous l’avez annulé, vous pouvez le redémarrer en cliquant sur le lien ci-dessous :\n\n$3\n\nNote : Si vous ne le faites pas maintenant, ce fichier de configuration généré ne sera plus disponible ultérieurement si vous quittez l’installation sans le télécharger.\n\nUne fois ceci fait, vous pouvez [$2 entrer dans votre wiki].", + "config-install-success": "MediaWiki a été installé correctement. Vous pouvez maintenant visiter <$1$2> pour voir votre wiki.\nSi vous avez des questions, consultez notre foire aux questions :\n ou utilisez un des\nforums de support liés sur cette page.", "config-install-db-success": "La base de données a été bien installée", "config-download-localsettings": "Télécharger LocalSettings.php", "config-help": "aide", @@ -342,8 +320,8 @@ "config-skins-screenshots": "$1 (captures d’écran : $2)", "config-extensions-requires": "$1 (nécessite $2)", "config-screenshot": "Captures d’écrans", - "config-extension-not-found": "Impossible de trouver le fichier d’inscription pour l’extension « $1 »", - "config-extension-dependency": "Une erreur de dépendance s’est produite en installant l’extension « $1 » : $2", + "config-extension-not-found": "Impossible de trouver le fichier d’inscription pour l’extension « $1 »", + "config-extension-dependency": "Une erreur de dépendance s’est produite en installant l’extension « $1 » : $2", "mainpagetext": "MediaWiki a été installé.", "mainpagedocfooter": "Consultez le [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents Guide de l’utilisateur] pour plus d’informations sur l’utilisation de ce logiciel de wiki.\n\n== Pour démarrer ==\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings Liste des paramètres de configuration]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ/fr Questions courantes sur MediaWiki]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Liste de discussion sur les distributions de MediaWiki]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation#Translation_resources Adaptez MediaWiki dans votre langue]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Combating_spam Apprendre comment lutter contre le pourriel dans votre wiki]" } diff --git a/includes/installer/i18n/frc.json b/includes/installer/i18n/frc.json index 7126f90284..20f30a3d7a 100644 --- a/includes/installer/i18n/frc.json +++ b/includes/installer/i18n/frc.json @@ -37,15 +37,10 @@ "config-db-wiki-account": "Compte d'useur pour le fonctionnement normal", "config-db-wiki-help": "Entrez le nom d'useur et le mot de passe qui seront usés pour se connecter à la base de données pendant le fonctionnement normal du wiki.\nSi le compte existe pas, et le compte d'installation dispose de privilèges suffisants, ce compte d'useur sera créé avec les privilèges minimum requis pour faire fonctionner le wiki.", "config-db-prefix": "Préfixe des tables de la base de données:", - "config-oracle-def-ts": "Espace de stockage (''tablespace'') par défaut:", - "config-oracle-temp-ts": "Espace de stockage (''tablespace'') temporaire:", "config-type-mysql": "MySQL (ou compatible)", - "config-type-mssql": "Microsoft SQL Server", "config-header-mysql": "Paramètres de MySQL", "config-header-postgres": "Paramètres de PostgreSQL", "config-header-sqlite": "Paramètres de SQLite", - "config-header-oracle": "Paramètres d’Oracle", - "config-header-mssql": "Paramètres de Microsoft SQL Server", "config-invalid-db-type": "Type de base de données non valide", "config-sqlite-name-help": "Choisir un nom qui identifie ton wiki.\nFait user pas ni d'espaces ni des traits d'union\nIl va user pour fichier de données SQLite.", "config-mysql-innodb": "InnoDB", diff --git a/includes/installer/i18n/frp.json b/includes/installer/i18n/frp.json index bc35fc24ac..3c4d60286e 100644 --- a/includes/installer/i18n/frp.json +++ b/includes/installer/i18n/frp.json @@ -35,10 +35,8 @@ "config-diff3-bad": "GNU diff3 entrovâblo.", "config-db-type": "Tipo de bâsa de balyês :", "config-db-host": "Hôto de la bâsa de balyês :", - "config-db-host-oracle": "TNS de la bâsa de balyês :", "config-db-wiki-settings": "Identifiar cél vouiqui", "config-db-name": "Nom de la bâsa de balyês :", - "config-db-name-oracle": "Plan de bâsa de balyês :", "config-db-install-account": "Compto usanciér por l’enstalacion", "config-db-username": "Nom d’usanciér de la bâsa de balyês :", "config-db-password": "Contresegno de la bâsa de balyês :", @@ -49,16 +47,12 @@ "config-db-schema": "Plan por MediaWiki", "config-pg-test-error": "Empossiblo de sè branchiér a la bâsa de donâs '''$1''' : $2", "config-sqlite-dir": "Dossiér de les balyês SQLite :", - "config-oracle-def-ts": "Èspâço de stocâjo (''tablespace'') per dèfôt :", - "config-oracle-temp-ts": "Èspâço de stocâjo (''tablespace'') temporèro :", "config-header-mysql": "Paramètres de MySQL", "config-header-postgres": "Paramètres de PostgreSQL", "config-header-sqlite": "Paramètres de SQLite", - "config-header-oracle": "Paramètres d’Oracle", "config-invalid-db-type": "Tipo de bâsa de balyês envalido", "config-missing-db-name": "Vos dête buchiér una valor por « Nom de la bâsa de balyês »", "config-missing-db-host": "Vos dête buchiér una valor por « Hôto de la bâsa de balyês »", - "config-missing-db-server-oracle": "Vos dête buchiér una valor por « TNS de la bâsa de balyês »", "config-sqlite-readonly": "Lo fichiér $1 est pas accèssiblo en ècritura.", "config-regenerate": "Refâre LocalSettings.php →", "config-show-table-status": "Falyita de la requéta SHOW TABLE STATUS !", diff --git a/includes/installer/i18n/gl.json b/includes/installer/i18n/gl.json index c75d129759..4394a85de3 100644 --- a/includes/installer/i18n/gl.json +++ b/includes/installer/i18n/gl.json @@ -87,13 +87,9 @@ "config-db-type": "Tipo de base de datos:", "config-db-host": "Servidor da base de datos:", "config-db-host-help": "Se o servidor da súa base de datos está nun servidor diferente, escriba o nome do servidor ou o enderezo IP aquí.\n\nSe está usando un aloxamento web compartido, o seu provedor de hospedaxe debe darlle o nome de servidor correcto na súa documentación.\n\nSe está a realizar a instalación nun servidor de Windows con MySQL, o nome \"localhost\" pode non valer como servidor. Se non funcionase, inténteo con \"127.0.0.1\" como enderezo IP local.\n\nSe está usando PostgreSQL, deixe este campo en branco para facer a conexión a través do conectador Unix.", - "config-db-host-oracle": "TNS da base de datos:", - "config-db-host-oracle-help": "Insira un [http://download.oracle.com/docs/cd/B28359_01/network.111/b28317/tnsnames.htm nome de conexión local] válido; cómpre que haxa visible un ficheiro tnsnames.ora para esta instalación.
Se está a empregar bibliotecas cliente versión 10g ou máis recentes, tamén pode usar o método de atribución de nomes [http://download.oracle.com/docs/cd/E11882_01/network.112/e10836/naming.htm Easy Connect].", "config-db-wiki-settings": "Identificar o wiki", "config-db-name": "Nome da base de datos:", "config-db-name-help": "Escolla un nome que identifique o seu wiki.\nNon debe conter espazos.\n\nSe está usando un aloxamento web compartido, o seu provedor de hospedaxe daralle un nome específico para a base de datos ou deixaralle crear unha a través do panel de control.", - "config-db-name-oracle": "Esquema da base de datos:", - "config-db-account-oracle-warn": "Existen tres escenarios soportados para a instalación de Oracle como fin da base de datos:\n\nSe quere crear unha conta para a base de datos como parte do proceso de instalación, proporcione unha conta co papel SYSDBA e especifique as credenciais desexadas para a conta; senón pode crear a conta manualmente e dar só esa conta (se ten os permisos necesarios para crear os obxectos do esquema) ou fornecer dous contas diferentes, unha con privilexios de creación e outra restrinxida para o acceso á web.\n\nA escritura para crear unha conta cos privilexios necesarios atópase no directorio \"maintenance/oracle/\" desta instalación. Teña en conta que o emprego de contas restrinxidas desactivará todas as operacións de mantemento da conta predeterminada.", "config-db-install-account": "Conta de usuario para a instalación", "config-db-username": "Nome de usuario da base de datos:", "config-db-password": "Contrasinal da base de datos:", @@ -112,37 +108,24 @@ "config-pg-test-error": "Non se pode conectar coa base de datos $1: $2", "config-sqlite-dir": "Directorio de datos SQLite:", "config-sqlite-dir-help": "SQLite recolle todos os datos nun ficheiro único.\n\nO servidor web debe ter permisos sobre o directorio para que poida escribir nel durante a instalación.\n\nAdemais, o servidor non debe ser accesible a través da web, motivo polo que non está no mesmo lugar ca os ficheiros PHP.\n\nAsemade, o programa de instalación escribirá un ficheiro .htaccess, pero se erra alguén pode obter acceso á súa base de datos.\nIsto inclúe datos de usuario (enderezos de correo electrónico, contrasinais codificados), así como revisións borradas e outros datos restrinxidos no wiki.\n\nConsidere poñer a base de datos nun só lugar, por exemplo en /var/lib/mediawiki/oseuwiki.", - "config-oracle-def-ts": "Espazo de táboas por defecto:", - "config-oracle-temp-ts": "Espazo de táboas temporal:", "config-type-mysql": "MariaDB, MySQL ou compatíbel", "config-type-postgres": "PostgreSQL", "config-type-sqlite": "SQLite", - "config-type-oracle": "Oracle", - "config-type-mssql": "Microsoft SQL Server", "config-support-info": "MediaWiki soporta os seguintes sistemas de bases de datos:\n\n$1\n\nSe non ve listado a continuación o sistema de base de datos que intenta usar, siga as instrucións ligadas enriba para activar o soporte.", "config-dbsupport-mysql": "* [{{int:version-db-mariadb-url}} MariaDB] é o obxectivo principal para MediaWiki e é o que mellor soportado está. MediaWiki tamén funciona con [{{int:version-db-mysql-url}} MySQL] e [{{int:version-db-percona-url}} Percona Server], que son compatibles con MariaDB. ([https://www.php.net/manual/en/mysqli.installation.php Como compilar PHP con compatibilidade MySQL])", "config-dbsupport-postgres": "* [{{int:version-db-postgres-url}} PostgreSQL] é un sistema de base de datos popular e de código aberto como alternativa a MySQL. ([https://www.php.net/manual/en/pgsql.installation.php Como compilar PHP con compatibilidade PostgreSQL])", "config-dbsupport-sqlite": "* [{{int:version-db-sqlite-url}} SQLite] é un sistema de base de datos lixeiro moi ben soportado. ([https://www.php.net/manual/en/pdo.installation.php Como compilar o PHP con soporte SQLite], emprega PDO)", - "config-dbsupport-oracle": "* [{{int:version-db-oracle-url}} Oracle] é un sistema comercial de xestión de base de datos a nivel empresarial. ([https://www.php.net/manual/en/oci8.installation.php Como compilar PHP con soporte OCI8])", - "config-dbsupport-mssql": "* [{{int:version-db-mssql-url}} Microsoft SQL Server] é un sistema comercial de xestión de base de datos de nivel empresarial para Windows. ([https://www.php.net/manual/en/sqlsrv.installation.php Como compilar o PHP con compatibilidade SQLSRV])", "config-header-mysql": "Configuración MariaDB/MySQL", "config-header-postgres": "Configuración do PostgreSQL", "config-header-sqlite": "Configuración do SQLite", - "config-header-oracle": "Configuración do Oracle", - "config-header-mssql": "Configuración de Microsoft SQL Server", "config-invalid-db-type": "Tipo de base de datos incorrecto", "config-missing-db-name": "Debe introducir un valor para \"{{int:config-db-name}}\".", "config-missing-db-host": "Debe introducir un valor para \"{{int:config-db-host}}\".", - "config-missing-db-server-oracle": "Debe introducir un valor para \"{{int:config-db-host-oracle}}\".", - "config-invalid-db-server-oracle": "O TNS da base de datos, \"$1\", é incorrecto.\nUtilice só \"TNS Name\" ou unha cadea de texto \"Easy Connect\" ([http://docs.oracle.com/cd/E11882_01/network.112/e10836/naming.htm métodos de nomeamento de Oracle])", "config-invalid-db-name": "O nome da base de datos, \"$1\", é incorrecto.\nSó pode conter letras ASCII (a-z, A-Z), números (0-9), guións baixos (_) e guións (-).", "config-invalid-db-prefix": "O prefixo da base de datos, \"$1\", é incorrecto.\nSó pode conter letras ASCII (a-z, A-Z), números (0-9), guións baixos (_) e guións (-).", "config-connection-error": "$1.\n\nComprobe o servidor, nome de usuario e contrasinal que hai a continuación e inténteo de novo.", "config-invalid-schema": "O esquema de MediaWiki, \"$1\", é incorrecto.\nSó pode conter letras ASCII (a-z, A-Z), números (0-9) e guións baixos (_).", - "config-db-sys-create-oracle": "O programa de instalación soamente soporta o emprego de contas SYSDBA como método para crear unha nova conta.", - "config-db-sys-user-exists-oracle": "A conta de usuario \"$1\" xa existe. SYSDBA soamente se pode empregar para a creación dunha nova conta!", "config-postgres-old": "Necesítase PostgreSQL $1 ou posterior. Vostede ten a versión $2.", - "config-mssql-old": "Necesítase Microsoft SQL Server $1 ou posterior. Vostede ten a versión $2.", "config-sqlite-name-help": "Escolla un nome que identifique o seu wiki.\nNon utilice espazos ou guións.\nEste nome será utilizado para o ficheiro de datos SQLite.", "config-sqlite-parent-unwritable-group": "Non se puido crear o directorio de datos $1, porque o servidor web non pode escribir no directorio pai $2.\n\nO programa de instalación determinou o usuario que executa o seu servidor web.\nPara continuar, faga que se poida escribir no directorio $3.\nNun sistema Unix/Linux cómpre realizar:\n\n
cd $2\nmkdir $3\nchgrp $4 $3\nchmod g+w $3
", "config-sqlite-parent-unwritable-nogroup": "Non se puido crear o directorio de datos $1, porque o servidor web non pode escribir no directorio pai $2.\n\nO programa de instalación non puido determinar o usuario que executa o seu servidor web.\nPara continuar, faga que se poida escribir globalmente no directorio $3.\nNun sistema Unix/Linux cómpre realizar:\n\n
cd $2\nmkdir $3\nchmod a+w $3
", @@ -166,11 +149,6 @@ "config-mysql-engine": "Motor de almacenamento:", "config-mysql-innodb": "InnoDB (recomendado)", "config-mysql-engine-help": "InnoDB é case sempre a mellor opción, dado que soporta ben os accesos simultáneos.\n\nMyISAM é máis rápido en instalacións de usuario único e de só lectura.\nAs bases de datos MyISAM tenden a se corromper máis a miúdo ca as bases de datos InnoDB.", - "config-mssql-auth": "Tipo de autenticación:", - "config-mssql-install-auth": "Seleccione o tipo de autenticación que se utilizará para conectarse á base de datos durante o proceso de instalación.\nSe selecciona \"{{int:config-mssql-windowsauth}}\", usaranse as credenciais do usuario co que se está a executar o servidor web.", - "config-mssql-web-auth": "Seleccione o tipo de autenticación que utilizará o servidor web para conectarse ao servidor da base de datos durante o funcionamiento normal do wiki.\nSe selecciona \"{{int:config-mssql-windowsauth}}\", usaranse as credenciais do usuario co que se está a executar o servidor web.", - "config-mssql-sqlauth": "Autenticación de SQL Server", - "config-mssql-windowsauth": "Autenticación de Windows", "config-site-name": "Nome do wiki:", "config-site-name-help": "Isto aparecerá na barra de títulos do navegador e noutros lugares.", "config-site-name-blank": "Escriba o nome do sitio.", diff --git a/includes/installer/i18n/he.json b/includes/installer/i18n/he.json index 3a869cfa03..ba846e6511 100644 --- a/includes/installer/i18n/he.json +++ b/includes/installer/i18n/he.json @@ -95,13 +95,9 @@ "config-db-type": "סוג מסד הנתונים:", "config-db-host": "שרת מסד הנתונים:", "config-db-host-help": "אם שרת מסד הנתונים שלך נמצא על שרת אחר, יש להקליד את שם המחשב או את כתובת ה־IP כאן.\n\nאם זה אירוח משותף, ספק האירוח שלכם אמור לתת לך את שם השרת הנכון במסמכים.\n\nאם זוהי התקנה בשרת Windows שמשתמשת ב־MySQL, השימוש ב־localhost עשוי לא לעבוד. אם הוא לא עובד, יש לנסות את \"127.0.0.1\" בתור כתובת ה־IP המקומית.\n\nאם זה PostgreSQL, תשאירו את השדה הזה ריק כדי להתחבר דרך שקע יוניקס.", - "config-db-host-oracle": "TNS של מסד הנתונים:", - "config-db-host-oracle-help": "יש להקליד [http://download.oracle.com/docs/cd/B28359_01/network.111/b28317/tnsnames.htm שם חיבור מקומי (Local Connect Name)] תקין; הקובץ tnsnames.ora צריך להיות זמין להתקנה הזאת.
\nאם יש פה ב־client libraries 10g או בגרסה חדשה יותר, אפשר להשתמש גם בשיטת מתן השמות [http://download.oracle.com/docs/cd/E11882_01/network.112/e10836/naming.htm Easy Connect].", "config-db-wiki-settings": "זיהוי ויקי זה", "config-db-name": "שם מסד הנתונים (ללא מקף):", "config-db-name-help": "נא לבחור שם שמזהה את הוויקי שלכם.\nלא צריכים להיות בו רווחים.\n\nאם זהו משתמשים באירוח משותף, ספק האירוח שלכם ייתן לכם שם מסד נתונים מסוים שתוכלו להשתמש בו או יאפשר לכם ליצור מסד נתונים דרך לוח בקרה.", - "config-db-name-oracle": "סכמה של מסד נתונים:", - "config-db-account-oracle-warn": "קיימים שלושה תרחישים נתמכים עבור התקנת אורקל בתור מסד הנתונים:\n\nאם הרצונך ליצור חשבון מסד נתונים כחלק מתהליך ההתקנה, נא לספק חשבון בעל תפקיד SYSDBA בתור חשבון מסד הנתונים עבור ההתקנה ולציין את האישורים המבוקשים עבור חשבון הגישה לאינטרנט, אחרת ניתן ליצור באופן ידני את חשבון הגישה לאינטרנט, ולספק חשבון זה בלבד (אם יש לו ההרשאות הדרושות ליצירת עצמי סכמה) או לספק שני חשבונות שונים, אחד עם הרשאות יצירה ואחד מוגבלת עבור גישה לאינטרנט.\n\nסקריפט ליצירת חשבון עם ההרשאות הנדרשות ניתן למצוא בתיקייה \"maintenance/oracle/\" של ההתקנה זו. נא לזכור כי שימוש בחשבון מוגבל יגרום להשבתת כל יכולות תחזוקה עם חשבון בררת המחדל.", "config-db-install-account": "חשבון משתמש להתקנה", "config-db-username": "שם המשתמש במסד הנתונים:", "config-db-password": "הססמה במסד הנתונים:", @@ -120,34 +116,22 @@ "config-pg-test-error": "ההתחברות למסד הנתונים '''$1''' לא מצליחה: $2", "config-sqlite-dir": "תיקיית נתונים (data directory) של SQLite:", "config-sqlite-dir-help": "SQLite שומר את כל הנתונים בקובץ אחד.\n\nלשרת הווב צריכה להיות הרשאה לכתוב לתיקייה שמוגדרת כאן.\n\nהיא לא צריכה נגישה לכולם דרך האינטרנט – בגלל זה איננו שמים אותה באותו מקום עם קובצי ה־PHP.\n\nתוכנת ההתקנה תכתוב קובץ .htaccess יחד אִתו, אבל אם זה ייכשל, מישהו יוכל להשיג גישה למסד הנתונים שלכם.\nשם נמצא מידע מפורש של משתמשים (כתובות דוא״ל, ססמאות מגובבות) וגם גרסאות מחוקות של דפים ומידע מוגבל אחר.\n\nכדאי לשקול לשים את מסד הנתונים במקום אחר לגמרי, למשל ב־/var/lib/mediawiki/yourwik.", - "config-oracle-def-ts": "מרחב טבלאות לפי בררת מחדל (default tablespace):", - "config-oracle-temp-ts": "מרחב טבלאות זמני (temporary tablespace):", "config-type-mysql": "MariaDB‏, MySQL, או תואם", - "config-type-mssql": "Microsoft SQL Server", "config-support-info": "מדיה־ויקי תומכת במערכות מסדי הנתונים הבאות:\n\n$1\n\nאם אינך רואה את מסד הנתונים שלך ברשימה, יש לעקוב אחר ההוראות המקושרות לעיל כדי להפעיל את התמיכה.", "config-dbsupport-mysql": "* [{{int:version-db-mariadb-url}} MariaDB] הוא היעד העיקרי עבור מדיה־ויקי ולו התמיכה הטובה ביותר. מדיה־ויקי עובדת גם עם [{{int:version-db-mysql-url}} MySQL] ועם [{{int:version-db-percona-url}} Percona Server], שתואמים ל־MariaDB. (ר׳ [https://www.php.net/manual/en/mysql.installation.php how to compile PHP with MySQL support])", "config-dbsupport-postgres": "* [{{int:version-db-postgres-url}} PostgreSQL] הוא מסד נתונים נפוץ בקוד פתוח והוא נפוץ בתור חלופה ל־MySQL. (ר׳ [https://www.php.net/manual/en/pgsql.installation.php how to compile PHP with PostgreSQL support]).", "config-dbsupport-sqlite": "* [{{int:version-db-sqlite-url}} SQLite] הוא מסד נתונים קליל עם תמיכה טובה מאוד. (ר׳ [https://www.php.net/manual/en/pdo.installation.php How to compile PHP with SQLite support], משתמש ב־PDO)", - "config-dbsupport-oracle": "* [{{int:version-db-oracle-url}} Oracle] הוא מסד נתונים עסקי מסחרי. (ר׳ [https://www.php.net/manual/en/oci8.installation.php How to compile PHP with OCI8 support])", - "config-dbsupport-mssql": "* [{{int:version-db-mssql-url}} Microsoft SQL Server] הוא מסד נתונים עסקי מסחרי לחלונות. ([https://www.php.net/manual/en/sqlsrv.installation.php How to compile PHP with SQLSRV support])", "config-header-mysql": "הגדרות MariaDB/MySQL", "config-header-postgres": "הגדרות PostgreSQL", "config-header-sqlite": "הגדרות SQLite", - "config-header-oracle": "הגדרות Oracle", - "config-header-mssql": "הגדרות Microsoft SQL Server", "config-invalid-db-type": "סוג מסד הנתונים שגוי", "config-missing-db-name": "יש להזין ערך עבור \"{{int:config-db-name}}\".", "config-missing-db-host": "יש להכניס ערך לשדה \"{{int:config-db-host}}\".", - "config-missing-db-server-oracle": "יש להכניס ערך לשדה \"{{int:config-db-host-oracle}}\".", - "config-invalid-db-server-oracle": "\"$1\" הוא TNS מסד־נתונים בלתי־‏תקין.\nיש להשתמש ב־\"TNS name\" או במחרוזת \"Easy Connect\" (ר' [http://docs.oracle.com/cd/E11882_01/network.112/e10836/naming.htm Oracle Naming Methods])", "config-invalid-db-name": "\"$1\" הוא שם מסד נתונים בלתי־תקין.\nיש להשתמש רק באותיות ASCII‏ (a עד z‏, A עד Z), סְפָרוֹת (0 עד 9), קווים תחתיים (_) ומינוסים (-).", "config-invalid-db-prefix": "\"$1\" היא תחילית מסד נתונים בלתי תקינה.\nיש להשתמש רק באותיות ASCII‏ (a עד z‏, A עד Z), סְפָרוֹת (0 עד 9), קווים תחתיים (_) ומינוסים (-).", "config-connection-error": "
$1.
\n\nיש לבדוק את שם השרת, את שם המשתמש ואת הססמה בטופס להלן ולנסות שוב.", "config-invalid-schema": "\"$1\" היא סכמה לא תקינה עבור מדיה־ויקי.\nיש להשתמש רק באותיות ASCII‏ (a עד z‏, A עד Z), סְפָרוֹת (0 עד 9) וקווים תחתיים (_).", - "config-db-sys-create-oracle": "תוכנית ההתקנה תומכת רק בשימוש בחשבון SYSDBA ליצירת חשבון חדש.", - "config-db-sys-user-exists-oracle": "חשבון המשתמש \"$1\" כבר קיים. SYSDBA יכול לשמש רק ליצירת חשבון חדש!", "config-postgres-old": "נדרש PostgreSQL $1 או גרסה חדשה יותר, הגרסה הנוכחית שלכם היא $2.", - "config-mssql-old": "חובה להשתמש ב־Microsoft SQL Server מגרסה $1 או גרסה חדשה יותר. הגרסה שלך היא $2.", "config-sqlite-name-help": "יש לבחור בשם שמזהה את הוויקי שלכם.\nאין להשתמש ברווחים או במינוסים.\nזה יהיה שם קובץ הנתונים ל־SQLite.", "config-sqlite-parent-unwritable-group": "לא ניתן ליצור את תיקיית הנתונים $1, כי לשָׁרַת הווב אין הרשאות לכתוב לתיקיית האם $2 .\n\nתוכנת ההתקנה זיהתה את החשבון שתחתיו רץ שרת הווב שלכם.\nיש לאפשר לשָׁרַת הווב לכתוב לתיקייה $3.\nבמערכת Unix/Linux יש לכתוב:\n\n
cd $2\nmkdir $3\nchgrp $4 $3\nchmod g+w $3
", "config-sqlite-parent-unwritable-nogroup": "לא ניתן ליצור את תיקיית הנתונים $1, כי לשָׁרַת הווב אין הרשאות לכתוב לתיקיית האם $2.\n\nתוכנת ההתקנה לא זיהתה את החשבון שתחתיו רץ שרת הווב שלכם.\nיש לאפשר לכל החשבונות לכתוב לתיקייה $3 כדי להמשיך.\nבמערכת Unix/Linux יש לכתוב:\n\n
cd $2\nmkdir $3\nchmod a+w $3
", @@ -171,11 +155,6 @@ "config-mysql-engine": "מנוע האחסון:", "config-mysql-innodb": "InnoDB (מומלץ)", "config-mysql-engine-help": "'''InnoDB''' היא כמעט תמיד האפשרות הטובה ביותר, כי במנוע הזה יש תמיכה טובה ביותר בעיבוד מקבילי.\n\n'''MyISAM''' עשוי להיות בהתקנות שמיועדות למשתמש אחד ולהתקנות לקריאה בלבד.\nמסדי נתונים עם MyISAM נוטים להיהרס לעתים קרובות יותר מאשר מסדי נתונים עם InnoDB.", - "config-mssql-auth": "סוג אימות:", - "config-mssql-install-auth": "נא לבחור את סוג האימות שישמש להתחברות למסד הנתונים בזמן תהליך ההתקנה. בחירה ב־\"{{int:config-mssql-windowsauth}}\" תשתמש בהרשאות של החשבון שמריץ את השרת הנוכחי.", - "config-mssql-web-auth": "נא לבחור את סוג האימות שישמש את השרת להתחברות למסד הנתונים בזמן הריצה הרגילה של הוויקי.\nבחירה ב־\"{{int:config-mssql-windowsauth}}\" תשתמש בהרשאות של החשבון שמריץ את השרת הנוכחי.", - "config-mssql-sqlauth": "SQL Server Authentication", - "config-mssql-windowsauth": "Windows Authentication", "config-site-name": "שם הוויקי:", "config-site-name-help": "זה יופיע בשורת הכותרת של הדפדפן ובמקומות רבים אחרים.", "config-site-name-blank": "נא להזין שם לאתר.", diff --git a/includes/installer/i18n/hi.json b/includes/installer/i18n/hi.json index 90b11a9cbb..9141611221 100644 --- a/includes/installer/i18n/hi.json +++ b/includes/installer/i18n/hi.json @@ -53,20 +53,16 @@ "config-using-32bit": "<विशेष>चेतावनी: आपका सिस्टम 32-बिट पूर्णांक के साथ चल रहा है यह [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:32-bit विवेचित नहीं है]।", "config-db-type": "डेटाबेस प्रकार:", "config-db-host": "डेटाबेस होस्ट:", - "config-db-host-oracle": "डेटाबेस टीएनएस:", "config-db-wiki-settings": "इस विकि को पहचानें", "config-db-name": "डेटाबेस का नाम:", "config-db-install-account": "इसे स्थापित करने हेतु सदस्य खाता", "config-db-username": "डेटाबेस सदस्यनाम:", "config-db-password": "डेटाबेस पासवर्ड:", "config-db-port": "डेटाबेस पोर्ट:", - "config-type-mssql": "माइक्रोसॉफ़्ट एसक्यूएल सर्वर", "config-invalid-db-type": "अमान्य डेटाबेस प्रकार", "config-regenerate": "LocalSettings.php फिर से निर्मित करें →", "config-db-web-account": "वेब पहुँच हेतु डेटाबेस खाता", "config-mysql-innodb": "इनोडीबी", - "config-mssql-auth": "प्रमाणन प्रकार:", - "config-mssql-sqlauth": "SQL सर्वर प्रमाणन", "config-site-name": "विकि का नाम:", "config-site-name-blank": "एक साइट का नाम लिखें", "config-project-namespace": "प्रकल्प नामस्थान:", diff --git a/includes/installer/i18n/hrx.json b/includes/installer/i18n/hrx.json index 191460076c..5aeb9d9ec3 100644 --- a/includes/installer/i18n/hrx.json +++ b/includes/installer/i18n/hrx.json @@ -77,13 +77,9 @@ "config-db-type": "Datebanksystem:", "config-db-host": "Datebankserver:", "config-db-host-help": "Soweit sich die Datebank uff en annre Server befindt, ist hier der Servernoome orrer die entsprechende IP-Adresse oonzugewe.\n\nSoweit en gemeinschaftlich genutzter Server verwendt weard, sollt der Hoster den zutreffend Servernoomen in seiner Dokumentation oongeb hoon.\n\nSoweit uff enem Windows-Server installiert und MySQL genutzt weard, funktioniert der Servername „localhost“ voaraussichtlich net. Wenn net, sollte „127.0.0.1“ orrer die lokale IP-Adress oongeb sin.\n\nSoweit PostgresQL benutzt weard, muss das Feld/Campo leer geloss sin, um üwer en Unix-Socket zu verbinne.", - "config-db-host-oracle": "Datebank-TNS:", - "config-db-host-oracle-help": "En gültiche [http://download.oracle.com/docs/cd/B28359_01/network.111/b28317/tnsnames.htm „Local Connect“-Namen] angeben. Die „tnsnames.ora“-Datei muss von die Installation erkannt werre könne.
Sofern die Client-Bibliotheke für Version 10g orrer neier verwennet weare, kann ooch [http://download.oracle.com/docs/cd/E11882_01/network.112/e10836/naming.htm „Easy Connect“] zu der Noomensgebung benutzt sin.", "config-db-wiki-settings": "Bitte Date zu der indeitiche Identifikatio von das Wiki oongewe", "config-db-name": "Datebanksystem:", "config-db-name-help": "Bitte en Noome oongewe, mit dem das Wiki identifiziert werre kann.\nDobei sollt ken Leerzeiche verwennet sin.\n\nSoweit en gemeinschaftlich genutzter Server verwennet weard, sollt der Hoster den Datebanknoome oongegewe orrer awer die Erstellung von en Datebank üwer en entsprechendes Interface gestattet hoon.", - "config-db-name-oracle": "Datebankschema:", - "config-db-account-oracle-warn": "Es gebt drei von MediaWiki unnerstützte Möchlichkeite Oracle als Datebank inzurichte:\n\nSoweit das Datebankbenutzerkonto im Moment (während des) von dem Installationsvoargang erstellt werre soll, muss en Datebankbenutzerkonto mit der SYSDBA-Berechtichung zusammer mit den entsprechende Onnmeldeinformatione oongeb sin, mit dem dann üwer das Web uff die Datebank zugegriff sin kann. Alternativ kann man ooch ledichlich en enzelnes manuell oongelechtes Datebankbenutzerkonto oongewe, mit dem üwer das Web uff die Datebank zugegriff werre kann, soweit das üwer die Berechtichung zur Erstellung von Datebankscheme verfücht. Zudem ist es möchlich zwooi Datebankbenutzerkonte oonzugew von dene enes die Berechtichung zu der Erstellung von Datebankscheme hot und das annere, um mit ihm üwer das Web uff die Datebank zuzugreife.\n\nEn Skript zu dem Oonlehn von en Datebankbenutzerkonto mit den notwendiche Berechtichunge findt man unner dem Pad „…/maintenance/oracle/“ von der MediaWiki-Installation. Das ist dobei zu bedenke, dass die Verwennung von en Datebankbenutzerkonto mit beschränkte Berechtichunge die Nutzung von der Wartungsfunktione für das Standarddatebankbenutzerkonto deaktiviert.", "config-db-install-account": "Benutzerkonto für die Installation", "config-db-username": "Der Datebankbenutzer sein Noome:", "config-db-password": "Der Datebankbenutzer sei Passwort:", @@ -102,34 +98,22 @@ "config-pg-test-error": "Do kann ken Verbinnung zur Datebank '''$1''' heargestellt sin: $2", "config-sqlite-dir": "SQLite-Dateverzeichnis:", "config-sqlite-dir-help": "SQLite speichert alle Date in en enziche Datei.\n\nDas für sie voargesiehn Verzeichnis muss (während des) im Momento von dem Installationsvoargang beschreibbar orrer beschrib fähich sin.\n\nDas sollt '''net''' üwer das Web zugänglich sin, was der Grund ist, warum die Datei net dort abgeleht weard, wo sich die PHP-Dateie befinne.\n\nDas Installationsprogramm weard mit der Datei zusammer en zusätzliche .htaccess-Datei erstelle. Soweit das scheitert, könne Dritte uff die Datedatei zugreife.\nDas umfasst die Nutzerdate (E-Mail-Adresse, Passwörter, und so weiter) wie ooch gelöschte Seiteversione und annere vertrauliche Date, die im Wiki gespeichert sind.\n\nTue konsideriere, erwäch die Datedatei an en gänz anner Platz abzulehn, zum beispiel im Verzeichnis ./var/lib/mediawiki/yourwiki.", - "config-oracle-def-ts": "Standardtabelleraum:", - "config-oracle-temp-ts": "Temporärer Tabelleraum:", "config-type-mysql": "MySQL (orrer kompatible Datebanksysteme)", - "config-type-mssql": "Microsoft SQL Server", "config-support-info": "MediaWiki unnerstützt die follichenne Datebanksysteme:\n\n$1\n\nSoweit net das Datebanksystem oongezeicht weard, das verwennt werre soll, gebt das uwe en Link zu der Oonleitung mit Informatione, wie das aktiviert sin kann.", "config-dbsupport-mysql": "* [{{int:version-db-mysql-url}} MySQL] ist das von MediaWiki primär unterstützte Datebanksystem. MediaWiki funktioniert ooch mit [{{int:version-db-mariadb-url}} MariaDB] und [{{int:version-db-percona-url}} Percona Server], die MySQL-kompatibel sind. ([https://www.php.net/manual/en/mysqli.installation.php Oonleitung zur Kompilierung von PHP mit MySQL-Unnerstützung] [englisch Sproch])", "config-dbsupport-postgres": "* [{{int:version-db-postgres-url}} PostgreSQL] ist en beliebtes Open-Source-Datebanksystem und ein Alternativ zu MySQL. Es gibt awer enche klenre Implementierungsfehler, so dass von der Nutzung in ener Produktivumgebung abgerat weard. ([https://www.php.net/manual/en/pgsql.installation.php Oonnleitung zur Kompilierung von PHP mit PostgreSQL-Unterstützung])", "config-dbsupport-sqlite": "* [{{int:version-db-sqlite-url}} SQLite] ist en verschlanktes Datebanksystem, das ooch gut unnerstützt weard ([http://www.php.net/manual/de/pdo.installation.php Oonleitung zur Kompilierung von PHP mit SQLite-Unterstützung], verwennt PHP Data Objects (PDO))", - "config-dbsupport-oracle": "* [{{int:version-db-oracle-url}} Oracle] ist en kommerzielle Unnernehmensdatebank ([http://www.php.net/manual/en/oci8.installation.php Oonleitung zur Kompilierung von PHP mit OCI8-Unnerstützung (en)])", - "config-dbsupport-mssql": "* [{{int:version-db-mssql-url}} Microsoft SQL Server] ist en gewerbliche Unnernehmensdatebank für Windows. ([Config-dbsupport-oracle/manual/de/sqlsrv.installation.php Oonleitung zur Kompilierung von PHP mithilfe SQLSRV-Unnerstützung])", "config-header-mysql": "MySQL-Instellunge", "config-header-postgres": "PostgreSQL-Instellunge", "config-header-sqlite": "SQLite-Instellunge", - "config-header-oracle": "Oracle-Instellunge", - "config-header-mssql": "Instellunge von Microsoft SQL Server", "config-invalid-db-type": "Unzulässiges Datebanksystem", "config-missing-db-name": "Bei \"{{int:config-db-name}}\" muss en Weart oongeb sin.", "config-missing-db-host": "Bei \"{{int:config-db-host}}\" muss en Weart oongeb sin.", - "config-missing-db-server-oracle": "Für das \"{{int:config-db-host-oracle}}\" muss en Weart ingeb sin.", - "config-invalid-db-server-oracle": "Ungültiches Datebank-TNS „$1“.\nEntweder „TNS Noome“ orrer ene „Easy Connect“-Zeichefolliche verwenne ([http://docs.oracle.com/cd/E11882_01/network.112/e10836/naming.htm Oracle-Benennungsmethode])", "config-invalid-db-name": "Ungülticher Datebankname „$1“.\nDo deerfe nuar ASCII-codierte Buchstoobe (a-z, A-Z), Zooahle (0-9), Unner- (_) sowie Binnestriche (-) verwennt sin.", "config-invalid-db-prefix": "Ungülticher Datebanktabellepräfix „$1“.\nEs dürfe nuar ASCII-codierte Buchstoobe (a-z, A-Z), Zoohle (0-9), Unner- (_) sowie Binnestriche (-) verwennt sin.", "config-connection-error": "$1.\n\nBitte unne oongeb Servernoome, Benutzernoome sowie das Passwort üwerprüfe und es dann erneit versuche.", "config-invalid-schema": "Ungültiches Dateschema für MediaWiki „$1“.\nEs dürfe nuar ASCII-codierte Buchstoobe (a-z, A-Z), Zoohle (0-9) und Unnerstriche (_) verwennt sin.", - "config-db-sys-create-oracle": "Das Installationsprogramm unnerstützt nuar die Verwennung von en Datebankbenutzerkonto mit SYSDBA-Berechtichung zum oonlehn von en neie Datebankbenutzerkonto.", - "config-db-sys-user-exists-oracle": "Das Datebankbenutzerkonto „$1“ ist schoon voarhand. En Datebankbenutzerkontos mit SYSDBA-Berechtichung kann nuar zum oonlehn von en neie Datebankbenutzerkonto benutzt sin.", "config-postgres-old": "MySQL $1 orrer höcher weard benöticht. MySQL $2 ist momentan voarhand.", - "config-mssql-old": "Es weard Microsoft SQL Server $1 orrer später benöticht. Dein Version ist $2.", "config-sqlite-name-help": "Bitte en Noome oongewe, mit dem das Wiki identifiziert werre kann.\nDobei bitte ken Leerzeiche orrer Binnestriche verwenne.\nDer Noome weard für die SQLite-Datedateinoome benutzt.", "config-sqlite-parent-unwritable-group": "Das Dateverzeichnis $1 kann net erzeicht werre, weil das üwergeoordnete Verzeichnis $2 net für den Webserver beschreibbar ist.\n\nDas Installationsprogramm konnt den Benutzer bestimme, mit dem Webserver ausgeführt weard.\nSchreibzugriff uff das $3-Verzeichnis muss für den ermöglicht werre, so das den Installationsvoargang fortgesetz sin kann.\n\nUff enem Unix- orrer Linux-System:\n
cd $2\nmkdir $3\nchgrp $4 $3\nchmod g+w $3
", "config-sqlite-parent-unwritable-nogroup": "Das Dateverzeichnis $1 kann net erzeicht sin, weil das üwergeordnete Verzeichnis $2 net für den Webserver beschreibbar ist.\n\nDas Installationsprogramm konnt den Benutzer bestimmen, mit dem Webserver ausgeführt weard.\nSchreibzugriff uff das $3-Verzeichnis muss global für den und annre Benutzer ermöglicht sin, so das den Installationsvoargang fortgesetzt sin kann.\n\nUff enem Unix- orrer Linux-System:\n
cd $2\nmkdir $3\nchmod a+w $3
", @@ -153,11 +137,6 @@ "config-mysql-engine": "Speicher-Engine:", "config-mysql-innodb": "InnoDB", "config-mysql-engine-help": "'''InnoDB''' ist nächst immer die bessre Wähl, weil es gleichzeitiche Zugriffe gut unnerstützt.\n\n'''MyISAM''' ist in Enzelnutzerumgebunge sowie bei schreibgeschützte Wikis schneller.\nBei MyISAM-Datebanke treten tendenziell häuficher Fehler uff als bei InnoDB-Datebanke.", - "config-mssql-auth": "Authentifikationstyp:", - "config-mssql-install-auth": "Wähl den Authentifikationstyp aus, der zur Verbinnung mit der Datebank während von der Installationsprozesses verwennt weard.\nFalls du „{{int:config-mssql-windowsauth}}“ auswählst, werre die Oonmeldeinformatione von en beliebiche Benutzer verwennt, wo den Webserver ausführt.", - "config-mssql-web-auth": "Wähl den Authentifikationstyp aus, der vom Webserver zur Verbinnung mit dem Datebankserver während / im Verloof von der gewöhnliche Betrieb von der Wiki verwennt weard.\nFalls du „{{int:config-mssql-windowsauth}}“ auswählst, werre die Oonmeldeinformatione von en beliebiche Benutzer verwennt, wo den Webserver ausführt.", - "config-mssql-sqlauth": "SQL-Server-Authentifikation", - "config-mssql-windowsauth": "Windows-Authentifikation", "config-site-name": "Der Wiki sein Noome:", "config-site-name-help": "Er weard in der Titelleiste von der Browser, wie ooch verschiedne annre Stelle, benutzt.", "config-site-name-blank": "Der Wiki sein Noome oongewe.", diff --git a/includes/installer/i18n/hsb.json b/includes/installer/i18n/hsb.json index 5a5f109263..59f56b5fd9 100644 --- a/includes/installer/i18n/hsb.json +++ b/includes/installer/i18n/hsb.json @@ -62,10 +62,8 @@ "config-using-uri": "Serwerowy URL \"$1$2\" so wužiwa.", "config-db-type": "Typ datoweje banki:", "config-db-host": "Serwer datoweje banki:", - "config-db-host-oracle": "Datowa banka TNS:", "config-db-wiki-settings": "Tutón wiki identifikować", "config-db-name": "Mjeno datoweje banki:", - "config-db-name-oracle": "Šema datoweje banki:", "config-db-install-account": "Wužiwarske konto za instalaciju", "config-db-username": "Wužiwarske mjeno datoweje banki:", "config-db-password": "Hesło datoweje banki:", @@ -81,30 +79,21 @@ "config-db-schema-help": "Tuta šema da so zwjetša derje wužiwać.\nZměń ju jenož, jeli su přeswědčiwe přičiny za to.", "config-pg-test-error": "Zwisk z datowej banku '''$1''' móžno njeje: $2", "config-sqlite-dir": "Zapis SQLite-datow:", - "config-oracle-def-ts": "Standardny tabelowy rum:", - "config-oracle-temp-ts": "Nachwilny tabelowy rum:", "config-type-mysql": "MySQL (abo kompatibelny)", "config-type-postgres": "PostgreSQL", "config-type-sqlite": "SQLite", - "config-type-oracle": "Oracle", "config-dbsupport-mysql": "* [{{int:version-db-mysql-url}} MySQL] je primarny cil za MediaWiki a podpěruje so najlěpje. MediaWiki funguje tež z [{{int:version-db-mariadb-url}} MariaDB] a [{{int:version-db-percona-url}} Percona Server], kotrejž stej kompatibelnej z MySQL. ([https://www.php.net/manual/en/mysqli.installation.php Nawod ke kompilowanju PHP z MySQL-podpěru])", "config-dbsupport-postgres": "* [{{int:version-db-postgres-url}} PostgreSQL] je popularny system datoweje banki zjawneho žórła jako alternatiwa k MySQL. Móhło hišće někotre zmylki eksistować, a njeporuča so jón w produktiwnej wokolinje wužiwać. ([https://www.php.net/manual/en/pgsql.installation.php Nawod za kompilowanje PHP z podpěru PostgreSQL])", - "config-dbsupport-oracle": "* [{{int:version-db-oracle-url}} Oracle] je komercielna předewzaćelska datowa banka. ([http://www.php.net/manual/en/oci8.installation.php Nawod za kompilowanje PHP z OCI8-podpěru])", "config-header-mysql": "Nastajenja MySQL", "config-header-postgres": "Nastajenja PostgreSQL", "config-header-sqlite": "Nastajenja SQLite", - "config-header-oracle": "Nastajenja Oracle", "config-invalid-db-type": "Njepłaćiwy typ datoweje banki", "config-missing-db-name": "Dyrbiš hódnotu za \"Mjeno datoweje banki\" zapodać", "config-missing-db-host": "Dyrbiš hódnotu za \"Database host\" zapodać", - "config-missing-db-server-oracle": "Dyrbiš hódnotu za \"Database TNS\" zapodać", - "config-invalid-db-server-oracle": "Njepłaćiwa datowa banka TNS \"$1\".\nWužij pak \"TNS Name\" pak znamješkowy rjećazk \"Easy Connect\" ([http://docs.oracle.com/cd/E11882_01/network.112/e10836/naming.htm Oracle - pomjenowanske metody])", "config-invalid-db-name": "Njepłaćiwe mjeno \"$1\" datoweje banki.\nWužij jenož pismiki ASCII (a-z, A-Z), ličby (0-9),a podsmužki (_) a wjazawki (-).", "config-invalid-db-prefix": "Njepłaćiwy prefiks \"$1\" datoweje banki.\nWužij jenož pismiki ASCII (a-z, A-Z), ličby (0-9), podsmužki (_) a wjazawki (-).", "config-connection-error": "$1.\n\nSkontroluj serwer, wužiwarske a hesło a spytaj hišće raz.", "config-invalid-schema": "Njepłaćiwe šema za MediaWiki \"$1\".\nWužij jenož pismiki ASCII (a-z, A-Z), ličby (0-9) a podsmužki (_).", - "config-db-sys-create-oracle": "Instalaciski program podpěruje jenož wužiwanje SYSDBA-konta za zakoženje noweho konta.", - "config-db-sys-user-exists-oracle": "Wužiwarske konto \"$1\" hižo eksistuje. SYSDBA hodźi so jenož za załoženje noweho konta wužiwać!", "config-postgres-old": "PostgreSQL $1 abo nowši trěbny, maš $2.", "config-sqlite-name-help": "Wubjer mjeno, kotrež twój wiki identifikuje.\nNjewužij mjezery abo wjazawki.\nTo budźe so za mjeno dataje SQLite-datow wužiwać.", "config-sqlite-mkdir-error": "Zmylk při wutworjenju datoweho zapisa \"$1\".\nSkontroluj městno a spytaj hišće raz.", diff --git a/includes/installer/i18n/hu.json b/includes/installer/i18n/hu.json index fed6854585..9226e61667 100644 --- a/includes/installer/i18n/hu.json +++ b/includes/installer/i18n/hu.json @@ -89,12 +89,9 @@ "config-db-type": "Adatbázis típusa:", "config-db-host": "Adatbázis hosztneve:", "config-db-host-help": "Ha az adatbázisszerver másik szerveren található, add meg a hosztnevét vagy az IP-címét.\n\nHa megosztott webtárhelyet használsz, a szolgáltató dokumentációjában megtalálható a helyes hosztnév.\n\nHa Windows-alapú szerverre telepítesz, és MySQL-t használsz, a „localhost” nem biztos, hogy működni fog. Ha így van, próbáld meg a „127.0.0.1” helyi IP-cím használatát.\n\nHa PostgreSQL-t használsz, hagyd ezt a mezőt üresen a Unix-socketon keresztül történő csatlakozáshoz.", - "config-db-host-oracle": "Adatbázis TNS:", "config-db-wiki-settings": "A wiki azonosítása", "config-db-name": "Adatbázisnév:", "config-db-name-help": "Válassz egy nevet a wiki azonosítására.\nNe tartalmazzon szóközt.\n\nHa megosztott webtárhelyet használsz, a szolgáltatód vagy megadja a használandó adatbázisnevet, vagy te magad hozhatsz létre adatbázisokat egy vezérlőpulton keresztül.", - "config-db-name-oracle": "Adatbázisséma:", - "config-db-account-oracle-warn": "Oracle adatbázisba való telepítésnek három támogatott módja van:\n\nHa a telepítési folyamat során adatbázisfiókot szeretnél létrehozni, akkor egy olyan fiókot kell használnod, mely rendelkezik SYSDBA jogosultsággal, majd meg kell adnod a létrehozandó, webes hozzáféréshez használt fiók adatait. Emellett a fiók kézzel is létrehozható, ekkor ennek az adatait kell megadni (a fióknak rendelkeznie kell megfelelő jogosul adatbázis-objektumok létrehozásához), vagy megadhatsz két fiókot: egyet a létrehozáshoz szükséges jogosultságokkal, és egy korlátozottat a webes hozzáféréshez.\n\nA megfelelő jogosultságokkal rendelkező fiók létrehozásához használható szkript a szoftver „maintenance/oracle/” könyvtárában található. Ne feledd, hogy korlátozott fiók használatakor az alapértelmezett fiókkal nem végezhetőek el a karbantartási műveletek.", "config-db-install-account": "A telepítéshez használt felhasználói fiók adatai", "config-db-username": "Adatbázis-felhasználónév:", "config-db-password": "Adatbázisjelszó:", @@ -113,34 +110,22 @@ "config-pg-test-error": "Nem sikerült csatlakozni a(z) '''$1''' adatbázishoz: $2", "config-sqlite-dir": "SQLite-adatkönyvtár:", "config-sqlite-dir-help": "Az SQLite minden adatot egyetlen fájlban tárol.\n\nA megadott könyvtárban írási jogosultsággal kell rendelkeznie a webszervernek.\n\n'''Nem''' szabad elérhetőnek lennie weben keresztül, ezért nem rakjuk oda, ahol a PHP-fájljaid vannak.\n\nA telepítő készít egy .htaccess fájlt az adatbázis mellé, azonban ha valamilyen okból nem sikerül, akkor akárki hozzáférhet a teljes adatbázisodhoz. Ez a felhasználók adatai (e-mail címek, jelszók hashei) mellett a törölt változatokat és más, korlátozott hozzáférésű információkat is tartalmaz.\n\nFontold meg az adatbázis más helyre történő elhelyezését, például a /var/lib/mediawiki/tewikid könyvtárba.", - "config-oracle-def-ts": "Alapértelmezett táblatér:", - "config-oracle-temp-ts": "Ideiglenes táblatér:", "config-type-mysql": "MySQL (vagy kompatibilis)", - "config-type-mssql": "Microsoft SQL Szerver", "config-support-info": "A MediaWiki a következő adatbázisrendszereket támogatja:\n\n$1\n\nHa az alábbi listán nem találod azt a rendszert, melyet használni szeretnél, a fenti linken található instrukciókat követve engedélyezheted a támogatását.", "config-dbsupport-mysql": "* A [{{int:version-db-mysql-url}} MySQL] a MediaWiki elsődleges célpontja, így a legjobban támogatott. A MediaWiki elfut [{{int:version-db-mariadb-url}} MariaDB-n] és [{{int:version-db-percona-url}} Percona Serveren] is, mivel ezek MySQL-kompatibilisek. ([https://www.php.net/manual/en/mysql.installation.php Hogyan fordítható a PHP MySQL-támogatással])", "config-dbsupport-postgres": "* A [{{int:version-db-postgres-url}} PostgreSQL] népszerű, nyílt forráskódú adatbázisrendszer, a MySQL alternatívája. ([https://www.php.net/manual/en/pgsql.installation.php Hogyan fordítható a PHP PostgreSQL-támogatással])", "config-dbsupport-sqlite": "* Az [{{int:version-db-sqlite-url}} SQLite] egy könnyű, nagyon jól támogatott adatbázisrendszer. ([http://www.php.net/manual/en/pdo.installation.php Hogyan fordítható a PHP SQLite-támogatással], PDO-t használ)", - "config-dbsupport-oracle": "* Az [{{int:version-db-oracle-url}} Oracle] kereskedelmi, vállalati adatbázisrendszer. ([http://www.php.net/manual/en/oci8.installation.php Hogyan fordítható a PHP OCI8-támogatással])", - "config-dbsupport-mssql": "* A [{{int:version-db-mssql-url}} Microsoft SQL Server] kereskedelmi, vállalati adatbázisrendszer. ([https://www.php.net/manual/en/sqlsrv.installation.php Hogyan fordítható a PHP SQLSRV-támogatással])", "config-header-mysql": "MySQL-beállítások", "config-header-postgres": "PostgreSQL-beállítások", "config-header-sqlite": "SQLite-beállítások", - "config-header-oracle": "Oracle-beállítások", - "config-header-mssql": "Microsoft SQL Server beállítások", "config-invalid-db-type": "Érvénytelen adatbázistípus", "config-missing-db-name": "Meg kell adnod a(z) „{{int:config-db-name}}” értékét.", "config-missing-db-host": "Meg kell adnod az „{{int:config-db-host}}” értékét.", - "config-missing-db-server-oracle": "Meg kell adnod az „{{int:config-db-host-oracle}}” értékét.", - "config-invalid-db-server-oracle": "Érvénytelen adatbázis TNS: „$1”\nHasználd a „TNS Name” vagy az Easy Connect” sztringet!\n([http://docs.oracle.com/cd/E11882_01/network.112/e10836/naming.htm Oracle Naming Methods]).", "config-invalid-db-name": "Érvénytelen adatbázisnév: „$1”.\nCsak ASCII-karakterek (a-z, A-Z), számok (0-9), alulvonás (_) és kötőjel (-) használható.", "config-invalid-db-prefix": "Érvénytelen adatbázisnév-előtag: „$1”.\nCsak ASCII-karakterek (a-z, A-Z), számok (0-9), alulvonás (_) és kötőjel (-) használható.", "config-connection-error": "$1.\n\nEllenőrizd a hosztot, felhasználónevet és jelszót, majd próbáld újra.", "config-invalid-schema": "Érvénytelen MediaWiki-séma: „$1”.\nCsak ASCII-karakterek (a-z, A-Z), számok (0-9) és alulvonás (_) használható.", - "config-db-sys-create-oracle": "A telepítő csak a SYSDBA fiókkal tud új felhasználói fiókot létrehozni.", - "config-db-sys-user-exists-oracle": "Már létezik „$1” nevű felhasználói fiók. A SYSDBA csak új fiók létrehozására használható!", "config-postgres-old": "A PostgreSQL $1 vagy újabb verziója szükséges, a rendszeren $2 van.", - "config-mssql-old": "Microsoft SQL Server $1 vagy későbbi szükséges. Te verziód: $2.", "config-sqlite-name-help": "Válassz egy nevet a wiki azonosítására.\nNe tartalmazzon szóközt vagy kötőjelet.\nEz lesz az SQLite-adatfájl neve.", "config-sqlite-parent-unwritable-group": "Nem hozható létre a(z) $1 adatkönyvtár, mert a szülőkönyvtárba ($2) nem írhat a webszerver.\n\nA telepítő megállapította, hogy mely felhasználó futtatja a webszervert.\nA folytatáshoz tedd írhatóvá a(z) $3 könyvtárat.\nUnix/Linux rendszeren tedd a következőt:\n\n
cd $2\nmkdir $3\nchgrp $4 $3\nchmod g+w $3
", "config-sqlite-parent-unwritable-nogroup": "Nem lehet létrehozni az adatok tárolásához szükséges $1 könyvtárat, mert a webszerver nem írhat a szülőkönyvtárba ($2).\n\nA telepítő nem tudta megállapíteni, hogy melyik felhasználói fiókon fut a webszerver.\nA folytatáshoz tedd írhatóvá ezen fiók (és más fiókok!) számára a következő könyvtárat: $3.\nUnix/Linux rendszereken tedd a következőt:\n\n
cd $2\nmkdir $3\nchmod a+w $3
", @@ -164,9 +149,6 @@ "config-mysql-engine": "Tárolómotor:", "config-mysql-innodb": "InnoDB", "config-mysql-engine-help": "A legtöbb esetben az '''InnoDB''' a legjobb választás, mivel megfelelően támogatja a párhuzamosságot.\n\nA '''MyISAM''' gyorsabb megoldás lehet egyfelhasználós vagy csak olvasható környezetekben, azonban a MyISAM-adatbázisok sokkal gyakrabban sérülnek meg, mint az InnoDB-adatbázisok.", - "config-mssql-auth": "Hitelesítés típusa:", - "config-mssql-sqlauth": "SQL Server hitelesítés", - "config-mssql-windowsauth": "Windows hitelesítés", "config-site-name": "A wiki neve:", "config-site-name-help": "A böngésző címsorában és még számos más helyen jelenik meg.", "config-site-name-blank": "Add meg az oldal nevét.", diff --git a/includes/installer/i18n/ia.json b/includes/installer/i18n/ia.json index 9e08055514..e5ebb42408 100644 --- a/includes/installer/i18n/ia.json +++ b/includes/installer/i18n/ia.json @@ -46,7 +46,11 @@ "config-restart": "Si, reinitia lo", "config-welcome": "=== Verificationes del ambiente ===\nVerificationes de base essera ora exequite pro determinar si iste ambiente es apte pro le installation de MediaWiki.\nNon oblida de includer iste information si tu cerca adjuta pro completar le installation.", "config-welcome-section-copyright": "=== Copyright and Terms ===\n\n$1\n\nIste programma es software libere; vos pote redistribuer lo e/o modificar lo sub le conditiones del Licentia Public General de GNU publicate per le Free Software Foundation; version 2 del Licentia, o (a vostre option) qualcunque version posterior.\n\nIste programma es distribuite in le sperantia que illo sia utile, ma '''sin garantia''', sin mesmo le implicite garantia de '''commercialisation''' o '''aptitude pro un proposito particular'''.\nVide le Licentia Public General de GNU pro plus detalios.\n\nVos deberea haber recipite [$2 un exemplar del Licentia Public General de GNU] con iste programma; si non, scribe al Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA, o [https://www.gnu.org/copyleft/gpl.html lege lo in linea].", - "config-sidebar": "* [https://www.mediawiki.org Pagina principal de MediaWiki]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents Guida pro usatores]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Contents Guida pro administratores]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ FAQ]\n----\n* Lege me\n* Notas de iste version\n* Conditiones de copia\n* Actualisation", + "config-sidebar": "* [https://www.mediawiki.org Pagina principal de MediaWiki]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents Guida pro usatores]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Contents Guida pro administratores]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ FAQ]", + "config-sidebar-readme": "Lege me", + "config-sidebar-relnotes": "Notas de iste version", + "config-sidebar-license": "Conditiones de copia", + "config-sidebar-upgrade": "Actualisation", "config-env-good": "Le ambiente ha essite verificate.\nTu pote installar MediaWiki.", "config-env-bad": "Le ambiente ha essite verificate.\nTu non pote installar MediaWiki.", "config-env-php": "PHP $1 es installate.", @@ -79,18 +83,14 @@ "config-uploads-not-safe": "'''Aviso:''' Le directorio predefinite pro files incargate $1 es vulnerabile al execution arbitrari de scripts.\nBen que MediaWiki verifica tote le files incargate contra le menacias de securitate, il es altemente recommendate [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Security#Upload_security remediar iste vulnerabilitate de securitate] ante de activar le incargamento de files.", "config-no-cli-uploads-check": "'''Attention:''' Le directorio predefinite pro files incargate ($1) non es verificate contra le vulnerabilitate\nal execution arbitrari de scripts durante le installation de CLI.", "config-brokenlibxml": "Vostre systema ha un combination de versiones de PHP e libxml2 que es defectuose e pote causar corruption celate de datos in MediaWiki e altere applicationes web.\nActualisa a libxml2 2.7.3 o plus recente ([https://bugs.php.net/bug.php?id=45996 problema reportate presso PHP]).\nInstallation abortate.", - "config-suhosin-max-value-length": "Suhosin es installate e limita parametro length de GET a $1 bytes.\nLe componente ResourceLoader de MediaWiki va contornar iste limite, ma isto prejudicara le rendimento.\nSi possibile, tu deberea mitter suhosin.get.max_value_length a 1024 o superior in php.ini, e mitter $wgResourceLoaderMaxQueryLength al mesme valor in LocalSettings.php.", + "config-suhosin-max-value-length": "Suhosin es installate e limita parametro length de GET a $1 bytes.\nMediaWiki require que suhosin.get.max_value_length sia al minus $2. Disactiva iste parametro, o augmenta iste valor a $3 in php.ini.", "config-using-32bit": "Attention: tu systema pare operar con integres de 32 bits. Isto [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:32-bit non es recommendate].", "config-db-type": "Typo de base de datos:", "config-db-host": "Servitor de base de datos:", "config-db-host-help": "Si tu servitor de base de datos es in un altere servitor, entra hic le nomine o adresse IP del servitor.\n\nSi tu usa un servitor web usate in commun, tu providitor deberea dar te le correcte nomine de servitor in su documentation.\n\nSi tu usa MySQL, le nomine \"localhost\" possibilemente non functiona como nomine de servitor. In tal caso, essaya \"127.0.0.1\", i.e. le adresse IP local.\n\nSi tu usa PostgreSQL, lassa iste campo vacue pro connecter via un \"socket\" de Unix.", - "config-db-host-oracle": "TNS del base de datos:", - "config-db-host-oracle-help": "Entra un [http://download.oracle.com/docs/cd/B28359_01/network.111/b28317/tnsnames.htm nomine Local Connect] valide; un file tnsnames.ora debe esser visibile a iste installation.
Si tu usa bibliothecas de cliente 10g o plus recente, tu pote anque usar le methodo de nomination [http://download.oracle.com/docs/cd/E11882_01/network.112/e10836/naming.htm Easy Connect].", "config-db-wiki-settings": "Identificar iste wiki", "config-db-name": "Nomine del base de datos (sin tractos de union):", "config-db-name-help": "Selige un nomine que identifica tu wiki.\nIllo non pote continer spatios.\n\nSi tu usa un servitor web usate in commun, tu providitor te fornira le nomine specific de un base de datos a usar, o te permitte crear un base de datos via un pannello de controlo.", - "config-db-name-oracle": "Schema del base de datos:", - "config-db-account-oracle-warn": "Il ha tres scenarios supportate pro le installation de Oracle como le base de datos de iste systema:\n\nSi tu vole crear un conto del base de datos como parte del processo de installation, per favor specifica un conto con le rolo SYSDBA como le conto del base de datos pro installation, e specifica le nomine e contrasigno desirate pro le conto de accesso per web. Alteremente tu pote crear le conto de accesso per web manualmente e specificar solmente iste conto (si illo ha le permissiones requisite pro crear le objectos de schema) o specifica duo contos differente, un con privilegios de creation e un conto restringite pro accesso per web.\n\nUn script pro crear un conto con le privilegios requisite se trova in le directorio \"maintenance/oracle/\" de iste installation. Non oblida que le uso de un conto restringite disactiva tote le capacitates de mantenentia in le conto predefinite.", "config-db-install-account": "Conto de usator pro installation", "config-db-username": "Nomine de usator del base de datos:", "config-db-password": "Contrasigno del base de datos:", @@ -109,37 +109,24 @@ "config-pg-test-error": "Impossibile connecter al base de datos '''$1''': $2", "config-sqlite-dir": "Directorio pro le datos de SQLite:", "config-sqlite-dir-help": "SQLite immagazina tote le datos in un sol file.\n\nLe directorio que tu forni debe permitter le accesso de scriptura al servitor web durante le installation.\n\nIllo '''non''' debe esser accessibile via web. Pro isto, nos non lo pone ubi tu files PHP es.\n\nLe installator scribera un file .htaccess insimul a illo, ma si isto falli, alcuno pote ganiar accesso directe a tu base de datos.\nIsto include le crude datos de usator (adresses de e-mail, contrasignos codificate) assi como versiones delite e altere datos restringite super le wiki.\n\nConsidera poner le base de datos in un loco completemente differente, per exemplo in /var/lib/mediawiki/yourwiki.", - "config-oracle-def-ts": "Spatio de tabellas predefinite:", - "config-oracle-temp-ts": "Spatio de tabellas temporari:", "config-type-mysql": "MariaDB, MySQL o compatibile", "config-type-postgres": "PostgreSQL", "config-type-sqlite": "SQLite", - "config-type-oracle": "Oracle", - "config-type-mssql": "Microsoft SQL Server", "config-support-info": "MediaWiki supporta le sequente systemas de base de datos:\n\n$1\n\nSi tu non vide hic infra le systema de base de datos que tu tenta usar, alora seque le instructiones ligate hic supra pro activar le supporto.", "config-dbsupport-mysql": "* [{{int:version-db-mariadb-url}} MariaDB] es le systema primari pro MediaWiki e le melio supportate. MediaWiki functiona anque con [{{int:version-db-mysql-url}} MySQL] e con [{{int:version-db-percona-url}} Percona Server], le quales es compatibile con MariaDB. ([https://www.php.net/manual/en/mysqli.installation.php Como compilar PHP con supporto de MySQL])", "config-dbsupport-postgres": "* [{{int:version-db-postgres-url}} PostgreSQL] es un systema de base de datos popular e open source, alternativa a MySQL. ([https://www.php.net/manual/en/pgsql.installation.php Como compilar PHP con supporto de PostgreSQL])", "config-dbsupport-sqlite": "* [{{int:version-db-sqlite-url}} SQLite] es un systema de base de datos legier que es multo ben supportate. ([https://www.php.net/manual/en/pdo.installation.php Como compilar PHP con supporto de SQLite], usa PDO)", - "config-dbsupport-oracle": "* [{{int:version-db-oracle-url}} Oracle] es un banca de datos commercial pro interprisas. ([https://www.php.net/manual/en/oci8.installation.php Como compilar PHP con supporto de OCI8])", - "config-dbsupport-mssql": "* [{{int:version-db-mssql-url}} Microsoft SQL Server] es un base de datos de interprisa commercial pro Windows. ([https://www.php.net/manual/en/sqlsrv.installation.php Como compilar PHP con supporto de SQLSRV])", "config-header-mysql": "Configuration de MariaDB/MySQL", "config-header-postgres": "Configuration de PostgreSQL", "config-header-sqlite": "Configuration de SQLite", - "config-header-oracle": "Configuration de Oracle", - "config-header-mssql": "Configuration de Microsoft SQL Server", "config-invalid-db-type": "Typo de base de datos invalide", "config-missing-db-name": "Es necessari entrar un valor pro \"{{int:config-db-name}}\".", "config-missing-db-host": "Es necessari entrar un valor pro \"{{int:config-db-host}}\".", - "config-missing-db-server-oracle": "Es necessari entrar un valor pro \"{{int:config-db-host-oracle}}\".", - "config-invalid-db-server-oracle": "TNS de base de datos \"$1\" invalide.\nUsa o \"TNS Name\" o un catena \"Easy Connect\". ([http://docs.oracle.com/cd/E11882_01/network.112/e10836/naming.htm Methodos de nomenclatura de Oracle])", "config-invalid-db-name": "Nomine de base de datos \"$1\" invalide.\nUsa solmente litteras ASCII (a-z, A-Z), numeros (0-9), characteres de sublineamento (_) e tractos de union (-).", "config-invalid-db-prefix": "Prefixo de base de datos \"$1\" invalide.\nUsa solmente litteras ASCII (a-z, A-Z), numeros (0-9), characteres de sublineamento (_) e tractos de union (-).", "config-connection-error": "$1.\n\nVerifica le servitor, nomine de usator e contrasigno e reproba. Si tu usa \"localhost\" como nomine de servitor, essaya substituer \"127.0.0.1\" (o vice versa).", "config-invalid-schema": "Schema invalide pro MediaWiki \"$1\".\nUsa solmente litteras ASCII (a-z, A-Z), numeros (0-9) e characteres de sublineamento (_).", - "config-db-sys-create-oracle": "Le installator supporta solmente le uso de un conto SYSDBA pro le creation de un nove conto.", - "config-db-sys-user-exists-oracle": "Le conto de usator \"$1\" ja existe. SYSDBA pote solmente esser usate pro le creation de un nove conto!", "config-postgres-old": "PostgreSQL $1 o plus recente es requirite, tu ha $2.", - "config-mssql-old": "Microsoft SQL Server $1 o plus recente es necessari. Tu ha $2.", "config-sqlite-name-help": "Selige un nomine que identifica tu wiki.\nNon usar spatios o tractos de union.\nIsto essera usate pro le nomine del file de datos de SQLite.", "config-sqlite-parent-unwritable-group": "Impossibile crear le directorio de datos $1, proque le directorio superjacente $2 non concede le accesso de scriptura al servitor web.\n\nLe installator ha determinate le usator sub que le servitor web es executate.\nConcede le accesso de scriptura in le directorio $3 a iste usator pro continuar.\nIn un systema Unix/Linux:\n\n
cd $2\nmkdir $3\nchgrp $4 $3\nchmod g+w $3
", "config-sqlite-parent-unwritable-nogroup": "Impossibile crear le directorio de datos $1, proque le directorio superjacente $2 non concede le accesso de scriptura al servitor web.\n\nLe installator non poteva determinar le usator sub que le servitor web es executate.\nConcede le accesso de scriptura in le directorio $3 a iste usator (e alteres!) pro continuar.\nIn un systema Unix/Linux:\n\n
cd $2\nmkdir $3\nchmod a+w $3
", @@ -164,11 +151,6 @@ "config-mysql-engine": "Motor de immagazinage:", "config-mysql-innodb": "InnoDB (recommendate)", "config-mysql-engine-help": "'''InnoDB''' es quasi sempre le melior option, post que illo ha bon supporto pro simultaneitate.\n\n'''MyISAM''' pote esser plus rapide in installationes a usator singule o a lectura solmente.\nLe bases de datos MyISAM tende a esser corrumpite plus frequentemente que le base de datos InnoDB.", - "config-mssql-auth": "Typo de authentication:", - "config-mssql-install-auth": "Selige le typo de authentication a usar pro connecter al base de datos durante le processo de installation.\nSi tu selige \"{{int:config-mssql-windowsauth}}\", le credentiales del usator que executa le servitor web essera usate.", - "config-mssql-web-auth": "Selige le typo de authentication que le servitor web usara pro connecter al base de datos durante le operation ordinari del wiki.\nSi tu selige \"{{int:config-mssql-windowsauth}}\", le credentiales del usator que executa le servitor web essera usate.", - "config-mssql-sqlauth": "Authentication per SQL Server", - "config-mssql-windowsauth": "Authentication per Windows", "config-site-name": "Nomine del wiki:", "config-site-name-help": "Isto apparera in le barra de titulo del navigator e in varie altere locos.", "config-site-name-blank": "Entra un nomine de sito.", diff --git a/includes/installer/i18n/id.json b/includes/installer/i18n/id.json index 2a9872955a..a14bef1d0a 100644 --- a/includes/installer/i18n/id.json +++ b/includes/installer/i18n/id.json @@ -57,12 +57,16 @@ "config-restart": "Ya, nyalakan ulang", "config-welcome": "=== Pengecekan lingkungan ===\nPengecekan dasar kini akan dilakukan untuk melihat apakah lingkungan ini memadai untuk instalasi MediaWiki.\nIngatlah untuk menyertakan informasi ini jika Anda mencari bantuan tentang cara menyelesaikan instalasi.", "config-welcome-section-copyright": "=== Hak cipta dan persyaratan ===\n\n$1\n\nProgram ini adalah perangkat lunak bebas; Anda dapat mendistribusikan dan/atau memodifikasinya di bawah persyaratan GNU General Public License seperti yang diterbitkan oleh Free Software Foundation; baik versi 2 lisensi, atau (sesuai pilihan Anda) versi yang lebih baru.\n\nProgram ini didistribusikan dengan harapan bahwa itu akan berguna, tetapi tanpa jaminan apa pun; bahkan tanpa jaminan tersirat untuk dapat diperjualbelikan atau sesuai untuk tujuan tertentu.\nLihat GNU General Public License untuk lebih jelasnya.\n\nAnda seharusnya telah menerima [$2 salinan dari GNU General Public License] bersama dengan program ini; jika tidak, kirimkan surat untuk Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA, atau [https://www.gnu.org/copyleft/gpl.html baca versi daring].", - "config-sidebar": "* [https://www.mediawiki.org/wiki/MediaWiki/id Situs MediaWiki]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents/id Pedoman Pengguna]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Contents/id Pedoman Administrator]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ/id FAQ]\n----\n* Read me\n* Release notes\n* Copying\n* Upgrading", + "config-sidebar": "* [https://www.mediawiki.org Halaman depan MediaWiki]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents Panduan Pengguna]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Contents Panduan Pengurus]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ Pertanyaan yang sering ditanyakan]", + "config-sidebar-readme": "Pelajari selengkapnya", + "config-sidebar-relnotes": "Catatan rilis", + "config-sidebar-license": "Menyalin", + "config-sidebar-upgrade": "Memperbarui", "config-env-good": "Kondisi telah diperiksa.\nAnda dapat menginstal MediaWiki.", "config-env-bad": "Kondisi telah diperiksa.\nAnda tidak dapat menginstal MediaWiki.", "config-env-php": "PHP $1 diinstal.", "config-env-hhvm": "HHVM $1 telah dipasang.", - "config-unicode-using-intl": "Menggunakan [https://pecl.php.net/intl ekstensi PECL intl] untuk normalisasi Unicode.", + "config-unicode-using-intl": "Menggunakan [https://php.net/manual/en/book.intl.php ekstensi internasional PHP] untuk normalisasi Unicode.", "config-unicode-pure-php-warning": "Peringatan: [https://pecl.php.net/intl intl Ekstensi PECL] tidak tersedia untuk menangani normalisasi Unicode, dikembalikan untuk melambatkan implementasi PHP asli.\nApabila Anda menjalankan situs dengan lalu-lintas tinggi, Anda harus membaca [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations normalisasi Unicode].", "config-unicode-update-warning": "Peringatan: Versi terinstal dari pembungkus normalisasi Unicode menggunakan versi lama pustaka [http://site.icu-project.org/ proyek ICU].\nAnda harus [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations meningkatkan versinya] jika ingin menggunakan Unicode.", "config-no-db": "Pengandar basis data yang sesuai tidak ditemukan! Anda perlu menginstal pengandar basis data untuk PHP.\n{{PLURAL:$2|Jenis|Jenis}} basis data yang didukung: $1.\n\nJika Anda mengompilasi PHP sendiri, ubahlah konfigurasinya dengan mengaktifkan klien basis data, misalnya menggunakan ./configure --with-mysqli.\nJika Anda menginstal PHP dari paket Debian atau Ubuntu, maka Anda juga perlu menginstal seperti paket php-mysql.", @@ -94,13 +98,9 @@ "config-db-type": "Jenis basis data:", "config-db-host": "Inang basis data:", "config-db-host-help": "Jika server basis data Anda berada di server yang berbeda, masukkan nama inang atau alamat IP di sini.\n\nJika Anda menggunakan inang web bersama, penyedia inang Anda harus memberikan nama inang yang benar di dokumentasi mereka.\n\nJika Anda menginstal pada server Windows dan menggunakan MySQL, \"localhost\" mungkin tidak dapat digunakan sebagai nama server. Jika demikian, coba \"127.0.0.1\" untuk alamat IP lokal.\n\nJika Anda menggunakan PostgreSQL, biarkan field ini kosong untuk menghubungkan lewat soket Unix.", - "config-db-host-oracle": "TNS basis data:", - "config-db-host-oracle-help": "Masukkan [http://download.oracle.com/docs/cd/B28359_01/network.111/b28317/tnsnames.htm Local Connect Name] yang sah; berkas tnsnames.ora harus dapat diakses oleh instalasi ini.
Jika Anda menggunakan pustaka klien 10g atau lebih baru, Anda juga dapat menggunakan metode penamaan [http://download.oracle.com/docs/cd/E11882_01/network.112/e10836/naming.htm Easy Connect].", "config-db-wiki-settings": "Identifikasi wiki ini", - "config-db-name": "Nama basis data:", + "config-db-name": "Nama basis data (tanpa tanda hubung):", "config-db-name-help": "Pilih nama yang mengidentifikasikan wiki Anda.\nNama tersebut tidak boleh mengandung spasi.\n\nJika Anda menggunakan inang web bersama, penyedia inang Anda dapat memberikan Anda nama basis data khusus untuk digunakan atau mengizinkan Anda membuat basis data melalui panel kontrol.", - "config-db-name-oracle": "Skema basis data:", - "config-db-account-oracle-warn": "Ada tiga skenario yang didukung untuk instalasi Oracle sebagai basis data pendukung:\n\nJika Anda ingin membuat akun basis data sebagai bagian dari proses instalasi, silakan masukkan akun dengan peran SYSDBA sebagai akun basis data untuk instalasi dan tentukan kredensial yang diinginkan untuk akun akses web. Jika tidak, Anda dapat membuat akun akses web secara manual dan hanya memberikan akun tersebut (jika memiliki izin yang diperlukan untuk membuat objek skema) atau memasukkan dua akun yang berbeda, satu dengan hak membuat objek dan satu dibatasi untuk akses web.\n\nSkrip untuk membuat akun dengan privilese yang diperlukan dapat ditemukan pada direktori \"maintenance/oracle/\" instalasi ini. Harap diingat bahwa penggunaan akun terbatas akan menonaktifkan semua kemampuan pemeliharaan dengan akun bawaan.", "config-db-install-account": "Akun pengguna untuk instalasi", "config-db-username": "Nama pengguna basis data:", "config-db-password": "Kata sandi basis data:", @@ -110,46 +110,33 @@ "config-db-account-lock": "Gunakan nama pengguna dan kata sandi yang sama selama operasi normal", "config-db-wiki-account": "Akun pengguna untuk operasi normal", "config-db-wiki-help": "Masukkan nama pengguna dan sandi yang akan digunakan untuk terhubung ke basis data wiki selama operasi normal.\nJika akun tidak ada, akun instalasi memiliki hak yang memadai, akun pengguna ini akan dibuat dengan hak akses minimum yang diperlukan untuk mengoperasikan wiki.", - "config-db-prefix": "Prefiks tabel basis data:", + "config-db-prefix": "Prefiks tabel basis data (tanpa tanda hubung):", "config-db-prefix-help": "Jika Anda perlu berbagi satu basis data di antara beberapa wiki, atau antara MediaWiki dan aplikasi web lain, Anda dapat memilih untuk menambahkan prefiks terhadap semua nama tabel demi menghindari konflik.\nJangan gunakan spasi.\n\nPrefiks ini biasanya dibiarkan kosong.", "config-mysql-old": "MySQL $1 atau versi terbaru diperlukan, Anda menggunakan $2.", "config-db-port": "Porta basis data:", - "config-db-schema": "Skema untuk MediaWiki", + "config-db-schema": "Skema untuk MediaWiki (tanpa tanda hubung):", "config-db-schema-help": "Skema ini biasanya berjalan baik.\nUbah hanya jika Anda tahu Anda perlu mengubahnya.", "config-pg-test-error": "Tidak dapat terhubung ke basis data $1: $2", "config-sqlite-dir": "Direktori data SQLite:", "config-sqlite-dir-help": "SQLite menyimpan semua data dalam satu berkas.\n\nDirektori yang Anda berikan harus dapat ditulisi oleh server web selama instalasi.\n\nDirektori itu '''tidak''' boleh dapat diakses melalui web, inilah sebabnya kami tidak menempatkannya bersama dengan berkas PHP lain.\n\nPenginstal akan membuat berkas .htaccess bersamaan dengan itu, tetapi jika gagal, orang dapat memperoleh akses ke basis data mentah Anda.\nItu termasuk data mentah pengguna (alamat surel, hash sandi) serta revisi yang dihapus dan data lainnya yang dibatasi pada wiki.\n\nPertimbangkan untuk menempatkan basis data di tempat lain, misalnya di /var/lib/mediawiki/yourwiki.", - "config-oracle-def-ts": "Tablespace bawaan:", - "config-oracle-temp-ts": "Tablespace sementara:", - "config-type-mysql": "MySQL (atau yang kompatibel)", + "config-type-mysql": "MariaDB, MySQL, atau yang kompatibel", "config-type-postgres": "PostgreSQL", "config-type-sqlite": "SQLite", - "config-type-oracle": "Oracle", - "config-type-mssql": "Microsoft SQL Server", "config-support-info": "MediaWiki mendukung sistem basis data berikut:\n\n$1\n\nJika Anda tidak melihat sistem basis data yang Anda gunakan tercantum di bawah ini, ikuti petunjuk terkait di atas untuk mengaktifkan dukungan.", "config-dbsupport-mysql": "* [{{int:version-db-mysql-url}} MySQL] adalah target utama MediaWiki dan memiliki dukungan terbaik. MediaWiki juga berjalan dengan [{{int:version-db-mariadb-url}} MariaDB] dan [{{int:version-db-percona-url}} Server Percona], yang kompatibel dengan MySQL. ([https://www.php.net/manual/en/mysql.installation.php Cara mengompilasi PHP dengan dukungan MySQL])", "config-dbsupport-postgres": "* [{{int:version-db-postgres-url}} PostgreSQL] adalah sistem basis data sumber terbuka populer sebagai alternatif MySQL.([https://www.php.net/manual/en/pgsql.installation.php Bagaimana mengompilasikan PHP dengan dukungan PostgreSQL])", - "config-dbsupport-sqlite": "* [{{int:version-db-sqlite-url}} SQLite] adalah sistem basis data yang ringan yang sangat baik dukungannya. ([http://www.php.net/manual/en/pdo.installation.php cara mengompilasi PHP dengan dukungan SQLite], menggunakan PDO)", - "config-dbsupport-oracle": "* [{{int:version-db-oracle-url}} Oracle] adalah basis data komersial untuk perusahaan. ([http://www.php.net/manual/en/oci8.installation.php cara mengompilasi PHP dengan dukungan OCI8])", - "config-dbsupport-mssql": "[{{int:version-db-mssql-url}} Microsoft SQL Server] adalah database perusahaan komersial untuk Windows. ([https://www.php.net/manual/en/sqlsrv.installation.php Bagaimana cara mengkompilasi PHP dengan dukungan SQLSRV])", + "config-dbsupport-sqlite": "* [{{int:version-db-sqlite-url}} SQLite] adalah sistem basis data yang ringan yang sangat baik dukungannya. ([http://www.php.net/manual/en/pdo.installation.php Bagaimana mengompilasi PHP dengan dukungan SQLite], menggunakan PDO)", "config-header-mysql": "Pengaturan MariaDB/MySQL", "config-header-postgres": "Pengaturan PostgreSQL", "config-header-sqlite": "Pengaturan SQLite", - "config-header-oracle": "Pengaturan Oracle", - "config-header-mssql": "Setelan Microsoft SQL Server", "config-invalid-db-type": "Jenis basis data tidak sah", "config-missing-db-name": "Anda harus memasukkan nilai untuk \"{{int:config-db-name}}\"", "config-missing-db-host": "Anda harus memasukkan nilai untuk \"{{int:config-db-host}}\"", - "config-missing-db-server-oracle": "Anda harus memasukkan nilai untuk \"{{int:config-db-host-oracle}}\"", - "config-invalid-db-server-oracle": "TNS basis data \"$1\" tidak sah.\nGunakan baik \"Nama TNS\" atau string \"Easy Connect\" ([http://docs.oracle.com/cd/E11882_01/network.112/e10836/naming.htm Metode Penamaan Oracle]).", "config-invalid-db-name": "Nama basis data \"$1\" tidak sah.\nGunakan hanya huruf ASCII (a-z, A-Z), angka (0-9), garis bawah (_), dan tanda hubung (-).", "config-invalid-db-prefix": "Prefiks basis data \"$1\" tidak sah.\nGunakan hanya huruf ASCII (a-z, A-Z), angka (0-9), garis bawah (_), dan tanda hubung (-).", - "config-connection-error": "$1.\n\nPeriksa nama inang, pengguna, dan sandi di bawah ini dan coba lagi.", + "config-connection-error": "$1.\n\nPeriksa nama inang, pengguna, dan kata sandi dan coba lagi. Jika menggunakan \"localhost\" sebagai inang basis data, coba gunakan \"127.0.0.1\" (atau sebaliknya).", "config-invalid-schema": "Skema MediaWiki \"$1\" tidak sah.\nGunakan hanya huruf ASCII (a-z, A-Z), angka (0-9), dan garis bawah (_).", - "config-db-sys-create-oracle": "Penginstal hanya mendukung penggunaan akun SYSDBA untuk membuat akun baru.", - "config-db-sys-user-exists-oracle": "Akun pengguna \"$1\"sudah ada. SYSDBA hanya dapat digunakan untuk membuat akun baru!", "config-postgres-old": "PostgreSQL $1 atau versi terbaru diperlukan, Anda menggunakan $2.", - "config-mssql-old": "Microsoft SQL Server $1 atau yang lebih baru dibutuhkan. Anda memiliki versi $2.", "config-sqlite-name-help": "Pilih nama yang mengidentifikasi wiki Anda.\nJangan gunakan spasi atau tanda hubung.\nNama ini akan digunakan untuk nama berkas data SQLite.", "config-sqlite-parent-unwritable-group": "Tidak dapat membuat direktori data $1, karena direktori induk $2 tidak bisa ditulisi oleh server web.\n\nPenginstal telah menentukan pengguna yang menjalankan server web Anda.\nBuat direktori $3 menjadi dapat ditulisi olehnya.\nPada sistem Unix/Linux lakukan hal berikut:\n\n
cd $2\nmkdir $3\nchgrp $4 $3\nchmod g+w $3
", "config-sqlite-parent-unwritable-nogroup": "Tidak dapat membuat direktori data $1, karena direktori induk $2 tidak bisa ditulisi oleh server web.\n\nPenginstal tidak dapat menentukan pengguna yang menjalankan server web Anda.\nBuat direktori $3 menjadi dapat ditulisi oleh semua orang.\nPada sistem Unix/Linux lakukan hal berikut:\n\n
cd $2\nmkdir $3\nchmod a+w $3
", @@ -160,6 +147,7 @@ "config-sqlite-cant-create-db": "Tidak dapat membuat berkas basis data $1.", "config-sqlite-fts3-downgrade": "PHP tidak memiliki dukungan FTS3, tabel dituruntarafkan.", "config-can-upgrade": "Ada tabel MediaWiki di basis dataini.\nUntuk memperbaruinya ke MediaWiki $1, klik '''Lanjut'''.", + "config-upgrade-error": "Terjadi sebuah galat ketika memperbarui tabel MediaWiki dalam basis data Anda.\n\nUntuk informasi lebih lanjut, lihat catatan di atas, untuk mencoba kembali klik Lanjutkan.", "config-upgrade-done": "Pemutakhiran selesai.\n\nAnda sekarang dapat [$1 mulai menggunakan wiki Anda].\n\nJika Anda ingin membuat ulang berkas LocalSettings.php, klik tombol di bawah ini.\nTindakan ini '''tidak dianjurkan''' kecuali jika Anda mengalami masalah dengan wiki Anda.", "config-upgrade-done-no-regenerate": "Pemutakhiran selesai.\n\nAnda sekarang dapat [$1 mulai menggunakan wiki Anda].", "config-regenerate": "Regenerasi LocalSettings.php →", @@ -171,13 +159,8 @@ "config-db-web-create": "Buat akun jika belum ada", "config-db-web-no-create-privs": "Akun Anda berikan untuk instalasi tidak memiliki hak yang cukup untuk membuat akun.\nAkun yang Anda berikan harus sudah ada.", "config-mysql-engine": "Mesin penyimpanan:", - "config-mysql-innodb": "InnoDB", + "config-mysql-innodb": "InnoDB (disarankan)", "config-mysql-engine-help": "'''InnoDB''' hampir selalu merupakan pilihan terbaik karena memiliki dukungan konkurensi yang baik.\n\n'''MyISAM''' mungkin lebih cepat dalam instalasi pengguna-tunggal atau hanya-baca.\nBasis data MyISAM cenderung lebih sering rusak daripada basis data InnoDB.", - "config-mssql-auth": "Jenis otentikasi:", - "config-mssql-install-auth": "Pilih jenis otentikasi yang akan digunakan untuk menyambung ke database selama proses instalasi.\nJika Anda memilih \"{{int:config-mssql-windowsauth}}\", kredensial dari pengguna apapun pada server web yang berjalan akan digunakan.", - "config-mssql-web-auth": "Pilih jenis otentikasi yang akan digunakan oleh server web untuk menyambung ke server basis data, selama operasi biasa dari wiki.\nJika Anda memilih \"{{int:config-mssql-windowsauth}}\", kredensial dari pengguna apapun pada server web yang berjalan akan digunakan.", - "config-mssql-sqlauth": "Otentikasi Server SQL", - "config-mssql-windowsauth": "Otentikasi Windows", "config-site-name": "Nama wiki:", "config-site-name-help": "Ini akan muncul di bilah judul peramban dan di berbagai tempat lainnya.", "config-site-name-blank": "Masukkan nama situs.", @@ -305,9 +288,12 @@ "config-install-subscribe-fail": "Tidak dapat berlangganan mediawiki-announce: $1", "config-install-subscribe-notpossible": "cURL tidak diinstal dan allow_url_fopen tidak tersedia.", "config-install-mainpage": "Membuat halaman utama dengan konten bawaan", + "config-install-mainpage-exists": "Halaman utama sudah ada, meloncati", "config-install-extension-tables": "Pembuatan tabel untuk ekstensi yang diaktifkan", "config-install-mainpage-failed": "Tidak dapat membuat halaman utama: $1", "config-install-done": "Selamat!\nAnda telah berhasil menginstal MediaWiki.\n\nPemasang telah membuat sebuah berkas LocalSettings.php.\nBerkas itu berisi semua setelan Anda.\n\nAnda perlu mengunduh berkas itu dan meletakkannya di direktori instalasi wiki (direktori yang sama dengan index.php). Pengunduhan akan dimulai secara otomatis.\n\nJika pengunduhan tidak terjadi, atau jika Anda membatalkannya, Anda dapat mengulangi pengunduhan dengan mengeklik tautan berikut:\n\n$3\n\nCatatan: Jika Anda tidak melakukannya sekarang, berkas konfigurasi yang dihasilkan ini tidak akan tersedia lagi setelah Anda keluar dari proses instalasi tanpa mengunduhnya.\n\nSetelah melakukannya, Anda dapat [$2 memasuki wiki Anda].", + "config-install-success": "MediaWiki telah dipasang dengan sukses. Anda dapat mengunjungi <$1$2> untuk melihat wiki ini. Jika Anda memiliki pertanyaan, lihat daftar pertanyaan yang sering ditanyakan: atau gunakan salah satu forum yang ada di halaman tersebut.", + "config-install-db-success": "Basis data telah sukses diatur", "config-download-localsettings": "Unduh LocalSettings.php", "config-help": "bantuan", "config-help-tooltip": "klik untuk memperluas", @@ -316,6 +302,7 @@ "config-skins-screenshots": "$1 (tangkapan layar: $2)", "config-extensions-requires": "$1 (memerlukan $2)", "config-screenshot": "tangkapan layar", + "config-extension-not-found": "Tidak dapat menemukan berkas registrasi untuk ekstensi \"$1\"", "mainpagetext": "MediaWiki telah terpasang dengan sukses.", "mainpagedocfooter": "Konsultasikan [https://www.mediawiki.org/wiki/Help:Contents Panduan Pengguna] untuk cara penggunaan perangkat lunak wiki ini.\n\n== Memulai ==\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings Daftar pengaturan konfigurasi]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ Pertanyaan yang sering diajukan mengenai MediaWiki]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Milis rilis MediaWiki]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation#Translation_resources Pelokalan MediaWiki untuk bahasa Anda]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Combating_spam Belajar bagaimana menghadapi spam di wiki lokal]" } diff --git a/includes/installer/i18n/io.json b/includes/installer/i18n/io.json index fac0ad7ebc..3ce3fe2591 100644 --- a/includes/installer/i18n/io.json +++ b/includes/installer/i18n/io.json @@ -12,6 +12,7 @@ "config-localsettings-upgrade": "L'arkivo LocalSettings.php trovesis.\nPor plubonigar l'instaluro, voluntez informar la valoro dil $wgUpgradeKey en l'infra buxo.\nVu trovos ol en LocalSettings.php.", "config-session-error": "Eroro dum komenco di seciono: $1", "config-session-expired": "Vua sesiono probable finis.\nSesioni programesis por durar $1\nVu povas augmentar to per modifiko di session.gc_maxlifetime en php.ini.\nRikomencez l'instalo-procedo.", + "config-no-session": "La dati pri vua sesiono desaparis!\nVerifikez vua arkivo php.ini e certigez su pri session.save_path indikas la korekta libro di adresaro.", "config-your-language": "Vua idiomo:", "config-your-language-help": "Selektez l'idiomo por uzar dum l'instalo-procedo.", "config-wiki-language": "Wiki linguo:", @@ -36,26 +37,38 @@ "config-help-restart": "Ka vu deziras efacar omna dati qui vu sparis, e rikomencar la procedi pri instalo?", "config-restart": "Yes, rikomencez ol", "config-welcome-section-copyright": "=== Autoroyuro e termini ===\n\n$1\n\nCa informatikoprogramo esas libera; vu povas ridistributar ol e/o modifikar ol segun la termini de la Generala Licenco Publika GNU, quale stipulata da Free Software Foundation; segun lua versiono 2 o sequanta.\n\nNi expektas ke ca programo esas utila, kande ni distributas ol. Tamen, ni ne povas grantar ke ol fakte esos utila. Ni anke ne povas afirmar ke ol esos vendebla o ke ol esos utila por specifika intenco.\nLektez la Generala Licenco Publika GNU por plusa detali.\n\nKune ica programo vu certe recevis [$2 kopiuro di la Generala Licenco Publika GNU]. Se vu ne recevis ol, voluntez skribar a la fonduro Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA, o [https://www.gnu.org/copyleft/gpl.html lektez ol che l'interreto].", + "config-sidebar-readme": "Lektez me", + "config-sidebar-relnotes": "Informi pri la versiono", + "config-sidebar-license": "Kopiuro", + "config-sidebar-upgrade": "Nova versiono", "config-env-good": "Omno verifikesis.\nVu povas intalar MediaWiki.", "config-env-bad": "Omno verifikesis.\nVu NE POVAS intalar MediaWiki.", "config-env-php": "PHP $1 instalesis.", "config-env-hhvm": "HHVM $1 instalesis.", + "config-unicode-using-intl": "Uzanta [https://php.net/manual/en/book.intl.php PHP intl extension] por normaligo Unicode.", "config-unicode-pure-php-warning": "Atencez: La [https://php.net/manual/en/book.intl.php prolonguro PHP intl] ne esas disponebla por traktar skribo-normaligo \"Unicode\". Vice, uzesas la plu lenta laborado en pura PHP.\nSe vu administras pagini multe vizitata, vu mustas lektar la [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations skribo-normaligo Unicode].", + "config-unicode-update-warning": "Atencez: La versiono di ''Unicode normalization wrapper'' instalata en vua komputero uzas obsoleta versiono di la biblioteko del [http://site.icu-project.org/ projeto ICU].\nVu mustas instalar [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations la nova versiono] se vu deziras uzar Unicode.", "config-memory-raised": "Parametro memory_limit esas $1, modifikata a $2.", "config-memory-bad": "Atences: la limito por PHP memory_limit esas $1.\nTo probable esas nesuficanta.\nL'instalo-procedo povas faliar!", "config-apc": "[https://www.php.net/apc APC] instalesis", "config-apcu": "[https://www.php.net/apcu APCu] instalesis", "config-wincache": "[https://www.iis.net/downloads/microsoft/wincache-extension WinCache] instalesis", + "config-no-uri": "Eroro: Ne esis posibla identifikar l'existanta URI.\nInstalo interruptata.", + "config-no-cli-uri": "Avizo: Nula --scriptpath informata. Uzanta la preexistanta: $1.", + "config-using-server": "Uzanta ret-adreso (URL) dil servero \"$1\".", "config-using-uri": "Ret-adreso (URL) dil servero \"$1$2\".", + "config-db-type": "Tipo di datumaro:", + "config-db-host": "Loko dil datumaro:", "config-db-wiki-settings": "Identifikez ca wiki", "config-db-name": "Nomo dil datumaro (sen strekteti):", "config-db-install-account": "Konto dil uzero por instalo", "config-db-username": "Uzero-nomo dil datumaro:", "config-db-password": "Pasovorto dil datumaro:", - "config-type-mssql": "Microsoft SQL Server", - "config-header-oracle": "Ajusti por Oracle-sistemo:", - "config-header-mssql": "Ajusti por Microsoft SQL Server", "config-invalid-db-type": "Nevalida tipo di datumaro.", + "config-missing-db-name": "Vu mustas informar valoro por \"{{int:config-db-name}}\".", + "config-missing-db-host": "Vu mustas informar valoro por \"{{int:config-db-host}}\".", + "config-sqlite-readonly": "L'arkivo $1 esas nur lektebla.", + "config-sqlite-cant-create-db": "Ne povis krear l'arkivo di datumaro $1.", "config-ns-generic": "Projeto", "config-ns-site-name": "Sama kam la wiki-nomo: $1", "config-ns-other": "Altra (definez precise)", diff --git a/includes/installer/i18n/is.json b/includes/installer/i18n/is.json index 59c23175a4..7c4a0fad33 100644 --- a/includes/installer/i18n/is.json +++ b/includes/installer/i18n/is.json @@ -42,7 +42,6 @@ "config-db-type": "Tegund gagnagrunns:", "config-db-host": "Netþjónn gagnagrunns:", "config-db-name": "Heiti gagnagrunns (engin bandstrik):", - "config-db-name-oracle": "Gagnagrunnsskema:", "config-db-username": "Notandanafn á gagnagrunni:", "config-db-password": "Lykilorð gagnagrunns:", "config-db-port": "Gátt gagnagrunns:", @@ -50,21 +49,14 @@ "config-type-mysql": "MariaDB, MySQL, eða samhæft", "config-type-postgres": "PostgreSQL", "config-type-sqlite": "SQLite", - "config-type-oracle": "Oracle", - "config-type-mssql": "Microsoft SQL Server", "config-header-mysql": "Stillingar MariaDB/MySQL", "config-header-postgres": "Stillingar PostgreSQL", "config-header-sqlite": "Stillingar SQLite", - "config-header-oracle": "Stillingar Oracle", - "config-header-mssql": "Stillingar Microsoft SQL Server", "config-regenerate": "Endurgera LocalSettings.php →", "config-show-table-status": "SHOW TABLE STATUS beiðni mistókst!", "config-db-web-account": "Gagnagrunnsreikningur fyrir vefaðgang", "config-mysql-engine": "Gagnagrunnshýsing:", "config-mysql-innodb": "InnoDB (mælt með)", - "config-mssql-auth": "Tegund auðkenningar:", - "config-mssql-sqlauth": "SQL Server auðkenning", - "config-mssql-windowsauth": "Windows auðkenning", "config-ns-generic": "Verkefni", "config-admin-name": "Notandanafnið þitt:", "config-admin-password": "Lykilorð:", diff --git a/includes/installer/i18n/it.json b/includes/installer/i18n/it.json index 165a0570c5..f385ab571c 100644 --- a/includes/installer/i18n/it.json +++ b/includes/installer/i18n/it.json @@ -101,18 +101,14 @@ "config-uploads-not-safe": "Attenzione: la directory predefinita per i caricamenti $1 è vulnerabile all'esecuzione arbitraria di script.\nAnche se, a difesa della sicurezza, MediaWiki controlla tutti i file caricati, è fortemente raccomandato di [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Security#Upload_security chiudere questa minaccia] prima di abilitare i caricamenti.", "config-no-cli-uploads-check": "Attenzione: la directory predefinita per i caricamenti ($1) non è stata verificata per la vulnerabilità sull'esecuzione arbitraria di script durante l'installazione da linea di comando.", "config-brokenlibxml": "Il tuo sistema ha una combinazione di versioni di PHP e libxml2 che è difettosa e che può provocare un danneggiamento non visibile di dati in MediaWiki ed in altre applicazioni per il web.\nAggiorna a libxml2 2.7.3 o successivo ([https://bugs.php.net/bug.php?id=45996 il bug è studiato dal lato PHP]).\nInstallazione interrotta.", - "config-suhosin-max-value-length": "Suhosin è installato e limita il parametro GET length a $1 byte.\nIl componente MediaWiki ResourceLoader funzionerà aggirando questo limite, ma riducendo le prestazioni.\nSe possibile, dovresti impostare suhosin.get.max_value_length a 1024 o superiore in php.ini, ed impostare $wgResourceLoaderMaxQueryLength allo stesso valore in LocalSettings.php.", + "config-suhosin-max-value-length": "Suhosin è installato e limita il parametro GET length a $1 byte.\nMediaWiki richiede un valore per suhosin.get.max_value_length di almeno $2. Disattiva questa impostazione, o aumenta questo valore a $3 in php.ini.", "config-using-32bit": "Attenzione sembra che il tuo sistema utilizzi interi a 32-bit, ciò [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:32-bit non è raccomandato].", "config-db-type": "Tipo di database:", "config-db-host": "Host del database:", "config-db-host-help": "Se il server del tuo database è su un server diverso, immetti qui il nome dell'host o il suo indirizzo IP.\n\nSe stai utilizzando un web hosting condiviso, il tuo hosting provider dovrebbe fornirti il nome host corretto nella sua documentazione.\n\nSe stai utilizzando MySQL, l'uso di \"localhost\" potrebbe non funzionare correttamente come nome del server. In caso di problemi, prova a impostare \"127.0.0.1\" come indirizzo IP locale.\n\nSe usi PostgreSQL, lascia questo campo vuoto per consentire di connettersi tramite un socket Unix.", - "config-db-host-oracle": "TNS del database:", - "config-db-host-oracle-help": "Inserisci un valido [http://download.oracle.com/docs/cd/B28359_01/network.111/b28317/tnsnames.htm Local Connect Name]; un file tnsnames.ora deve essere visibile a questa installazione.
Se stai usando la libreria cliente 10g o più recente puoi anche usare il metodo di denominazione [http://download.oracle.com/docs/cd/E11882_01/network.112/e10836/naming.htm Easy Connect].", "config-db-wiki-settings": "Identifica questo wiki", "config-db-name": "Nome del database (no trattini):", "config-db-name-help": "Scegli un nome che identifica il tuo wiki.\nNon deve contenere spazi.\n\nSe utilizzi un web hosting condiviso, il tuo hosting provider o ti fornisce uno specifico nome di database da utilizzare, oppure ti consentirà di creare il database tramite un pannello di controllo.", - "config-db-name-oracle": "Schema del database:", - "config-db-account-oracle-warn": "Ci sono tre scenari supportati per l'installazione di Oracle come database di backend:\n\nSe vuoi creare un'utenza di database come parte del processo di installazione, fornisci un account con ruolo SYSDBA come utenza di database per l'installazione e specifica le credenziali volute per l'utenza di accesso web, altrimenti è possibile creare manualmente l'utenza di accesso web e fornire solo quell'account (se dispone delle autorizzazioni necessario per creare gli oggetti dello schema) o fornire due diverse utenze, una con i permessi di creazione e una per l'accesso web.\n\nScript per la creazione di un'utenza con le autorizzazioni necessarie può essere trovato nella directory \"maintenance/oracle/\" di questa installazione. Tieni presente che l'uso di un'utenza con restrizioni disabiliterà tutte le funzionalità di manutenzione con l'account predefinito.", "config-db-install-account": "Account utente per l'installazione", "config-db-username": "Nome utente del database:", "config-db-password": "Password del database:", @@ -131,34 +127,22 @@ "config-pg-test-error": "Impossibile connettersi al database '''$1''': $2", "config-sqlite-dir": "Directory data di SQLite:", "config-sqlite-dir-help": "SQLite memorizza tutti i dati in un unico file.\n\nLa directory che indicherai deve essere scrivibile dal server web durante l'installazione.\n\nDovrebbe essere non accessibile via web, è per questo che non la stiamo mettendo dove ci sono i file PHP.\n\nL'installatore scriverà insieme ad essa un file .htaccess, ma se il tentativo fallisse qualcuno potrebbe avere accesso al database grezzo.\nQuesto include dati utente grezzi (indirizzi, password cifrate) così come versioni eliminate e altri dati ad accesso limitato del wiki.\n\nConsidera l'opportunità di sistemare allo stesso tempo il database da qualche altra parte, per esempio in /var/lib/mediawiki/tuowiki.", - "config-oracle-def-ts": "Tablespace di default:", - "config-oracle-temp-ts": "Tablespace temporaneo:", "config-type-mysql": "MariaDB, MySQL o compatibile", - "config-type-mssql": "Microsoft SQL Server", "config-support-info": "MediaWiki supporta i seguenti sistemi di database:\n\n$1\n\nSe fra quelli elencati qui sotto non vedi il sistema di database che vorresti utilizzare, seguire le istruzioni linkate sopra per abilitare il supporto.", "config-dbsupport-mysql": "* [{{int:version-db-mariadb-url}} MariaDB] è la configurazione preferibile per MediaWiki ed è quella meglio supportata. MediaWiki funziona anche con [{{int:version-db-mysql-url}} MySQL] e [{{int:version-db-percona-url}} Percona Server], che sono compatibili con MariaDB.([https://www.php.net/manual/en/mysqli.installation.php Come compilare PHP con supporto MySQL])", "config-dbsupport-postgres": "* [{{int:version-db-postgres-url}} PostgreSQL] è un popolare sistema di database open source come alternativa a MySQL. ([https://www.php.net/manual/en/pgsql.installation.php Come compilare PHP con supporto PostgreSQL])", "config-dbsupport-sqlite": "* [{{int:version-db-sqlite-url}} SQLite] è un sistema di database leggero, che è supportato molto bene. ([https://www.php.net/manual/en/pdo.installation.php Come compilare PHP con supporto SQLite], utilizza PDO)", - "config-dbsupport-oracle": "* [{{int:version-db-oracle-url}} Oracle] è un database di un'impresa commerciale. ([https://www.php.net/manual/en/oci8.installation.php Come compilare PHP con supporto OCI8])", - "config-dbsupport-mssql": "* [{{int:version-db-mssql-url}} Microsoft SQL Server] è un database di un'impresa commerciale per Windows. ([https://www.php.net/manual/en/sqlsrv.installation.php Come compilare PHP con supporto SQLSRV])", "config-header-mysql": "Impostazioni MySQL", "config-header-postgres": "Impostazioni PostgreSQL", "config-header-sqlite": "Impostazioni SQLite", - "config-header-oracle": "Impostazioni Oracle", - "config-header-mssql": "Impostazioni di Microsoft SQL Server", "config-invalid-db-type": "Tipo di database non valido", "config-missing-db-name": "È necessario immettere un valore per \"{{int:config-db-name}}\".", "config-missing-db-host": "È necessario immettere un valore per \"{{int:config-db-host}}\".", - "config-missing-db-server-oracle": "È necessario immettere un valore per \"{{int:config-db-host-oracle}}\".", - "config-invalid-db-server-oracle": "TNS database \"$1\" non valido.\nUsa \"TNS Name\" o una stringa \"Easy Connect\" ([http://docs.oracle.com/cd/E11882_01/network.112/e10836/naming.htm Oracle Naming Methods]).", "config-invalid-db-name": "Nome di database \"$1\" non valido.\nUtilizza soltanto caratteri ASCII come lettere (a-z, A-Z), numeri (0-9), sottolineatura (_) e trattini (-).", "config-invalid-db-prefix": "Prefisso database \"$1\" non valido.\nUtilizza soltanto caratteri ASCII come lettere (a-z, A-Z), numeri (0-9), sottolineatura (_) e trattini (-).", "config-connection-error": "$1.\n\nControlla host, nome utente e password e prova ancora. Se stai usando \"localhost\" come host database, prova invece ad utilizzare \"127.0.0.1\" (o viceversa).", "config-invalid-schema": "Schema MediaWiki \"$1\" non valido.\nUsa solo lettere ASCII (a-z, A-Z), numeri (0-9) e caratteri di sottolineatura (_).", - "config-db-sys-create-oracle": "Il programma di installazione supporta solo l'utilizzo di un account SYSDBA per la creazione di un nuovo account.", - "config-db-sys-user-exists-oracle": "L'account utente \"$1\" esiste già. SYSDBA può essere usato solo per la creazione di un nuovo account!", "config-postgres-old": "PostgreSQL $1 o una versione successiva è necessaria, rilevata la $2.", - "config-mssql-old": "Si richiede Microsoft SQL Server $1 o successivo. Tu hai la versione $2.", "config-sqlite-name-help": "Scegli un nome che identifichi il tuo wiki.\nNon utilizzare spazi o trattini.\nQuesto servirà per il nome del file di dati SQLite.", "config-sqlite-parent-unwritable-group": "Non è possibile creare la directory dati $1, perché la directory superiore $2 non è scrivibile dal webserver.\n\nIl programma di installazione ha determinato l'utente con cui il server web è in esecuzione.\nForniscigli la possibilità di scrivere nella directory $3 per continuare.\nSu un sistema Unix/Linux:\n\n
cd $2\nmkdir $3\nchgrp $4 $3\nchmod g+w $3
", "config-sqlite-parent-unwritable-nogroup": "Non è possibile creare la directory dati $1, perché la directory superiore $2 non è scrivibile dal webserver.\n\nIl programma di installazione non ha potuto determinare l'utente con cui il server web è in esecuzione.\nFornisci ad esso (ed altri!) la possibilità di scrivere globalmente nella directory $3 per continuare.\nSu un sistema Unix/Linux:\n\n
cd $2\nmkdir $3\nchmod a+w $3
", @@ -183,11 +167,6 @@ "config-mysql-engine": "Storage engine:", "config-mysql-innodb": "InnoDB (consigliato)", "config-mysql-engine-help": "InnoDB è quasi sempre l'opzione migliore, in quanto ha un buon supporto della concorrenza.\n\nMyISAM potrebbe essere più veloce nelle installazioni monoutente o in sola lettura.\nI database MyISAM tendono a danneggiarsi più spesso dei database InnoDB.", - "config-mssql-auth": "Tipo di autenticazione:", - "config-mssql-install-auth": "Seleziona il tipo di autenticazione che verrà utilizzato per connettersi al database durante il processo di installazione.\nSe si seleziona \"{{int:config-mssql-windowsauth}}\", saranno utilizzate le credenziali dell'utente con cui viene eseguito il server web, qualunque esso sia.", - "config-mssql-web-auth": "Seleziona il tipo di autenticazione che il server web utilizzerà per connettersi al database, durante il normale funzionamento del wiki.\nSe si seleziona \"{{int:config-mssql-windowsauth}}\", saranno utilizzate le credenziali dell'utente con cui viene eseguito il server web, qualunque esso sia.", - "config-mssql-sqlauth": "Autenticazione di SQL Server", - "config-mssql-windowsauth": "Autenticazione di Windows", "config-site-name": "Nome del wiki:", "config-site-name-help": "Questo verrà visualizzato nella barra del titolo del browser e in vari altri posti.", "config-site-name-blank": "Inserisci il nome del sito.", diff --git a/includes/installer/i18n/ja.json b/includes/installer/i18n/ja.json index 94f3b9f009..511b0dafa7 100644 --- a/includes/installer/i18n/ja.json +++ b/includes/installer/i18n/ja.json @@ -104,18 +104,14 @@ "config-uploads-not-safe": "警告: アップロードの既定ディレクトリ $1 に、任意のスクリプト実行に関する脆弱性があります。\nMediaWiki はアップロードされたファイルのセキュリティ上の脅威を確認しますが、アップロードを有効化する前に、[https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Security#Upload_security このセキュリティ上の脆弱性を解決する]ことを強く推奨します。", "config-no-cli-uploads-check": "警告: アップロード用のデフォルトディレクトリ ($1) が、CLIでのインストール中に任意のスクリプト実行の脆弱性チェックを受けていません。", "config-brokenlibxml": "このシステムで使われているPHPとlibxml2のバージョンのこの組み合わせにはバグがあります。具体的には、MediaWikiやその他のウェブアプリケーションでhiddenデータが破損する可能性があります。\nlibxml2を2.7.3以降のバージョンにアップグレードしてください([https://bugs.php.net/bug.php?id=45996 PHPでのバグ情報])。\nインストールを終了します。", - "config-suhosin-max-value-length": "Suhosin がインストールされており、GET パラメーターの length を $1 バイトに制限しています。\nMediaWiki の ResourceLoader コンポーネントはこの制限を回避しますが、パフォーマンスは低下します。\n可能な限り、php.ini で suhosin.get.max_value_length を 1024 以上に設定し、同じ値を LocalSettings.php 内で $wgResourceLoaderMaxQueryLength に設定してください。", + "config-suhosin-max-value-length": "Suhosin がインストールされており、GET パラメーターの length を $1 バイトに制限しています。(訳注:\nMediaWiki の ResourceLoader コンポーネントはこの制限を回避しますが、パフォーマンスは低下します。)\n可能な限り、 suhosin.get.max_value_length を $2 以上に設定します。これを無効に変更するか、php.ini で $3 に増加してください。", "config-using-32bit": "警告:システムが32ビットで動作しているようです。 これは[https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:32-bit 非推奨]です。", "config-db-type": "データベースの種類:", "config-db-host": "データベースのホスト:", "config-db-host-help": "異なるサーバー上にデータベースサーバーがある場合、ホスト名またはIPアドレスをここに入力してください。\n\nもし、共有されたウェブホスティングを使用している場合、ホスティングプロバイダーは正確なホスト名を解説しているはずです。\n\nMySQLを使用している場合、「localhost」は、サーバー名としてはうまく働かないでしょう。もしそのような場合は、ローカルIPアドレスとして「127.0.0.1」を試してみてください。\n\nPostgreSQLを使用している場合、UNIXソケットで接続するにはこの欄を空欄のままにしてください。", - "config-db-host-oracle": "データベース TNS:", - "config-db-host-oracle-help": "有効な[http://download.oracle.com/docs/cd/B28359_01/network.111/b28317/tnsnames.htm ローカル接続名]を入力してください。tnsnames.ora ファイルは、このインストール先から参照できる場所に置いてください。
ご使用中のクライアント ライブラリが 10g 以降の場合、[http://download.oracle.com/docs/cd/E11882_01/network.112/e10836/naming.htm Easy Connect] ネーミング メソッドを使用できます。", "config-db-wiki-settings": "このウィキの識別情報", "config-db-name": "データベース名 (ハイフンは使用不可):", "config-db-name-help": "このウィキを識別する名前を入力してください。\n空白を含めることはできません。\n\n共有ウェブホストを利用している場合、ホスティングプロバイダーが特定の使用可能なデータベース名を提供するか、あるいは管理パネルからデータベースを作成できるようにしているでしょう。", - "config-db-name-oracle": "データベースのスキーマ:", - "config-db-account-oracle-warn": "バックエンドのデータベースとして Oracle をインストールする場合、3つのシナリオが考えられます。\n\nデータベース用のアカウントをインストールのプロセス途中で作成したい場合、インストールに使うデータベース用のアカウントしては SYSDBAロール付きのアカウントを指定し、ウェブアクセス用アカウントには必要なログイン情報を指定してください。あるいは、ウェブアクセス用のアカウントを手動で作成して、そのアカウント(スキーマオブジェクトの作成のパーミッションを要求する場合)だけを使うか、二つの異なるアカウントを用意して一つは特権を付与できるもの、もう一つをウェブアクセス用の制限アカウントとしてください。\n\n要求された特権でアカウントを作成するスクリプトは、このインストール環境では、\"maintenance/oracle/\" にあります。制限アカウントを使用することは、デフォルトアカウントでのすべてのメンテナンス特権を無効にすることにご注意ください。", "config-db-install-account": "インストールで使用する利用者アカウント", "config-db-username": "データベースのユーザー名:", "config-db-password": "データベースのパスワード:", @@ -134,37 +130,24 @@ "config-pg-test-error": "データベース $1 に接続できません: $2", "config-sqlite-dir": "SQLite データ ディレクトリ:", "config-sqlite-dir-help": "SQLite は単一のファイル内にすべてのデータを格納しています。\n\n指定したディレクトリは、インストール時にウェブ サーバーが書き込めるようにしておく必要があります。\n\nこのディレクトリはウェブからアクセス不可能である必要があります。PHP ファイルがある場所には配置できないのはこのためです。\n\nインストーラーは .htaccess ファイルにも書き込みます。しかし、これが失敗した場合は、誰かが生のデータベースにアクセスできてしまいます。\nデータベースは、生のデータ (メールアドレス、パスワードのハッシュ値) の他、削除された版、その他ウィキ上の制限されているデータを含んでいます。\n\n例えば /var/lib/mediawiki/yourwiki のように、別の場所にデータベースを配置することを検討してください。", - "config-oracle-def-ts": "既定のテーブル領域:", - "config-oracle-temp-ts": "一時的なテーブル領域:", "config-type-mysql": "MariaDB、MySQLまたは互換製品", "config-type-postgres": "PostgreSQL", "config-type-sqlite": "SQLite", - "config-type-oracle": "Oracle", - "config-type-mssql": "マイクロソフト SQL Server", "config-support-info": "MediaWiki は以下のデータベース システムに対応しています:\n\n$1\n\n使用しようとしているデータベース システムが下記の一覧にない場合は、上記リンク先の手順に従ってインストールしてください。", "config-dbsupport-mysql": "* [{{int:version-db-mariadb-url}} MariaDB]はMediaWikiの主要な対象であり、最もよくサポートされています。MediaWikiはMariaDBと互換性のある[{{int:version-db-mysql-url}} MySQL]、[{{int:version-db-percona-url}} Percona Server]でも動きます。 ([https://www.php.net/manual/ja/mysqli.installation.php PHPをMySQLサポート付きでコンパイルする方法])", "config-dbsupport-postgres": "* [{{int:version-db-postgres-url}} PostgreSQL] は、MySQLの代替として人気がある公開のデータベースシステムです。([https://www.php.net/manual/en/pgsql.installation.php PHPをPostgreSQLサポート付きでコンパイルする方法])", "config-dbsupport-sqlite": "* [{{int:version-db-sqlite-url}} SQLite]は、良くサポートされている、軽量データベースシステムです。([https://www.php.net/manual/ja/pdo.installation.php SQLiteに対応したPHPをコンパイルする方法]、PDOを使用)", - "config-dbsupport-oracle": "* [{{int:version-db-oracle-url}} Oracle]は商業企業のデータベースです。([https://www.php.net/manual/en/oci8.installation.php OCI8サポートなPHPをコンパイルする方法])", - "config-dbsupport-mssql": "* [{{int:version-db-mssql-url}} Microsoft SQL Server]は商業企業のWindows用データベースです。([https://www.php.net/manual/en/sqlsrv.installation.php SQLSRVサポートなPHPをコンパイルする方法])", "config-header-mysql": "MariaDB/MySQL の設定", "config-header-postgres": "PostgreSQL の設定", "config-header-sqlite": "SQLite の設定", - "config-header-oracle": "Oracle の設定", - "config-header-mssql": "Microsoft SQL Server の設定", "config-invalid-db-type": "データベースの種類が無効です。", "config-missing-db-name": "「{{int:config-db-name}}」を入力してください", "config-missing-db-host": "「{{int:config-db-host}}」を入力してください。", - "config-missing-db-server-oracle": "「{{int:config-db-host-oracle}}」の値を入力してください", - "config-invalid-db-server-oracle": "「$1」は無効なデータベース TNS です。\n「TNS 名」「Easy Connect」文字列のいずれかを使用してください ([http://docs.oracle.com/cd/E11882_01/network.112/e10836/naming.htm Oracle ネーミング メソッド])。", "config-invalid-db-name": "「$1」は無効なデータベース名です。\n半角の英数字 (a-z、A-Z、0-9)、アンダースコア (_)、ハイフン (-) のみを使用してください。", "config-invalid-db-prefix": "「$1」は無効なデータベース接頭辞です。\n半角の英数字 (a-z、A-Z、0-9)、アンダースコア (_)、ハイフン (-) のみを使用してください。", "config-connection-error": "$1。\n\n以下のホスト名、ユーザー名、パスワードを確認してから再度試してください。データベースホストとして「localhost」を使用している場合は、代わりに 「127.0.0.1」を使用してください(またはその逆)。", "config-invalid-schema": "「$1」は MediaWiki のスキーマとして無効です。\n半角の英数字 (a-z、A-Z、0-9)、アンダースコア (_) のみを使用してください。", - "config-db-sys-create-oracle": "インストーラーは、新規アカウント作成にはSYSDBAアカウントの利用のみをサポートしています。", - "config-db-sys-user-exists-oracle": "利用者アカウント「$1」は既に存在します。SYSDBA は新しいアカウントの作成のみに使用できます!", "config-postgres-old": "PostgreSQL $1 以降が必要です。ご使用中の PostgreSQL は $2 です。", - "config-mssql-old": "Microsoft SQL Server $1 以降が必要です。ご使用中の Microsoft SQL Server は $2 です。", "config-sqlite-name-help": "あなたのウェキと同一性のある名前を選んでください。\n空白およびハイフンは使用しないでください。\nSQLiteのデータファイル名として使用されます。", "config-sqlite-parent-unwritable-group": "データ ディレクトリ $1 を作成できません。ウェブ サーバーは親ディレクトリ $2 に書き込めませんでした。\n\nインストーラーは、ウェブ サーバーの実行ユーザーを特定しました。\n続行するには、ディレクトリ $3 に書き込めるようにしてください。\nUnix または Linux であれば、以下を実行してください:\n\n
cd $2\nmkdir $3\nchgrp $4 $3\nchmod g+w $3
", "config-sqlite-parent-unwritable-nogroup": "データ ディレクトリ $1 を作成できません。ウェブ サーバーは、親ディレクトリ $2 に書き込めませんでした。\n\nインストーラーは、ウェブ サーバーの実行ユーザーを特定できませんでした。\n続行するには、ディレクトリ $3 に、ウェブ サーバー (と、あらゆる人々!) がグローバルに書き込めるようにしてください。\nUnix または Linux では、以下を実行してください:\n\n
cd $2\nmkdir $3\nchmod a+w $3
", @@ -189,11 +172,6 @@ "config-mysql-engine": "ストレージ エンジン:", "config-mysql-innodb": "InnoDB(推奨)", "config-mysql-engine-help": "InnoDBは、並行処理のサポートに優れているので、ほとんどの場合において最良の選択肢です。\n\nMyISAMは、利用者が1人の場合、あるいは読み込み専用でインストールする場合に、より処理が早くなるでしょう。\nただし、MyISAMのデータベースは、InnoDBより高頻度で破損する傾向があります。", - "config-mssql-auth": "認証の種類:", - "config-mssql-install-auth": "インストール過程でデータベースに接続するために使用する認証の種類を選択してください。\n「{{int:config-mssql-windowsauth}}」を選択した場合、ウェブサーバーを実行しているユーザーの認証情報が使用されます。", - "config-mssql-web-auth": "ウィキの通常の操作の際にウェブサーバーがデータベースサーバーに接続するために使用する認証の種類を選択してください。\n「{{int:config-mssql-windowsauth}}」を選択した場合、ウェブサーバーを実行しているユーザーの認証情報が使用されます。", - "config-mssql-sqlauth": "SQL Server 認証", - "config-mssql-windowsauth": "Windows 認証", "config-site-name": "ウィキ名:", "config-site-name-help": "この欄に入力したウィキ名は、ブラウザーのタイトルバーなど様々な場所で利用されます。", "config-site-name-blank": "サイト名を入力してください。", diff --git a/includes/installer/i18n/ka.json b/includes/installer/i18n/ka.json index 9c4c3fb4e3..86b0f55527 100644 --- a/includes/installer/i18n/ka.json +++ b/includes/installer/i18n/ka.json @@ -31,18 +31,14 @@ "config-wincache": "[https://www.iis.net/downloads/microsoft/wincache-extension WinCache] დაყენდა", "config-diff3-bad": "GNU diff3 ვერ მოიძებნა.", "config-db-type": "მონაცემთა ბაზის ტიპი:", - "config-db-host-oracle": "მონაცემთა ბაზის TNS:", "config-db-name": "მონაცემთა ბაზის სახელი:", - "config-db-name-oracle": "მონაცემთა ბაზის სქემა:", "config-db-username": "მონაცემთა ბაზის მომხმარებლის სახელი:", "config-db-password": "მონაცემთა ბაზის პაროლი:", "config-db-port": "მონაცემთა ბაზის პორტი:", "config-db-schema": "მედიავიკის სქემა:", - "config-type-mssql": "Microsoft SQL Server", "config-header-mysql": "MySQL-ის პარამეტრები", "config-header-postgres": "PostgreSQL-ის პარამეტრები", "config-header-sqlite": "SQLite-ის პარამეტრები", - "config-header-oracle": "Oracle-ის პარამეტრები", "config-invalid-db-type": "არასწორი მონაცემთა ბაზის ტიპი", "config-sqlite-readonly": "ფაილი $1 ჩასაწერად მიუწვდომელია.", "config-mysql-innodb": "InnoDB", diff --git a/includes/installer/i18n/ko.json b/includes/installer/i18n/ko.json index a891d19361..e7008636c0 100644 --- a/includes/installer/i18n/ko.json +++ b/includes/installer/i18n/ko.json @@ -92,13 +92,9 @@ "config-db-type": "데이터베이스 종류:", "config-db-host": "데이터베이스 호스트:", "config-db-host-help": "데이터베이스 서버가 다른 서버에 있으면 여기에 호스트 이름이나 IP 주소를 입력하세요.\n\n공유하는 웹 호스팅을 사용하고 있으면 호스팅 제공 업체는 올바른 호스트 이름을 설명하고 있을 것입니다.\n\nWindows 서버에 설치하고 MySQL을 사용하면 \"localhost\"가 서버 이름으로는 작동하지 않을 수 있습니다. 그렇게 된다면 로컬 IP 주소로 \"127.0.0.1\"을 시도하세요.\n\nPostgreSQL을 사용하면 유닉스 소켓을 통해 연결되도록 입력란을 비워두세요.", - "config-db-host-oracle": "데이터베이스 TNS:", - "config-db-host-oracle-help": "올바른 [http://download.oracle.com/docs/cd/B28359_01/network.111/b28317/tnsnames.htm 로컬 연결 이름]을 입력하세요. tnsnames.ora 파일이 이 설치 위치에서 참조할 수 있는 곳에 있어야 합니다.
10g 이후의 클라이언트 라이브러리를 사용하는 경우 [http://download.oracle.com/docs/cd/E11882_01/network.112/e10836/naming.htm 쉬운 연결] 네이밍 메서드도 사용할 수 있습니다.", "config-db-wiki-settings": "이 위키 식별", "config-db-name": "데이터베이스 이름 (하이픈 없음):", "config-db-name-help": "위키를 식별하기 위한 이름을 선택하세요.\n공백이 없어야 합니다.\n\n공유하는 웹 호스팅 사용하면 호스팅 제공 업체가 특정 데이터베이스 이름을 제공하거나 제어판에서 데이터베이스를 만들 수 있습니다.", - "config-db-name-oracle": "데이터베이스 스키마:", - "config-db-account-oracle-warn": "데이터베이스 백엔드로 Oracle을 설치하기 위해 지원하는 세 가지 시나리오가 있습니다:\n\n설치 과정의 일부로 데이터베이스 계정을 만들려면 설치를 위해 데이터베이스 계정으로 SYSDBA 역할을 가진 계정을 제공하고 웹 접근 계정에 대해 원하는 자격 증명을 지정하세요, 그렇지 않으면 수동으로 웹 접근 계정을 만들 수 있으며 (스키마 개체를 만들 권한이 필요한 경우) 또는 생성 권한을 가진 계정과 웹 접근이 제한된 계정의 두 가지 다른 계정을 제공할 수도 있습니다\n\n필요한 권한을 가진 계정을 만드는 스크립트는 이 설치 위치의 \"maintenance/oracle/\" 디렉터리에서 찾을 수 있습니다. 제한된 계정을 사용하면 기본 계정의 모든 유지 관리 기능이 비활성화된다는 점에 유의하십시오.", "config-db-install-account": "설치를 위한 사용자 계정", "config-db-username": "데이터베이스 사용자 이름:", "config-db-password": "데이터베이스 비밀번호:", @@ -117,35 +113,22 @@ "config-pg-test-error": "$1 데이터베이스에 연결할 수 없습니다: $2", "config-sqlite-dir": "SQLite 데이터 디렉터리:", "config-sqlite-dir-help": "SQLite는 하나의 파일에 모든 데이터를 저장합니다.\n\n입력한 디렉토리는 설치하는 동안 웹 서버가 쓸 수 있어야 합니다.\n\n이 디렉토리는 웹을 통해 접근할 수 없어야 합니다. PHP 파일이 있는 곳에 넣을 수 없는 것은 이 때문입니다.\n\n설치 관리자는 .htaccess 파일을 작성하지만, 이것이 실패하면 누군가가 원본 데이터베이스에 접근할 수 있습니다.\n데이터베이스는 원본 사용자 데이터(이메일 주소, 해시한 비밀번호)뿐만 아니라 삭제된 판과 위키의 다른 제한된 데이터를 포함합니다.\n\n예를 들어 /var/lib/mediawiki/yourwiki와 같이 다른 곳에 데이터베이스를 넣는 것이 좋습니다.", - "config-oracle-def-ts": "기본 테이블공간:", - "config-oracle-temp-ts": "임시 테이블공간:", "config-type-mysql": "MariaDB, MySQL 및 호환", - "config-type-oracle": "Oracle", - "config-type-mssql": "Microsoft SQL 서버", "config-support-info": "미디어위키는 다음의 데이터베이스 시스템을 지원합니다:\n\n$1\n\n데이터베이스 시스템이 표시되지 않을 때 아래에 나열된 다음 지원을 활성화하려면 위의 링크된 지시에 따라 설치해볼 수 있습니다.", "config-dbsupport-mysql": "* [{{int:version-db-mariadb-url}} MariaDB]는 미디어위키의 기본 대상이며 가장 잘 지원됩니다. 미디어위키는 또한 MariaDB와 호환되는 [{{int:version-db-mysql-url}} MySQL]과 [{{int:version-db-percona-url}} Percona 서버]에서도 작동합니다. ([https://www.php.net/manual/en/mysql.installation.php MySQL 지원으로 PHP를 컴파일하는 방법])", "config-dbsupport-postgres": "* [{{int:version-db-postgres-url}} PostgreSQL]은 MySQL의 대안으로서 인기 있는 오픈 소스 데이터베이스 시스템입니다. ([https://www.php.net/manual/en/pgsql.installation.php PostgreSQL 지원으로 PHP를 컴파일하는 방법])", "config-dbsupport-sqlite": "* [{{int:version-db-sqlite-url}} SQLite]는 매우 잘 지원되고 가벼운 데이터베이스 시스템입니다. ([https://www.php.net/manual/en/pdo.installation.php SQLite 지원으로 PHP를 컴파일하는 방법], PDO 사용)", - "config-dbsupport-oracle": "* [{{int:version-db-oracle-url}} Oracle]은 상용 기업 데이터베이스입니다. ([https://www.php.net/manual/en/oci8.installation.php OCI8 지원으로 PHP를 컴파일하는 방법])", - "config-dbsupport-mssql": "* [{{int:version-db-mssql-url}} Microsoft SQL 서버]는 Windows용 상용 기업 데이터베이스입니다. ([https://www.php.net/manual/en/sqlsrv.installation.php SQLSRV 지원으로 PHP를 컴파일하는 방법])", "config-header-mysql": "MariaDB/MySQL 설정", "config-header-postgres": "PostgreSQL 설정", "config-header-sqlite": "SQLite 설정", - "config-header-oracle": "Oracle 설정", - "config-header-mssql": "Microsoft SQL 서버 설정", "config-invalid-db-type": "잘못된 데이터베이스 종류", "config-missing-db-name": "\"{{int:config-db-name}}\"에 대한 값을 입력해야 합니다.", "config-missing-db-host": "\"{{int:config-db-host}}\"에 대한 값을 입력해야 합니다.", - "config-missing-db-server-oracle": "\"{{int:config-db-host-oracle}}\"에 대한 값을 입력해야 합니다.", - "config-invalid-db-server-oracle": "\"$1\" 데이터베이스 TNS가 잘못됐습니다.\n\"TNS Name\"이나 \"Easy Connect\" 문자열 중 하나를 사용하세요 ([http://docs.oracle.com/cd/E11882_01/network.112/e10836/naming.htm Oracle 네이밍 메서드]).", "config-invalid-db-name": "\"$1\" 데이터베이스 이름이 잘못되었습니다.\nASCII 글자 (a-z, A-Z), 숫자 (0-9), 밑줄 (_)과 하이픈 (-)만 사용하세요.", "config-invalid-db-prefix": "\"$1\" 데이터베이스 접두어가 잘못됐습니다.\nASCII 글자 (a-z, A-Z), 숫자 (0-9), 밑줄 (_)과 하이픈 (-)만 사용하세요.", "config-connection-error": "$1.\n\n호스트, 계정 이름과 비밀번호를 확인하고 다시 시도하세요.", "config-invalid-schema": "미디어위키 \"$1\"에 대한 스키마가 잘못됐습니다.\nASCII 글자 (a-z, A-Z), 숫자 (0-9), 밑줄 (_)과 하이픈 (-)만 사용하세요.", - "config-db-sys-create-oracle": "설치 관리자는 새 계정을 만들기 위한 SYSDBA 계정만을 지원합니다.", - "config-db-sys-user-exists-oracle": "\"$1\" 사용자 계정이 이미 존재합니다. SYSDBA는 새 계정을 만드는 데에만 사용할 수 있습니다!", "config-postgres-old": "PostgreSQL $1 이상이 필요합니다. $2이(가) 있습니다.", - "config-mssql-old": "Microsoft SQL 서버 $1 이상의 버전이 필요합니다. 현재 버전은 $2입니다.", "config-sqlite-name-help": "위키를 식별하기 위한 이름을 선택하세요.\n공백이나 하이픈을 사용하지 마십시오.\nSQLite 데이터 파일 이름에 사용됩니다.", "config-sqlite-parent-unwritable-group": "$1 데이터 디렉토리를 만들 수 없으며, 이는 웹 서버는 상위 디렉토리인 $2에 쓸 수 없기 때문입니다.\n\n설치 관리자는 웹 서버로 실행 중인 사용자를 지정할 수 없습니다.\n계속하려면 웹 서버가 쓸 수 있는 $3 디렉토리를 만드세요.\n유닉스/리눅스 시스템에서의 수행:\n\n
cd $2\nmkdir $3\nchgrp $4 $3\nchmod g+w $3
", "config-sqlite-parent-unwritable-nogroup": "$1 데이터 디렉토리를 만들 수 없으며, 이는 웹 서버가 상위 디렉토리인 $2에 쓸 수 없기 때문입니다.\n\n설치 관리자는 웹 서버로 실행 중인 사용자를 지정할 수 없습니다.\n계속하려면 웹 서버(와 그 외 서버!)가 전역으로 쓸 수 있는 $3 디렉토리를 만드세요.\n유닉스/리눅스 시스템에서의 수행:\n\n
cd $2\nmkdir $3\nchmod a+w $3
", @@ -170,11 +153,6 @@ "config-mysql-engine": "저장소 엔진:", "config-mysql-innodb": "InnoDB (권장)", "config-mysql-engine-help": "InnoDB는 동시 실행 지원이 우수하기 때문에 대부분의 경우 최고의 옵션입니다.\n\nMyISAM은 단일 사용자나 읽기 전용 설치에서 더 빠를 수 있습니다.\nMyISAM 데이터베이스는 InnoDB 데이터베이스보다 더 자주 손실될 수 있습니다.", - "config-mssql-auth": "인증 형식:", - "config-mssql-install-auth": "설치 과정 중 데이터베이스에 연결하는 데 사용할 인증 형식을 선택하세요.\n\"{{int:config-mssql-windowsauth}}\"을 선택하시면 웹서버를 실행 중인 아무 사용자의 자격 증명이 사용됩니다.", - "config-mssql-web-auth": "위키가 일반적인 작업을 수행하는 동안 데이터베이스 서버에 연결하는 데 사용할 인증 형식을 선택하세요.\n\n\"{{int:config-mssql-windowsauth}}\"을 선택하시면 웹서버를 실행 중인 아무 사용자의 자격 증명이 사용됩니다.", - "config-mssql-sqlauth": "SQL 서버 인증", - "config-mssql-windowsauth": "Windows 인증", "config-site-name": "위키 이름:", "config-site-name-help": "브라우저 제목 표시줄과 다른 여러 곳에 나타납니다.", "config-site-name-blank": "사이트 이름을 입력하세요.", diff --git a/includes/installer/i18n/ksh.json b/includes/installer/i18n/ksh.json index 2a23c6d5f3..5a8148e9fc 100644 --- a/includes/installer/i18n/ksh.json +++ b/includes/installer/i18n/ksh.json @@ -81,13 +81,9 @@ "config-db-type": "De Zoot Daatebangk:", "config-db-host": "Dä Name vun däm Rääschner met dä Daatebangk:", "config-db-host-help": "Wann Dinge ẞööver för de Daatebangk ob enem andere Rääschner es, donn heh dämm singe Name udder dämm sing IP-Addräß enjävve.\n\nWann De ob enem Meetẞööver beß, weet Der Dinge Provaider odder däm sing Dokemäntazjuhn saare, wat De endraare moß.\n\nWann De ob enem ẞööver onger Windows am enshtalleere bes un en MySQL-Daatebangk häs, künnd_et sin, dat „localhost“ nit douch för der Name vum ẞööver. Wann dad-esu es, versöhg et ens met „127.0.0.1“ als IP-Addräß vum eije Rääschner.\n\nWann De ene PostgreSQL-ẞööver häs, donn dat Fäld läddesch lohße, öm en Verbendung övver e Unix socket opzemaache.", - "config-db-host-oracle": "Dä Daatebangk ier TNS:", - "config-db-host-oracle-help": "Donn ene jöltije [http://download.oracle.com/docs/cd/B28359_01/network.111/b28317/tnsnames.htm „Local Connect“-Name] aanjävve. De Dattei „tnsnames.ora“ moß för heh dat Projamm seschbaa un ze Lässe sin.
Wann heh de Projamm_Biblijoteeke für de Aanwänderprojramme för de Version 10g udder neuer enjesaz wääde, kam_mer och et [http://download.oracle.com/docs/cd/E11882_01/network.112/e10836/naming.htm „Easy Connect“] jenumme wääde för der Name ze verjävve.", "config-db-wiki-settings": "De Daate vum Wiki", "config-db-name": "Dä Nahme vun dä Daatebangk:", "config-db-name-help": "Jiff ene Name aan, dä för Ding Wiki passe deiht.\nDoh sullte kei Zweschrereum un kein Stresche dren sin.\n\nWann De nit op Dingem eije Rääschner bes, künnt et sin, dat Dinge Provaider Der extra ene beshtemmpte Name för de Daatebangk jejovve hät, uffr dat de dä drom froore moß udder dat De de Daatebangke övver e Fommulaa selver enreeschte moß.", - "config-db-name-oracle": "Schema för de Daatebangk:", - "config-db-account-oracle-warn": "Mer han drei Aate, wi mer Oracle als Dahtebangk aanbenge künne.\n\nWann De ene neue Zohjang op de Dahtenbangk met Nahme un Paßwoot mem Projramm för et Opsäze aanlääje wells, dann jif ene Zohjang met däm Rääsch „SYSDBA“ aan, dä et alld jitt, un jif däm di Daate aan för dä neue Zohjang aanzelääje.\nDo kanns och dä neue Zohjang vun Hand aanlääje un heh beim Opsäze nur dää aanjävve — wann dä dat Rääsch hät, en de Daatebangk Schema_Objäkte aanzelääje.\nUdder De jiß zwei ongerscheidlijje Zohjäng op de Daatenbangk aan, woh eine vun dat Rääsch zom Aanlääje hät un dä andere moß dat nit un es för der nomaale Bedrief zohshtändesch.\n\nEn Skrep, wat ene Zohjang op de Dahtenbangk aanlääsch met all dä nüüdejje Rääschde, fengks De em Verzeishneß maintenance/oracle/ vun Dingem MediaWiki. Donn draan dengke, dat ene Zohjang met beschrängkte Rääschde all di Müjjeleschkeite för et Waade un Repareere nit hät, di de jewöhnlejje Zoot Zohjang met sesh brängk.", "config-db-install-account": "Der Zohjang för en Enreeschte", "config-db-username": "Dä Name vun däm Aanwender för dä Zohjref op de Daatebangk:", "config-db-password": "Et Paßwoot vun däm Aanwender för dä Zohjref op de Daatebangk:", @@ -106,37 +102,24 @@ "config-pg-test-error": "Mer krijje kein Verbendung zor Daatebank '''$1''': $2", "config-sqlite-dir": "Dem SQLite sing Daateverzeishnes:", "config-sqlite-dir-help": "SQLite hät all sing Daate zosamme en en einzel Dattei.\n\nEn dat Verzeishneß, wat De aanjiß, moß dat Web_ẞööver_Projramm beim Opsäze eren schriive dörrve.\n\nDat Verzeishneß sullt '''nit''' övver et Web zohjänglesch sin, dröm dom_mer et nit dohen, woh de PHP-Datteije sin.\n\nMer donn beim Opsäze zwa uß Vöörssh en .htaccess Dattei dobei, ävver wann di nit werrek, künnte Lück vun ußerhallef aan Ding Daatebangk_Dattei eraan kumme.\nDoh shtonn Saache dren, wi de Addräße för de Metmaacher ier e-mail un de verschlößelte Paßwööter un de vershtoche un de fottjeschmeße Sigge un ander Saache ussem Wiki, di mer nit öffentlesch maache darref.\n\nDonn Ding Daatebangk et beß janz woh anders hen, noh /var/lib/mediawiki/''wikiname'' för e Beishpell.", - "config-oracle-def-ts": "Tabälleroum för der Shtandattjebruch:", - "config-oracle-temp-ts": "Tabälleroum för der Jebruch zweschedorsh:", "config-type-mysql": "MySQL (udder en jlischwääteje)", "config-type-postgres": "PostgreSQL", "config-type-sqlite": "SQLite", - "config-type-oracle": "Oracle", - "config-type-mssql": "Dä SQL-ẞööver vun Microsoft", "config-support-info": "MediaWiki kann met heh dä Daatebangk_Süßteeme zosamme jonn:\n\n$1\n\nWann dat Daatebangk_Süßteem, wat De nämme wells, onge nit dobei es, dann donn desch aan di Aanleidonge hallde, di bovve verlengk sen, öm et op Dingem ẞööver singem Süßteem müjjelesh ze maache, se aan et Loufe ze krijje.", "config-dbsupport-mysql": "* [{{int:version-db-mysql-url}} MySQL] es dat vum MediaWiki et eets un et bäß ongerschtöz Daatebangksüßtehm. Et leuf ävver och met [{{int:version-db-mariadb-url}} MariaDB] un [{{int:version-db-percona-url}} Percona Server]. Di sin kumpatihbel mem MySQL. ([https://www.php.net/manual/de/mysql.installation.php Aanleidung för et Övversäze un Enreeschte von PHP met MySQL dobei, op Deutsch])", "config-dbsupport-postgres": "* [{{int:version-db-postgres-url}} PostgreSQL] es e bikannt Daatebangksüßtehm met offe Quälltäxde, un ed es och en Wahl nävve MySQL. Et sinn_er ävver paa klein Fählersche bekannt, um mer künne et em Momang för et reschtijje Werke nit ämfähle. ([https://www.php.net/manual/de/pgsql.installation.php Aanleidung för et Övversäze un Enreeschte von PHP met PostgreSQL dobei, op Deutsch])", "config-dbsupport-sqlite": "* [{{int:version-db-sqlite-url}} SQLite] es e eijfach Daatebangksüßtehm, wat joot en Schoß jehallde weed. ([http://www.php.net/manual/de/pdo.installation.php Aanleidong för et Övversäze un Enreeschte von PHP met SQLite dobei, op Deutsch])", - "config-dbsupport-oracle": "* [{{int:version-db-oracle-url}} Oracle] es e jeschäfflesch Daatebangksüßtehm för Ferme. ([http://www.php.net/manual/de/oci8.installation.php Aanleidong för et Övversäze un Enreeschte von PHP met OCI8 dobei, op Deutsch])", - "config-dbsupport-mssql": "* Dä [{{int:version-db-mssql-url}} Microsoft SQL Server] es e jeschäfflesch Dahtebangksüßtehm för Rääschner met Windows. ([https://www.php.net/manual/en/sqlsrv.installation.php Aanleidong för et Övversäze un Enreeschte von PHP met SQLSRV dobei, op Deutsch])", "config-header-mysql": "De Enshtällunge för de MySQL Daatebangk", "config-header-postgres": "De Enshtällunge för de PostgreSQL Daatebangk", "config-header-sqlite": "De Enshtällunge för de SQLite Daatebangk", - "config-header-oracle": "De Enshtällunge för de Oracle Daatebangk", - "config-header-mssql": "Enschtällonge för der SQL-ẞööver vun Microsoft", "config-invalid-db-type": "Dat es en onjöltijje Zoot Daatebangk.", "config-missing-db-name": "Do moß jäd enjävve för \"{{int:config-db-name}}\".", "config-missing-db-host": "Do moß jät enjävve för \"{{int:config-db-host}}\".", - "config-missing-db-server-oracle": "Do moß jät enjävve för \"{{int:config-db-host-oracle}}\".", - "config-invalid-db-server-oracle": "Dä Daatebangk ier TNS kann nit „$1“ sin, dat es esu nit jöltesch.\nNemm en „TNS-Nahme“ udder ene „Easy-Connect“-Easy-ConnectString(Lor noh dädohwähje noh de [http://docs.oracle.com/cd/E11882_01/network.112/e10836/naming.htm Oracle Naming Methods])", "config-invalid-db-name": "Dä Daatebangk iere Name kann nit „$1“ sin, dä es esu nit jöltesch.\nDöh dörve bloß ASCII Boochshtaabe (a-z, A-Z), Zahle (0-9), Ongerstresh (_), un Bendeshtresh (-) dren vörkumme.", "config-invalid-db-prefix": "Dä Vörsaz för de Name vun de Tabälle en de Daatebangk kann nit „$1“ sin, dä es esu nit jöltesch.\nDöh dörve bloß ASCII Boochshtaabe (a-z, A-Z), Zahle (0-9), Ongerstreshe (_), un Bendeshtreshe (-) dren vörkumme.", "config-connection-error": "$1.\n\nDonn de Name för dä Rääschner, vun däm Aanwender för dä Zohjref op de Daatebangk, un et Paßwoot prööfe, repareere, un dann versöhg et norr_ens.", "config-invalid-schema": "Dat Schema för MediaWiki kann nit „$1“ sin, dä Name wöhr esu nit jöltesch.\nDöh dörve bloß ASCII Boochshtaabe (a-z, A-Z), Zahle (0-9), un Ongerstreshe (_) dren vörkumme.", - "config-db-sys-create-oracle": "Dat Projramm för MehdijaWikki opzesäze kann blohß ene SYSDBA-Zohjang bruche för ene neuje Zohjang zor Dahtebangk ennzereeschte.", - "config-db-sys-user-exists-oracle": "Dä Aanwender „$1“ för dä Zohjref op de Daatebangk jidd_et ald. SYSDBA kam_mer bloß bruche, för ene neue Zohjang enzereeschte!", "config-postgres-old": "Mer bruche PostgreSQL $1 udder neuer. Em Momang es PostgreSQL $2 aam Loufe.", - "config-mssql-old": "Dä SQL-ẞööver vun Microsoft aff de Väsjohn $1 es nüüdesch. Heh es bloß d Väsjohn $2 ze fenge.", "config-sqlite-name-help": "Söhk ene Nahme uß, dä Ding Wikki beschrief.\nDonn kein Bendeschresch un Zweschräum en däm Name bruche.\nDä Name weed för der Datteinahme för de SQLite Dahtebangk jenumme.", "config-sqlite-parent-unwritable-group": "Mer kunnte dat Verzeischneß för de Daate, $1, nit enreeschte, weil dat Projramm fö dä Web_ẞööver en dat Verzeischneß doh drövver, $2, nix erin donn darref.\n\nMer han dä Name vun däm Zohjang op et Süßteem eruß jefonge, onger dämm dat Web_ẞööver_Projramm läuf. Jez moß De bloß doför sorrje, dat dä en dat Verzeischneß $3 schrieve kann, öm heh wigger maache ze künne.\nOb enem Süßteem met Unix- oder Linux jeiht dat esu:\n
cd $2\nmkdir $3\nchgrp $4 $3\nchmod g+w $3
", "config-sqlite-parent-unwritable-nogroup": "Mer kunnte dat Verzeischneß för de Daate, $1, nit enreeschte, weil dat Projramm fö dä Web_ẞööver en dat Verzeischneß doh drövver, $2, nix erin donn darref.\n\nMer han dä Name vun däm Zohjang op et Süßteem nit eruß fenge künne, onger dämm dat Web_ẞööver_Projramm läuf. Jez moß De bloß doför sorrje, dat dä en dat Verzeischneß $3 schrieve kann, öm heh wigger maache ze künne. Wann De dä Name och nit weiß, maach, dat jeeder_ein doh schrieve kann.\nOb enem Süßteem met Unix- oder Linux jeiht dat esu:\n
cd $2\nmkdir $3\nchmod a+w $3
", @@ -160,11 +143,6 @@ "config-mysql-engine": "De Zoot udder et Fommaat vun de Tabälle:", "config-mysql-innodb": "InnoDB", "config-mysql-engine-help": "InnoDB es fö jewöhnlesch et beß, weil vill Zohjreffe op eijmohl joot ongershtöz wääde.\n\nMyISAM es flöcker op Rääschnere met bloß einem Minsch draan, un bei Wikis, di mer bloß lässe un nit schrieeve kann.\nMyISAM-Daatebangke han em Schnett mih Fähler un jon flöcker kappott, wi InnoDB-Daatebangke.", - "config-mssql-auth": "De Zoot Aanmäldong:", - "config-mssql-install-auth": "Söhk us, wi dat Aanmälde aan dä Daatebangk vor sesch jonn sull för de Enschtallazjuhn.\nWann De {{int:Config-mssql-windowsauth}} nemms, weed jenumme, met wat emmer dä Wäbßööver aam loufe es.", - "config-mssql-web-auth": "Söhk us, wi dat Aanmälde aan dä Daatebangk vör sesch jonn sull för de nommaale Ärbeid vum Wiki.\nWann De {{int:Config-mssql-windowsauth}} nemms, weed dat jenumme, wohmet dä Wäbßööver aam loufe es.", - "config-mssql-sqlauth": "De Aanmäldong bemm SQL-ẞööver vun Microsoft", - "config-mssql-windowsauth": "De Annmäldong bemm Windows", "config-site-name": "Däm Wikki singe Nahme:", "config-site-name-help": "Dä douch en dä Övverschreff vun de Brauserfinstere un aan ätlije andere Schtälle op.", "config-site-name-blank": "Donn ene Name för di Sait aanjävve.", diff --git a/includes/installer/i18n/ku-latn.json b/includes/installer/i18n/ku-latn.json index 231271c6d2..ca617d00f6 100644 --- a/includes/installer/i18n/ku-latn.json +++ b/includes/installer/i18n/ku-latn.json @@ -35,7 +35,6 @@ "config-db-install-account": "Hesabê bikarhêner bo avakirinê", "config-db-username": "Navê bikarhêner bo danagehê:", "config-db-password": "Şîfreya danegehê:", - "config-type-mssql": "Microsoft SQL Server", "config-invalid-db-type": "Cureya danegehê ya nederbasdar", "config-sqlite-readonly": "Dosyeya $1 ne nivîsbar e.", "config-db-web-account": "Hesabê danegehê bô têgihiştina tora înternetê", diff --git a/includes/installer/i18n/lb.json b/includes/installer/i18n/lb.json index e6e12f1ef1..d38c73397d 100644 --- a/includes/installer/i18n/lb.json +++ b/includes/installer/i18n/lb.json @@ -61,10 +61,8 @@ "config-using-uri": "D'Server URL \"$1$2\" gëtt benotzt.", "config-db-type": "Datebanktyp:", "config-db-host": "Host vun der Datebank:", - "config-db-host-oracle": "Datebank-TNS:", "config-db-wiki-settings": "Dës Wiki identifizéieren", "config-db-name": "Numm vun der Datebank:", - "config-db-name-oracle": "Datebankschema:", "config-db-install-account": "Benotzerkont fir d'Installatioun", "config-db-username": "Datebank-Benotzernumm:", "config-db-password": "Passwuert vun der Datebank:", @@ -78,29 +76,18 @@ "config-db-schema-help": "D'Schemaen hei driwwer si gewéinlech korrekt.\nÄnnert se nëmme wann Dir wësst datt et néideg ass.", "config-pg-test-error": "Et ass net méiglech d'Datebank '''$1''' ze kontaktéieren: $2", "config-sqlite-dir": "Repertoire vun den SQLite-Donnéeën", - "config-oracle-def-ts": "Standard 'tablespace':", - "config-oracle-temp-ts": "Temporären 'tablespace':", "config-type-mysql": "MariaDB, MySQL, oder kompatibel", "config-type-postgres": "PostgreSQL", "config-type-sqlite": "SQLite", - "config-type-oracle": "Oracle", - "config-type-mssql": "Microsoft SQL Server", "config-dbsupport-postgres": "* [{{int:version-db-postgres-url}} PostgreSQL] ass e beléiften Open-Source-Datebanksystem an eng Alternativ zu MySQL. ([https://www.php.net/manual/en/pgsql.installation.php Uleedung fir d'Kompilatoun vu PHP mat PostgreSQL-Ënnerstëtzung])", - "config-dbsupport-oracle": "* [{{int:version-db-oracle-url}} Oracle] ass eng kommerziell Datebank-Software. ([http://www.php.net/manual/en/oci8.installation.php How to compile PHP mat OCI8 Ënnerstëtzung])", - "config-dbsupport-mssql": "* [{{int:version-db-mssql-url}} Microsoft SQL Server] ass eng kommerziell Datebank-Software fir Windows. ([https://www.php.net/manual/en/sqlsrv.installation.php Wéi PHP mat SQLSRV Ënnerstëtzung kompiléieren])", "config-header-mysql": "MariaDB/MySQL-Astellungen", "config-header-postgres": "PostgreSQL-Astellungen", "config-header-sqlite": "SQLite-Astellungen", - "config-header-oracle": "Oracle-Astellungen", - "config-header-mssql": "Microsoft SQL Server Astellungen", "config-invalid-db-type": "Net valabelen Datebank-Typ", "config-missing-db-name": "Dir musst e Wäert fir \"{{int:config-db-name}}\" aginn", "config-missing-db-host": "Dir musst e Wäert fir \"{{int:config-db-host}}\" aginn.", - "config-missing-db-server-oracle": "Dir musst e Wäert fir \"{{int:config-db-host-oracle}}\" aginn", "config-connection-error": "$1.\n\nKuckt den Numm vum Server, de Benotzernumm an d'Passwuert no a probéiert et nach eng Kéier.", - "config-db-sys-user-exists-oracle": "De Benotzerkont \"$1\" gëtt et schonn. SYSDBA kann nëmme benotzt gi fir en neie Benotzerkont opzemaachen.", "config-postgres-old": "PostgreSQL $1 oder eng méi nei Versioun gëtt gebraucht, Dir hutt $2.", - "config-mssql-old": "Microsoft SQL Server $1 oder eng méi rezent Versioun gëtt gebraucht. Dir hutt d'Versioun $2.", "config-sqlite-name-help": "Sicht en Numm deen Är wiki identifizéiert.\nBenotzt keng Espacen a Bindestrécher.\nE gëtt fir den Numm vum SQLite Date-Fichier benotzt.", "config-sqlite-readonly": "An de Fichier $1 Kann net geschriwwe ginn.", "config-sqlite-cant-create-db": "Den Datebank-Fichier $1 konnt net ugeluecht ginn.", @@ -111,9 +98,6 @@ "config-db-web-account-same": "Dee selwechte Kont wéi bei der Installatioun benotzen", "config-db-web-create": "De Kont uleeë wann et e net scho gëtt", "config-mysql-innodb": "InnoDB (recommandéiert)", - "config-mssql-auth": "Typ vun der Authentifikatioun:", - "config-mssql-sqlauth": "SOL-Server-Authentifikatioun", - "config-mssql-windowsauth": "Windows-Authentifikatioun", "config-site-name": "Numm vun der Wiki:", "config-site-name-help": "Dësen daucht an der Titelleescht vum Browser an op verschiddenen anere Plazen op.", "config-site-name-blank": "Gitt den Numm vum Site un.", diff --git a/includes/installer/i18n/li.json b/includes/installer/i18n/li.json index a1b55a8ecb..17a0c6e754 100644 --- a/includes/installer/i18n/li.json +++ b/includes/installer/i18n/li.json @@ -41,7 +41,6 @@ "config-diff3-bad": "GNU diff3 neet gevónje.", "config-db-type": "Databanksaort:", "config-db-host": "Databankgashieër:", - "config-db-host-oracle": "Databank-TNS:", "config-db-wiki-settings": "Identificeer deze wiki", "config-db-name": "Databanknaam:", "mainpagetext": "MediaWiki software geïnsjtalleerd.", diff --git a/includes/installer/i18n/lij.json b/includes/installer/i18n/lij.json index d555bc72b8..7ad56ff9d4 100644 --- a/includes/installer/i18n/lij.json +++ b/includes/installer/i18n/lij.json @@ -80,13 +80,9 @@ "config-db-type": "Tipo de database:", "config-db-host": "Host do database:", "config-db-host-help": "Se o serviou do to database o l'è insce 'n serviou despægio, inmetti chì o nomme de l'host ò o so adresso IP.\n\nSe ti doeuvi un web hosting condiviso, o to hosting provider o doviæ fornite o nomme host corretto inta so documentaçion.\n\nSe t'ê aproeuvo a instalâ insce 'n serviou Windows con uso de MySQL, l'uso de \"localhost\" o porriæ no fonçionâ correttamente comme nomme do serviou. In caxo de problemi, proeuva a impostâ \"127.0.0.1\" comme adresso IP locale.\n\nSe ti t'adoeuvi PostgreSQL, lascia questo campo voeuo pe consentî de connettise trammite un socket Unix.", - "config-db-host-oracle": "TNS do database:", - "config-db-host-oracle-help": "Inseisci un vallido [http://download.oracle.com/docs/cd/B28359_01/network.111/b28317/tnsnames.htm Local Connect Name]; un file tnsnames.ora o dev'ese vixibbile a questa installaçion.
Se ti t'adoeuvi a libraia cliente 10g o ciù reçente ti poeu ascì doeuviâ o mettodo de denominaçion [http://download.oracle.com/docs/cd/E11882_01/network.112/e10836/naming.htm Easy Connect].", "config-db-wiki-settings": "Identiffica questo wiki", "config-db-name": "Nomme do database:", "config-db-name-help": "Çerni un nomme ch'o l'identiffiche o to wiki.\nO no deve contegnî de spaççi.\n\nSe ti doeuvi un web hosting condiviso, o to hosting provider o te fornisce un speciffico nomme de database da doeuviâ, opû o ti consentiâ de creâ o database trammite un panello de controllo.", - "config-db-name-oracle": "Schema do database:", - "config-db-account-oracle-warn": "Gh'è trei scenarri supportæ pe instalâ l'Oracle comme database de backend:\n\nSe t'oeu creâ 'n'utença de database comme parte do processo d'instalaçion, fornisci un account con rollo SYSDBA comme utença de database pe l'instalaçion e speciffica e credençiæ vosciue pe l'utença d'accesso web, sedonque l'è poscibbile creâ manoalmente l'utença d'accesso web e fornî solo quell'account (s'o g'ha e aotorizaçioin necessaie pe creâ i ogetti do schema) ò fornî doe utençe despæge, un-a co-i permissi de creaçion e un-a pe l'accesso web.\n\nO Script pe creâ un'utença co-e aotorizaçioin necessaie o se troeuva inta directory \"maintenance/oracle/\" de questa instalaçion. Tegnit'amente che l'uzo de 'n'utença con restriçioin o dizabilitiâ tutte e fonçionalitæ de manutençion con l'account predefinio.", "config-db-install-account": "Account utente pe l'instalaçion", "config-db-username": "Nomme utente do database:", "config-db-password": "Password do database:", @@ -105,34 +101,22 @@ "config-pg-test-error": "Imposcibbile conettise a-o database '''$1''': $2", "config-sqlite-dir": "Cartella dæti de SQLite:", "config-sqlite-dir-help": "SQLite o memorizza tutti i dæti inte 'n unnico file.\n\nA directory che t'indichiæ a dev'ese scrivibile da-o serviou web durante l'instalaçion.\n\nA dev'ese non acescibbile via web, l'è pe questo che no a mettemmo donde gh'è i file PHP.\n\nL'instalou o ghe scriviâ insemme un file .htaccess, ma se o tentativo o falisse quarcun poriæ avei accesso a-o database sgroeuzzo.\nQuesto o l'includde di dæti utente sgroeuzzi (adressi, password çiffræ) coscì comme de vercsioin eliminæ e atri dæti a accesso limitou da wiki.\n\nConsciddera a-a dreitua l'oportunitæ d'alugâ o database da quarch'atra parte, prezempio in /var/lib/mediawiki/tuowiki.", - "config-oracle-def-ts": "Tablespace pe difetto:", - "config-oracle-temp-ts": "Tablespace tempoannio:", "config-type-mysql": "MariaDB, MySQL ò compatibbile", - "config-type-mssql": "Microsoft SQL Server", "config-support-info": "MediaWiki o supporta i seguenti scistemi de database:\n\n$1\n\nSe fra quelli elencæ chì de sotta no ti veddi o scistema de database che ti voriesci doeuviâ, segui e instruçioin inganciæ de d'ato pe abilitâ o supporto.", "config-dbsupport-mysql": "* [{{int:version-db-mysql-url}} MySQL] a l'è a primma scelta pe MediaWiki e a l'è quella megio suportâ. MediaWiki a fonçion-a ascì con [{{int:version-db-mariadb-url}} MariaDB] e [{{int:version-db-percona-url}} Percona Server], che son compatibbili con MySQL.([https://www.php.net/manual/en/mysqli.installation.php Comme compilâ PHP con suporto MySQL])", "config-dbsupport-postgres": "* [{{int:version-db-postgres-url}} PostgreSQL] o l'è un popolare scistema de database open source comme alternativa a MySQL. ([https://www.php.net/manual/en/pgsql.installation.php Comme compilâ PHP con suporto PostgreSQL])", "config-dbsupport-sqlite": "* [{{int:version-db-sqlite-url}} SQLite] o l'è un scistema de database leggio, ch'o l'è suportou molto ben. ([http://www.php.net/manual/en/pdo.installation.php Comme compilâ PHP con suporto SQLite], o l'utilizza PDO)", - "config-dbsupport-oracle": "* [{{int:version-db-oracle-url}} Oracle] o l'è un database de un'impreiza comerciâ. ([http://www.php.net/manual/en/oci8.installation.php Comme compilâ PHP con suporto OCI8])", - "config-dbsupport-mssql": "* [{{int:version-db-mssql-url}} Microsoft SQL Server] o l'è un database de un'impreiza commerciâ per Windows. ([https://www.php.net/manual/en/sqlsrv.installation.php Comme compilâ PHP con supporto SQLSRV])", "config-header-mysql": "Impostaçioin MySQL", "config-header-postgres": "Impostaçioin PostgreSQL", "config-header-sqlite": "Impostaçioin SQLite", - "config-header-oracle": "Impostaçioin Oracle", - "config-header-mssql": "Impostaçioin do Microsoft SQL Server", "config-invalid-db-type": "Tipo de database non vallido", "config-missing-db-name": "Ti g'hæ da mettighe un valô pe \"{{int:config-db-name}}\".", "config-missing-db-host": "Ti g'hæ da mettighe un valô pe \"{{int:config-db-host}}\".", - "config-missing-db-server-oracle": "L'è necessaio inmettere un valô pe \"{{int:config-db-host-oracle}}\".", - "config-invalid-db-server-oracle": "TNS database \"$1\" non vallido.\nAdoeuvia \"TNS Name\" ò 'na stringa \"Easy Connect\" ([http://docs.oracle.com/cd/E11882_01/network.112/e10836/naming.htm Oracle Naming Methods]).", "config-invalid-db-name": "Nomme de database \"$1\" non vallido.\nAdoeuvia solo che di caratteri ASCII comme lettie (a-z, A-Z), nummeri (0-9), sottoliniatua (_) e trattin (-).", "config-invalid-db-prefix": "Prefisso database \"$1\" non vallido.\nChe ti doeuvi solo di caratteri ASCII comme lettie (a-z, A-Z), nummeri (0-9), sottoliniatua (_) e trattin (-).", "config-connection-error": "$1.\n\nControlla l'host, o nomme utente e a password, e proeuva torna.", "config-invalid-schema": "Schema MediaWiki \"$1\" non vallido.\nChe ti doeuvi solo lettie ASCII (a-z, A-Z), nummeri (0-9) ò caratteri de sottoliniatua (_).", - "config-db-sys-create-oracle": "O programma d'instalaçion o suporta solo l'utilizzo de 'n account SYSDBA pe-a creaçion de 'n noeuvo account.", - "config-db-sys-user-exists-oracle": "L'utença \"$1\" a l'existe za. SYSDBA o poeu vese doeuviou solo che pe-a creaçion de 'na noeuva utença!", "config-postgres-old": "Ghe voeu MySQL $1 ò 'na verscion succesciva. Ti ti g'hæ a $2.", - "config-mssql-old": "Ghe voeu Microsoft SQL Server $1 ò succescivo. Ti ti g'hæ a verscion $2.", "config-sqlite-name-help": "Çerni un nomme ch'o l'identiffiche a to wiki.\nNo doeuviâ spaÇçi ò trattin.\nQuesto o serviâ pe-o nomme do file di dæti SQLite.", "config-sqlite-parent-unwritable-group": "No se poeu creâ a directory dæti $1, percose a directory supeiô $2 a no l'è scrivibbile da-o webserver.\n\nO programma d'instalaÇion o l'ha determinou l'utente con chi o serviou web o l'è in esecuçion.\nDagghe a poscibilitæ de scrive inta directory $3 pe continoâ.\nInsce un scistema Unix/Linux fanni:\n\n
cd $2\nmkdir $3\nchgrp $4 $3\nchmod g+w $3
", "config-sqlite-parent-unwritable-nogroup": "No se poeu creâ a directory dæti $1, percose a directory supeiô $2 a no l'è scrivibbile da-o webserver.\n\nO programma d'instalaçion o no l'ha posciuo determinâ l'utente con chi o serviou web o l'è in esecuçion.\nRendi a directory $3 scrivibbile globalmente, da esso (e da atri) pe continoâ.\nInsce un scistema Unix/Linux fanni:\n\n
cd $2\nmkdir $3\nchmod a+w $3
", @@ -156,11 +140,6 @@ "config-mysql-engine": "Motô d'archiviaçion:", "config-mysql-innodb": "InnoDB", "config-mysql-engine-help": "InnoDB o l'è quæxi sempre a megio opçion, in quante o g'ha 'n bon supporto da concorença.\n\nMyISAM o poriæ vese ciu veloçe inte installaçioin mono-utente ò in sola-lettua.\nI database MyISAM tendan a dannezâse ciu soventi di database InnoDB.", - "config-mssql-auth": "Tipo d'aotenticaçion:", - "config-mssql-install-auth": "Seleçion-a o tipo d'aotenticaçion ch'o saiâ doeuviou pe conettise a-o database durante o processo de instalaçion.\nSe ti seleçion-i \"{{int:config-mssql-windowsauth}}\", saiâ doeuviou e credençiæ de quæ se segge utente segge aproeuv'a fâ giâ o serviou web.", - "config-mssql-web-auth": "Seleçion-a o tipo d'aotenticaçion che o serviou web o doeuviâ pe conettise a-o database. \nSe ti seleçion-i \"{{int:config-mssql-windowsauth}}\", saiâ doeuviou e credençiæ de quæ se segge utente segge aproeuv'a fâ giâ o serviou web.", - "config-mssql-sqlauth": "Aotenticaçion de SQL Server", - "config-mssql-windowsauth": "Aotenticaçion de Windows", "config-site-name": "Nomme da wiki:", "config-site-name-help": "Questo saiâ vixualizou inta bara do tittolo do navegatô e in atri varri recanti.", "config-site-name-blank": "Inseisci o nomme de 'n scito.", diff --git a/includes/installer/i18n/lki.json b/includes/installer/i18n/lki.json index a637c097bf..6d06e259dd 100644 --- a/includes/installer/i18n/lki.json +++ b/includes/installer/i18n/lki.json @@ -62,7 +62,6 @@ "config-memory-bad": "'''هشدار:''' PHP's memory_limit نسخهٔ $1 است.\nاین ممکن است خیلی پایین باشد.\nممکن است نصب با مشکل رو‌به‌رو شود.", "config-db-type": "نوع پایگاه اطلاعات:", "config-db-host": "میزبان پایگاه اطلاعات:", - "config-db-host-oracle": "ای ویکیۀ شناسایی کۀ.", "config-db-name": "نام پایگاه اطلاعات:", "config-upgrade-done": "تکمیل ارتقاء.\nاکنون شما می‌توانید [$1 start using your wiki].\nاگر می‌خواهید پوشهٔ LocalSettings.php را احیا کنید،دکمهٔ زیر را کلیک کنید.\nاین ''' توصیه نمی‌شود ''' مگر اینکه با ویکی خود مشکل داشته باشید.", "config-site-name-blank": "نام سایتئ وارد کۀن.", diff --git a/includes/installer/i18n/lt.json b/includes/installer/i18n/lt.json index a4c524b310..8cb1ae7aaa 100644 --- a/includes/installer/i18n/lt.json +++ b/includes/installer/i18n/lt.json @@ -64,10 +64,8 @@ "config-using-uri": "Naudojamas serverio URL „$1$2“.", "config-db-type": "Duomenų bazės tipas:", "config-db-host": "Duomenų bazės serveris:", - "config-db-host-oracle": "Duomenų bazės TNS:", "config-db-wiki-settings": "Identifikuoti šią viki", "config-db-name": "Duomenų bazės pavadinimas:", - "config-db-name-oracle": "Duomenų bazės schema:", "config-db-install-account": "Vartotojo paskyra diegimui", "config-db-username": "Duomenų bazės vartotojo vardas:", "config-db-password": "Duomenų bazės slaptažodis:", @@ -78,19 +76,13 @@ "config-db-schema": "MediaWiki schema:", "config-pg-test-error": "Negalima prisijungti prie duomenų bazės $1: $2", "config-sqlite-dir": "SQLite duomenų katalogas:", - "config-oracle-def-ts": "Numatytoji lentelių sritis:", - "config-oracle-temp-ts": "Laikina lentelių sritis:", "config-type-mysql": "MySQL (arba suderinama)", - "config-type-mssql": "Microsoft SQL serveris", "config-header-mysql": "MySQL nustatymai", "config-header-postgres": "PostgreSQL nustatymai", "config-header-sqlite": "SQLite nustatymai", - "config-header-oracle": "Oracle nustatymai", - "config-header-mssql": "„Microsoft“ SQL serverio nustatymai", "config-invalid-db-type": "Neteisingas duomenų bazės tipas", "config-missing-db-name": "Privalote įvesti „{{int:config-db-name}}“ reikšmę.", "config-missing-db-host": "Privalote įvesti „{{int:config-db-host}}“ reikšmę.", - "config-missing-db-server-oracle": "Privalote įvesti „{{int:config-db-host-oracle}}“ reikšmę.", "config-postgres-old": "PostgreSQL $1 ar vėlesnė yra reikalinga. Jūs turite $2.", "config-sqlite-cant-create-db": "Nepavyko sukurti duomenų bazės failo $1.", "config-regenerate": "Pergeneruoti LocalSettings.php →", @@ -99,9 +91,6 @@ "config-db-web-create": "Sukurti paskyrą, jeigu jos nėra", "config-mysql-engine": "Saugojimo variklis:", "config-mysql-innodb": "InnoDB", - "config-mssql-auth": "Autentifikavimo tipas:", - "config-mssql-sqlauth": "SQL Serverio autentifikavimas", - "config-mssql-windowsauth": "Windows autentifikavimas", "config-site-name": "Viki pavadinimas:", "config-site-name-blank": "Įveskite svetainės pavadinimą.", "config-project-namespace": "Projekto vardų sritis:", diff --git a/includes/installer/i18n/lv.json b/includes/installer/i18n/lv.json index 675e3db1b5..3546be0c2a 100644 --- a/includes/installer/i18n/lv.json +++ b/includes/installer/i18n/lv.json @@ -29,7 +29,6 @@ "config-env-hhvm": "HHVM $1 ir uzstādīts.", "config-apcu": "[https://www.php.net/apcu APCu] ir uzstādīts", "config-diff3-bad": "GNU diff3 nav atrasts.", - "config-db-host-oracle": "Datubāzes TNS:", "config-db-name": "Datubāzes nosaukums:", "config-db-username": "Datubāzes lietotājvārds:", "config-db-password": "Datubāzes parole:", @@ -40,10 +39,7 @@ "config-header-mysql": "MySQL iestatījumi", "config-header-postgres": "PostgreSQL iestatījumi", "config-header-sqlite": "SQLite iestatījumi", - "config-header-oracle": "Oracle iestatījumi", - "config-header-mssql": "Microsoft SQL servera iestatījumi", "config-mysql-innodb": "InnoDB", - "config-mssql-windowsauth": "Windows Autentifikācija", "config-ns-generic": "Projekts", "config-ns-site-name": "Tāds pats kā viki nosaukums: $1", "config-ns-other": "Cits (jānorāda)", diff --git a/includes/installer/i18n/mg.json b/includes/installer/i18n/mg.json index a3d824bec3..820b9b0fce 100644 --- a/includes/installer/i18n/mg.json +++ b/includes/installer/i18n/mg.json @@ -48,7 +48,6 @@ "config-db-type": "Karazana banky angona:", "config-db-host": "Anaran'ny lohamilin'ny banky angona:", "config-db-host-help": "Raha lohamila hafa ny lohamilin'ny banky angona, dia atsofohy eto ny anaran'ilay lohamilina na ny adiresy IP-ny.\n\nRaha mampiasa fampiantranoana iombonana ianao dia tokony hanome anao ny anaran-dohamilina izy ao amin'ny toromariny.\n\nRaha mametraka amin'ny lohamilina Windows ianao sady mampiasa MySQL, dia mety tsy mandeha ny anaran-dohamilina \"localhost\". Raha tsy mandeha ilay izy dia \"127.0.0.1\" no atao adiresy IP an-toerana.\n\nRaha mampiasa PostgreSQL ianao, dia avelaho ho fotsy ity saha ity ahafahana mifandray amin'ny alalan'ny socket Unix.", - "config-db-host-oracle": "TNS an'ny banky angona:", "config-db-username": "Anaram-pikamban'ny banky angona :", "config-db-password": "Tenimiafin'ny banky angona :", "config-db-prefix": "Tovom-banky angona:", @@ -56,8 +55,6 @@ "config-header-mysql": "Parametatr'i MySQL", "config-header-postgres": "Parametatra PostgreSQL", "config-header-sqlite": "Parametatr'i SQLite", - "config-header-oracle": "Parametatr'i Oracle", - "config-header-mssql": "Parametatry ny lohamilina Microsoft SQL Server", "config-invalid-db-type": "Karazana banky angona tsy ekena.", "config-mysql-innodb": "innoDB", "config-ns-generic": "Tetikasa", diff --git a/includes/installer/i18n/mk.json b/includes/installer/i18n/mk.json index e6526837a8..c5b9932d39 100644 --- a/includes/installer/i18n/mk.json +++ b/includes/installer/i18n/mk.json @@ -88,13 +88,9 @@ "config-db-type": "Тип на база:", "config-db-host": "Домаќин на базата:", "config-db-host-help": "Ако вашата база е на друг опслужувач, тогаш тука внесете го името на домаќинот или IP-адресата.\n\nАко користите заедничко (споделено) вдомување, тогаш вашиот вдомител треба да го наведе точното име на домаќинот во неговата документација.\n\nАко користите MySQL, можноста „localhost“ може да не функционира за опслужувачкото име. Во тој случај, обидете се со внесување на „127.0.0.1“ како месна IP-адреса.\n\nАко користите PostgreSQL, оставете го полево празно за да се поврзете преку Unix-приклучок.", - "config-db-host-oracle": "TNS на базата:", - "config-db-host-oracle-help": "Внесете важечко [http://download.oracle.com/docs/cd/B28359_01/network.111/b28317/tnsnames.htm месно име за поврзување]. На оваа воспоставка мора да ѝ биде видлива податотеката tnsnames.ora.
Ако користите клиентски библиотеки 10g или понови, тогаш можете да го користите и методот на иметнување на [http://download.oracle.com/docs/cd/E11882_01/network.112/e10836/naming.htm Easy Connect].", "config-db-wiki-settings": "Идентификувај го викиво", "config-db-name": "Име на базата (без цртички):", "config-db-name-help": "Одберете име што ќе го претставува вашето вики.\nИмето не смее да содржи празни места.\n\nАко користите заедничко (споделено) вдомување, тогаш вашиот вдомител ќе ви даде конкретно име на база за користење, или пак ќе ви даде да создавате бази преку управувачницата.", - "config-db-name-oracle": "Шема на базата:", - "config-db-account-oracle-warn": "Постојат три поддржани сценарија за воспоставка на Oracle како базен услужник:\n\nАко сакате да создадете сметка на базата како дел од постапката за воспоставка, наведете сметка со SYSDBA-улога како сметка за базата што ќе се воспостави и наведете ги саканите податоци за сметката за мрежен пристап. Во друг случај, можете да создадете сметка за мрежен пристап рачно и да ја наведете само таа сметка (ако има дозволи за создавање на шематски објекти) или пак да наведете две различни сметки, една со привилегии за создавање, а друга (ограничена) за мрежен пристап.\n\nСкриптата за создавање сметка со задолжителни привилегии ќе ја најдете во папката „maintenance/oracle/“ од оваа воспоставка. Имајте на ум дека ако користите ограничена сметка ќе ги оневозможите сите функции за одржување со основната сметка.", "config-db-install-account": "Корисничка смета за воспоставка", "config-db-username": "Корисничко име за базата:", "config-db-password": "Лозинка за базата:", @@ -113,37 +109,24 @@ "config-pg-test-error": "Не можам да се поврзам со базата $1: $2", "config-sqlite-dir": "Папка на SQLite-податоци:", "config-sqlite-dir-help": "SQLite ги складира сите податоци во една податотека.\n\nПапката што ќе ја наведете мора да е запислива од мрежниот опслужувач во текот на воспоставката.\n\nТаа '''не''' смее да биде достапна преку семрежјето, и затоа не ја ставаме кајшто ви се наоѓаат PHP-податотеките.\n\nВоспоставувачот воедно ќе создаде податотека .htaccess, но ако таа не функционира како што треба, тогаш некој ќе може да ви влезе во вашата необработена (сирова) база на податоци.\nТука спаѓаат необработени кориснички податоци (е-поштенски адреси, хеширани лозинки) како и избришани преработки и други податоци за викито до кои се има ограничен пристап.\n\nСе препорачува целата база да ја сместите некаде, како на пр. /var/lib/mediawiki/yourwiki.", - "config-oracle-def-ts": "Стандарден таблеарен простор:", - "config-oracle-temp-ts": "Привремен табеларен простор:", "config-type-mysql": "MariaDB, MySQL или складно", "config-type-postgres": "PostgreSQL", "config-type-sqlite": "SQLite", - "config-type-oracle": "Oracle", - "config-type-mssql": "Microsoft SQL Server", "config-support-info": "МедијаВики ги поддржува следниве системи на бази на податоци:\n\n$1\n\nАко системот што сакате да го користите не е наведен подолу, тогаш проследете ја горенаведената врска со инструкции за да овозможите поддршка за тој систем.", "config-dbsupport-mysql": "* [{{int:version-db-mariadb-url}} MariaDB] е главната цел на МедијаВики и најдобро е поддржан. МедијаВики работи и со [{{int:version-db-mysql-url}} MySQL] и [{{int:version-db-percona-url}} Percona], кои се складни со MariaDB. ([https://www.php.net/manual/en/mysqli.installation.php Како да срочите PHP со поддршка за MySQL])", "config-dbsupport-postgres": "* [{{int:version-db-postgres-url}} PostgreSQL] е популарен систем на бази на податоци со отворен код кој претставува алтернатива на MySQL ([https://www.php.net/manual/en/pgsql.installation.php како да составите PHP со поддршка за PostgreSQL]). ([https://www.php.net/manual/en/pgsql.installation.php Како да срочите PHP со поддршка за PostgreSQL])", "config-dbsupport-sqlite": "* [{{int:version-db-sqlite-url}} SQLite] е лесен систем за бази на податоци кој е многу добро поддржан. ([https://www.php.net/manual/en/pdo.installation.php Како да составите PHP со поддршка за SQLite], користи PDO)", - "config-dbsupport-oracle": "* [{{int:version-db-oracle-url}} Oracle] е база на податоци на комерцијално претпријатие. ([https://www.php.net/manual/en/oci8.installation.php Како да составите PHP со поддршка за OCI8])", - "config-dbsupport-mssql": "* [{{int:version-db-mssql-url}} Microsoft SQL Server] е база на податоци на комерцијално претпријатиe за Windows ([https://www.php.net/manual/en/sqlsrv.installation.php How to compile PHP with SQLSRV поддршка])", "config-header-mysql": "Нагодувања на MariaDB/MySQL", "config-header-postgres": "Нагодувања на PostgreSQL", "config-header-sqlite": "Нагодувања на SQLite", - "config-header-oracle": "Нагодувања на Oracle", - "config-header-mssql": "Нагодувања за Microsoft SQL Server", "config-invalid-db-type": "Неважечки тип на база", "config-missing-db-name": "Мора да внесете значење за параметарот „{{int:config-db-name}}“.", "config-missing-db-host": "Мора да внесете вредност за „{{int:config-db-host}}“.", - "config-missing-db-server-oracle": "Мора да внесете вредност за „{{int:config-db-host-oracle}}“.", - "config-invalid-db-server-oracle": "Неважечки TNS „$1“.\nКористете или „TNS Name“ или низата „Easy Connect“ ([http://docs.oracle.com/cd/E11882_01/network.112/e10836/naming.htm Методи на именување за Oracle])", "config-invalid-db-name": "Неважечко име на базата „$1“.\nКористете само ASCII-букви (a-z, A-Z), бројки (0-9), долни црти (_) и цртички (-).", "config-invalid-db-prefix": "Неважечка претставка за базата „$1“.\nКористете само ASCII-букви (a-z, A-Z), бројки (0-9), долни црти (_) и цртички (-).", "config-connection-error": "$1.\n\nПроверете го долунаведениот домаќин, корисничко име и лозинка и обидете се повторно. Ако користите „localhost“ како домаќин на базата, заменете го со „127.0.0.1“ (или обратно).", "config-invalid-schema": "Неважечка шема за МедијаВики „$1“.\nКористете само букви, бројки и долни црти.", - "config-db-sys-create-oracle": "Воспоставувачот поддржува само употреба на SYSDBA-сметка за создавање на нова сметка.", - "config-db-sys-user-exists-oracle": "Корисничката сметка „$1“ веќе постои. SYSDBA служи само за создавање на нова сметка!", "config-postgres-old": "Се бара PostgreSQL $1 или поново, а вие имате $2.", - "config-mssql-old": "Се бара Microsoft SQL Server $1 или понова верзија. Вие имате $2.", "config-sqlite-name-help": "Одберете име кое ќе го претставува вашето вики.\nНе користете празни простори и црти.\nОва ќе се користи за податотечното име на SQLite-податоците.", "config-sqlite-parent-unwritable-group": "Не можам да ја создадам папката $1 бидејќи мрежниот опслужувач не може да запише во матичната папка $2.\n\nВоспоставувачот го утврди корисникот под кој работи вашиот мрежен опслужувач.\nЗа да продолжите, наместете да може да запишува во папката $3.\nНа Unix/Linux систем направете го следново:\n\n
cd $2\nmkdir $3\nchgrp $4 $3\nchmod g+w $3
", "config-sqlite-parent-unwritable-nogroup": "Не можам да ја создадам папката $1 бидејќи мрежниот опслужувач не може да запише во матичната папка $2.\n\nВоспоставувачот не можеше го утврди корисникот под кој работи вашиот мрежен опслужувач.\nЗа да продолжите, наместете тој (и други!) да може глобално да запишува во папката $3\nНа Unix/Linux систем направете го следново:\n\n
cd $2\nmkdir $3\nchmod a+w $3
", @@ -168,11 +151,6 @@ "config-mysql-engine": "Складишен погон:", "config-mysql-innodb": "InnoDB (препорачано)", "config-mysql-engine-help": "'''InnoDB''' речиси секогаш е најдобар избор, бидејќи има добра поддршка за едновременост.\n\n'''MyISAM''' може да е побрз кај воспоставките наменети за само еден корисник или незаписни воспоставки (само читање).\nБазите на податоци од MyISAM почесто се расипуваат од базите на InnoDB.", - "config-mssql-auth": "Тип на заверка:", - "config-mssql-install-auth": "Изберете го типот на заверка што ќе се користи за поврзување со базата на податоци во текот на воспоставката.\nАко изберете „{{int:config-mssql-windowsauth}}“, ќе се користат најавните податоци или корисникот како кој работи мрежниот опслужувач.", - "config-mssql-web-auth": "Изберете го типот на заверка што мрежниот послужувач ќе го користи за поврзување со опслужувачот на базата во текот на редовната работа на викито.\nАко изберете „{{int:config-mssql-windowsauth}}“, ќе се користат најавните податоци или корисникот како кој работи мрежниот опслужувач.", - "config-mssql-sqlauth": "Заверка за SQL Server", - "config-mssql-windowsauth": "Заверка за Windows", "config-site-name": "Име на викито:", "config-site-name-help": "Ова ќе се појавува во заглавната лента на прелистувачот и на разни други места.", "config-site-name-blank": "Внесете име на мрежното место.", diff --git a/includes/installer/i18n/ml.json b/includes/installer/i18n/ml.json index c1bbec0265..8b506252bf 100644 --- a/includes/installer/i18n/ml.json +++ b/includes/installer/i18n/ml.json @@ -42,7 +42,6 @@ "config-db-type": "ഡേറ്റാബേസ് തരം:", "config-db-host": "ഡേറ്റാബേസ് ഹോസ്റ്റ്:", "config-db-name": "ഡേറ്റാബേസിന്റെ പേര്:", - "config-db-name-oracle": "ഡേറ്റാബേസ് സ്കീമ:", "config-db-install-account": "ഇൻസ്റ്റലേഷനുള്ള ഉപയോക്തൃ അംഗത്വം", "config-db-username": "ഡേറ്റാബേസ് ഉപയോക്തൃനാമം:", "config-db-password": "ഡേറ്റാബേസ് രഹസ്യവാക്ക്:", diff --git a/includes/installer/i18n/mr.json b/includes/installer/i18n/mr.json index 9f31fda72b..a1b7b37471 100644 --- a/includes/installer/i18n/mr.json +++ b/includes/installer/i18n/mr.json @@ -59,24 +59,15 @@ "config-db-type": "डाटाबेसचा प्रकार:", "config-db-host": "डाटाबेसचा यजमान:", "config-db-name": "डाटाबेसचे नाव:", - "config-db-name-oracle": "डाटाबेस आकृतीबंध:", "config-db-install-account": "उभारणीसाठी सदस्य खाते", "config-db-username": "डाटाबेसवरील सदस्यनाव:", "config-db-password": "डाटाबेसवरील परवलीचा शब्द:", "config-db-prefix": "डाटाबेस सारणी उपसर्ग:", "config-db-port": "डाटाबेस द्वार:", "config-pg-test-error": "विदागाराशी अनुबंधन करता येत नाही $1: $2", - "config-type-mssql": "मायक्रोसॉफ्ट एसक्युएल सर्व्हर", - "config-header-mssql": "मायक्रोसॉफ्ट एसक्युएल सर्व्हर मांडणावळ", "config-invalid-db-type": "डाटाबेसचा अवैध प्रकार.", "config-connection-error": "$1.\n\nयजमान,सदस्यनाव व परवलीचा शब्द तपासा व पुन्हा प्रयत्न करा.", - "config-mssql-old": "मायक्रोसॉफ्ट एसक्युएल सर्व्हर $1 किंवा त्यानंतरची आवृत्ती हवी. आपणापाशी $2 आहे.", "config-upgrade-done-no-regenerate": "दर्जोन्नती पूर्ण.\n\nआपण आता [$1 आपला विकिचा वापर करु शकता].", - "config-mssql-auth": "अधिप्रमाणन प्रकार:", - "config-mssql-install-auth": "उभारणीच्या(इन्स्टॉलेशन) प्रक्रियेदरम्यान,'अधिप्रमाणन प्रकार'( ऑथेंटीकेशन टाईप) निवडा, ज्याचा वापर डाटाबेसशी अनुबंधनात करण्यात येईल.जर आपण \"{{int:config-mssql-windowsauth}} निवडले तर,ज्याकोणत्याही सदस्याची अधिकारपत्रे(क्रेडेंटियल्स) वेबसर्व्हरवर सुरू असतील,तशीच वापरल्या जातील.", - "config-mssql-web-auth": "'अधिप्रमाणन प्रकार'( ऑथेंटीकेशन टाईप) निवडा, ज्यास,या विकिचे सामान्य चालनादरम्यान, वेब सर्व्हर हा डाटाबेसशी अनुबंधन करण्यास वापरेल.जर आपण\"{{int:config-mssql-windowsauth}}\" निवडले तर,ज्याकोणत्याही सदस्याची अधिकारपत्रे(क्रेडेंटियल्स) वेबसर्व्हरवर सुरू असतील,तशीच वापरल्या जातील.", - "config-mssql-sqlauth": "एसक्युएल सर्व्हर अधिप्रमाणन", - "config-mssql-windowsauth": "विंडोजचे अधिप्रमाणन", "config-site-name": "विकिचे नाव:", "config-site-name-help": "हे न्याहाळकाच्या शीर्षक पट्टीत व इतर ठिकाणीही दिसेल .", "config-site-name-blank": "संकेतस्थळाचे नाव टाका.", diff --git a/includes/installer/i18n/ms.json b/includes/installer/i18n/ms.json index 1163486095..6e1aa7171e 100644 --- a/includes/installer/i18n/ms.json +++ b/includes/installer/i18n/ms.json @@ -70,9 +70,7 @@ "config-no-cli-uploads-check": "Amaran: Direktori asali anda untuk muat naikan ($1) belum diperiksa untuk kerentanan\nkepada pelaksanaan skrip yang menyeleweng sewaktu pemasangan CLI.", "config-db-type": "Jenis pangkalan data:", "config-db-host": "Hos pangkalan data:", - "config-db-host-oracle": "TNS pangkalan data:", "config-db-name": "Nama pangkalan data:", - "config-db-name-oracle": "Skema pangkalan data:", "config-db-username": "Nama pengguna pangkalan data:", "config-db-password": "Kata laluan pangkalan data:", "config-db-prefix": "Awalan jadual pangkalan data:", @@ -80,15 +78,10 @@ "config-db-schema": "Skema untuk MediaWiki:", "config-pg-test-error": "Tidak boleh bersambung dengan pangkalan data $1: $2", "config-sqlite-dir": "Direktori data SQLite:", - "config-oracle-def-ts": "Ruang jadual lalai:", - "config-oracle-temp-ts": "Ruang jadual sementara:", "config-type-mysql": "MySQL (atau yang serasi)", - "config-type-mssql": "Microsoft SQL Server", "config-header-mysql": "Keutamaan MySQL", "config-header-postgres": "Keutamaan PostgreSQL", "config-header-sqlite": "Keutamaan SQLite", - "config-header-oracle": "Keutamaan Oracle", - "config-header-mssql": "Tetapan Microsoft SQL Server", "config-invalid-db-type": "Jenis pangkalan data tidak sah", "config-can-upgrade": "Terdapat jadual MediaWiki dalam pangkalan data ini. Untuk menaik tarafnya kepada MediaWiki $1, klik Teruskan.", "config-unknown-collation": "Amaran: Pangkalan data sedang menggunakan kolasi yang tidak dikenali.", @@ -96,7 +89,6 @@ "config-db-web-create": "Ciptakan akaun jika belum wujud", "config-mysql-engine": "Enjin storan:", "config-mysql-innodb": "InnoDB", - "config-mssql-auth": "Jenis pengesahan:", "config-site-name": "Nama wiki:", "config-site-name-help": "Ini akan dipaparkan pada bar tajuk perisian pelayar dan tempat-tempat lain yang berkenaan.", "config-site-name-blank": "Isikan nama tapak.", diff --git a/includes/installer/i18n/mzn.json b/includes/installer/i18n/mzn.json index 5662f0a319..5b4f2d2f6d 100644 --- a/includes/installer/i18n/mzn.json +++ b/includes/installer/i18n/mzn.json @@ -35,7 +35,6 @@ "config-apcu": "[https://www.php.net/apcu APCu] نصب بیه.", "config-wincache": "[https://www.iis.net/downloads/microsoft/wincache-extension WinCache] نصب بیه.", "config-diff3-bad": "GNU diff3 پیدا نیه.", - "config-mssql-auth": "نوع تأیید:", "config-ns-generic": "پروژه", "config-ns-other": "دیگه ( تعیین هاکنین)", "config-ns-other-default": "مه‌ویکی", diff --git a/includes/installer/i18n/nap.json b/includes/installer/i18n/nap.json index 5379e8da5f..ad0d285fbb 100644 --- a/includes/installer/i18n/nap.json +++ b/includes/installer/i18n/nap.json @@ -4,7 +4,8 @@ "C.R.", "Chelin", "Macofe", - "Fitoschido" + "Fitoschido", + "Ruthven" ] }, "config-desc": "'O prugramma d'istallazione 'e MediaWiki", @@ -83,13 +84,9 @@ "config-db-type": "Tipo 'e database:", "config-db-host": "Host d' 'o database:", "config-db-host-help": "Si 'o server database vuosto stà mpizzato dint' 'a nu server differente, miette 'o nomme d' 'o host o l'indirizzo IP sujo.\n\nSi state ausanno nu servizio spartuto web hosting, 'aggenzia 'e hosting v'avess'a dà 'o nomme buono 'e nomme host dint' 'a documentaziona suoja.\n\nSi state installanno chisto dint'a nu server Windows cu MySQL, ausanno \"localhost\" può darse ca nun funziona p' 'o nomme server. Si chisto nun funziona, mettite \"127.0.0.1\" p' 'o ndirizzo locale vuosto.\n\nSi state ausanno PostgreSQL, lassate abbacante stu campo pe' v'accucchià cu nu socket Unix.", - "config-db-host-oracle": "TNS d' 'o database:", - "config-db-host-oracle-help": "Mettite nu valido [http://download.oracle.com/docs/cd/B28359_01/network.111/b28317/tnsnames.htm Nomme 'e conessione lucale]; nu file \"tnsnames.ora\" adda essere vesibbele dint'a sta installazione.
Si state ausanno 'a libbreria cliente 10g o cchiù ricente putite pure ausà 'o metodo 'e denominazione [http://download.oracle.com/docs/cd/E11882_01/network.112/e10836/naming.htm Easy Connect].", "config-db-wiki-settings": "Identifica stu wiki", "config-db-name": "Nomme d' 'o database:", "config-db-name-help": "Sciglite nu nomme ca identificasse 'a wiki vosta.\nNun avess'a cuntenè spazie.\n\nSi ausate nu web hosting spartuto, 'o furnitore d' 'o hosting v'avesse 'a specificà nu nomme 'e database specifico pe' ve permettere 'e crià database pe' bbìa 'e nu pannello 'e cuntrollo.", - "config-db-name-oracle": "Schema d' 'o database:", - "config-db-account-oracle-warn": "Ce stanno tre scenarie suppurtate p' 'a installazione d'Oracle comme database 'e backend:\n\nSi vulite crià n'utenza 'e database comme parte d' 'o prucesso 'e installazione, dàte nu cunto c' 'o ruolo SYSDBA comme utenza d' 'o database pe ne fà installazione e specificate 'e credenziale vulute pe' ne fà l'utenza d'acciesso web, sinò è possibbele crià manualmente l'utenza d'accesso web e dà surtanto chillu cunto (si tenite autorizzaziune neccessarie pe' crià oggette 'e stu schema) po dà dduje utenze divierze, una ch' 'e permesse 'e criazione e n'ata pe ne putè trasì ô web.\n\n'O script p' 'a criazione 'e n'utenza cu tutte st'autorizzaziune neccessarie 'o putite truvà dint' 'a cartella \"maintenance/oracle\" 'e sta installazione. Tenite a mmente che l'uso 'e n'utenza cu sti restriziune stutarrà tutt' 'e funziune 'e manutenzione c' 'o cunto predefinito.", "config-db-install-account": "Cunto utente pe' l'installazione", "config-db-username": "Nomme utente p' 'o database:", "config-db-password": "Password d' 'o database:", @@ -107,35 +104,23 @@ "config-db-schema-help": "Stu schema 'n genere sarrà buono.\nSi 'o vulite cagnà facite sulamente si ne tenite abbesuogno.", "config-pg-test-error": "Nun se può connettà a 'o database $1: $2", "config-sqlite-dir": "Cartella 'e data 'e SQLite:", - "config-sqlite-dir-help": "SQLite astipa tutte 'e date dint'a n'uneco file.\n\n'A cartella ca starraje a innecà adda essere scrivibbele d' 'o server webe pe' tramente ca sta l'istallazione.\n\nAvess'a essere nun trasibbele via web, è pecchesto ca nun se sta mettenno addò stanno 'e file PHP.\n\nL'installatore scriverrà nzieme a chesta nu file .htaccess, ma si 'o tentativo scassasse, coccheruno putesse tenè acciesso dint' 'o database ncruro.\nChesto pure cunzidera 'e date ncruro 'e ll'utente (indirizze, password cifrate) accussì comme 'e verziune luvate e ati dati d'accesso limmetato dint' 'o wiki.\n\nCunzidera ll'opportunità 'e sistimà ô tiempo 'o database 'a n'ata parte, p'esempio int'a /var/lib/mediawiki/tuowiki.", - "config-oracle-def-ts": "Tablespace 'e default:", - "config-oracle-temp-ts": "Tablespace temporaneo:", + "config-sqlite-dir-help": "SQLite astipa tutte 'e date dint'a n'uneco file.\n\n'A cartella ca starraje a innecà adda essere scrivibbele d' 'o server webe pe' tramente ca sta l'istallazione.\n\nAvess'a essere nun trasibbele via web, è pecchesto ca nun se sta mettenno addò stanno 'e file PHP.\n\nL'installatore scriverrà nzieme a chesta nu file .htaccess, ma si 'o tentativo scassasse, coccheruno putesse tenè acciesso dint' 'o database ncruro.\nChesto cunzidera purzì 'e date ncruro 'e ll'utente (indirizze, password cifrate) accussì comme 'e verziune luvate e ati dati d'accesso limmetato dint' 'o wiki.\n\nCunzidera ll'opportunità 'e sistimà ô tiempo 'o database 'a n'ata parte, p'esempio int'a /var/lib/mediawiki/tuowiki.", "config-type-mysql": "MariaDB, MySQL o compatibbele", - "config-type-mssql": "Microsoft SQL Server", "config-support-info": "MediaWiki supporta 'e sisteme 'e database ccà abbascio:\n\n$1\n\nSi nfra chiste ccà nun vedite 'o sistema 'e database ca vulite ausà, allora avite liegge 'e instruziune ccà ncoppa pe' ne dà supporto.", "config-dbsupport-mysql": "* [{{int:version-db-mariadb-url}} MariaDB] è 'a configurazione cchiù mmeglio p' 'o MediaWiki e è chilla meglio suppurtata. MediaWiki può faticà pure cu' [{{int:version-db-mariadb-url}} MariaDB] e [{{int:version-db-percona-url}} Percona Server], ca fossero MariaDB cumpatibbele. ([https://www.php.net/manual/en/mysqli.installation.php Comme s'adda fà pe' cumpilà PHP cu suppuorto MySQL])", "config-dbsupport-postgres": "* [{{int:version-db-postgres-url}} PostgreSQL] è nu sistema canusciuto 'e database open source ca fosse n'alternativa a MySQL.\n([https://www.php.net/manual/en/pgsql.installation.php Comme s'avess'a cumpilà PHP cu suppuorto PostgreSQL])", "config-dbsupport-sqlite": "* [{{int:version-db-sqlite-url}} SQLite] è nu sistema 'e database leggero, ca fosse assaje buono suppurtato.\n([https://www.php.net/manual/en/pdo.installation.php Comme cumpilà PHP cu suppuorto SQLite], aùsa PDO)", - "config-dbsupport-oracle": "* [{{int:version-db-oracle-url}} Oracle] è nu database 'e na fraveca commerciale. ([https://www.php.net/manual/en/oci8.installation.php Comme cumpilà PHP cu suppuorto OCI8])", - "config-dbsupport-mssql": "* [{{int:version-db-mssql-url}} Microsoft SQL Server] è nu database 'e na fraveca commerciale p' 'o Windows. ([https://www.php.net/manual/en/sqlsrv.installation.php Comme cumpilà PHP cu suppuorto SQLSRV])", "config-header-mysql": "Mpustaziune MariaDB/MySQL", "config-header-postgres": "Mpustaziune PostgreSQL", "config-header-sqlite": "Mpustaziune SQLite", - "config-header-oracle": "Mpustaziune Oracle", - "config-header-mssql": "Mpustaziune 'e Microsoft SQL Server", "config-invalid-db-type": "'O tipo 'e database nun è buono.", "config-missing-db-name": "Avita miette nu valore p' 'o \"{{int:config-db-name}}\"", "config-missing-db-host": "Avita miette nu valore p' 'o \"{{int:config-db-host}}\"", - "config-missing-db-server-oracle": "Avita miette nu valore p' 'o \"{{int:config-db-host-oracle}}\"", - "config-invalid-db-server-oracle": "'O database 'e TNS \"$1\" nun è buono.\nAusate 'o \"TNS Name\" o na catena d' \"Easy Connect\"([http://docs.oracle.com/cd/E11882_01/network.112/e10836/naming.htm Metude 'e Nommena Oracle]).", "config-invalid-db-name": "Nomme 'e database \"$1\" nun valido.\nAúsa surtanto carattere ASCII comme lettere (a-z, A-Z), nummere (0-9), sottolineatura (_) e trattine (-).", "config-invalid-db-prefix": "Prefisso database \"$1\" nun valido.\nAúsa surtanto carattere ASCII comme lettere (a-z, A-Z), nummere (0-9), sottolineatura (_) e trattine (-).", "config-connection-error": "$1.\n\nCuntrullate 'o host, nomme utente e password e tentate n'ata vota.", "config-invalid-schema": "Schema MediaWiki \"$1\" nun è buono.\nAusate surtanto 'e lettere ASCII (a-z, A-Z), nummere (0-9) e carattere 'e sottolineatura (_).", - "config-db-sys-create-oracle": "'O prugramma 'e installazione supporta surtanto l'uso 'e nu cunto SYSDBA pe' putè crià nu cunto nuovo.", - "config-db-sys-user-exists-oracle": "'O cunto utente \"$1\" esiste già. SYSDBA se pò ausà surtanto pe' crià cunte nuove!", "config-postgres-old": "PostgreSQL $1 o cchiù muderno è necessario. Vuje tenite $2.", - "config-mssql-old": "Microsoft SQL Server $1 o cchiù muderno è necessario. Vuje tenite $2.", "config-sqlite-name-help": "Sciglite nu nomme ca identificasse 'o wiki vuosto.\nNun ausà spazie o trattine.\nChesto serverrà pe' putè miettere 'o nomme ro file 'e date SQLite.", "config-sqlite-parent-unwritable-group": "Nun se pò crià 'a cartella 'e date $1, pecché 'a cartella supiriore $2 nun se pò scrivere 'a 'o webserver.\n\n'O prugramma d'installazione ha determinato l'utente c' 'o quale 'o server web se stà a esecutà.\nDàte 'a pussibbelità 'e scrivere dint' 'a cartella $3 pe' cuntinuà\nNcopp'a nu sistema Unix/Linux:\n\n
cd $2\nmkdir $3\nchgrp $4 $3\nchmod g+w $3
", "config-sqlite-parent-unwritable-nogroup": "Nun se può crià na cartella 'e date $1, pecché 'a cartella patre $2 nun è scrivibbele p' 'o server web.\n\n'O prugramma 'e installazione nun ave pututo determinà l'utente c' 'o quale se stà ausanno 'o server web.\nFacite 'a cartella $3 screvibbele globbalmente pe chisto (e ll'ati!) pe' putè cuntinuà:\nDint'a nu sistema Unix/Linux facite:\n\n
cd $2\nmkdir $3\nchmod a+w $3
", @@ -154,16 +139,11 @@ "config-db-web-account": "Cunto d' 'o database pe' ne fà acciesso web", "config-db-web-help": "Scigliete 'o nomme utente e passwrod ca 'o web server ausarrà pe' se cullegà 'o server database, pe' tramente ca se fa' operazione normale d' 'o wiki.", "config-db-web-account-same": "Aúsa 'o stisso cunto comme quanno s'è fatta 'a installazione", - "config-db-web-create": "Crìa 'o cunto si nun esiste ancora", + "config-db-web-create": "Crìa 'o cunto si nun esiste perzi", "config-db-web-no-create-privs": "'O cunto ausato pe' ne fà l'installazione nun tene diritte necessarie pe' ne putè crià n'atu cunto.\n'O cunto zegnàto ccà adda esistere già.", "config-mysql-engine": "Mutore d'astipo:", "config-mysql-innodb": "InnoDB (fosse 'o cunzigliato)", "config-mysql-engine-help": "InnoDB è quase sempe 'a meglia opzione, pecché ave nu buono suppuorto concorrente.\n\nMyISAM putesse ghì cchiù ampressa int'a na installazione mono-utente e liegge-surtanto.\n'E database MyISAM se scassano cchiù spisso d' 'e database InnoDB.", - "config-mssql-auth": "Tipo d'autenticazione:", - "config-mssql-install-auth": "Sceglie 'o tipo d'autenticazziona ca s'ausarrà pe cunnettà â database, durante ll'operazziona d'istallazziona. Si piglie \"{{int:config-mssql-windowsauth}}\", 'e credenziale 'e qualunque fosse ll'utenza ca 'o webserver sta pruciessanno sarranno ausate.", - "config-mssql-web-auth": "Sceglie 'o tipo d'autenticazziona ca 'o web server pigliarrà pe se cunnettà a 'o server 'e bbase 'e dati, durante ll'operazziona nurmale d' 'a wiki.\nSi piglie \"{{int:config-mssql-windowsauth}}\", 'e credenziale 'e qualunque fosse ll'utenza ca 'o webserver sta pruciessanno sarranno ausate.", - "config-mssql-sqlauth": "Autenticazione 'e SQL Server", - "config-mssql-windowsauth": "Autenticazione 'e Windows", "config-site-name": "Nomme d' 'o wiki:", "config-site-name-help": "Chisto cumparerrà dint' 'a barra d' 'o titolo d' 'o navigatore e pure dint'a n'ati pizze.", "config-site-name-blank": "Scrive 'o nomme d' 'o sito.", diff --git a/includes/installer/i18n/nb.json b/includes/installer/i18n/nb.json index 522294f440..6195104461 100644 --- a/includes/installer/i18n/nb.json +++ b/includes/installer/i18n/nb.json @@ -50,16 +50,20 @@ "config-restart": "Ja, start på nytt", "config-welcome": "=== Miljøsjekker ===\nGrunnleggende sjekker utføres for å se om dette miljøet er egnet for en MediaWiki-installasjon.\nDu bør oppgi resultatene fra disse sjekkene om du trenger hjelp under installasjonen.", "config-welcome-section-copyright": "=== Opphavsrett og vilkår ===\n\n$1\n\nMediaWiki er fri programvare; du kan redistribuere det og/eller modifisere det under betingelsene i GNU General Public License som publisert av Free Software Foundation; enten versjon 2 av lisensen, eller (etter eget valg) enhver senere versjon.\n\nDette programmet er distribuert i håp om at det vil være nyttig, men '''uten noen garanti'''; ikke engang implisitt garanti av '''salgbarhet''' eller '''egnethet for et bestemt formål'''.\nSe GNU General Public License for flere detaljer.\n\nDu skal ha mottatt [$2 en kopi av GNU General Public License] sammen med dette programmet; hvis ikke, skriv til Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA eller [https://www.gnu.org/copyleft/gpl.html les det på nettet].", - "config-sidebar": "* [https://www.mediawiki.org MediaWiki hjem]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents Brukerguide]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Contents Administratorguide]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ OSS]\n----\n* Les meg\n* Utgivelsesnotater\n* Kopiering\n* Oppgradering", + "config-sidebar": "* [https://www.mediawiki.org MediaWiki.org]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents Brukerguide]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Contents Administratorguide]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ OSS]", + "config-sidebar-readme": "Les meg", + "config-sidebar-relnotes": "Utgivelsesnotater", + "config-sidebar-license": "Kopiering", + "config-sidebar-upgrade": "Oppgradering", "config-env-good": "Miljøet har blitt sjekket.\nDu kan installere MediaWiki.", "config-env-bad": "Miljøet har blitt sjekket.\nDu kan ikke installere MediaWiki.", "config-env-php": "PHP $1 er installert.", "config-env-hhvm": "HHVM $1 er installert.", - "config-unicode-using-intl": "Bruker [https://pecl.php.net/intl intl PECL-utvidelsen] for Unicode-normalisering.", - "config-unicode-pure-php-warning": "'''Advarsel''': [https://pecl.php.net/intl intl PECL-utvidelsen] er ikke tilgjengelig for å håndtere Unicode-normaliseringen, faller tilbake til en langsommere ren-PHP-implementasjon.\nOm du kjører et nettsted med høy trafikk bør du lese litt om [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations Unicode-normalisering].", + "config-unicode-using-intl": "Bruker [https://php.net/manual/en/book.intl.php PHPs intl-utvidelse] for Unicode-normalisering.", + "config-unicode-pure-php-warning": "Advarsel: [https://php.net/manual/en/book.intl.php PHPs intl-utvidelse] er ikke tilgjengelig for å håndtere Unicode-normaliseringen, faller tilbake til en langsommere ren-PHP-implementasjon.\nOm du kjører et nettsted med høy trafikk bør du lese om [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations Unicode-normalisering].", "config-unicode-update-warning": "Advarsel: Den installerte versjonen av Unicode-normalisereren bruker en eldre versjon av [http://site.icu-project.org/ ICU-prosjektets] bibliotek.\nDu bør [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations oppgradere] om du er bekymret for å bruke Unicode.", "config-no-db": "Fant ingen passende databasedriver! Du må installere en databasedriver for PHP.\nFølgende {{PLURAL:$2|databasetype|databasetyper}} støttes: $1\n\nOm du kompilerte PHP selv, rekonfigurer den med en aktivert databaseklient, for eksempel ved å bruke ./configure --with-mysqli.\nOm du installerte PHP fra en Debian- eller Ubuntu-pakke, må du også installere for eksempel php-mysql-pakken.", - "config-outdated-sqlite": "'''Advarsel''': Du har SQLite $1, som er en eldre versjon enn minimumskravet SQLite $2. SQLite vil ikke være tilgjengelig.", + "config-outdated-sqlite": "Advarsel: Du har SQLite $2, som er en eldre versjon enn minimumskravet SQLite $1. SQLite vil ikke være tilgjengelig.", "config-no-fts3": "'''Advarsel''': SQLite er kompilert uten [//sqlite.org/fts3.html FTS3-modulen], søkefunksjoner vil ikke være tilgjengelig på dette bakstykket.", "config-pcre-old": "'''Alvorlig:''' PCRE $1 eller senere kreves.\nDin PHP-kode er lenket med PCRE $2.\n[https://www.mediawiki.org/wiki/Manual:Errors_and_symptoms/PCRE Nærmere informasjon].", "config-pcre-no-utf8": "'''Fatal''': PHPs PCRE modul ser ut til å være kompilert uten PCRE_UTF8-støtte.\nMediaWiki krever UTF-8-støtte for å fungere riktig.", @@ -83,18 +87,14 @@ "config-uploads-not-safe": "'''Advarsel:''' Din standardmappe for opplastinger $1 er sårbar for kjøring av vilkårlige skript.\nSelv om MediaWiki sjekker alle opplastede filer for sikkerhetstrusler er det sterkt anbefalt å [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Security#Upload_security lukke denne sikkerhetssårbarheten] før du aktiverer opplastinger.", "config-no-cli-uploads-check": "'''Advarsel:''' Din standard-katalog for opplastinger ($1) er ikke kontrollert for sårbarhet overfor vilkårlig skript-kjøring under CLI-installasjonen.", "config-brokenlibxml": "Ditt system bruker en kombinasjon av PHP- og libxml2-versjoner som har feil og kan forårsake skjult dataødeleggelse i MediaWiki og andre web-applikasjoner.\nOppgrader til libxml2 2.7.3 eller nyere ([https://bugs.php.net/bug.php?id=45996 Feil-liste for PHP]).\nInstalleringen ble abortert.", - "config-suhosin-max-value-length": "Suhosin er installert og begrenser GET-parameterlengder til $1 bytes. MediaWiki sin ResourceLoader-komponent klarer å komme rundt denne begrensningen, men med redusert ytelse. Om mulig bør du sette suhosin.get.max_value_length til minst 1024 i php.ini, og sette $wgResourceLoaderMaxQueryLength til samme verdi i LocalSettings.php.", + "config-suhosin-max-value-length": "Suhosin er installert og begrenser GET-parameterlengder til $1 bytes.\nMediaWiki krever at suoshin.get.max_value_length er minst $2. Slå av denne innstillingen eller øk verdien til $3 i php.ini.", "config-using-32bit": "Adversel: Systemet ditt ser ut til å være 32-bit-basert, mens dette er [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:32-bit not advised].", "config-db-type": "Databasetype:", "config-db-host": "Databasevert:", "config-db-host-help": "Hvis databasen kjører på en annen tjenermaskin, skriv inn vertsnavnet eller IP-adressen her.\n\nHvis du bruker et webhotell, vil du kunne be om aktuelt vertsnavn fra din leverandør.\n\nHvis du bruker MySQL, kan det hende at «localhost» ikke brukes som tjenernavn. Hvis så er tilfelle, prøv «127.0.0.1» som lokal IP-adresse.\n\nHvis du bruker PostgreSQL, la dette feltet være blankt slik at koplingen gjøres via en \"Unix socket\".", - "config-db-host-oracle": "Database TNS:", - "config-db-host-oracle-help": "Skriv inn et gyldig [http://download.oracle.com/docs/cd/B28359_01/network.111/b28317/tnsnames.htm Local Connect Name]; en tnsnames.ora-fil må være synlig for installasjonsprosessen.
Hvis du bruker klientbibliotek 10g eller nyere kan du også bruke navngivingsmetoden [http://download.oracle.com/docs/cd/E11882_01/network.112/e10836/naming.htm Easy Connect].", "config-db-wiki-settings": "Identifiser denne wikien", "config-db-name": "Databasenavn (ingen bindestreker):", "config-db-name-help": "Velg et navn som identifiserer wikien din.\nDet bør ikke inneholde mellomrom.\n\nHvis du bruker en delt nettvert vil verten din enten gi deg et spesifikt databasenavn å bruke, eller la deg opprette databaser via kontrollpanelet.", - "config-db-name-oracle": "Databaseskjema:", - "config-db-account-oracle-warn": "Det finnes tre mulig fremgangsmåter for å installere Oracle som database:\n\nHvis du ønsker å opprette en databasekonto som del av installasjonsprosessen, oppgi da en konto med SYSDBA-rolle som databasekonto for installasjonen og angi påkrevd autentiseringsinformasjon for web-aksesskontoen. Ellers kan du enten opprette web-aksesskontoen manuelt eller kun oppgi den kontoen (hvis den har påkrevede tillatelser for å opprette skjemeobjektene) , alternativt oppgi to ulike kontoer, en med opprettelsesprivilegier (create) og en begrenset konto for web-aksess.\n\nSkript for å opprette en konto med påkrevde privilegier finnes i \"maintenance/oracle/\"-folderen av denne installasjonen. Husk at det å bruke en begrenset konto vil blokkere all vedlikeholdsfunksjonalitet med standard konto.", "config-db-install-account": "Brukerkonto for installasjon", "config-db-username": "Databasebrukernavn:", "config-db-password": "Databasepassord:", @@ -113,37 +113,24 @@ "config-pg-test-error": "Får ikke kontakt med database '''$1''': $2", "config-sqlite-dir": "SQLite datamappe:", "config-sqlite-dir-help": "SQLite lagrer alle data i en enkelt fil.\n\nMappen du oppgir må være skrivbar for nettjeneren under installasjonen.\n\nDen bør '''ikke''' være tilgjengelig fra nettet, dette er grunnen til at vi ikke legger det der PHP-filene dine er.\n\nInstallasjonsprogrammet vil skrive en .htaccess-fil sammen med det, men om det mislykkes kan noen få tilgang til din råe database. Dette inkluderer rå brukerdata (e-postadresser, hashede passord) samt slettede revisjoner og andre begrensede data på wikien.\n\nVurder å plassere databasen et helt annet sted, for eksempel i /var/lib/mediawiki/yourwiki.", - "config-oracle-def-ts": "Standard tabellrom:", - "config-oracle-temp-ts": "Midlertidig tabellrom:", "config-type-mysql": "MariaDB, MySQL eller kompatibel", "config-type-postgres": "PostgreSQL", "config-type-sqlite": "SQLite", - "config-type-oracle": "Oracle", - "config-type-mssql": "Microsoft SQLServer", "config-support-info": "MediaWiki støtter følgende databasesystem:\n\n$1\n\nHvis du ikke ser databasesystemet du prøver å bruke i listen nedenfor, følg instruksjonene det er lenket til over for å aktivere støtte.", "config-dbsupport-mysql": "* [{{int:version-db-mariadb-url}} MariaDB] er den foretrukne databasetypen for MediaWiki og har best støtte. MediaWiki fungerer også med [{{int:version-db-mysql-url}} MySQL] og [{{int:version-db-percona-url}} Percona Server], som begge er MariaDB-kompatible. ([https://www.php.net/manual/en/mysqli.installation.php Hvordan kompilere PHP med MySQL-støtte])", "config-dbsupport-postgres": "* [{{int:version-db-postgres-url}} PostgreSQL] er et populært åpen kildekode-databasesystem og et alternativ til MySQL. ([https://www.php.net/manual/en/pgsql.installation.php Hvordan kompilere PHP med PostgreSQL-støtte])", "config-dbsupport-sqlite": "* [{{int:version-db-sqlite-url}} SQLite] er et lettvekts-databasesystem som har veldig god støtte. ([http://www.php.net/manual/en/pdo.installation.php Hvordan kompilere PHP med SQLite-støtte], bruker PDO)", - "config-dbsupport-oracle": "* [{{int:version-db-oracle-url}} Oracle] er en kommersiell database for bedrifter. ([https://www.php.net/manual/en/oci8.installation.php Hvordan kompilere PHP med OCI8-støtte])", - "config-dbsupport-mssql": "* [{{int:version-db-mssql-url}} Microsoft SQL Server] er et kommersielt databasesystem under Windows for bedrifter. ([https://www.php.net/manual/en/sqlsrv.installation.php Hvordan kompilere PHP med SQLSRV-støtte])", "config-header-mysql": "MariadB/MySQL-innstillinger", "config-header-postgres": "PostgreSQL-innstillinger", "config-header-sqlite": "SQLite-innstillinger", - "config-header-oracle": "Oracle-innstillinger", - "config-header-mssql": "Microsoft SQLServer-innstillinger", "config-invalid-db-type": "Ugyldig databasetype", "config-missing-db-name": "Du må skrive inn en verdi for «{{int:config-db-name}}»", "config-missing-db-host": "Du må skrive inn en verdi for «{{int:config-db-host}}»", - "config-missing-db-server-oracle": "Du må skrive inn en verdi for «{{int:config-db-host-oracle}}»", - "config-invalid-db-server-oracle": "Ugyldig database-TNS «$1».\nBruk enten \"TNS Name\" eller en \"Easy Connect\"-streng ([http://docs.oracle.com/cd/E11882_01/network.112/e10836/naming.htm Oracle Naming Methods])", "config-invalid-db-name": "Ugyldig databasenavn «$1».\nBruk bare ASCII-bokstaver (a-z, A-Z), tall (0-9), undestreker (_) og bindestreker (-).", "config-invalid-db-prefix": "Ugyldig databaseprefiks «$1».\nBruk bare ASCII-bokstaver (a-z, A-Z), tall (0-9), undestreker (_) og bindestreker (-).", "config-connection-error": "$1.\n\nSjekk verten, brukernavnet og passordet nedenfor og prøv igjen. Hvis du brukte «localhost» som databasevert, prøv å bruke «127.0.0.1» i stedet (eller motsatt).", "config-invalid-schema": "Ugyldig skjema for MediaWiki «$1».\nBruk bare ASCII-bokstaver (a-z, A-Z), tall (0-9) og undestreker (_).", - "config-db-sys-create-oracle": "Installasjonsprogrammet støtter kun bruk av en SYSDBA-konto for opprettelse av en ny konto.", - "config-db-sys-user-exists-oracle": "Brukerkontoen «$1» finnes allerede. SYSDBA kan kun brukes for oppretting av nye kontoer!", "config-postgres-old": "PostgreSQL $1 eller senere kreves, du har $2.", - "config-mssql-old": "Microsoft SQLServer $1 eller senere kreves. Du har $2.", "config-sqlite-name-help": "Velg et navn som identifiserer wikien din.\nIkke bruk mellomrom eller bindestreker.\nDette vil bli brukt til SQLite-datafilnavnet.", "config-sqlite-parent-unwritable-group": "Kan ikke opprette datamappen $1 fordi foreldremappen $2 ikke er skrivbar for nettjeneren.\n\nInstallasjonsprogrammet har bestemt brukeren nettjeneren din kjører som.\nGjør $3-mappen skrivbar for denne for å fortsette.\nPå et Unix/Linux-system, gjør:\n\n
cd $2\nmkdir $3\nchgrp $4 $3\nchmod g+w $3
", "config-sqlite-parent-unwritable-nogroup": "Kan ikke opprette datamappen $1 fordi foreldremappen $2 ikke er skrivbar for nettjeneren.\n\nInstallasjonsprogrammet kunne ikke bestemme brukeren nettjeneren din kjører som.\nGjør $3-mappen globalt skrivbar for denne (og andre!) for å fortsette.\nPå et Unix/Linux-system, gjør:\n\n
cd $2\nmkdir $3\nchmod a+w $3
", @@ -168,11 +155,6 @@ "config-mysql-engine": "Lagringsmotor:", "config-mysql-innodb": "InnoDB (anbefalt)", "config-mysql-engine-help": "'''InnoDB''' er nesten alltid det beste alternativet siden den har god støtte for samtidighet («concurrency»).\n\n'''MyISAM''' kan være raskere i enbruker- eller les-bare-installasjoner.\nMyISAM-databaser har en tendens til å bli ødelagt oftere enn InnoDB-databaser.", - "config-mssql-auth": "Autentiseringstype:", - "config-mssql-install-auth": "Valg autentiseringstypen som skal brukes for å koble til databasen under installeringsprosessen. Hvis du velger «{{int:config-mssql-windowsauth}}», vil påloggingsinformasjonen for brukeren som kjører webtjeneren blir brukt.", - "config-mssql-web-auth": "Velg autentiseringstype som webtjeneren vil bruke for å koble til databasetjeneren under normal kjøring av wikien.\nHvis du velger «{{int:config-mssql-windowsauth}}», vil påloggingsinformasjonen til brukeren som kjører webtjeneren blir brukt.", - "config-mssql-sqlauth": "SQLServer-autentisering", - "config-mssql-windowsauth": "Windows-autentisering", "config-site-name": "Navn på wiki:", "config-site-name-help": "Dette vil vises i tittellinjen i nettleseren og diverse andre steder.", "config-site-name-blank": "Skriv inn et nettstedsnavn.", diff --git a/includes/installer/i18n/ne.json b/includes/installer/i18n/ne.json index 2677d47057..e20b9e9b2d 100644 --- a/includes/installer/i18n/ne.json +++ b/includes/installer/i18n/ne.json @@ -38,16 +38,13 @@ "config-env-hhvm": "HHVM $1 स्थापना गरिएको छ ।", "config-db-type": "डाटाबेस प्रकारः", "config-db-host": "डेटाबेस होस्ट:", - "config-db-host-oracle": "डेटाबेस TNS:", "config-db-name": "डाटाबेस नामः", - "config-db-name-oracle": "डेटाबेस स्केमा:", "config-db-username": "डाटाबेस प्रयोगकर्ता नामः", "config-db-password": "डाटाबेस पासबर्डः", "config-db-port": "डेटाबेस पोर्ट:", "config-header-mysql": "MySQL सेटिङ", "config-header-postgres": "PostgreSQL सेटिङहरू", "config-header-sqlite": "SQLite सेटिङ्हरू", - "config-header-oracle": "ओरेकल सेटिङहरू", "config-site-name": "विकीको नाम:", "config-site-name-blank": "साइटको नाम लेख्नुहोस।", "config-project-namespace": "आयोजना नेमस्पेस:", diff --git a/includes/installer/i18n/nl-informal.json b/includes/installer/i18n/nl-informal.json index 3450712796..c0c754be94 100644 --- a/includes/installer/i18n/nl-informal.json +++ b/includes/installer/i18n/nl-informal.json @@ -28,9 +28,7 @@ "config-no-cli-uploads-check": "''Waarschuwing:'' je standaardmap voor uploads ($1) wordt niet gecontroleerd op kwetsbaarheden voor het uitvoeren van willekeurige scripts gedurende de CLI-installatie.", "config-brokenlibxml": "Je systeem heeft een combinatie van PHP- en libxml2-versies geïnstalleerd die is foutgevoelig is en kan leiden tot onzichtbare beschadiging van gegevens in MediaWiki en andere webapplicaties.\nUpgrade naar PHP 5.2.9 of hoger en libxml2 2.7.3 of hoger([https://bugs.php.net/bug.php?id=45996 bij PHP gerapporteerde fout]).\nDe installatie wordt afgebroken.", "config-db-host-help": "Als je databaseserver een andere server is, voer dan de hostnaam of het IP-adres hier in.\n\nAls je gebruik maakt van gedeelde webhosting, hoort je provider je de juiste hostnaam te hebben verstrekt.\n\nAls je MediaWiki op een Windowsserver installeert en MySQL gebruikt, dan werkt \"localhost\" mogelijk niet als servernaam.\nAls het inderdaad niet werkt, probeer dan \"127.0.0.1\" te gebruiken als lokaal IP-adres.\n\nAls je PostgreSQL gebruikt, laat dit veld dan leeg om via een Unix-socket te verbinden.", - "config-db-host-oracle-help": "Voer een geldige [http://download.oracle.com/docs/cd/B28359_01/network.111/b28317/tnsnames.htm Local Connect Name] in; een tnsnames.ora-bestand moet zichtbaar zijn voor deze installatie.
Als je gebruik maakt van clientlibraries 10g of een latere versie, kan je ook gebruik maken van de naamgevingsmethode [http://download.oracle.com/docs/cd/E11882_01/network.112/e10836/naming.htm Easy Connect].", "config-db-name-help": "Kies een naam die je wiki identificeert.\nEr mogen geen spaties gebruikt worden.\nAls je gebruik maakt van gedeelde webhosting, dan hoort je provider ofwel jou een te gebruiken databasenaam gegeven te hebben, of je aangegeven te hebben hoe je databases kunt aanmaken.", - "config-db-account-oracle-warn": "Er zijn drie ondersteunde scenario's voor het installeren van Oracle als databasebackend:\n\nAls je een database-account wilt aanmaken als onderdeel van het installatieproces, geef dan de gegevens op van een database-account in met de rol SYSDBA voor de installatie en voer de gewenste aanmeldgegevens in voor het account met webtoegang. Je kunt ook het account met webtoegang handmatig aanmaken en alleen van dat account de aanmeldgegevens opgeven als deze de vereiste rechten heeft om schemaobjecten aan te maken. Als laatste is het mogelijk om aanmeldgegevens van twee verschillende accounts op te geven; een met de rechten om schemaobjecten aan te maken, en een met alleen webtoegang.\n\nEen script voor het aanmaken van een account met de vereiste rechten is te vinden in de map \"maintenance/oracle/\" van deze installatie. Onthoud dat het gebruiken van een account met beperkte rechten alle mogelijkheden om beheerscripts uit te voeren met het standaardaccount onmogelijk maakt.", "config-db-prefix-help": "Als je een database moet gebruiken voor meerdere wiki's, of voor MediaWiki en een andere toepassing, dan kan je ervoor kiezen om een voorvoegsel toe te voegen aan de tabelnamen om conflicten te voorkomen.\nGebruik geen spaties.\n\nDit veld wordt meestal leeg gelaten.", "config-mysql-old": "Je moet MySQL $1 of later gebruiken.\nJij gebruikt $2.", "config-db-schema-help": "Dit schema klopt meestal.\nWijzig het alleen als je weet dat dit nodig is.", @@ -38,7 +36,6 @@ "config-support-info": "MediaWiki ondersteunt de volgende databasesystemen:\n\n$1\n\nAls je het databasesysteem dat je wilt gebruiken niet in de lijst terugvindt, volg dan de handleiding waarnaar hierboven wordt verwezen om ondersteuning toe te voegen.", "config-missing-db-name": "Je moet een waarde opgeven voor \"Databasenaam\"", "config-missing-db-host": "Je moet een waarde invoeren voor \"Databaseserver\"", - "config-missing-db-server-oracle": "Je moet een waarde opgeven voor \"Database-TNS\"", "config-postgres-old": "PostgreSQL $1 of hoger is vereist.\nJij gebruikt $2.", "config-sqlite-name-help": "Kies een naam die je wiki identificeert.\nGebruik geen spaties of koppeltekens.\nDeze naam wordt gebruikt voor het gegevensbestand van SQLite.", "config-upgrade-done": "Het bijwerken is afgerond.\n\nJe kunt [$1 je wiki nu gebruiken].\n\nAls je je LocalSettings.php opnieuw wilt aanmaken, klik dan op de knop hieronder.\nDit is '''niet aan te raden''' tenzij je problemen hebt met je wiki.", diff --git a/includes/installer/i18n/nl.json b/includes/installer/i18n/nl.json index 23835cd36e..21ca17c8dd 100644 --- a/includes/installer/i18n/nl.json +++ b/includes/installer/i18n/nl.json @@ -62,7 +62,7 @@ "config-page-existingwiki": "Bestaande wiki", "config-help-restart": "Wilt u alle opgeslagen gegevens die u hebt ingevoerd wissen en het installatieproces opnieuw starten?", "config-restart": "Ja, opnieuw starten", - "config-welcome": "=== Controle omgeving ===\nEr worden een aantal basiscontroles uitgevoerd met als doel vast te stellen of deze omgeving geschikt is voor een installatie van MediaWiki.\nLever deze gegevens aan als u ondersteuning vraagt bij de installatie.", + "config-welcome": "=== Omgevingscontrole ===\nEr worden een aantal basiscontroles uitgevoerd met als doel vast te stellen of deze omgeving geschikt is voor een installatie van MediaWiki.\nLever deze gegevens aan als u ondersteuning vraagt bij de installatie.", "config-welcome-section-copyright": "=== Auteursrechten en voorwaarden ===\n\n$1\n\nDit programma is vrije software. U mag het verder verspreiden en/of aanpassen in overeenstemming met de voorwaarden van de GNU General Public License zoals uitgegeven door de Free Software Foundation; ofwel versie 2 van de Licentie of - naar uw keuze - enige latere versie.\n\nDit programma wordt verspreid in de hoop dat het nuttig is, maar '''zonder enige garantie''', zelfs zonder de impliciete garantie van '''verkoopbaarheid''' of '''geschiktheid voor een bepaald doel'''.\nZie de GNU General Public License voor meer informatie.\n\nSamen met dit programma hoort u een [$2 exemplaar van de GNU General Public License] ontvangen te hebben; zo niet, schrijf dan aan de Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, Verenigde Staten. Of [https://www.gnu.org/copyleft/gpl.html lees de licentie online].", "config-sidebar": "* [https://www.mediawiki.org MediaWiki-thuispagina]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents Gebruikershandleiding]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Contents Beheerdershandleiding]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ Veelgestelde vragen]", "config-sidebar-readme": "Leesmij", @@ -83,9 +83,9 @@ "config-pcre-no-utf8": "Onherstelbare fout: de module PRCE van PHP lijkt te zijn gecompileerd zonder ondersteuning voor PCRE_UTF8.\nMediaWiki heeft ondersteuning voor UTF-8 nodig om correct te kunnen werken.", "config-memory-raised": "PHP's memory_limit is $1 en is verhoogd tot $2.", "config-memory-bad": "'''Waarschuwing:''' PHP's memory_limit is $1.\nDit is waarschijnlijk te laag.\nDe installatie kan mislukken!", - "config-apc": "[https://www.php.net/apc APC] is op dit moment geïnstalleerd", + "config-apc": "[https://www.php.net/apc APC] is geïnstalleerd", "config-apcu": "[https://www.php.net/apcu APCu] is geïnstalleerd", - "config-wincache": "[https://www.iis.net/downloads/microsoft/wincache-extension WinCache] is op dit moment geïnstalleerd", + "config-wincache": "[https://www.iis.net/downloads/microsoft/wincache-extension WinCache] is geïnstalleerd", "config-no-cache-apcu": "Waarschuwing: [https://www.php.net/apcu APCu], [http://xcache.lighttpd.net/ XCache] of [https://www.iis.net/downloads/microsoft/wincache-extension WinCache] is niet aangetroffen.\nHet cachen van objecten is niet ingeschakeld.", "config-mod-security": "Waarschuwing: Uw webserver heeft de module [https://modsecurity.org/ mod_security]/mod_security2 ingeschakeld. Veel standaard instellingen hiervan zorgen voor problemen in combinatie met MediaWiki en andere software die gebruikers in staat stelt willekeurige inhoud te posten.\nIndien mogelijk, zou deze moeten worden uitgeschakeld. Lees anders de [https://modsecurity.org/documentation/ documentatie over mod_security] of neem contact op met de helpdesk van uw provider als u tegen problemen aanloopt.", "config-diff3-bad": "GNU diff3 niet aangetroffen. U kunt dit voorlopig negeren, maar bewerkingsconflicten kunnen vaker voorkomen.", @@ -101,18 +101,14 @@ "config-uploads-not-safe": "Waarschuwing: uw uploadmap $1 kan gebruikt worden voor het arbitrair uitvoeren van scripts.\nHoewel MediaWiki alle toegevoegde bestanden controleert op bedreigingen, is het zeer aan te bevelen het [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Security#Upload_security beveiligingslek te verhelpen] alvorens uploads in te schakelen.", "config-no-cli-uploads-check": "''Waarschuwing:'' uw standaardmap voor uploads ($1) wordt niet gecontroleerd op kwetsbaarheden voor het uitvoeren van willekeurige scripts gedurende de CLI-installatie.", "config-brokenlibxml": "Uw systeem heeft een combinatie van PHP- en libxml2-versies geïnstalleerd die is foutgevoelig is en kan leiden tot onzichtbare beschadiging van gegevens in MediaWiki en andere webapplicaties.\nUpgrade naar libxml2 2.7.3 of hoger([https://bugs.php.net/bug.php?id=45996 bij PHP gerapporteerde fout]).\nDe installatie wordt afgebroken.", - "config-suhosin-max-value-length": "Suhosin is geïnstalleerd en beperkt de GET-parameter length tot $1 bytes.\nDe ResourceLoader van MediaWiki omzeilt deze beperking, maar dat is slecht voor de prestaties.\nAls het mogelijk is, moet u de waarde \"suhosin.get.max_value_length\" in php.ini instellen op 1024 of hoger en $wgResourceLoaderMaxQueryLength in LocalSettings.php op dezelfde waarde instellen.", + "config-suhosin-max-value-length": "Suhosin is geïnstalleerd en beperkt de GET-parameter length tot $1 bytes.\nMediaWiki vereist dat suhosin.get.max_value_length ten minste $2 is. Schakel deze instelling uit, of verhoog deze in php.ini naar $3.", "config-using-32bit": "Pas op: uw systeem lijkt met 32-bit integers te werken. Dit is [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:32-bit anders dan aangeraden].", "config-db-type": "Databasetype:", "config-db-host": "Databasehost:", "config-db-host-help": "Als uw databaseserver een andere server is, voer dan de hostnaam of het IP-adres hier in.\n\nAls u gebruik maakt van gedeelde webhosting, hoort uw provider u de juiste hostnaam te hebben verstrekt.\n\nAls u MySQL gebruikt, dan werkt \"localhost\" mogelijk niet als servernaam.\nAls het inderdaad niet werkt, probeer dan \"127.0.0.1\" te gebruiken als lokaal IP-adres.\n\nAls u PostgreSQL gebruikt, laat dit veld dan leeg om via een Unix-socket te verbinden.", - "config-db-host-oracle": "Database-TNS:", - "config-db-host-oracle-help": "Voer een geldige [http://download.oracle.com/docs/cd/B28359_01/network.111/b28317/tnsnames.htm Local Connect Name] in; een tnsnames.ora-bestand moet zichtbaar zijn voor deze installatie.
Als u gebruik maakt van clientlibraries 10g of een latere versie, kunt u ook gebruik maken van de naamgevingsmethode [http://download.oracle.com/docs/cd/E11882_01/network.112/e10836/naming.htm Easy Connect].", "config-db-wiki-settings": "Identificeer deze wiki", "config-db-name": "Databasenaam (zonder koppeltekens):", "config-db-name-help": "Kies een naam die uw wiki identificeert.\nEr mogen geen spaties gebruikt worden.\nAls u gebruik maakt van gedeelde webhosting, dan hoort uw provider ofwel u een te gebruiken databasenaam gegeven te hebben, of u aangegeven te hebben hoe u databases kunt aanmaken.", - "config-db-name-oracle": "Databaseschema:", - "config-db-account-oracle-warn": "Er zijn drie ondersteunde scenario's voor het installeren van Oracle als databasebackend:\n\nAls u een database-account wilt aanmaken als onderdeel van het installatieproces, geef dan de gegevens op van een database-account in met de rol SYSDBA voor de installatie en voer de gewenste aanmeldgegevens in voor het account met webtoegang. U kunt ook het account met webtoegang handmatig aanmaken en alleen van dat account de aanmeldgegevens opgeven als deze de vereiste rechten heeft om schemaobjecten aan te maken. Als laatste is het mogelijk om aanmeldgegevens van twee verschillende accounts op te geven; een met de rechten om schemaobjecten aan te maken, en een met alleen webtoegang.\n\nEen script voor het aanmaken van een account met de vereiste rechten is te vinden in de map \"maintenance/oracle/\" van deze installatie. Onthoud dat het gebruiken van een account met beperkte rechten alle mogelijkheden om beheerscripts uit te voeren met het standaardaccount onmogelijk maakt.", "config-db-install-account": "Gebruiker voor installatie", "config-db-username": "Gebruikersnaam voor database:", "config-db-password": "Wachtwoord voor database:", @@ -131,37 +127,24 @@ "config-pg-test-error": "Kan geen verbinding maken met database '''$1''': $2", "config-sqlite-dir": "Gegevensmap voor SQLite:", "config-sqlite-dir-help": "SQLite slaat alle gegevens op in een enkel bestand.\n\nDe map die u opgeeft moet beschrijfbaar zijn voor de webserver tijdens de installatie.\n\nDeze mag '''niet toegankelijk''' zijn via het web en het bestand mag dus niet tussen de PHP-bestanden staan.\n\nHet installatieprogramma schrijft het bestand .htaccess weg met het databasebestand, maar als dat niet werkt kan iemand zich toegang tot het ruwe databasebestand verschaffen.\nOok de gebruikersgegevens (e-mailadressen, wachtwoordhashes) en verwijderde versies en overige gegevens met beperkte toegang via MediaWiki zijn dan onbeschermd.\n\nOverweeg om de database op een totaal andere plaats neer te zetten, bijvoorbeeld in /var/lib/mediawiki/yourwiki.", - "config-oracle-def-ts": "Standaard tablespace:", - "config-oracle-temp-ts": "Tijdelijke tablespace:", "config-type-mysql": "MariaDB, MySQL of compatibele systemen", "config-type-postgres": "PostgreSQL", "config-type-sqlite": "SQLite", - "config-type-oracle": "Oracle", - "config-type-mssql": "Microsoft SQL Server", "config-support-info": "MediaWiki ondersteunt de volgende databasesystemen:\n\n$1\n\nAls u het databasesysteem dat u wilt gebruiken niet in de lijst terugvindt, volg dan de handleiding waarnaar hierboven wordt verwezen om ondersteuning toe te voegen.", "config-dbsupport-mysql": "* [{{int:version-db-mariadb-url}} MariaDB] is de primaire database voor MediaWiki en wordt het best ondersteund. MediaWiki werkt ook met [{{int:version-db-mysql-url}} MySQL] en [{{int:version-db-percona-url}} Percona Server], die MariaDB-compatibel zijn ([https://www.php.net/manual/en/mysqli.installation.php hoe PHP te compileren met MySQL-ondersteuning]).", "config-dbsupport-postgres": "* [{{int:version-db-postgres-url}} PostgreSQL] is een populair open source databasesysteem als alternatief voor MySQL.([https://www.php.net/manual/en/pgsql.installation.php Hoe u PHP kunt compileren met ondersteuning voor PostgreSQL])", "config-dbsupport-sqlite": "* [{{int:version-db-sqlite-url}} SQLite] is een zeer goed ondersteund lichtgewicht databasesysteem ([https://www.php.net/manual/en/pdo.installation.php hoe PHP gecompileerd moet zijn met ondersteuning voor SQLite]; gebruikt PDO).", - "config-dbsupport-oracle": "* [{{int:version-db-oracle-url}} Oracle] is een commerciële database voor grote bedrijven ([https://www.php.net/manual/en/oci8.installation.php PHP compileren met ondersteuning voor OCI8]).", - "config-dbsupport-mssql": "* [{{int:version-db-mssql-url}} Microsoft SQL Server] is een commerciële enterprisedatabase voor Windows ([https://www.php.net/manual/en/sqlsrv.installation.php PHP compileren met ondersteuning voor SQLSRV]).", "config-header-mysql": "MariaDB/MySQL-instellingen", "config-header-postgres": "PostgreSQL-instellingen", "config-header-sqlite": "SQLite-instellingen", - "config-header-oracle": "Oracle-instellingen", - "config-header-mssql": "Instellingen voor Microsoft SQL Server", "config-invalid-db-type": "Ongeldig databasetype.", "config-missing-db-name": "U moet een waarde opgeven voor \"{{int:config-db-name}}\".", "config-missing-db-host": "U moet een waarde invoeren voor \"{{int:config-db-host}}\".", - "config-missing-db-server-oracle": "U moet een waarde opgeven voor \"{{int:config-db-host-oracle}}\".", - "config-invalid-db-server-oracle": "Ongeldige database-TNS \"$1\".\nGebruik \"TNS Names\" of een \"Easy Connect\" tekst ([http://docs.oracle.com/cd/E11882_01/network.112/e10836/naming.htm Oracle naamgevingsmethoden])", "config-invalid-db-name": "Ongeldige databasenaam \"$1\".\nGebruik alleen letters (a-z, A-Z), cijfers (0-9) en liggende streepjes (_) en streepjes (-).", "config-invalid-db-prefix": "Ongeldig databasevoorvoegsel \"$1\".\nGebruik alleen letters (a-z, A-Z), cijfers (0-9) en liggende streepjes (_) en streepjes (-).", "config-connection-error": "$1.\n\nControleer de host, gebruikersnaam en wachtwoord en probeer het opnieuw. Probeer \"127.0.0.1\" in plaats van \"localhost\" als database host. (of omgekeerd)", "config-invalid-schema": "Ongeldig schema voor MediaWiki \"$1\".\nGebruik alleen letters (a-z, A-Z), cijfers (0-9) en liggende streepjes (_).", - "config-db-sys-create-oracle": "Het installatieprogramma biedt alleen de mogelijkheid een nieuw account aan te maken met een SYSDBA-account.", - "config-db-sys-user-exists-oracle": "Gebruikersaccount \"$1\" bestaat al. SYSDBA kan alleen gebruikt worden voor het aanmaken van een nieuw account!", "config-postgres-old": "PostgreSQL $1 of hoger is vereist.\nU gebruikt $2.", - "config-mssql-old": "Microsoft SQL Server $1 of hoger is vereist. U hebt $2.", "config-sqlite-name-help": "Kies een naam die uw wiki identificeert.\nGebruik geen spaties of koppeltekens.\nDeze naam wordt gebruikt voor het gegevensbestand van SQLite.", "config-sqlite-parent-unwritable-group": "Het was niet mogelijk de gegevensmap $1 te maken omdat in de bovenliggende map $2 niet geschreven mag worden door de webserver.\n\nHet installatieprogramma heeft vast kunnen stellen onder welke gebruiker de webserver draait.\nMaak de map $3 beschrijfbaar om door te kunnen gaan.\nVoer op een Linux-systeem de volgende opdrachten uit:\n\n
cd $2\nmkdir $3\nchgrp $4 $3\nchmod g+w $3
", "config-sqlite-parent-unwritable-nogroup": "Het was niet mogelijk de gegevensmap $1 te maken omdat in de bovenliggende map $2 niet geschreven mag worden door de webserver.\n\nHet installatieprogramma heeft niet vast kunnen stellen onder welke gebruiker de webserver draait.\nMaak de map $3 beschrijfbaar voor de webserver (en anderen!) om door te kunnen gaan.\nVoer op een Linux-systeem de volgende opdrachten uit:\n\n
cd $2\nmkdir $3\nchmod a+w $3
", @@ -186,11 +169,6 @@ "config-mysql-engine": "Opslagmethode:", "config-mysql-innodb": "InnoDB (aanbevolen)", "config-mysql-engine-help": "'''InnoDB''' is vrijwel altijd de beste instelling, omdat deze goed omgaat met meerdere verzoeken tegelijkertijd.\n\n'''MyISAM''' is bij een zeer beperkt aantal gebruikers mogelijk sneller, of als de wiki alleen-lezen is.\nMyISAM-databases raken vaker beschadigd dan InnoDB-databases.", - "config-mssql-auth": "Authenticatietype:", - "config-mssql-install-auth": "Selecteer de authenticatiemethode die wordt gebruikt om met de database te verbinden tijdens het installatieproces.\nAls u \"{{int:config-mssql-windowsauth}}\" selecteert, dan worden de aanmeldgegevens van de gebruiker waaronder de webserver draait voor authenticatie gebruikt.", - "config-mssql-web-auth": "Selecteer de authenticatiemethode die de webserver gebruikt om met de database te verbinden tijdens het installatieproces.\nAls u \"{{int:config-mssql-windowsauth}}\" selecteert, dan worden de aanmeldgegevens van de gebruiker waaronder de webserver draait voor authenticatie gebruikt.", - "config-mssql-sqlauth": "SQL Server Authenticatie", - "config-mssql-windowsauth": "Windowsauthenticatie", "config-site-name": "Naam van de wiki:", "config-site-name-help": "Deze naam verschijnt in de titelbalk van browsers en op andere plaatsen.", "config-site-name-blank": "Geef een naam op voor de site.", diff --git a/includes/installer/i18n/oc.json b/includes/installer/i18n/oc.json index 3aaecb53f7..814764a849 100644 --- a/includes/installer/i18n/oc.json +++ b/includes/installer/i18n/oc.json @@ -53,10 +53,8 @@ "config-using-uri": "Utilizacion de l'URL de servidor \"$1$2\".", "config-db-type": "Tipe de basa de donadas :", "config-db-host": "Nom d’òste de la basa de donadas :", - "config-db-host-oracle": "Nom TNS de la basa de donadas :", "config-db-wiki-settings": "Identificar aqueste wiki", "config-db-name": "Nom de la basa de donadas :", - "config-db-name-oracle": "Esquèma de basa de donadas :", "config-db-install-account": "Compte d'utilizaire per l'installacion", "config-db-username": "Nom d'utilizaire de la basa de donadas :", "config-db-password": "Senhal de la basa de donadas :", @@ -68,19 +66,13 @@ "config-db-schema": "Esquèma per MediaWiki", "config-pg-test-error": "Impossible de se connectar a la basa de donadas '''$1''' : $2", "config-sqlite-dir": "Dorsièr de las donadas SQLite :", - "config-oracle-def-ts": "Espaci d'emmagazinatge (''tablespace'') per defaut :", - "config-oracle-temp-ts": "Espaci d'emmagazinatge (''tablespace'') temporari :", "config-type-mysql": "MariaDB, MySQL o compatible", - "config-type-mssql": "Microsoft SQL Server", "config-header-mysql": "Paramètres de MySQL", "config-header-postgres": "Paramètres de PostgreSQL", "config-header-sqlite": "Paramètres de SQLite", - "config-header-oracle": "Paramètres d’Oracle", - "config-header-mssql": "Paramètres de Microsoft SQL Server", "config-invalid-db-type": "Tipe de basa de donadas invalid", "config-missing-db-name": "Vos cal entrar una valor per « {{int:config-db-name}} ».", "config-missing-db-host": "Vos cal entrar una valor per « {{int:config-db-host}} ».", - "config-missing-db-server-oracle": "Vos cal entrar una valor per « {{int:config-db-oracle}} ».", "config-postgres-old": "PostgreSQL $1 o version ulteriora es requesit, avètz $2.", "config-sqlite-readonly": "Lo fichièr $1 es pas accessible en escritura.", "config-sqlite-cant-create-db": "Impossible de crear lo fichièr de basa de donadas $1.", @@ -91,9 +83,6 @@ "config-db-web-create": "Creatz lo compte se existís pas ja", "config-mysql-engine": "Motor d'emmagazinatge :", "config-mysql-innodb": "InnoDB", - "config-mssql-auth": "Tipe d’autentificacion :", - "config-mssql-sqlauth": "Autentificacion de SQL Server", - "config-mssql-windowsauth": "Autentificacion Windows", "config-site-name": "Nom del wiki :", "config-site-name-blank": "Entratz un nom de site.", "config-project-namespace": "Espaci de noms del projècte :", diff --git a/includes/installer/i18n/olo.json b/includes/installer/i18n/olo.json index 6fdcd8204f..8926e6cbb4 100644 --- a/includes/installer/i18n/olo.json +++ b/includes/installer/i18n/olo.json @@ -28,12 +28,9 @@ "config-db-name": "Tiedokannan nimi:", "config-db-username": "Tiedokannan käyttäinimi:", "config-db-password": "Tiedokannan salasana:", - "config-type-mssql": "Microsoft SQL Server", "config-header-mysql": "MariaDB/MySQL-azetukset", "config-header-postgres": "PostgreSQL-azetukset", "config-header-sqlite": "SQLite-azetukset", - "config-header-oracle": "Oracle-azetukset", - "config-header-mssql": "Microsoft SQL Server azetukset", "config-mysql-innodb": "InnoDB", "config-site-name": "Wikin nimi:", "config-site-name-blank": "Kirjuta sivun nimi.", diff --git a/includes/installer/i18n/pl.json b/includes/installer/i18n/pl.json index d34367e3e8..f3f211eb1d 100644 --- a/includes/installer/i18n/pl.json +++ b/includes/installer/i18n/pl.json @@ -24,7 +24,8 @@ "Sethakill", "Peter Bowman", "Ankam", - "Railfail536" + "Railfail536", + "Rail" ] }, "config-desc": "Instalator MediaWiki", @@ -107,13 +108,9 @@ "config-db-type": "Typ bazy danych:", "config-db-host": "Adres serwera bazy danych:", "config-db-host-help": "Jeśli serwer bazy danych jest na innej maszynie, wprowadź jej nazwę domenową lub adres IP.\n\nJeśli korzystasz ze współdzielonego hostingu, operator serwera powinien podać Ci prawidłową nazwę serwera w swojej dokumentacji.\n\nJeśli korzystasz z MySQL, użycie „localhost” może nie zadziałać jako nazwa hosta. Jeśli wystąpi ten problem, użyj „127.0.0.1” jako lokalnego adresu IP.\n\nJeżeli korzystasz z PostgreSQL, pozostaw to pole puste, aby połączyć się poprzez gniazdo Unixa.", - "config-db-host-oracle": "Nazwa instancji bazy danych (TNS):", - "config-db-host-oracle-help": "Wprowadź prawidłową [http://download.oracle.com/docs/cd/B28359_01/network.111/b28317/tnsnames.htm nazwę połączenia lokalnego]. Plik „tnsnames.ora” musi być widoczny dla instalatora.
Jeśli używasz biblioteki klienckiej 10g lub nowszej możesz również skorzystać z metody nazw [http://download.oracle.com/docs/cd/E11882_01/network.112/e10836/naming.htm łatwego łączenia].", "config-db-wiki-settings": "Zidentyfikuj tę wiki", "config-db-name": "Nazwa bazy danych:", "config-db-name-help": "Wybierz nazwę, która zidentyfikuje Twoją wiki.\nNie może ona zawierać spacji.\n\nJeśli korzystasz ze współdzielonego hostingu, dostawca usługi hostingowej może wymagać użycia konkretnej nazwy bazy danych lub pozwalać na tworzenie baz danych za pośrednictwem panelu użytkownika.", - "config-db-name-oracle": "Nazwa schematu bazy danych:", - "config-db-account-oracle-warn": "Bazę danych Oracle można przygotować do pracy z MediaWiki na trzy sposoby:\n\nMożesz utworzyć konto użytkownika bazy danych podczas instalacji MediaWiki. Wówczas należy podać nazwę i hasło użytkownika z rolą SYSDBA w celu użycia go przez instalator do utworzenia nowe konta użytkownika, z którego korzystać będzie MediaWiki.\n\nMożesz również skorzystać z konta użytkownika bazy danych utworzonego bezpośrednio w Oracle i wówczas wystarczy podać tylko nazwę i hasło tego użytkownika. Konto z rolą SYSDBA nie będzie potrzebne, jednak konto użytkownika powinno mieć uprawnienia do utworzenia obiektów w schemacie bazy danych. Możesz też podać dwa konta - konto dla instalatora, z pomocą którego zostaną obiekty w schemacie bazy danych i drugie konto, z którego będzie MediaWiki korzystać będzie do pracy.\n\nW podkatalogu \"maintenance/oracle\" znajduje się skrypt do tworzenia konta użytkownika. Korzystanie z konta użytkownika z ograniczonymi uprawnieniami spowoduje wyłączenie funkcji związanych z aktualizacją oprogramowania MediaWiki.", "config-db-install-account": "Konto użytkownika dla instalatora", "config-db-username": "Nazwa użytkownika bazy danych:", "config-db-password": "Hasło bazy danych:", @@ -132,34 +129,22 @@ "config-pg-test-error": "Nie można połączyć się z bazą danych''' $1 ''': $2", "config-sqlite-dir": "Katalog danych SQLite:", "config-sqlite-dir-help": "SQLite przechowuje wszystkie dane w pojedynczym pliku.\n\nWskazany katalog musi być dostępny do zapisu przez webserver podczas instalacji.\n\nPowinien '''nie''' być dostępny za z sieci web, dlatego nie umieszczamy ich tam, gdzie znajdują się pliki PHP.\n\nInstalator zapisze plik .htaccess obokniego, ale jeśli to zawiedzie, ktoś może uzyskać dostęp do nieprzetworzonej bazy danych.\nZawiera ona nieopracowane dane użytkownika (adresy e-mail, zahaszowane hasła) jak również usunięte wersje oraz inne dane o ograniczonym dostępie na wiki.\n\nWarto rozważyć umieszczenie w bazie danych zupełnie gdzie indziej, na przykład w /var/lib/mediawiki/yourwiki .", - "config-oracle-def-ts": "Domyślna przestrzeń tabel:", - "config-oracle-temp-ts": "Przestrzeń tabel tymczasowych:", "config-type-mysql": "MariaDB, MySQL lub kompatybilna", - "config-type-mssql": "Microsoft SQL Server", "config-support-info": "MediaWiki może współpracować z następującymi systemami baz danych:\n\n$1\n\nPoniżej wyświetlone są systemy baz danych gotowe do użycia. Jeżeli poniżej brakuje bazy danych, z której chcesz skorzystać, oznacza to, że brakuje odpowiedniego oprogramowania lub zostało ono niepoprawnie skonfigurowane. Powyżej znajdziesz odnośniki do dokumentacji, która pomoże w konfiguracji odpowiednich komponentów.", "config-dbsupport-mysql": "* [{{int:version-db-mariadb-url}} MariaDB] jest bazą danych, na której rozwijane jest oprogramowanie MediaWiki. MediaWiki działa również z [{{int:version-db-mysql-url}} MySQL] i [{{int:version-db-percona-url}} Percona Server], które są zgodne z MariaDB. ([https://www.php.net/manual/en/mysqli.installation.php Zobacz, jak skompilować PHP ze wsparciem dla MySQL])", "config-dbsupport-postgres": "* [{{int:version-db-postgres-url}} PostgreSQL] jest popularnym, otawrtym systemem baz danych, często stosowanym jako alternatywa dla MySQL. ([https://www.php.net/manual/en/pgsql.installation.php Zobacz, jak skompilować PHP z obsługą PostgreSQL])", "config-dbsupport-sqlite": "* [{{int:version-db-sqlite-url}} SQLite] jest niewielkim systemem bazy danych, z którym MediaWiki bardzo dobrze współpracuje. ([https://www.php.net/manual/pl/pdo.installation.php Zobacz, jak skompilować PHP ze wsparciem dla SQLite], korzystając z PDO)", - "config-dbsupport-oracle": "* [{{int:version-db-oracle-url}} Oracle] jest komercyjną profesjonalną bazą danych. ([https://www.php.net/manual/pl/oci8.installation.php Jak skompilować PHP ze wsparciem dla OCI8])", - "config-dbsupport-mssql": "* [{{int:version-db-mssql-url}} Microsoft SQL Server] jest komercyjną profesjonalną bazą danych. ([https://www.php.net/manual/pl/sqlsrv.installation.php Jak skompilować PHP ze wsparciem dla SQLSRV])", "config-header-mysql": "Ustawienia MariaDB/MySQL", "config-header-postgres": "Ustawienia PostgreSQL", "config-header-sqlite": "Ustawienia SQLite", - "config-header-oracle": "Ustawienia Oracle", - "config-header-mssql": "Ustawienia Microsoft SQL Server", "config-invalid-db-type": "Nieprawidłowy typ bazy danych", "config-missing-db-name": "Należy wpisać wartość w polu „{{int:config-db-name}}”.", "config-missing-db-host": "Należy wpisać wartość w polu „{{int:config-db-host}}”.", - "config-missing-db-server-oracle": "Należy wpisać wartość w polu „{{int:config-db-host-oracle}}”.", - "config-invalid-db-server-oracle": "Nieprawidłowa nazwa instancji bazy danych (TNS) „$1”.\nUżyj \"TNS Name\" lub \"Easy Connect\" ([http://docs.oracle.com/cd/E11882_01/network.112/e10836/naming.htm Oracle Naming Methods])", "config-invalid-db-name": "Nieprawidłowa nazwa bazy danych „$1”.\nUżywaj wyłącznie liter ASCII (a-z, A-Z), cyfr (0-9), podkreślenia (_) lub znaku odejmowania (-).", "config-invalid-db-prefix": "Nieprawidłowy prefiks bazy danych „$1”.\nUżywaj wyłącznie liter ASCII (a-z, A-Z), cyfr (0-9), podkreślenia (_) lub znaku odejmowania (-).", "config-connection-error": "$1.\n\nSprawdź adres serwera, nazwę użytkownika i hasło, a następnie spróbuj ponownie. Jeżeli korzystasz z „localhosta” jako serwera bazy danych, spróbuj zamiast tego użyć „127.0.0.1” (lub na odwrót).", "config-invalid-schema": "Nieprawidłowa nazwa schematu dla MediaWiki „$1”.\nNazwa może zawierać wyłącznie liter ASCII (a-z, A-Z), cyfr (0-9) i podkreślenia (_).", - "config-db-sys-create-oracle": "Instalator może wykorzystać wyłącznie konto SYSDBA do tworzenia nowych kont użytkowników.", - "config-db-sys-user-exists-oracle": "Konto użytkownika „$1” już istnieje. SYSDBA można użyć tylko do utworzenia nowego konta!", "config-postgres-old": "Korzystasz z wersji $2 oprogramowania PostgreSQL, a potrzebna jest wersja co najmniej $1.", - "config-mssql-old": "Wymagany jest Microsoft SQL Server w wersji $1 lub nowszej. Masz zainstalowaną wersję $2.", "config-sqlite-name-help": "Wybierz nazwę, która będzie identyfikować Twoją wiki.\nNie wolno używać spacji ani myślników.\nZostanie ona użyta jako nazwa pliku danych SQLite.", "config-sqlite-parent-unwritable-group": "Nie można utworzyć katalogu danych $1 , ponieważ katalog nadrzędny $2 nie jest dostępny do zapisu przez webserwer.\n\nInstalator nie może określić, jako kttóry użytkownik działa webserwer.\nZezwól by katalog $3 był dostępny do zapisu przez niego, aby przejść dalej.\nW systemie Unix/Linux wykonaj:\n\n
cd $2\nmkdir $3\nchgrp $4 $3\nchmod g+w $3
", "config-sqlite-parent-unwritable-nogroup": "Nie można utworzyć katalogu danych $1 , ponieważ katalog nadrzędny $2 nie jest dostępny do zapisu przez webserwer.\n\nInstalator nie może określić, jako kttóry użytkownik działa webserwer.\nZezwól by katalog $3 był globalnie modyfikowalny przez niego (i innych!) aby przejść dalej.\nW systemie Unix/Linux wykonaj:\n\n
cd $2\nmkdir $3\nchmod a+w $3
", @@ -184,11 +169,6 @@ "config-mysql-engine": "Silnik przechowywania", "config-mysql-innodb": "InnoDB (zalecane)", "config-mysql-engine-help": "'''InnoDB''' jest prawie zawsze najlepszą opcją, ponieważ posiada dobrą obsługę współbieżności.\n\n'''MyISAM''' może być szybsze w instalacjach pojedynczego użytkownika lub tylko do odczytu.\nBazy danych MyISAM mają tendencję do ulegania uszkodzeniom częściej niż bazy InnoDB.", - "config-mssql-auth": "Typ uwierzytelniania:", - "config-mssql-install-auth": "Wybierz typ uwierzytelniania, który będzie używany do łączenia się z bazą danych w trakcie procesu instalacji.\nJeśli wybierzesz „{{int:config-mssql-windowsauth}}”, będą wykorzystywane dane konta użytkownika, pod którym działa serwer www.", - "config-mssql-web-auth": "Wybierz typ uwierzytelniania, który będzie używany przez serwer www do łączenia się z bazą danych podczas normalnego funkcjonowania wiki.\nJeśli wybierzesz „{{int:config-mssql-windowsauth}}”, użyte zostaną dane konta użytkownika, pod którym działa serwer www.", - "config-mssql-sqlauth": "Uwierzytelnianie serwera SQL", - "config-mssql-windowsauth": "Autoryzacja Windows", "config-site-name": "Nazwa wiki:", "config-site-name-help": "Ten napis pojawi się w pasku tytułowym przeglądarki oraz w różnych innych miejscach.", "config-site-name-blank": "Wprowadź nazwę witryny.", diff --git a/includes/installer/i18n/pms.json b/includes/installer/i18n/pms.json index b6ab572056..b8b97de396 100644 --- a/includes/installer/i18n/pms.json +++ b/includes/installer/i18n/pms.json @@ -81,13 +81,9 @@ "config-db-type": "Sòrt ëd base ëd dàit:", "config-db-host": "Ospitant ëd la base ëd dàit:", "config-db-host-help": "Se sò servent ëd base ëd dàit a l'é su un servent diferent, ch'a anserissa ambelessì ël nòm dl'ospitant o l'adrëssa IP.\n\nS'a deuvra n'ospitalità partagià, sò fornidor d'ospitalità a dovrìa deje ël nòm dl'ospitant giust ant soa documentassion.\n\nSe a anstala su un servent Windows e a deuvra MySQL, dovré «localhost» a podrìa funsioné nen com nòm dël servent. S'a marcia nen, ch'a preuva «127.0.0.1» com adrëssa IP local.\n\nS'a deuvra PostgresSQL, ch'a lassa sto camp bianch për coleghesse a travers un socket UNIX.", - "config-db-host-oracle": "TNS dla base ëd dàit:", - "config-db-host-oracle-help": "Anserì un [http://download.oracle.com/docs/cd/B28359_01/network.111/b28317/tnsnames.htm nòm ëd conession local] bon; n'archivi tnsnames.ora a dev esse visìbil da costa anstalassion..
S'a deuvra le librarìe cliente 10g o pi neuve a peul ëdcò dovré ël métod ëd nominassion [http://download.oracle.com/docs/cd/E11882_01/network.112/e10836/naming.htm Easy Connect].", "config-db-wiki-settings": "Identìfica sta wiki", "config-db-name": "Nòm dla base ëd dàit:", "config-db-name-help": "Ch'a serna un nòm ch'a identìfica soa wiki.\nA dovrìa conten-e gnun ëspassi.\n\nS'a deuvra n'ospitalità partagià, sò fornidor ëd l'ospitalità a-j darà un nòm ëd base ëd dàit specìfich da dovré o a lassrà ch'a lo crea via un panel ëd contròl.", - "config-db-name-oracle": "Schema dla base ëd dàit:", - "config-db-account-oracle-warn": "A-i é tre possibilità mantnùe për istalé Oracle tanme terminal ëd base ëd dàit:\n\nS'a veul creé un cont ëd base ëd dàit com part dël process d'istalassion, për piasì ch'a fornissa un cont con ël ròl SYSDBA com cont ëd base ëd dàit për l'istalassion e ch'a specìfica le credensiaj vorsùe për ël cont d'acess an sl'aragnà, dësnò a peul ëdcò creé ël cont d'acess an sl'aragnà manualment e mach fornì col cont (se a l'ha ij përmess necessari për creé j'oget dë schema) o fornì doi cont diferent, un con ij privilegi ëd creé e un limità për l'acess an sla Ragnà.\n\nIj senari për creé un cont con ij privilegi necessari a peul esse trovà ant la cartela «manutension/oracol/» ëd costa istalassion. Ch'a ten-a da ment che dovrand un cont limità a disabiliterà tute le funsion ëd manutension con ël cont predefinì.", "config-db-install-account": "Cont d'utent për l'instalassion.", "config-db-username": "Nòm d'utent dla base ëd dàit:", "config-db-password": "Ciav dla base ëd dàit:", @@ -106,28 +102,20 @@ "config-pg-test-error": "Impossìbil coleghesse a la base ëd dàit '''$1'''; $2", "config-sqlite-dir": "Dossié dij dat SQLite:", "config-sqlite-dir-help": "SQLite a memorisa tùit ij dat ant n'archivi ùnich.\n\nËl dossié che chiel a forniss a dev esse scrivìbil dal servent durant l'instalassion.\n\nA dovrìa '''pa''' esse acessìbil da l'aragnà, sossì a l'é për sòn ch'i l'oma pa butalo andova a-i son ij sò file PHP.\n\nL'instalador a scriverà n'archivi .htaccess ansema con chiel, ma se lòn a faliss quaidun a peul intré an soa base ëd dàit originaria.\nLòn a comprend ij dat brut ëd l'utent (adrëssa ëd pòsta eletrònica, ciav tërbola) e ëdcò le revision scancelà e d'àutri dat segret ëd la wiki.\n\nCh'a consìdera ëd buté la base ëd dàit tuta antrega da n'àutra part, për esempi an /var/lib/mediawiki/yourwiki.", - "config-oracle-def-ts": "Spassi dla tàula dë stàndard:", - "config-oracle-temp-ts": "Spassi dla tàula temporani:", "config-support-info": "MediaWiki a manten ij sistema ëd base ëd dàit sì-dapress:\n\n$1\n\nS'a vëd pa listà sì-sota ël sistema ëd base ëd dàit ch'a preuva a dovré, antlora va andaré a j'istrussion dl'anliura sì-dzora për abilité ël manteniment.", "config-dbsupport-mysql": "* $1 e l'é l'obietiv primari për MediaWiki e a l'é mej mantnù ([https://www.php.net/manual/en/mysql.installation.php com compilé PHP con ël manteniment MySQL])", "config-dbsupport-postgres": "* $1 e l'é un sistema ëd base ëd dàit popolar a sorgiss duverta com alternativa a MySQL ([https://www.php.net/manual/en/pgsql.installation.php com compilé PHP con ël manteniment ëd PostgreSQL]). A peulo ess-ie chèich cit bigat, e a l'é nen arcomandà ëd dovrelo an n'ambient ëd produssion.", "config-dbsupport-sqlite": "* $1 e l'é un sistema ëd base ëd dàit leger che a l'é motobin bin mantnù ([http://www.php.net/manual/en/pdo.installation.php com compilé PHP con ël manteniment ëd SQLite], a deuvra PDO)", - "config-dbsupport-oracle": "* $1 a l'é na base ëd dàit comersial për j'amprèise. ([http://www.php.net/manual/en/oci8.installation.php Com compilé PHP con ël manteniment OCI8])", "config-header-mysql": "Ampostassion MySQL", "config-header-postgres": "Ampostassion PostgreSQL", "config-header-sqlite": "Ampostassion SQLite", - "config-header-oracle": "Ampostassion Oracle", "config-invalid-db-type": "Sòrt ëd ëd base ëd dàit pa bon-a", "config-missing-db-name": "A dev buteje un valor për \"Nòm ëd la base ëd dàit\"", "config-missing-db-host": "A dev buteje un valor për \"l'òspit ëd la base ëd dàit\"", - "config-missing-db-server-oracle": "A dev buteje un valor për \"TNS ëd la base ëd dat\"", - "config-invalid-db-server-oracle": "TNS ëd la base ëd dat pa bon \"$1\".\nDovré mach dle litre ASCII (a-z, A-Z), nùmer (0-9), sotlignadure (_) e pontin (.).", "config-invalid-db-name": "Nòm ëd la base ëd dàit pa bon \"$1\".\nDovré mach litre ASCII (a-z, A-Z), nùmer (0-9), sotlignadure (_) e tratin (-).", "config-invalid-db-prefix": "Prefiss dla base ëd dàit pa bon \"$1\".\nDovré mach litre ASCII (a-z, A-Z), nùmer (0-9), sotlignadure (_) e tratin (-).", "config-connection-error": "$1.\n\nControla l'ospitant, lë stranòm d'utent e la ciav sì-sota e prové torna.", "config-invalid-schema": "Schema pa bon për MediaWiki \"$1\".\nDovré mach litre ASCII (a-z, A-Z), nùmer (0-9) e sotlignadure (_).", - "config-db-sys-create-oracle": "L'istalador a arconòss mach ij cont SYSDBA durant la creassion d'un cont neuv.", - "config-db-sys-user-exists-oracle": "Ël cont utent \"$1\" a esist già. SYSDBA a peul mach esse dovrà për creé un cont neuv!", "config-postgres-old": "A-i é da manca ëd PostgreSQL $1 o pi recent, chiel a l'ha $2.", "config-sqlite-name-help": "Serne un nòm ch'a identìfica soa wiki.\nDovré nì dë spassi nì ëd tratin.\nSòn a sarà dovrà për ël nòm ëd l'archivi ëd dat SQLite.", "config-sqlite-parent-unwritable-group": "As peul pa creesse ël dossié ëd dat $1, përchè ël dossié a mont $2 a l'é pa scrivìbil dal servent.\n\nL'instalador a l'ha determinà sota che utent a gira sò servent.\nFé an manera che ël dossié $3 a sia scrivìbil da chiel për continué.\nSu un sistema Unix/Linux buté:\n
cd $2\nmkdir $3\nchgrp $4 $3\nchmod g+w $3
", diff --git a/includes/installer/i18n/ps.json b/includes/installer/i18n/ps.json index 632813ca56..4517ca1908 100644 --- a/includes/installer/i18n/ps.json +++ b/includes/installer/i18n/ps.json @@ -37,20 +37,15 @@ "config-using-uri": "د پالنگر URL \"$1$2\" کارېږي.", "config-db-type": "د توکبنسټ ډول:", "config-db-host": "د توکبنسټ کوربه:", - "config-db-host-oracle": "د توکبنسټ TNS:", "config-db-wiki-settings": "دا ويکي پېژندل", "config-db-name": "د توکبنسټ نوم:", - "config-db-name-oracle": "د اومتوکبنسټ طرحه:", "config-db-username": "د توکبنسټ کارن-نوم:", "config-db-password": "د توکبنسټ پټنوم:", "config-db-port": "د توکبنسټ ور:", "config-db-schema": "د مېډياويکي طرحه:", - "config-type-mssql": "مايکروسافټ SQL پالنگر", "config-header-mysql": "د MySQL امستنې", "config-header-postgres": "د PostgreSQL امستنې", "config-header-sqlite": "د SQLite امستنې", - "config-header-oracle": "د اورېکل امستنې", - "config-header-mssql": "د مايکروسافټ SQL پالنگر امستنې", "config-sqlite-readonly": "د $1 دوتنه د ليکلو وړ نه ده.", "config-sqlite-cant-create-db": "د توکبنسټ دوتنه $1 جوړه نه شوه.", "config-site-name": "د ويکي نوم:", diff --git a/includes/installer/i18n/pt-br.json b/includes/installer/i18n/pt-br.json index 5d1cc8fd02..a267cb85ac 100644 --- a/includes/installer/i18n/pt-br.json +++ b/includes/installer/i18n/pt-br.json @@ -106,13 +106,9 @@ "config-db-type": "Tipo do banco de dados:", "config-db-host": "Servidor do banco de dados:", "config-db-host-help": "Se o banco de dados do seu servidor está em um servidor diferente, digite o nome do host ou o endereço IP aqui.\n\nSe você está utilizando uma hospedagem web compartilhada, o seu provedor de hospedagem deverá fornecer o nome do host correto na sua documentação.\n\nSe você está usando o MySQL, usar \"localhost\" pode não funcionar para o nome de servidor. Se não funcionar, tente \"127.0.0.1\" para o endereço de IP local.\n\nSe você está usando PostgreSQl, deixe este campo em branco para se conectar através de um socket Unix.", - "config-db-host-oracle": "TNS do banco de dados:", - "config-db-host-oracle-help": "Digite um [http://download.oracle.com/docs/cd/B28359_01/network.111/b28317/tnsnames.htm Nome de Conexão local] válido; o arquivo tnsnames.ora precisa estar visível para esta instalação.
Se você estiver usando bibliotecas cliente 10g ou mais recente, você também pode usar o método [http://download.oracle.com/docs/cd/E11882_01/network.112/e10836/naming.htm Easy Connect].", "config-db-wiki-settings": "Identifique esta wiki", "config-db-name": "Nome da base de dados (sem hífen):", "config-db-name-help": "Escolha um nome que identifique a sua wiki.\nEle não deve conter espaços.\n\nSe você está utilizando uma hospedagem web compartilhada, o provedor de hospedagem lhe dará um nome especifico de banco de dados para usar ou o deixará criar a partir do painel de controle.", - "config-db-name-oracle": "Esquema do banco de dados:", - "config-db-account-oracle-warn": "Há três cenários suportados para instalar o Oracle como backend do banco de dados:\n\nSe você deseja criar a conta do banco de dados como parte do processo de instalação, forneça uma conta com função SYSDBA como conta do banco de dados para instalação e especifique as credenciais desejadas para a conta de acesso pela web, caso contrário, você poderá criar a conta de acesso via web manualmente e fornecer apenas aquela conta (se tiver permissões necessárias para criar os objetos schema) ou fornecer duas contas diferentes, uma com privilégios de criação e uma restrita para acesso à web.\n\nO script para criar uma conta com os privilégios necessários pode ser encontrado no diretório \"maintenance/oracle/\" desta instalação. Lembre-se de que usar uma conta restrita desativará todos os recursos de manutenção com a conta padrão.", "config-db-install-account": "Conta de usuário para instalação", "config-db-username": "Nome de usuário do banco de dados:", "config-db-password": "Senha do banco de dados:", @@ -131,37 +127,24 @@ "config-pg-test-error": "Não foi possível se conectar com o banco de dados $1: $2", "config-sqlite-dir": "Diretório de dados do SQLite:", "config-sqlite-dir-help": "O SQLite armazena todos os dados em um único arquivo.\n\nO diretório que você fornecer deve permitir a sua escrita pelo servidor web durante a instalação.\n\nO diretório não deve ser acessível pela web, por isso não estamos colocando onde estão os seus arquivos PHP.\n\nO instalador escreverá um arquivo .htaccess, mas se isso falhar alguém poderá ganhar acesso a toda sua base de dados.\nIsso inclui dados brutos dos usuários (endereços de e-mail, senhas criptografadas) assim como todas revisões deletadas e outros dados restritos na wiki.\n\nConsidere colocar a banco de dados em algum outro lugar, por exemplo /var/lib/mediawiki/yourwiki.", - "config-oracle-def-ts": "Espaço de tabela padrão:", - "config-oracle-temp-ts": "Tablespace temporário:", "config-type-mysql": "MariaDB, MySQL (ou compatível)", "config-type-postgres": "PostgreSQL", "config-type-sqlite": "SQLite", - "config-type-oracle": "Oracle", - "config-type-mssql": "Microsoft SQL Server", "config-support-info": "O MediaWiki suporta os sistemas de banco de dados a seguir:\n\n$1\n\nSe você não vê o sistema de banco de dados que você está tentando usar listados abaixo, siga as instruções relacionadas acima, para ativar o suporte.", "config-dbsupport-mysql": "* [{{int:version-db-mariadb-url}} MariaDB] é a base de dados preferida para o MediaWiki e a melhor suportada. O MediaWiki também trabalha com [{{int:version-db-mysql-url}} MySQL] e [{{int:version-db-percona-url}} Percona Server], que são compatíveis com MariaDB. ([https://www.php.net/manual/pt_BR/mysqli.installation.php Como compilar PHP com suporte para MySQL].)", "config-dbsupport-postgres": "* [{{int:version-db-postgres-url}} PostgreSQL] é um popular sistema de banco de dados de código aberto como uma alternativa para o MySQL. ([https://www.php.net/manual/en/pgsql.installation.php Como compilar o PHP com suporte PostgreSQL])", "config-dbsupport-sqlite": "* O [{{int:version-db-sqlite-url}} SQLite] é uma plataforma de base de dados ligeira muito bem suportada. ([https://www.php.net/manual/en/pdo.installation.php Como compilar PHP com suporte para SQLite], usa PDO.)", - "config-dbsupport-oracle": "* A [{{int:version-db-oracle-url}} Oracle] é uma base de dados comercial para empresas. ([https://www.php.net/manual/pt_BR/oci8.installation.php Como compilar PHP com suporte para OCI8].)", - "config-dbsupport-mssql": "* O [{{int:version-db-mssql-url}} Microsoft SQL Server] é uma base de dados comercial do Windows para empresas. ([https://www.php.net/manual/en/sqlsrv.installation.php Como compilar PHP com suporte para SQLSRV].)", "config-header-mysql": "Definições MariaDB/MySQL", "config-header-postgres": "Configurações PostgreSQL", "config-header-sqlite": "Configurações SQLite", - "config-header-oracle": "Configurações Oracle", - "config-header-mssql": "Configurações Microsoft SQL Server", "config-invalid-db-type": "Tipo do banco de dados inválido.", "config-missing-db-name": "Você deve inserir um valor para \"{{int:config-db-name}}\".", "config-missing-db-host": "Você deve inserir um valor para \"{{int:config-db-host}}\".", - "config-missing-db-server-oracle": "Você deve inserir um valor para \"{{int:config-db-host-oracle}}\".", - "config-invalid-db-server-oracle": "Banco de dados TNS inválido \"$1\".\nUse \"TNS Name\" ou \"Easy Connect\" ([http://docs.oracle.com/cd/E11882_01/network.112/e10836/naming.htm Métodos de nomeação da Oracle]).", "config-invalid-db-name": "O nome do banco de dados é inválido \"$1\".\nUse apenas letras ASCII (a-z, A-Z), números (0-9), underscores (_) e hifens (-).", "config-invalid-db-prefix": "O prefixo do banco de dados é inválido \"$1\".\nUse apenas letras ASCII (a-z, A-Z), números (0-9), underscores (_) e hifens (-).", "config-connection-error": "$1.\n\nVerifique o servidor, nome de usuário e senha e tente novamente. Se estiver usando \"localhost\" como o servidor do banco de dados, tente usar \"127.0.0.1\" em vez disso (ou vice versa).", "config-invalid-schema": "Schema inválido para o MediaWiki \"$1\".\nUse apenas letras ASCII (a-z, A-Z), números (0-9) e underscores (_).", - "config-db-sys-create-oracle": "O instalador só permite criar uma conta nova usando uma conta SYSDBA.", - "config-db-sys-user-exists-oracle": "A conta de usuário \"$1\" já existe. SYSDBA somente pode ser utilizado na criação de uma nova conta!", "config-postgres-old": "PostgreSQL $1 ou posterior é necessário. Você tem $2.", - "config-mssql-old": "Microsoft SQL Server $1 ou posterior é necessário. Você tem $2.", "config-sqlite-name-help": "Escolha um nome que identifique a sua wiki.\nNão utilize espaços ou hifens.\nIsto será utilizado como nome do arquivo de dados do SQLite.", "config-sqlite-parent-unwritable-group": "Não é possível criar o diretório de dados $1, porque o diretório pai $2 não pode ser gravado pelo servidor web.\n\nO instalador conseguiu determinar o usuário em que seu servidor web está sendo executado.\nDe permissão de gravação global ao diretório $3 para o instalador para continuar.\nEm um sistema Unix/Linux faça:\n\n
cd $2\nmkdir $3\nchgrp $4 $3\nchmod g+w $3
", "config-sqlite-parent-unwritable-nogroup": "Não é possível criar o diretório de dados $1, porque o diretório pai $2 não pode ser gravado pelo servidor web.\n\nO instalador não conseguiu determinar o usuário em que seu servidor web está sendo executado.\nDe permissão de gravação global ao diretório $3 para o instalador (e outros!) para continuar.\nEm um sistema Unix/Linux faça:\n\n
cd $2\nmkdir $3\nchmod a+w $3
", @@ -186,11 +169,6 @@ "config-mysql-engine": "Mecanismo de armazenamento:", "config-mysql-innodb": "InnoDB (recomendado)", "config-mysql-engine-help": "InnoDB é quase sempre a melhor opção, uma vez que possui um bom suporte de concorrência.\n\nMyISAM pode ser mais rápido em instalações de usuário único ou somente leitura.\\O banco de dados MyISAM tendem a ficar corrompidos mais frequentemente do que os bancos de dados InnoDB.", - "config-mssql-auth": "Tipo de autenticação:", - "config-mssql-install-auth": "Selecione o tipo de autenticação que será usado para se conectar ao banco de dados durante o processo de instalação.\nSe você selecionar \"{{int:config-mssql-windowsauth}}\", as credenciais de qualquer usuário que o servidor web esteja executando serão usadas.", - "config-mssql-web-auth": "Selecione o tipo de autenticação que o servidor web usará para se conectar ao servidor do banco de dados, durante a operação normal da wiki.\nSe você selecionar \"{{int:config-mssql-windowsauth}}\", as credenciais de qualquer usuário no qual o servidor web está rodando serão usadas.", - "config-mssql-sqlauth": "Autenticação do SQL Server", - "config-mssql-windowsauth": "Autenticação do Windows", "config-site-name": "Nome da wiki:", "config-site-name-help": "Isto aparecerá na barra de títulos do navegador e em vários outros lugares.", "config-site-name-blank": "Digite o nome do site.", diff --git a/includes/installer/i18n/pt.json b/includes/installer/i18n/pt.json index 223e26ede2..75e1cadb84 100644 --- a/includes/installer/i18n/pt.json +++ b/includes/installer/i18n/pt.json @@ -22,48 +22,49 @@ "MokaAkashiyaPT", "Athena in Wonderland", "CaiusSPQR", - "Waldyrious" + "Waldyrious", + "Mansil alfalb" ] }, "config-desc": "O instalador do MediaWiki", "config-title": "Instalação do MediaWiki $1", "config-information": "Informação", - "config-localsettings-upgrade": "Foi detetado um ficheiro LocalSettings.php.\nPara atualizar esta instalação, por favor introduza o valor de $wgUpgradeKey na caixa abaixo.\nEncontra este valor em LocalSettings.php.", - "config-localsettings-cli-upgrade": "Foi detetado um ficheiro LocalSettings.php.\nPara atualizar esta instalação, execute o update.php, por favor", + "config-localsettings-upgrade": "Foi detetado um ficheiro LocalSettings.php.\nPara atualizar esta instalação, por favor, insira o valor de $wgUpgradeKey na caixa abaixo.\nIrá encontrar este valor em LocalSettings.php.", + "config-localsettings-cli-upgrade": "Foi detetado um ficheiro LocalSettings.php.\nPara atualizar esta instalação, por favor, execute o update.php", "config-localsettings-key": "Chave de atualização:", "config-localsettings-badkey": "A chave de atualização que forneceu está incorreta.", - "config-upgrade-key-missing": "Foi detetada uma instalação existente do MediaWiki.\nPara atualizar esta instalação, por favor coloque a seguinte linha no final do seu LocalSettings.php:\n\n$1", - "config-localsettings-incomplete": "O ficheiro LocalSettings.php existente parece estar incompleto.\nA variável $1 não está definida.\nPor favor, defina esta variável no LocalSettings.php e clique \"{{int:Config-continue}}\".", + "config-upgrade-key-missing": "Foi detetada uma instalação existente do MediaWiki.\nPara atualizar esta instalação, por favor, coloque a seguinte linha no fim do seu LocalSettings.php:\n\n$1", + "config-localsettings-incomplete": "O ficheiro existente LocalSettings.php parece estar incompleto.\nA variável $1 não está definida.\nPor favor, altere LocalSettings.php e assim esta variável é definida, e clique em \"{{int:Config-continue}}\".", "config-localsettings-connection-error": "Ocorreu um erro ao ligar à base de dados usando as configurações especificadas no LocalSettings.php. Por favor corrija essas configurações e tente novamente.\n\n$1", "config-session-error": "Erro ao iniciar a sessão: $1", "config-session-expired": "Os seus dados de sessão parecem ter expirado.\nAs sessões estão configuradas para uma duração de $1.\nPode aumentar esta duração configurando session.gc_maxlifetime no php.ini.\nReinicie o processo de instalação.", "config-no-session": "Os seus dados de sessão foram perdidos!\nVerifique o seu php.ini e certifique-se de que em session.save_path está definido um diretório apropriado.", - "config-your-language": "A sua língua:", - "config-your-language-help": "Selecione a língua que será usada durante o processo de instalação.", - "config-wiki-language": "Língua da wiki:", - "config-wiki-language-help": "Selecione a língua que será predominante na wiki.", + "config-your-language": "O seu idioma:", + "config-your-language-help": "Selecione o idioma que será utilizado durante o processo de instalação.", + "config-wiki-language": "Idioma da wiki:", + "config-wiki-language-help": "Selecione o idioma que a wiki será predominante escrita.", "config-back": "← Voltar", "config-continue": "Continuar →", - "config-page-language": "Língua", - "config-page-welcome": "Bem-vindo(a) ao MediaWiki!", + "config-page-language": "Idioma", + "config-page-welcome": "Bem-vindo ao MediaWiki!", "config-page-dbconnect": "Ligar à base de dados", "config-page-upgrade": "Atualizar a instalação existente", "config-page-dbsettings": "Configurações da base de dados", "config-page-name": "Nome", "config-page-options": "Opções", "config-page-install": "Instalar", - "config-page-complete": "Terminado!", + "config-page-complete": "Concluída!", "config-page-restart": "Reiniciar a instalação", "config-page-readme": "Leia-me", "config-page-releasenotes": "Notas de lançamento", "config-page-copying": "A copiar", "config-page-upgradedoc": "A atualizar", "config-page-existingwiki": "Wiki existente", - "config-help-restart": "Deseja limpar todos os dados gravados que introduziu e reiniciar o processo de instalação?", + "config-help-restart": "Deseja limpar todos os dados guardados que inseriu e reiniciar o processo de instalação?", "config-restart": "Sim, reiniciar", "config-welcome": "=== Verificações do ambiente ===\nSerão agora realizadas verificações básicas para determinar se este ambiente é apropriado para instalação do MediaWiki.\nLembre-se de fornecer esta informação se necessitar de pedir ajuda para concluir a instalação.", - "config-welcome-section-copyright": "=== Direitos de autor e Condições de utilização ===\n\n$1\n\nEste programa é software livre; pode redistribuí-lo e/ou modificá-lo nos termos da licença GNU General Public License, tal como publicada pela Free Software Foundation; tanto a versão 2 da Licença, como (por opção sua) qualquer versão posterior.\n\nEste programa é distribuído na esperança de que seja útil, mas '''sem qualquer garantia'''; inclusive, sem a garantia implícita da '''possibilidade de ser comercializado''' ou de '''adequação para qualquer finalidade específica'''.\nConsulte a licença GNU General Public License para mais detalhes.\n\nEm conjunto com este programa deve ter recebido [$2 uma cópia da licença GNU General Public License]; se não a recebeu, peça-a por escrito a Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA ou [https://www.gnu.org/copyleft/gpl.html leia-a na Internet].", - "config-sidebar": "* [https://www.mediawiki.org/wiki/MediaWiki/pt Página principal do MediaWiki]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents/pt Ajuda]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Contents/pt Manual técnico]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ/pt FAQ]", + "config-welcome-section-copyright": "=== Direitos de Autor e Termos ===\n\n$1\n\nEste programa é um software livre; pode redistribuí-lo e/ou modificá-lo nos termos da Licença Pública Geral GNU, tal como publicada pela Free Software Foundation; tanto a versão 2 da Licença, como (por opção sua) qualquer versão mais recente.\n\nEste programa é distribuído na esperança de que seja útil, mas '''sem qualquer garantia'''; inclusive, sem a garantia implícita da '''possibilidade de ser comercializado''' ou de '''adequação para qualquer finalidade específica'''.\nConsulte a Licença Pública Geral GNU para mais detalhes.\n\nEm conjunto com este programa deveria ter recebido [$2 uma cópia da Licença Pública Geral GNU]; se não, peça-a por escrito a Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA ou [https://www.gnu.org/copyleft/gpl.html leia-a on-line].", + "config-sidebar": "* [https://www.mediawiki.org/wiki/MediaWiki/pt Página principal do MediaWiki]\n* [https://www.mediawiki.org/wiki/Help:Contents/pt Guia do Utilizador]\n* [https://www.mediawiki.org/wiki/Manual:Contents/pt Guia do Administrador]\n* [https://www.mediawiki.org/wiki/Manual:FAQ/pt Perguntas Mais Frequentes]", "config-sidebar-readme": "Leia-me", "config-sidebar-relnotes": "Notas de lançamento", "config-sidebar-license": "Copiar", @@ -81,12 +82,12 @@ "config-pcre-old": "Erro fatal: É necessário o PCRE $1 ou versão posterior.\nO seu binário PHP foi linkado com o PCRE $2.\n[https://www.mediawiki.org/wiki/Manual:Errors_and_symptoms/PCRE Mais informações].", "config-pcre-no-utf8": "'''Erro fatal''': O módulo PCRE do PHP parece ter sido compilado sem suporte PCRE_UTF8.\nO MediaWiki necessita do suporte UTF-8 para funcionar corretamente.", "config-memory-raised": "A configuração memory_limit do PHP era $1; foi aumentada para $2.", - "config-memory-bad": "Aviso: A configuração memory_limit do PHP é $1.\nIsto é provavelmente demasiado baixo.\nA instalação poderá falhar!", - "config-apc": "[https://www.php.net/apc APC] instalada", - "config-apcu": "[https://www.php.net/apcu APCu] instalado", - "config-wincache": "[https://www.iis.net/downloads/microsoft/wincache-extension WinCache] instalada", - "config-no-cache-apcu": "Aviso: Não foi encontrado o [https://www.php.net/apcu APCu] nem o [https://www.iis.net/downloads/microsoft/wincache-extension WinCache].\nA cache de objetos não está ativa.", - "config-mod-security": "Aviso: O seu servidor de Internet tem o [https://modsecurity.org/ mod_security]/mod_security2 ativado. Muitas das suas configurações normais podem causar problemas ao MediaWiki e a outros programas, permitindo que os utilizadores publiquem conteúdos arbitrários.\nSe possível, isto deve ser desativado. Se não, consulte a [https://modsecurity.org/documentation/ mod_security documentação] ou peça apoio ao fornecedor do alojamento do seu servidor se encontrar erros aleatórios.", + "config-memory-bad": "Aviso: o parâmetro memory_limit é $1.\nIsto é provavelmente demasiado baixo.\nA instalação poderá falhar!", + "config-apc": "[https://www.php.net/apc APC] está instalada", + "config-apcu": "[https://www.php.net/apcu APCu] está instalado", + "config-wincache": "[https://www.iis.net/downloads/microsoft/wincache-extension WinCache] está instalada", + "config-no-cache-apcu": "Aviso: não foi possível encontrar o [https://www.php.net/apcu APCu] ou a [https://www.iis.net/downloads/microsoft/wincache-extension WinCache].\nA colocação em cache dos objetos não está ativa.", + "config-mod-security": "Aviso: o seu servidor da Web tem o [https://modsecurity.org/ mod_security]/mod_security2 ativado. Muitas configurações comuns irão causar problemas ao MediaWiki e a outro software que permite que os utilizadores publiquem conteúdos arbitrários.\nSe possível, isto deverá ser desativado. Se não, consulte a [https://modsecurity.org/documentation/ mod_security documentação] ou contacte o apoio do seu alojamento se encontrar erros aleatórios.", "config-diff3-bad": "O utilitário de comparação de texto GNU diff3 não foi encontrado. Pode ignorar esta situação por agora, mas poderá encontrar conflitos entre edições mais frequentemente.", "config-git": "Foi encontrado o software de controlo de versões Git: $1.", "config-git-bad": "Não foi encontrado o programa de controlo de versões Git. Pode ignorar esta situação por agora. Note que Especial:Versão não apresentará os resumos criptográficos das efetivações.", @@ -100,96 +101,74 @@ "config-uploads-not-safe": "Aviso: O diretório por omissão para carregamentos, $1, está vulnerável à execução arbitrária de listas de comandos (scripts).\nEmbora o MediaWiki verifique a existência de ameaças de segurança em todos os ficheiros enviados, é altamente recomendado que [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Security#Upload_security vede esta vulnerabilidade de segurança] antes de possibilitar uploads.", "config-no-cli-uploads-check": "Aviso: O diretório por omissão para carregamentos, $1, não é verificado para determinar se é vulnerável à execução de listas arbitrárias de comandos durante a instalação por CLI (\"Command-line Interface\").", "config-brokenlibxml": "O seu sistema tem uma combinação de versões do PHP e do libxml2 conhecida por ser problemática, podendo causar corrupção de dados no MediaWiki e noutras aplicações da Internet.\nAtualize para a versão 2.7.3 ou posterior do libxml2 ([https://bugs.php.net/bug.php?id=45996 incidência reportada no PHP]).\nInstalação cancelada.", - "config-suhosin-max-value-length": "O Suhosin está instalado e limita o parâmetro GET length a $1 bytes.\nO componente ResourceLoader do MediaWiki consegue exceder este limite, mas prejudicando o desempenho.\nSe lhe for possível, deve atribuir ao parâmetro suhosin.get.max_value_length o valor 1024 ou maior no ficheiro php.ini, e definir o mesmo valor para $wgResourceLoaderMaxQueryLength no ficheiro LocalSettings.php.", + "config-suhosin-max-value-length": "O Suhosin está instalado e limita o parâmetro GET length a $1 bytes.\nO MediaWiki requer que suhosin.get.max_value_length seja pelo menos $2. Desative esta definição, ou aumente este valor para $3 no php.ini.", "config-using-32bit": "Aviso: o seu sistema parece estar a funcionar com inteiros de 32 bits. Isto [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:32-bit não é recomendado].", "config-db-type": "Tipo da base de dados:", "config-db-host": "Servidor da base de dados:", "config-db-host-help": "Se a base de dados estiver num servidor separado, introduza aqui o nome ou o endereço IP desse servidor.\n\nSe estiver a usar um servidor partilhado, o fornecedor do alojamento deve fornecer o nome do servidor na documentação.\n\nSe estiver a usar MySQL, usar como nome do servidor \"localhost\" poderá não funcionar. Se não funcionar, tente usar \"127.0.0.1\" como endereço IP local.\n\nSe estiver a usar PostgreSQL, deixe este campo em branco para fazer a ligação através de um socket Unix.", - "config-db-host-oracle": "TNS (Transparent Network Substrate) da base de dados:", - "config-db-host-oracle-help": "Introduza um [http://download.oracle.com/docs/cd/B28359_01/network.111/b28317/tnsnames.htm Nome Local de Ligação] válido; tem de estar visível para esta instalação um ficheiro tnsnames.ora.
Se está a usar bibliotecas cliente versão 10g ou posterior, também pode usar o método [http://download.oracle.com/docs/cd/E11882_01/network.112/e10836/naming.htm Ligação Fácil] de atribuição do nome.", "config-db-wiki-settings": "Identifique esta wiki", "config-db-name": "Nome da base de dados (sem hífenes):", "config-db-name-help": "Escolha um nome para identificar a sua wiki.\nO nome não deve conter espaços.\n\nSe estiver a usar um servidor partilhado, o fornecedor do alojamento deve poder fornecer-lhe o nome de uma base de dados que possa usar, ou permite-lhe criar bases de dados através de um painel de controlo.", - "config-db-name-oracle": "Esquema ''(schema)'' da base de dados:", - "config-db-account-oracle-warn": "Há três cenários suportados na instalação do servidor de base de dados Oracle:\n\nSe pretende criar a conta de acesso pela internet na base de dados durante o processo de instalação, forneça como conta para a instalação uma conta com o papel de SYSDBA na base de dados e especifique as credenciais desejadas para a conta de acesso pela internet. Se não pretende criar a conta de acesso pela internet durante a instalação, pode criá-la manualmente e fornecer só essa conta para a instalação (se ela tiver as permissões necessárias para criar os objetos do esquema ''(schema)''). A terceira alternativa é fornecer duas contas diferentes; uma com privilégios de criação e outra com privilégios limitados para o acesso pela internet.\n\nExiste um script para criação de uma conta com os privilégios necessários no diretório \"maintenance/oracle/\" desta instalação. Mantenha em mente que usar uma conta com privilégios limitados impossibilita todas as operações de manutenção com a conta padrão.", "config-db-install-account": "Conta do utilizador para a instalação", "config-db-username": "Nome do utilizador da base de dados:", - "config-db-password": "Palavra-passe do utilizador da base de dados:", - "config-db-install-username": "Introduza o nome de utilizador que será usado para aceder à base de dados durante o processo de instalação. Este utilizador não é o do MediaWiki; é o utilizador da base de dados.", + "config-db-password": "Palavra-passe da base de dados:", + "config-db-install-username": "Insira o nome de utilizador que será utilizado para aceder à base de dados durante o processo de instalação.\nNão é o nome de utilizador da conta MediaWiki; é o nome de utilizador da sua base de dados.", "config-db-install-password": "Introduza a palavra-passe do utilizador que será usado para aceder à base de dados durante o processo de instalação.\nEsta palavra-passe não é a do utilizador do MediaWiki; é a palavra-passe do utilizador da base de dados.", "config-db-install-help": "Introduza o nome de utilizador e a palavra-passe que serão usados para aceder à base de dados durante o processo de instalação.", "config-db-account-lock": "Usar o mesmo nome de utilizador e palavra-passe durante a operação normal", "config-db-wiki-account": "Conta de utilizador para a operação normal", "config-db-wiki-help": "Introduza o nome de utilizador e a palavra-passe que serão usados para aceder à base de dados durante a operação normal da wiki.\nSe o utilizador não existir na base de dados, mas a conta de instalação tiver privilégios suficientes, o utilizador que introduzir será criado na base de dados com os privilégios mínimos necessários para a operação normal da wiki.", - "config-db-prefix": "Prefixo para as tabelas da base de dados (sem hífenes):", + "config-db-prefix": "Prefixo para a tabela da base de dados (sem hífenes):", "config-db-prefix-help": "Se necessitar de partilhar uma só base de dados entre várias wikis, ou entre o MediaWiki e outra aplicação, pode escolher adicionar um prefixo ao nome de todas as tabelas desta instalação, para evitar conflitos.\nNão use espaços.\n\nNormalmente, este campo deve ficar vazio.", - "config-mysql-old": "É necessário o MySQL $1 ou posterior; tem a versão $2.", + "config-mysql-old": "É necessário o MySQL $1 ou superior. Tem a versão $2.", "config-db-port": "Porta da base de dados:", - "config-db-schema": "Esquema ''(schema)'' do MediaWiki (sem hífenes):", - "config-db-schema-help": "Normalmente, este esquema estará correto.\nAltere-o só se souber que precisa de o fazer.", - "config-pg-test-error": "Não foi possível criar uma ligação à base de dados $1: $2", - "config-sqlite-dir": "Diretório de dados do SQLite:", + "config-db-schema": "Esquema para o MediaWiki (sem hífenes):", + "config-db-schema-help": "Normalmente, este esquema estará correto.\nAltere-o apenas se souber que precisa de o fazer.", + "config-pg-test-error": "Não é possível aceder à base de dados $1: $2", + "config-sqlite-dir": "Diretoria de dados do SQLite:", "config-sqlite-dir-help": "O SQLite armazena todos os dados num único ficheiro.\n\nDurante a instalação, o servidor de Internet precisa de ter permissão de escrita no diretório que especificar.\n\nEste diretório não deve poder ser acedido diretamente da Internet, por isso está a ser colocado onde estão os seus ficheiros PHP.\n\nJuntamente com o diretório, o instalador irá criar um ficheiro .htaccess, mas se esta operação falhar é possível que alguém venha a ter acesso direto à base de dados.\nIsto inclui acesso aos dados dos utilizadores (endereços de correio eletrónico, palavras-passe encriptadas), às revisões eliminadas e a outros dados de acesso restrito na wiki.\n\nConsidere colocar a base de dados num local completamente diferente, como, por exemplo, em /var/lib/mediawiki/asuawiki.", - "config-oracle-def-ts": "Tablespace padrão:", - "config-oracle-temp-ts": "Tablespace temporário:", "config-type-mysql": "MariaDB, MySQL (ou compatível)", "config-type-postgres": "PostgreSQL", "config-type-sqlite": "SQLite", - "config-type-oracle": "Oracle", - "config-type-mssql": "Microsoft SQL Server", - "config-support-info": "O MediaWiki suporta as seguintes plataformas de base de dados:\n\n$1\n\nSe a plataforma que pretende usar não está listada abaixo, siga as instruções nas hiperligações acima para ativar o suporte.", - "config-dbsupport-mysql": "* [{{int:version-db-mariadb-url}} MariaDB] é a base de dados preferida para o MediaWiki e a melhor suportada. O MediaWiki também trabalha com [{{int:version-db-mysql-url}} MySQL] e [{{int:version-db-percona-url}} Percona Server], que são compatíveis com MariaDB. ([https://www.php.net/manual/pt_BR/mysqli.installation.php Como compilar PHP com suporte para MySQL].)", + "config-support-info": "O MediaWiki suporta os seguintes sistemas de base de dados:\n\n$1\n\nSe não vê o sistema da base de dados que está a tentar utilizar listado abaixo, siga as instruções nas hiperligações acima para ativar o apoio.", + "config-dbsupport-mysql": "* [{{int:version-db-mariadb-url}} MariaDB] é a base de dados primária para o MediaWiki e a melhor suportada. O MediaWiki também trabalha com [{{int:version-db-mysql-url}} MySQL] e [{{int:version-db-percona-url}} Servidor Percona], que são compatíveis com MariaDB. ([https://www.php.net/manual/en/mysqli.installation.php Como compilar PHP com suporte para MySQL].)", "config-dbsupport-postgres": "* O [{{int:version-db-postgres-url}} PostgreSQL] é uma plataforma popular de base de dados de código aberto, alternativa ao MySQL. ([https://www.php.net/manual/en/pgsql.installation.php Como compilar PHP com suporte para PostgreSQL].)", "config-dbsupport-sqlite": "* O [{{int:version-db-sqlite-url}} SQLite] é uma plataforma de base de dados ligeira muito bem suportada. ([https://www.php.net/manual/en/pdo.installation.php Como compilar PHP com suporte para SQLite], usa PDO.)", - "config-dbsupport-oracle": "* A [{{int:version-db-oracle-url}} Oracle] é uma base de dados comercial para empresas. ([https://www.php.net/manual/en/oci8.installation.php Como compilar PHP com suporte para OCI8].)", - "config-dbsupport-mssql": "* O [{{int:version-db-mssql-url}} Microsoft SQL Server] é uma base de dados comercial do Windows para empresas. ([https://www.php.net/manual/en/sqlsrv.installation.php Como compilar PHP com suporte para SQLSRV].)", - "config-header-mysql": "Definições MariaDB/MySQL", - "config-header-postgres": "Definições PostgreSQL", - "config-header-sqlite": "Definições SQLite", - "config-header-oracle": "Definições Oracle", - "config-header-mssql": "Configurações do Microsoft SQL Server", - "config-invalid-db-type": "O tipo de base de dados é inválido", - "config-missing-db-name": "Tem de introduzir um valor para \"{{int:config-db-name}}\".", - "config-missing-db-host": "Tem de introduzir um valor para \"{{int:config-db-host}}\".", - "config-missing-db-server-oracle": "Tem de introduzir um valor para \"{{int:config-db-host-oracle}}\".", - "config-invalid-db-server-oracle": "O TNS da base de dados, \"$1\", é inválido.\nUse \"TNS Name\" ou o método \"Easy Connect\" ([http://docs.oracle.com/cd/E11882_01/network.112/e10836/naming.htm Métodos de Configuração da Conectividade em Oracle])", - "config-invalid-db-name": "O nome da base de dados, \"$1\", é inválido.\nUse só letras (a-z, A-Z), algarismos (0-9), sublinhados (_) e hífens (-) dos caracteres ASCII.", - "config-invalid-db-prefix": "O prefixo da base de dados, \"$1\", é inválido.\nUse só letras (a-z, A-Z), algarismos (0-9), sublinhados (_) e hífens (-) dos caracteres ASCII.", + "config-header-mysql": "Definições de MariaDB/MySQL", + "config-header-postgres": "Definições de PostgreSQL", + "config-header-sqlite": "Definições de SQLite", + "config-invalid-db-type": "O tipo de base de dados é inválido.", + "config-missing-db-name": "Deve inserir um valor para \"{{int:config-db-name}}\".", + "config-missing-db-host": "Deve inserir um valor para \"{{int:config-db-host}}\".", + "config-invalid-db-name": "O nome da base de dados \"$1\" é inválido.\nUtilize apenas letras ASCII (a-z, A-Z), números (0-9), sublinhados (_) e hífenes (-).", + "config-invalid-db-prefix": "O prefixo da base de dados \"$1\" é inválido.\nUtilize apenas letras ASCII (a-z, A-Z), números (0-9), sublinhados (_) e hífenes (-).", "config-connection-error": "$1.\n\nVerifique o servidor, o nome do utilizador e a palavra-passe e tente novamente. Se estiver a usar \"localhost\" como servidor da base de dados, tente antes usar \"127.0.0.1\" (ou vice-versa).", "config-invalid-schema": "O esquema ''(schema)'' do MediaWiki, \"$1\", é inválido.\nUse só letras (a-z, A-Z), algarismos (0-9) e sublinhados (_) dos caracteres ASCII.", - "config-db-sys-create-oracle": "O instalador só permite criar uma conta nova usando uma conta SYSDBA.", - "config-db-sys-user-exists-oracle": "A conta \"$1\" já existe. A conta SYSDBA só pode criar uma conta nova!", - "config-postgres-old": "É necessário o PostgreSQL $1 ou posterior; tem a versão $2.", - "config-mssql-old": "É necessário o Microsoft SQL Server $1 ou posterior. Tem a versão $2.", - "config-sqlite-name-help": "Escolha o nome que identificará a sua wiki.\nNão use espaços ou hífens.\nEste nome será usado como nome do ficheiro de dados do SQLite.", - "config-sqlite-parent-unwritable-group": "Não é possível criar o diretório de dados $1, porque o servidor de internet não tem permissão de escrita no diretório que o contém $2.\n\nO instalador determinou em que nome de utilizador o seu servidor de internet está a correr.\nPara continuar, configure o diretório $3 para poder ser escrito por este utilizador.\nPara fazê-lo em sistemas Unix ou Linux, use:\n\n
cd $2\nmkdir $3\nchgrp $4 $3\nchmod g+w $3
", - "config-sqlite-parent-unwritable-nogroup": "Não é possível criar o diretório de dados $1, porque o servidor de internet não tem permissão de escrita no diretório que o contém $2.\n\nNão foi possível determinar em que nome de utilizador o seu servidor de internet está a correr.\nPara continuar, configure o diretório $3 para que este possa ser globalmente escrito por esse utilizador (e por outros!).\nPara fazê-lo em sistemas Unix ou Linux, use:\n\n
cd $2\nmkdir $3\nchmod a+w $3
", - "config-sqlite-mkdir-error": "Ocorreu um erro ao criar o diretório de dados \"$1\".\nVerifique a localização e tente novamente.", - "config-sqlite-dir-unwritable": "Não foi possível escrever no diretório \"$1\".\nAltere as permissões para que ele possa ser escrito pelo servidor de internet e tente novamente.", - "config-sqlite-connection-error": "$1.\n\nVerifique o diretório de dados e o nome da base de dados abaixo e tente novamente.", - "config-sqlite-readonly": "Não é possível escrever no ficheiro $1.", + "config-postgres-old": "É necessário o PostgreSQL $1 ou superior. Tem a versão $2.", + "config-sqlite-name-help": "Escolha um nome que identifique a sua wiki.\nNão utilize espaços ou hífenes.\nIsto será utilizado para o nome do ficheiro dos dados de SQLite.", + "config-sqlite-parent-unwritable-group": "Não é possível criar a diretoria de dados $1, porque a diretoria fonte $2 não é gravável pelo servidor da Web.\n\nO instalador determinou o nome de utilizador que o seu servidor da Web está a executar.\nTorne a diretoria $3 gravável para continuar.\nPara fazê-lo no sistema Unix/Linux:\n\n
cd $2\nmkdir $3\nchgrp $4 $3\nchmod g+w $3
", + "config-sqlite-parent-unwritable-nogroup": "Não é possível criar a diretoria de dados $1, porque o a diretoria fonte $2 não é gravável pelo servidor da Web.\n\nO instalador não conseguiu determinar em que nome de utilizador o seu servidor da Web está em execução.\nTorne a diretoria $3 globalmente gravável por esta (e por outros!) para continuar.\nPara fazê-lo no sistema Unix/Linux:\n\n
cd $2\nmkdir $3\nchmod a+w $3
", + "config-sqlite-mkdir-error": "Ocorreu um erro ao criar a diretoria de dados \"$1\".\nVerifique a localização e tente novamente.", + "config-sqlite-dir-unwritable": "Não foi possível escrever na diretoria \"$1\".\nAltere as suas permissões para que o servidor da Web possa gravar na mesma, e tente novamente.", + "config-sqlite-connection-error": "$1.\n\nVerifique a diretoria de dados e o nome da base de dados abaixo e tente novamente.", + "config-sqlite-readonly": "O ficheiro $1 não é gravável.", "config-sqlite-cant-create-db": "Não foi possível criar o ficheiro da base de dados $1.", - "config-sqlite-fts3-downgrade": "O PHP não tem suporte FTS3; a reverter o esquema das tabelas para o anterior", - "config-can-upgrade": "Esta base de dados contém tabelas do MediaWiki.\nPara atualizá-las para o MediaWiki $1, clique '''Continuar'''.", - "config-upgrade-error": "Ocorreu um erro ao atualizar as tabelas do MediaWiki na sua base de dados.\n\nPara mais informações consulte o registo acima, para tentar novamente clique Continuar.", - "config-upgrade-done": "Atualização terminada.\n\nAgora pode [$1 começar a usar a sua wiki].\n\nSe quiser regenerar o seu ficheiro LocalSettings.php, clique o botão abaixo.\nEsta operação '''não é recomendada''' a menos que esteja a ter problemas com a sua wiki.", - "config-upgrade-done-no-regenerate": "Atualização terminada.\n\nAgora pode [$1 começar a usar a sua wiki].", - "config-regenerate": "Regenerar o LocalSettings.php →", - "config-show-table-status": "A consulta SHOW TABLE STATUS falhou!", - "config-unknown-collation": "'''Aviso:''' A base de dados está a utilizar uma colação ''(collation)'' desconhecida.", - "config-db-web-account": "Conta na base de dados para acesso pela internet", - "config-db-web-help": "Selecione o nome de utilizador e a palavra-passe que o servidor de Internet irá utilizar para aceder ao servidor da base de dados, durante a operação normal da wiki.", - "config-db-web-account-same": "Usar a mesma conta usada na instalação", + "config-sqlite-fts3-downgrade": "PHP tem em falta o suporte para FTS3; a reverter as tabelas para estado antigo.", + "config-can-upgrade": "Existem tabelas do MediaWiki nesta base de dados.\nPara atualizá-las para o MediaWiki $1, clique em Continuar.", + "config-upgrade-error": "Ocorreu um erro ao atualizar as tabelas do MediaWiki na sua base de dados.\n\nPara mais informação consulte o registo acima, para tentar novamente clique em Continuar.", + "config-upgrade-done": "Atualização concluída.\n\nAgora pode [$1 começar a utilizar a sua wiki].\n\nSe quiser regenerar o seu ficheiro LocalSettings.php, clique no botão abaixo.\nIsto não é recomendada a menos que esteja a ter problemas com a sua wiki.", + "config-upgrade-done-no-regenerate": "Atualização concluída.\n\nAgora pode [$1 começar a utilizar a sua wiki].", + "config-regenerate": "Regenerar LocalSettings.php →", + "config-show-table-status": "A consulta de SHOW TABLE STATUS falhou!", + "config-unknown-collation": "Aviso: a base de dados está a utilizar uma colação desconhecida.", + "config-db-web-account": "Conta da base de dados para aceder à Web", + "config-db-web-help": "Selecione o nome de utilizador e a palavra-passe que o servidor da Web irá utilizar para aceder ao servidor da base de dados, durante a operação normal da wiki.", + "config-db-web-account-same": "Utilizar a mesma conta usada na instalação", "config-db-web-create": "Criar a conta se ainda não existir", "config-db-web-no-create-privs": "A conta que especificou para a instalação não tem privilégios suficientes para criar uma conta.\nA conta que especificar aqui já tem de existir.", "config-mysql-engine": "Motor de armazenamento:", "config-mysql-innodb": "InnoDB (recomendado)", "config-mysql-engine-help": "InnoDB é quase sempre a melhor opção, porque suporta bem acessos simultâneos (concurrency).\n\nMyISAM pode ser mais rápido no modo de utilizador único ou em instalações somente para leitura.\nAs bases de dados MyISAM tendem a perder integridade de dados com mais frequência do que as bases de dados InnoDB.", - "config-mssql-auth": "Tipo de autenticação:", - "config-mssql-install-auth": "Selecione o tipo de autenticação a usar para ligar à base de dados durante o processo de instalação.\nSe selecionar \"{{int:config-mssql-windowsauth}}\", serão usadas as credenciais do utilizador com que o servidor de Internet está a ser executado.", - "config-mssql-web-auth": "Selecione o tipo de autenticação que o servidor de Internet irá usar para se ligar ao servidor da base de dados durante a operação normal da wiki.\nSe selecionar \"{{int:config-mssql-windowsauth}}\", serão usadas as credenciais do utilizador com que o servidor de Internet está a ser executado.", - "config-mssql-sqlauth": "Autenticação do SQL Server", - "config-mssql-windowsauth": "Autenticação do Windows", "config-site-name": "Nome da wiki:", "config-site-name-help": "Este nome aparecerá no título da janela do seu navegador e em vários outros sítios.", "config-site-name-blank": "Introduza o nome do sítio.", diff --git a/includes/installer/i18n/qqq.json b/includes/installer/i18n/qqq.json index 039cd263af..6c58c43795 100644 --- a/includes/installer/i18n/qqq.json +++ b/includes/installer/i18n/qqq.json @@ -101,18 +101,14 @@ "config-uploads-not-safe": "Used as a part of environment check result. Parameters:\n* $1 - name of directory for images: $IP/images/", "config-no-cli-uploads-check": "CLI = [[w:Command-line interface|command-line interface]] (i.e. the installer runs as a command-line script, not using HTML interface via an internet browser)", "config-brokenlibxml": "Status message in the MediaWiki installer environment checks.", - "config-suhosin-max-value-length": "{{doc-important|Do not translate \"length\", \"suhosin.get.max_value_length\", \"php.ini\", \"$wgResourceLoaderMaxQueryLength\" and \"LocalSettings.php\".}}\nMessage shown when PHP parameter suhosin.get.max_value_length is between 0 and 1023 (that max value is hard set in MediaWiki software).", + "config-suhosin-max-value-length": "{{doc-important|Do not translate \"length\", \"suhosin.get.max_value_length\", and \"php.ini\".}}\nThis error message is shown when PHP configuration suhosin.get.max_value_length is not high enough.\n\n* $1 - The current value\n* $2 - The minimum required value\n* $3 - The recommended value", "config-using-32bit": "Warning message shown when installing on a 32-bit system.", "config-db-type": "Field label in the MediaWiki installer followed by possible database types.", "config-db-host": "Used as label.\n\nAlso used in {{msg-mw|Config-missing-db-host}}.", "config-db-host-help": "{{doc-singularthey}}", - "config-db-host-oracle": "TNS = [[w:Transparent Network Substrate]].\n\nUsed as label.\n\nAlso used in {{msg-mw|Config-missing-db-server-oracle}}.", - "config-db-host-oracle-help": "See also:\n* {{msg-mw|Config-invalid-db-server-oracle}}", "config-db-wiki-settings": "This is more acurate: \"Enter identifying or distinguishing data for this wiki\" since a MySQL database can host tables of several wikis.", "config-db-name": "Used as label.\n\nAlso used in {{msg-mw|Config-missing-db-name}}.\n{{Identical|Database name}}", "config-db-name-help": "Help box text in the MediaWiki installer.", - "config-db-name-oracle": "Field label in the MediaWiki installer where an Oracle database schema can be specified.", - "config-db-account-oracle-warn": "A \"[[:wikipedia:Front and back ends|backend]]\" is a system or component that ordinary users don't interact with directly and don't need to know about, and that is responsible for a distinct task or service - for example, a storage back-end is a generic system for storing data which other applications can use. Possible alternatives for back-end are \"system\" or \"service\", or (depending on context and language) even leave it untranslated.", "config-db-install-account": "Legend in the MediaWiki installer for the section where database username and password have to be provided.", "config-db-username": "Used as label.", "config-db-password": "Field label in the MediaWiki installer where database password has to be provided.", @@ -131,37 +127,24 @@ "config-pg-test-error": "Parameters:\n* $1 - database name\n* $2 - error message", "config-sqlite-dir": "Field label for a folder location.", "config-sqlite-dir-help": "{{doc-important|Do not translate .htaccess and /var/lib/mediawiki/yourwiki.}}\nUsed in help box.", - "config-oracle-def-ts": "Field label for an Oracle default tablespace.", - "config-oracle-temp-ts": "Field label for an Oracle temporary tablespace.", "config-type-mysql": "\"Or compatible\" refers to several database systems that are compatible with MySQL, as explained in {{msg-mw|config-dbsupport-mysql}}, and thus also work with this choice of database type.", "config-type-postgres": "{{optional}}", "config-type-sqlite": "{{optional}}", - "config-type-oracle": "{{optional}}", - "config-type-mssql": "{{optional}}", - "config-support-info": "Parameters:\n* $1 - a list of DBMSs that MediaWiki supports, composed with config-dbsupport-* messages.\nSee also:\n* {{msg-mw|Config-dbsupport-mysql}}\n* {{msg-mw|Config-dbsupport-postgres}}\n* {{msg-mw|Config-dbsupport-oracle}}\n* {{msg-mw|Config-dbsupport-sqlite}}\n* {{msg-mw|Config-dbsupport-mssql}}", + "config-support-info": "Parameters:\n* $1 - a list of DBMSs that MediaWiki supports, composed with config-dbsupport-* messages.\nSee also:\n* {{msg-mw|Config-dbsupport-mysql}}\n* {{msg-mw|Config-dbsupport-postgres}}\n* {{msg-mw|Config-dbsupport-sqlite}}", "config-dbsupport-mysql": "Used in:\n* {{msg-mw|config-support-info}}\n{{Related|Config-dbsupport}}", "config-dbsupport-postgres": "Used in:\n* {{msg-mw|config-support-info}}\n{{Related|Config-dbsupport}}", "config-dbsupport-sqlite": "Used in:\n* {{msg-mw|config-support-info}}\n{{Related|Config-dbsupport}}", - "config-dbsupport-oracle": "Used in:\n* {{msg-mw|Config-support-info}}.\n{{Related|Config-dbsupport}}", - "config-dbsupport-mssql": "Used in:\n* {{msg-mw|Config-support-info}}\n{{Related|Config-dbsupport}}", "config-header-mysql": "Header for MySQL database settings in the MediaWiki installer.", "config-header-postgres": "Header for PostgreSQL database settings in the MediaWiki installer.", "config-header-sqlite": "Header for SQLite database settings in the MediaWiki installer.", - "config-header-oracle": "Header for Oracle database settings in the MediaWiki installer.", - "config-header-mssql": "Used as a section heading on the installer form, inside of a fieldset", "config-invalid-db-type": "Error message in MediaWiki installer when an invalid database type has been provided.", "config-missing-db-name": "Refers to {{msg-mw|Config-db-name}}.\n{{Related|Config-missing}}", "config-missing-db-host": "Refers to {{msg-mw|Config-db-host}}.\n{{Related|Config-missing}}", - "config-missing-db-server-oracle": "Refers to {{msg-mw|Config-db-host-oracle}}.\n{{Related|Config-missing}}", - "config-invalid-db-server-oracle": "Used as error message. Parameters:\n* $1 - database server name\nSee also:\n* {{msg-mw|Config-db-host-oracle-help}}", "config-invalid-db-name": "Used as error message. Parameters:\n* $1 - database name\nSee also:\n* {{msg-mw|Config-invalid-db-prefix}}", "config-invalid-db-prefix": "Used as error message. Parameters:\n* $1 - database prefix\nSee also:\n* {{msg-mw|Config-invalid-db-name}}", "config-connection-error": "$1 is the external error from the database, such as \"DB connection error: Access denied for user 'dba'@'localhost' (using password: YES) (localhost).\"\n\nIf you're translating this message to a right-to-left language, consider writing
$1.
. (When the bidi features for HTML5 will be implemented in the browsers, it will probably be a good idea to write it as
$1.
.)", "config-invalid-schema": "*$1 - schema name", - "config-db-sys-create-oracle": "Error message in the MediaWiki installer when Oracle is used as database and an incorrect user account type has been provided.", - "config-db-sys-user-exists-oracle": "Used as error message. Parameters:\n* $1 - database username", "config-postgres-old": "Used as error message. Used as warning. Parameters:\n* $1 - minimum version\n* $2 - the version of PostgreSQL that has been installed\n{{Related|Config-old}}", - "config-mssql-old": "Used as an error message. Parameters:\n* $1 - minimum version\n* $2 - the version of Microsoft SQL Server that has been installed\n{{Related|Config-old}}", "config-sqlite-name-help": "Help text for the form field for the SQLite data file name.", "config-sqlite-parent-unwritable-group": "Used as SQLite error message. Parameters:\n* $1 - data directory\n* $2 - \"dirname\" part of $1\n* $3 - \"basename\" part of $1\n* $4 - web server's primary group name\nSee also:\n* {{msg-mw|Config-sqlite-parent-unwritable-nogroup}}", "config-sqlite-parent-unwritable-nogroup": "Used as SQLite error message. Parameters:\n* $1 - data directory\n* $2 - \"dirname\" part of $1\n* $3 - \"basename\" part of $1\nSee also:\n* {{msg-mw|Config-sqlite-parent-unwritable-group}}", @@ -186,11 +169,6 @@ "config-mysql-engine": "Field label for MySQL storage engine in the MediaWiki installer.", "config-mysql-innodb": "Option for the MySQL storage engine in the MediaWiki installer.", "config-mysql-engine-help": "Help text in MediaWiki installer with advice for picking a MySQL storage engine.", - "config-mssql-auth": "Radio button group label.\n\nFollowed by the following radio button labels:\n* {{msg-mw|Config-mssql-sqlauth}}\n* {{msg-mw|Config-mssql-windowsauth}}", - "config-mssql-install-auth": "Used as the help text for the \"Authentication type\" radio button when typing in database settings for installation.\n\nRefers to {{msg-mw|Config-mssql-windowsauth}}.\n\nSee also:\n* {{msg-mw|Config-mssql-web-auth}}", - "config-mssql-web-auth": "Used as the help text for the \"Authentication type\" radio button when typing in database settings for normal wiki usage.\n\nRefers to {{msg-mw|Config-mssql-windowsauth}}.\n\nSee also:\n* {{msg-mw|Config-mssql-install-auth}}", - "config-mssql-sqlauth": "Radio button.\n\n\"SQL Server\" refers to \"Microsoft SQL Server\".\n\nSee also:\n* {{msg-mw|Config-mssql-windowsauth}}", - "config-mssql-windowsauth": "Radio button. The official term is \"Integrated Windows Authentication\" but Microsoft itself uses \"Windows Authentication\" elsewhere in Microsoft SQL Server as a synonym.\n\nAlso used in:\n* {{msg-mw|Config-mssql-install-auth}}\n* {{msg-mw|Config-mssql-web-auth}}\n\nSee also:\n* {{msg-mw|Config-mssql-sqlauth}}", "config-site-name": "Field label for the form field where a wiki name has to be entered.", "config-site-name-help": "Help text for the form field where a wiki name has to be entered.", "config-site-name-blank": "Error text in the MediaWiki installer when the site name is left empty.", diff --git a/includes/installer/i18n/ro.json b/includes/installer/i18n/ro.json index fb68b305ad..3d2311e6a0 100644 --- a/includes/installer/i18n/ro.json +++ b/includes/installer/i18n/ro.json @@ -52,15 +52,19 @@ "config-welcome": "=== Verificări ale mediului ===\nVerificări de bază vor fi efectuate pentru a vedea dacă este potrivit pentru instalarea MediaWiki.\nNu uitați să includeți aceste informații dacă doriți asistență pentru completarea instalării.", "config-welcome-section-copyright": "=== Drepturi de autor și termeni ===\n\n$1\n\nAcest program este un software liber; îl puteți redistribui și / sau modifica în conformitate cu termenii Licenței Publice Generale GNU, publicată de Fundația pentru Software Liber; fie versiunea 2 a Licenței, fie (la alegere) orice versiune ulterioară.\nAcest program este distribuit în speranța că va fi util, dar fără nicio garanție; fără nici măcar garanția implicită de vandabilitate sau fitness pentru un anumit scop.\nPentru mai multe detalii, consultați Licența publică generală GNU.\nAr fi trebuit să fi primit [$2 o copie a GNU General Public License] împreună cu acest program; dacă nu, scrieți la Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, SUA, sau [https://www.gnu.org/copyleft/gpl.html citiți-o online] .", "config-sidebar": "* [https://www.mediawiki.org Acasă MediaWiki]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents User's Guide]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Contents Administrator's Guide]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ FAQ]\n----\n* Read me\n* Release notes\n* Copying\n* Upgrading", + "config-sidebar-readme": "Read me", + "config-sidebar-relnotes": "Note de lansare", + "config-sidebar-license": "Copiere", + "config-sidebar-upgrade": "Actualizare", "config-env-good": "Verificarea mediului a fost efectuată cu succes.\nPuteți instala MediaWiki.", "config-env-bad": "Verificarea mediului a fost efectuată.\nNu puteți instala MediaWiki.", "config-env-php": "PHP $1 este instalat.", "config-env-hhvm": "HHVM $1 este instalat.", - "config-unicode-using-intl": "Utilizarea extensiei [https://pecl.php.net/intl intl PECL] pentru normalizarea Unicode.", - "config-unicode-pure-php-warning": "Atenție: Extensia [https://pecl.php.net/intl intl PECL] nu este disponibilă pentru a face față normalizării Unicode, revenind la o implementare lentă pur PHP.\nDacă rulați un site cu trafic ridicat, ar trebui să citiți puțin în [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations Normalizarea Unicode].", + "config-unicode-using-intl": "Se folosește extensia [https://php.net/manual/en/book.intl.php PHP intl] pentru normalizarea Unicode.", + "config-unicode-pure-php-warning": "Atenție: Extensia [https://php.net/manual/en/book.intl.php PHP intl] nu este disponibilă pentru a procesa normalizarea Unicode, se folosește o implementare lentă pur PHP.\nDacă rulați un site cu trafic ridicat, ar trebui să citiți despre [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations Normalizarea Unicode].", "config-unicode-update-warning": "Avertisment: Versiunea instalată a pachetului de normalizare Unicode utilizează o versiune mai veche a bibliotecii [http://site.icu-project.org/ proiectul ICU].\nAr trebui să faceți upgrade [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations] dacă sunteți preocupat de utilizarea Unicode.", "config-no-db": "Nu am găsit un driver de bază de date potrivit! Trebuie să instalați un driver de bază de date pentru PHP.\nUrmătoarea bază de date {{PLURAL:$2|tip este|tipuri sunt}} este acceptată: $1.\nDacă ați compilat singuri PHP, reconfigurați-l cu un client de bază de date activat, de exemplu, utilizând ./ configure --with-mysqli.\nDacă ați instalat PHP dintr-un pachet Debian sau Ubuntu, atunci trebuie să instalați, de exemplu, pachetul php-mysql", - "config-outdated-sqlite": "Atenție: ai SQLite $1, care este mai mic decât minimul necesar pentru versiunea $2. SQLite va fi nedisponibil.", + "config-outdated-sqlite": "Atenție: aveții SQLite $2, care este mai mic decât versiunea minimă $1. SQLite nu va fi disponibil.", "config-no-fts3": "Atenție: SQLite este compus fără [//sqlite.org/fts3.html modulu FTS3], caută caracteristici care nu vor fi disponibile la finalul acesta.", "config-pcre-old": "Fatal: PCRE $1 sau mai târziu este necesar este necesar. \nPHP tău este binar este legat de PCRE $2. \n[https://www.mediawiki.org/wiki/Manual:Errors_and_symptoms/PCRE Mai multe informații].", "config-pcre-no-utf8": "Fatal: Modul PCRE al PHP pare să fie compilat fără suport PCRE_UTF8.\nMediaWiki necesită ca suportul UTF-8 să funcționeze corect.", @@ -78,10 +82,8 @@ "config-no-uri": "Eroare: Nu pot determina URI-ul curent.\nInstalare întreruptă.", "config-db-type": "Tipul bazei de date:", "config-db-host": "Gazdă bază de date:", - "config-db-host-oracle": "Baza de date TNS:", "config-db-wiki-settings": "Identificați acest wiki", "config-db-name": "Numele bazei de date (fără cratime):", - "config-db-name-oracle": "Schema bazei de date:", "config-db-install-account": "Contul de utilizator pentru instalare", "config-db-username": "Nume de utilizator pentru baza de date:", "config-db-password": "Parola bazei de date:", @@ -94,19 +96,13 @@ "config-db-port": "Portul bazei de date:", "config-db-schema": "Schema pentru MediaWiki (fără cratime):", "config-sqlite-dir": "Director de date SQLite:", - "config-oracle-def-ts": "Spațiu de stocare („tablespace”) implicit:", - "config-oracle-temp-ts": "Spațiu de stocare („tablespace”) temporar:", "config-type-mysql": "MariaDB, MySQL sau compatibil", - "config-type-mssql": "Microsoft SQL Server", "config-header-mysql": "Setările MariaDB/MySQL", "config-header-postgres": "Setări PostgreSQL", "config-header-sqlite": "Setări SQLite", - "config-header-oracle": "Setări Oracle", - "config-header-mssql": "Setări Microsoft SQL Server", "config-invalid-db-type": "Tip de bază de date incorect", "config-missing-db-name": "Trebuie să introduceți o valoare pentru „{{int:config-db-name}}”.", "config-missing-db-host": "Trebuie să introduceți o valoare pentru „{{int:config-db-host}}”.", - "config-missing-db-server-oracle": "Trebuie să introduceți o valoare pentru „{{int:config-db-host-oracle}}”.", "config-connection-error": "$1.\n\nVerificați hostul, numele de utilizator și parola și reîncercați. Dacă folosiți „localhost” drept host al bazei de date, încercați mai bine „127.0.0.1” (sau invers).", "config-upgrade-done-no-regenerate": "Actualizare completă.\n\nAcum puteți [$1 începe să vă folosiți wikiul].", "config-regenerate": "Regenerare LocalSettings.php →", @@ -115,7 +111,6 @@ "config-db-web-create": "Creați contul dacă nu există deja", "config-mysql-engine": "Motor de stocare:", "config-mysql-innodb": "InnoDB (recomandat)", - "config-mssql-auth": "Tip de autentificare:", "config-site-name": "Numele wikiului:", "config-site-name-blank": "Introduceți un nume pentru sit.", "config-project-namespace": "Spațiul de nume al proiectului:", @@ -127,6 +122,7 @@ "config-admin-name": "Numele dumneavoastră de utilizator:", "config-admin-password": "Parolă:", "config-admin-password-confirm": "Parola, din nou:", + "config-admin-name-blank": "Introduceți numele de utilizator al administratorului.", "config-admin-password-blank": "Introduceți o parolă pentru contul de administrator.", "config-admin-password-mismatch": "Cele două parole introduse nu corespund.", "config-admin-email": "Adresa de e-mail:", @@ -149,6 +145,8 @@ "config-license-pd": "Domeniu public", "config-license-cc-choose": "Alegeți o licență Creative Commons personalizată", "config-email-settings": "Setări pentru e-mail", + "config-enable-email": "Permiteți trimiterea de e-mail", + "config-email-user": "Permiteți e-mailurile între utilizatori", "config-email-usertalk": "Activați notificările pentru pagina de discuții a utilizatorului", "config-upload-settings": "Încărcare de imagini și fișiere", "config-upload-deleted": "Director pentru fișierele șterse:", diff --git a/includes/installer/i18n/roa-tara.json b/includes/installer/i18n/roa-tara.json index df1ca1adc6..d56298bfc1 100644 --- a/includes/installer/i18n/roa-tara.json +++ b/includes/installer/i18n/roa-tara.json @@ -40,8 +40,6 @@ "config-env-hhvm": "HHVM $1 ha state installate.", "config-outdated-sqlite": "Iapre l'uecchjie: tu è SQLite $2, ca jè 'na versione troppe vecchie respette a quedda minime $1. SQLite non g'è disponibbele.", "config-db-type": "Tipe de database:", - "config-db-host-oracle": "Database TNS:", - "config-db-name-oracle": "Scheme d'u database:", "config-db-username": "Nome utende d'u database:", "config-db-password": "Password d'u database:", "config-db-port": "Porte d'u database:", @@ -49,13 +47,9 @@ "config-type-mysql": "MariaDFB, MySQL, o combatibbile", "config-type-postgres": "PostgreSQL", "config-type-sqlite": "SQLite", - "config-type-oracle": "Oracle", - "config-type-mssql": "Microsoft SQL Server", "config-header-mysql": "'Mbostaziune de MariaDB/MySQL", "config-header-postgres": "'Mbostaziune de PostgreSQL", "config-header-sqlite": "'Mbostaziune de SQLite", - "config-header-oracle": "'Mbostaziune de Oracle", - "config-header-mssql": "'Mbostaziune de Microsoft SQL Server", "config-invalid-db-type": "Tipe de database invalide.", "config-mysql-innodb": "InnoDB (conzigliate)", "config-ns-generic": "Proggette", diff --git a/includes/installer/i18n/ru.json b/includes/installer/i18n/ru.json index 03ef206bd5..2dbc294c96 100644 --- a/includes/installer/i18n/ru.json +++ b/includes/installer/i18n/ru.json @@ -107,18 +107,14 @@ "config-uploads-not-safe": "'''Внимание:''' директория, используемая по умолчанию для загрузок ($1) уязвима к выполнению произвольных скриптов.\nХотя MediaWiki проверяет все загружаемые файлы на наличие угроз, настоятельно рекомендуется [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Security#Upload_security закрыть данную уязвимость] перед включением загрузки файлов.", "config-no-cli-uploads-check": "'''Предупреждение:''' каталог для загрузки по умолчанию ( $1 ) не проверялся на уязвимости\n на выполнение произвольного сценария во время установки CLI.", "config-brokenlibxml": "В вашей системе имеется сочетание версий PHP и libxml2, которое может привести к скрытым повреждениям данных в MediaWiki и других веб-приложениях.\nОбновите libxml2 до версии 2.7.3 или старше ([https://bugs.php.net/bug.php?id=45996 сведения об ошибке]).\nУстановка прервана.", - "config-suhosin-max-value-length": "Suhosin установлен и ограничивает параметр GET length до $1 {{PLURAL:$1|байт|байта|байт}}. Компонент MediaWiki ResourceLoader будет обходить это ограничение, но это снизит производительность. Если это возможно, следует установить suhosin.get.max_value_length в значение 1024 или выше в php.ini, а также установить для $wgResourceLoaderMaxQueryLength такое же значение в LocalSettings.php.", + "config-suhosin-max-value-length": "Suhosin установлен и ограничивает параметр GET length до $1 {{PLURAL:$1|байт|байта|байт}}. Для MediaWiki требуется, чтоб значение suhosin.get.max_value_length было хотя бы $2. Отключите эту настройку или увеличьте значение в php.ini до $3.", "config-using-32bit": "Внимание: похоже, ваша система работает с 32-битными целыми числами. Это [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:32-bit не рекомендуется].", "config-db-type": "Тип базы данных:", "config-db-host": "Хост базы данных:", "config-db-host-help": "Если ваш сервер базы данных находится на другом сервере, введите здесь его имя хоста или IP-адрес.\n\nЕсли вы используете совместный виртуальный хостинг, ваш провайдер хостинга должен сообщить вам правильное имя хоста в своей документации.\n\nЕсли вы используете MySQL, «localhost» может не подойти в качестве имени сервера. В этом случае попробуйте указать 127.0.0.1 в качестве локального IP-адреса.\n\nЕсли вы используете PostgreSQL, оставьте это поле пустым для подключения через сокет Unix.", - "config-db-host-oracle": "TNS базы данных:", - "config-db-host-oracle-help": "Введите действительный [http://download.oracle.com/docs/cd/B28359_01/network.111/b28317/tnsnames.htm Local Connect Name]; файл tnsnames.ora должен быть видимым для этой инсталляции.
При использовании клиентских библиотек версии 10g и старше также возможно использовать метод именования [http://download.oracle.com/docs/cd/E11882_01/network.112/e10836/naming.htm Easy Connect].", "config-db-wiki-settings": "Идентификация этой вики", "config-db-name": "Имя базы данных (без дефисов):", "config-db-name-help": "Выберите название-идентификатор для вашей вики.\nОно не должно содержать пробелов.\n\nЕсли вы используете виртуальный хостинг, провайдер или выдаст вам конкретное имя базы данных, или позволит создавать базы данных с помощью панели управления.", - "config-db-name-oracle": "Схема базы данных:", - "config-db-account-oracle-warn": "Поддерживаются три сценария установки Oracle в качестве базы данных:\n\nЕсли вы хотите создать учётную запись базы данных в процессе установки, пожалуйста, укажите учётную запись роли SYSDBA для установки и укажите желаемые полномочия учётной записи с веб-доступом. вы также можете учётную запись с веб-доступом вручную и указать только её (если у неё есть необходимые разрешения на создание объектов схемы) или указать две учётные записи, одну с правами создания объектов, а другую с ограничениями для веб-доступа.\n\nСценарий для создания учётной записи с необходимыми привилегиями можно найти в папке «maintenance/oracle/» этой программы установки. Имейте в виду, что использование ограниченной учётной записи приведёт к отключению всех возможностей обслуживания с учётной записи по умолчанию.", "config-db-install-account": "Учётная запись для установки", "config-db-username": "Имя пользователя базы данных:", "config-db-password": "Пароль базы данных:", @@ -137,37 +133,24 @@ "config-pg-test-error": "Не удаётся подключиться к базе данных $1: $2", "config-sqlite-dir": "Директория данных SQLite:", "config-sqlite-dir-help": "SQLite хранит все данные в одном файле.\n\nДиректория, указываемая вами, должна быть доступна для записи веб-сервером во время установки.\n\nОна '''не должна''' быть доступна через Интернет, поэтому не должна совпадать с той, где хранятся PHP файлы.\n\nУстановщик запишет в эту директорию файл .htaccess, но если это не сработает, кто-нибудь может получить доступ ко всей базе данных.\nВ этой базе находится в том числе и информация о пользователях (адреса электронной почты, хэши паролей), а также удалённые страницы и другие секретные данные о вики.\n\nПо возможности, расположите базу данных где-нибудь в стороне, например, в /var/lib/mediawiki/yourwiki.", - "config-oracle-def-ts": "Пространство таблиц по умолчанию:", - "config-oracle-temp-ts": "Временное пространство таблиц:", "config-type-mysql": "MariaDB, MySQL или совместимая", "config-type-postgres": "PostgreSQL", "config-type-sqlite": "SQLite", - "config-type-oracle": "Oracle", - "config-type-mssql": "Microsoft SQL Server", "config-support-info": "MediaWiki поддерживает следующие СУБД:\n\n$1\n\nЕсли вы не видите своей системы хранения данных в этом списке, следуйте инструкциям, на которые есть ссылка выше, чтобы получить поддержку.", "config-dbsupport-mysql": "* [{{int:version-db-mysql-url}} MySQL] — основная база данных для MediaWiki, которая поддерживается лучше всего. MediaWiki также работает с [{{int:version-db-mariadb-url}} MariaDB] и [{{int:version-db-percona-url}} Percona Server], которые являются MariaDB-совместимыми. (См.[https://www.php.net/manual/ru/mysql.installation.php Как собрать PHP с поддержкой MySQL])", "config-dbsupport-postgres": "* [{{int:version-db-postgres-url}} PostgreSQL] — популярная СУБД с открытым исходным кодом, альтернатива MySQL. ([https://www.php.net/manual/ru/pgsql.installation.php Как собрать PHP с поддержкой PostgreSQL]).", "config-dbsupport-sqlite": "* [{{int:version-db-sqlite-url}} SQLite] — это легковесная система баз данных, имеющая очень хорошую поддержку. ([https://www.php.net/manual/ru/pdo.installation.php Как собрать PHP с поддержкой SQLite], работающей посредством PDO)", - "config-dbsupport-oracle": "* [{{int:version-db-oracle-url}} Oracle] — это коммерческая корпоративная база данных. ([https://www.php.net/manual/ru/oci8.installation.php Как собрать PHP с поддержкой OCI8])", - "config-dbsupport-mssql": "* [{{int:version-db-mssql-url}} Microsoft SQL Server] — это коммерческая корпоративная база данных для Windows. ([https://www.php.net/manual/ru/sqlsrv.installation.php Как собрать PHP с поддержкой SQLSRV])", "config-header-mysql": "Настройки MariaDB/MySQL", "config-header-postgres": "Настройки PostgreSQL", "config-header-sqlite": "Настройки SQLite", - "config-header-oracle": "Настройки Oracle", - "config-header-mssql": "Параметры Microsoft SQL Server", "config-invalid-db-type": "Неверный тип базы данных", "config-missing-db-name": "Вы должны ввести значение «{{int:config-db-name}}».", "config-missing-db-host": "Необходимо ввести значение параметра «{{int:config-db-host}}».", - "config-missing-db-server-oracle": "Вы должны заполнить поле «{{int:config-db-host-oracle}}»", - "config-invalid-db-server-oracle": "Неверное TNS базы данных «$1».\nИспользуйте либо «TNS Name», либо строку «Easy Connect» ([http://docs.oracle.com/cd/E11882_01/network.112/e10836/naming.htm Методы наименования Oracle])", "config-invalid-db-name": "Неверное имя базы данных «$1».\nИспользуйте только ASCII-символы (a-z, A-Z), цифры (0-9), знак подчёркивания (_) и дефис(-).", "config-invalid-db-prefix": "Неверный префикс базы данных «$1».\nИспользуйте только буквы ASCII (a-z, A-Z), цифры (0-9), знак подчёркивания (_) и дефис (-).", "config-connection-error": "$1.\n\nПроверьте хост, имя пользователя и пароль и попробуйте ещё раз. Если в качестве хоста базы данных используется \"localhost\", попробуйте использовать вместо него \"127.0.0.1\" (или наоборот).", "config-invalid-schema": "Неправильная схема для MediaWiki «$1».\nИспользуйте только ASCII символы (a-z, A-Z), цифры(0-9) и знаки подчёркивания(_).", - "config-db-sys-create-oracle": "Программа установки поддерживает только использование SYSDBA для создания новой учётной записи.", - "config-db-sys-user-exists-oracle": "Учётная запись «$1». SYSDBA может использоваться только для создания новой учётной записи!", "config-postgres-old": "Необходим PostgreSQL $1 или более поздняя версия. У вас установлен PostgreSQL $2.", - "config-mssql-old": "Требуется Microsoft SQL Server версии $1 или более поздней. У вас установлена версия $2.", "config-sqlite-name-help": "Выберите имя-идентификатор для вашей вики.\nНе используйте дефисы и пробелы.\nЭта строка будет использоваться в имени файла SQLite.", "config-sqlite-parent-unwritable-group": "Не удалось создать директорию данных $1, так как у веб-сервера нет прав записи в родительскую директорию $2.\n\nУстановщик определил пользователя, под которым работает веб-сервер.\nСделайте директорию $3 доступной для записи и продолжите.\nВ Unix/Linux системе выполните:\n\n
cd $2\nmkdir $3\nchgrp $4 $3\nchmod g+w $3
", "config-sqlite-parent-unwritable-nogroup": "Не удалось создать директорию для данных $1, так как у веб-сервера нет прав на запись в родительскую директорию $2.\n\nПрограмма установки не смогла определить пользователя, под которым работает веб-сервер.\nДля продолжения сделайте каталог $3 глобально доступным для записи серверу (и другим).\nВ Unix/Linux сделайте:\n\n
cd $2\nmkdir $3\nchmod a+w $3
", @@ -192,11 +175,6 @@ "config-mysql-engine": "Движок базы данных:", "config-mysql-innodb": "InnoDB (рекомендуется)", "config-mysql-engine-help": "'''InnoDB''' почти всегда предпочтительнее, так как он лучше справляется с параллельным доступом.\n\n'''MyISAM''' может оказаться быстрее для вики с одним пользователем или с минимальным количеством поступающих правок, однако базы данных на нём портятся чаще, чем на InnoDB.", - "config-mssql-auth": "Тип аутентификации:", - "config-mssql-install-auth": "Выберите тип проверки подлинности, который будет использоваться для подключения к базе данных во время процесса установки.\nЕсли вы выберите «{{int:config-mssql-windowsauth}}», будут использоваться учётные данные пользователя, под которым работает веб-сервер.", - "config-mssql-web-auth": "Выберите тип проверки подлинности, который веб-сервер будет использовать для подключения к серверу базы данных во время обычного функционирования вики.\nЕсли вы выберите «{{int:config-mssql-windowsauth}}», будут использоваться учётные данные пользователя, под которым работает веб-сервер.", - "config-mssql-sqlauth": "Проверка подлинности SQL Server", - "config-mssql-windowsauth": "Проверка подлинности Windows", "config-site-name": "Название вики:", "config-site-name-help": "Название будет отображаться в заголовке окна браузера и в некоторых других местах вики.", "config-site-name-blank": "Введите название сайта.", diff --git a/includes/installer/i18n/sco.json b/includes/installer/i18n/sco.json index 3fc52abd98..b0a171b09a 100644 --- a/includes/installer/i18n/sco.json +++ b/includes/installer/i18n/sco.json @@ -82,13 +82,9 @@ "config-db-type": "Dâtabase type:", "config-db-host": "Dâtabase host:", "config-db-host-help": "Gif yer database server is oan ae different server, enter the host name or IP address here.\n\nGif ye'r uisin shaired wab hostin, yer hostin provider shid gie ye the richt host name in their documentation.\n\nGif ye'r installin oan ae Windows server n uisin MySQL, uisin \"localhost\" michtna wark fer the server name. Gif it disna, try \"127.0.0.1\" fer the local IP address.\n\nGif ye'r uisin PostgreSQL, lea this field blank tae connect bi wa o ae Unix socket.", - "config-db-host-oracle": "Dâtabase TNS:", - "config-db-host-oracle-help": "Enter ae valid [http://download.oracle.com/docs/cd/B28359_01/network.111/b28317/tnsnames.htm Local Connect Name]; ae tnsnames.ora file maun be veesible til this instawation.
Gif ye'r uisin client libries 10g or newer ye can uise forby the [http://download.oracle.com/docs/cd/E11882_01/network.112/e10836/naming.htm Easy Connect] namin methyd.", "config-db-wiki-settings": "Identifie this wiki", "config-db-name": "Dâtabase name:", "config-db-name-help": "Chuise ae name that identifies yer wiki.\nIt shidna contain spaces.\n\nGif ye'r uisin shaired wab hoastin, yer hoastin provider will either gie ye ae speceefic database name tae uise or let ye mak databases bi waa o ae control panel.", - "config-db-name-oracle": "Dâtabase schema:", - "config-db-account-oracle-warn": "Thaur's three supportit scenaríos fer instawin Oracle aes ae database backend:\n\nGif ye wish tae cræft ae database accoont aes pairt o the instawation process, please supplie aen accoont wi SYSDBA role aes database accoont fer instawation n speceefie the desired creedentials fer the wab-access accoont, itherwise ye can eether cræft the wab-access accoont manuallie n supplie yinlie that accoont (gif it haes the needit permeessions tae cræft the schema objects) or supplie twa differant accoonts, yin wi cræft preevileges n ae restreectit yin fer wab access.\n\nScreept fer cræftin aen accoont wi the needit preevileges can be foond in the \"maintenance/oracle/\" directerie o this instawation. Keep in mynd that uisin ae restreectit accoont will disable aw maintenance capabileeties wi the defaut accoont.", "config-db-install-account": "Uiser accoont fer installâtion", "config-db-username": "Database uisername:", "config-db-password": "Database passwaird:", @@ -107,34 +103,22 @@ "config-pg-test-error": "Canna connect til database $1: $2", "config-sqlite-dir": "SQLite data directerie:", "config-sqlite-dir-help": "SQLite stores aw data in ae single file.\n\nThe directerie ye provide maun be writable bi the wabserver durin instawation.\n\nIt shid no be accessible bi waa o the wab, this is why we'r no puttin it whaur yer PHP files ar.\n\nThe instawer will write ae .htaccess file alang wi it, but gif that fails somebodie can gain access til yer raw database.\nThat incluides raw uiser data (wab-mail addresses, hashed passwairds) aes weel aes delytit reveesions n ither restreected data oan the wiki.\n\nConsider puttin the database some ither place awthegether, fer example in /var/lib/mediawiki/yourwiki.", - "config-oracle-def-ts": "Defaut buirdspace:", - "config-oracle-temp-ts": "Temperie buirdspace:", "config-type-mysql": "MaSQL (or compâtible)", - "config-type-mssql": "Micræsaff SQL Server", "config-support-info": "MediaWiki supports the follaein database systems:\n\n$1\n\nGif ye dinna see the database system ye'r tryin tae uise listed ablow, than follae the instructions linked abuin tae enable support.", "config-dbsupport-mysql": "* [{{int:version-db-mysql-url}} MySQL] is the primarie tairget fer MediaWiki n is best supported. MediaWiki warks forby wi [{{int:version-db-mariadb-url}} MariaDB] n [{{int:version-db-percona-url}} Percona Server], thir ar MySQL compatible. ([https://www.php.net/manual/en/mysqli.installation.php Hou tae compile PHP wi MySQL support])", "config-dbsupport-postgres": "* [{{int:version-db-postgres-url}} PostgreSQL] is ae popular apen soorce database system aes aen alternative til MySQL. Thaur micht be some wee bugs still hingin roond, n it's na recommendit fer uiss in ae production environment. ([https://www.php.net/manual/en/pgsql.installation.php Hou tae compile PHP wi PostgreSQL support])", "config-dbsupport-sqlite": "* [{{int:version-db-sqlite-url}} SQLite] is ae lichtweicht database system that is ver weel supportit. ([http://www.php.net/manual/en/pdo.installation.php Hou tae compile PHP wi SQLite support], uises PDO)", - "config-dbsupport-oracle": "* [{{int:version-db-oracle-url}} Oracle] is ae commercial enterprise database. ([http://www.php.net/manual/en/oci8.installation.php Hou tae compile PHP wi OCI8 support])", - "config-dbsupport-mssql": "* [{{int:version-db-mssql-url}} Microsoft SQL Server] is ae commercial enterprise database fer Windows. ([https://www.php.net/manual/en/sqlsrv.installation.php Hou tae compile PHP wi SQLSRV support])", "config-header-mysql": "MaSQL settins", "config-header-postgres": "PostgreSQL settins", "config-header-sqlite": "SQLite settins", - "config-header-oracle": "Oracle settins", - "config-header-mssql": "Microsoft SQL Server settings", "config-invalid-db-type": "Onvalid database type", "config-missing-db-name": "Ye maun enter ae value fer \"{{int:config-db-name}}\".", "config-missing-db-host": "Ye maun enter ae value fer \"{{int:config-db-host}}\".", - "config-missing-db-server-oracle": "Ye maun enter ae value fer \"{{int:config-db-host-oracle}}\".", - "config-invalid-db-server-oracle": "Onvalid database TNS \"$1\".\nUise either \"TNS Name\" or aen \"Easy Connect\" string ([http://docs.oracle.com/cd/E11882_01/network.112/e10836/naming.htm Oracle Naming Methods])", "config-invalid-db-name": "Onvalid database name \"$1\".\nUise yinly ASCII letters (a-z, A-Z), nummers (0-9), unnerscores (_) an hyphens (-).", "config-invalid-db-prefix": "Onvalid database prefix \"$1\".\nUise yinly ASCII letters (a-z, A-Z), nummers (0-9), unnerscores (_) an hyphens (-).", "config-connection-error": "$1.\n\nCheck the host, uisername n passwaird n gie it anither shot.", "config-invalid-schema": "Onvalid schema fer MediaWiki \"$1\".\nUise yinly ASCII letters (a-z, A-Z), nummers (0-9) an unnerscores (_).", - "config-db-sys-create-oracle": "Installer yinly supports usin ae SYSDBA accoont fer makin ae new accoont.", - "config-db-sys-user-exists-oracle": "Uiser accoont \"$1\" awreadie exeests. SYSDBA can yinly be uised fer the makin o ae new accoont!", "config-postgres-old": "PostgreSQL $1 or later is required. Ye hae $2.", - "config-mssql-old": "Microsoft SQL Server $1 or newer is needed. Ye hae $2.", "config-sqlite-name-help": "Chuise ae name that identifies yer wiki.\nDinna uise spaces or hyphens.\nThis will be uised fer the SQLite data file name.", "config-sqlite-parent-unwritable-group": "Canna mak the data directerie $1, cause the parent directerie $2 isna writable bi the wabserver.\n\nThe installer haes determined the uiser yer wabserver is runnin aes.\nMak the $3 directerie writable bi it tae continue.\nOan ae Unix/Linux system dae:\n\n
cd $2\nmkdir $3\nchgrp $4 $3\nchmod g+w $3
", "config-sqlite-parent-unwritable-nogroup": "Canna cræft the data directerie $1, cause the pairent directerie $2 isna writable bi the wabserver.\n\nThe instawer coudna determine the uiser yer wabserver is rinnin aes.\nMak the $3 directerie globallie writable bi it (n ithers!) tae continue.\nOan ae Unix/Linux system dae:\n\n
cd $2\nmkdir $3\nchmod a+w $3
", @@ -158,11 +142,6 @@ "config-mysql-engine": "Storage engine:", "config-mysql-innodb": "InnoDB", "config-mysql-engine-help": "InnoDB is awmaist aye the best optie, aes it haes guid concurrencie support.\n\nMyISAM micht be faster in single-uiser or read-yinly installâtions.\nMyISAM databases tend tae rot mair aften than InnoDB databases.", - "config-mssql-auth": "Authentication type:", - "config-mssql-install-auth": "Select the authentication type that's tae be uised tae connect wi the database durin the installation process.\nGif ye select \"{{int:config-mssql-windowsauth}}\", the credeentials o whitever uiser the wabserver is rinnin aes will be uised.", - "config-mssql-web-auth": "Select the authentication type that the wab server will uise tae connect wi the database server, durin ordinair operation o the wiki.\nGif ye select \"{{int:config-mssql-winowsauth}}\", the credeentials o whitever uiser the wabserver is rinnin aes will be uised.", - "config-mssql-sqlauth": "SQL Server Authentication", - "config-mssql-windowsauth": "Windows Authentication", "config-site-name": "Name o wiki:", "config-site-name-help": "This will kyth in the title baur o the brouser n in varioos ither places.", "config-site-name-blank": "Enter ae site name.", diff --git a/includes/installer/i18n/sh.json b/includes/installer/i18n/sh.json index 06090ca797..b83cf18ea1 100644 --- a/includes/installer/i18n/sh.json +++ b/includes/installer/i18n/sh.json @@ -87,13 +87,9 @@ "config-db-type": "Tip baze podataka:", "config-db-host": "Domaćin baze podataka:", "config-db-host-help": "Ako je vaša baza podataka na drugom serveru, tada ovdje unesite ime domaćina ili IP adresu.\n\nAko koristite zajednički (deljen) hosting, tada će vaš vjerovnik navesti ispravno ime domaćina u njegovoj dokumentaciji.\n\nAko koristite MySQL, mogućnost \"localhost\" možda neće raditi za serversko ime. U tom slučaju, pokušajte unijeti \"127.0.0.1\" kao lokalnu IP adresu.\n\nAko koristite PostgreSQL, ostavite polje prazno za povezivanje putem Unix priključka.", - "config-db-host-oracle": "TNS baze podataka:", - "config-db-host-oracle-help": "Unesite valjano [http://download.oracle.com/docs/cd/B28359_01/network.111/b28317/tnsnames.htm lokalno ime za povezivanje]. Ovoj uspostavi mora biti vidljiva datotekata tnsnames.ora.
Ako koristite klijentske biblioteke 10g ili novije, tada možete koristiti i metodu imenovanja [http://download.oracle.com/docs/cd/E11882_01/network.112/e10836/naming.htm Easy Connect].", "config-db-wiki-settings": "Identifikuj ovaj wiki", "config-db-name": "Ime baze podataka (bez crtica):", "config-db-name-help": "Odaberite ime koje će predstavljati vaš wiki.\nIme ne smije sadržavati razmake.\n\nAko koristite zajednički (deljen) hosting, tada vaš će vam poslodavac dati određeno ime baze podataka za korištenje, ili pak će vas pustiti da pravite baze podataka putem upravljačnice.", - "config-db-name-oracle": "Šema baze podataka:", - "config-db-account-oracle-warn": "Postoje tri podržana scenarija za uspostavu Oraclea kao bazni davatelja usluga:\n\nAko želite stvoriti račun baze podataka kao dio postupka uspostave, navedite račun s SYSDBA-ulogom kao račun za bazu koja se uspostavlja i navedite željene podatke za račun mrežnog pristupa. U drugom slučaju, možete izraditi račun za pristup mreži ručno i navesti samo taj račun (ako postoje dozvole za izradu shematskih objekata) ili pak navesti dva različita računa, jedan s povlasticama izrade, a drugi (ograničeno) za mrežni pristup.\n\nSkriptu za izradu računa s obveznim ovlastima naći ćete u direktorijumu „maintenance/oracle/“ ove uspostave. Imajte na umu da ćete, ako koristite ograničeni račun, onemogućiti sve funkcije održavanja s primarnim računom.", "config-db-install-account": "Korisnički račun za uspostavu", "config-db-username": "Korisničko ime baze podataka:", "config-db-password": "Lozinka baze podataka:", @@ -112,34 +108,22 @@ "config-pg-test-error": "Nije moguće povezati se sa bazom podataka $1: $2", "config-sqlite-dir": "Direktorijum za SQLite-podatke:", "config-sqlite-dir-help": "SQLite pohranjuje sve podatke u jednu datoteku.\n\nDirektorijum što navedete mora biti zapisljiv iz mrežnog servera tijekom uspostave.\n\nTaj je '''ne''' mora biti dostupno putem svemrežja, tako da ne stavljamo gdje su vaše PHP datoteke.\n\nUspostavljač će također stvoriti datoteku .htaccess, ali ako ta ne radi kako treba, tada netko može unijeti vašu neobrađenu (sirovu) bazu podataka.\nTo uključuje neobrađene korisničke podatke (adrese e-pošte, hash lozinke) kao i izbrisane revizije i druge podatke za wiki koji ima ograničen pristup.\n\nPreporučuje se da cijelu bazu smjestite negdje, primjerice /var/lib/mediawiki/yourwiki.", - "config-oracle-def-ts": "Podrazumevani tablearni prostor:", - "config-oracle-temp-ts": "Privremeni tabelarni prostor:", "config-type-mysql": "MariaDB, MySQL ili kompatibilan", - "config-type-mssql": "Microsoft SQL Server", "config-support-info": "MediaWiki podržava sljedeće sustave baza podataka:\n\n$1\n\nAko sustav koji želite koristiti nije naveden u nastavku, slijedite vezu gore navedenih uputa kako biste omogućili podršku za taj sustav.", "config-dbsupport-mysql": "* [{{int:version-db-mariadb-url}} MariaDB] je glavna meta MediaWikija i najbolje je podržan. MediaWiki također radi sa [{{int:version-db-mysql-url}} MySQL-om] i [{{int:version-db-percona-url}} Percona], koji su skladni sa MariaDB-om. ([https://www.php.net/manual/en/mysqli.installation.php Kako kompajlirati PHP sa podrškom MySQL-a])", "config-dbsupport-postgres": "* [{{int:version-db-postgres-url}} PostgreSQL] je popularan sistem baza podataka otvorenog koda koji predstavlja alternativu MySQL-u. ([https://www.php.net/manual/en/pgsql.installation.php Kako kompajlirati PHP sa podrškom PostgreSQL-a])", "config-dbsupport-sqlite": "* [{{int:version-db-sqlite-url}} SQLite] je lagan sistem baze podataka koji je veoma dobro podržan. ([https://www.php.net/manual/en/pdo.installation.php Kako kompajlirati PHP sa podrškom SQLite-a], koristi PDO)", - "config-dbsupport-oracle": "* [{{int:version-db-oracle-url}} Oracle] je baza podataka komercijalnih preduzeća. ([https://www.php.net/manual/en/oci8.installation.php Kako kompajlirati PHP sa podrškom OCI8-a])", - "config-dbsupport-mssql": "* [{{int:version-db-mssql-url}} Microsoft SQL Server] je baza podataka komercijalnih preduzeća za Windows. ([https://www.php.net/manual/en/sqlsrv.installation.php Kako kompajlirati PHP sa podrškom SQLSRV-a])", "config-header-mysql": "Podešavanja MariaDB/MySQL-a", "config-header-postgres": "Podešavanja PostgreSQL-a", "config-header-sqlite": "Podešavanja SQLite-a", - "config-header-oracle": "Podešavanja Oracle-a", - "config-header-mssql": "Podešavanja za Microsoft SQL Server", "config-invalid-db-type": "Nevažeći tip baze", "config-missing-db-name": "Morate unijeti vrijednost za \"{{int:config-db-name}}\".", "config-missing-db-host": "Morate unijeti vrijednost za \"{{int:config-db-host}}\".", - "config-missing-db-server-oracle": "Morate unijeti vrijednost za \"{{int:config-db-host-oracle}}\".", - "config-invalid-db-server-oracle": "Nevažeći TNS „$1”.\nKoristite ili „TNS Name” ili nisku „Easy Connect”.\n([http://docs.oracle.com/cd/E11882_01/network.112/e10836/naming.htm Oracle metodi imenovanja]).", "config-invalid-db-name": "Ime baze podataka „$1” nije važeće.\nKoristite samo ASCII-slova (a-z, A-Z), brojeve (0-9) i podvlake (_).", "config-invalid-db-prefix": "Prefiks baze podataka „$1” nije važeći.\nKoristite samo ASCII-slova (a-z, A-Z), brojeve (0-9), podvlake (_) i crtice (-).", "config-connection-error": "$1.\n\nProvjerite host, korisničko ime i lozinku i pokušajte ponovno. Ako kao host baze podataka koristite \"localhost\", zamijenite ga \"127.0.0.1\" (ili obrnuto).", "config-invalid-schema": "Šema za MediaWiki „$1” nije važeća.\nKoristite samo ASCII slova (a-z, A-Z), brojeve (0-9) i podvlake (_).", - "config-db-sys-create-oracle": "Uspostavljač podržava samo upotrebu SYSDBA-računa za pravljenje novih računa.", - "config-db-sys-user-exists-oracle": "Korisnički račun \"$1\" već postoji. SYSDBA samo služi za stvaranje novog računa!", "config-postgres-old": "Zahtijeva se PostgreSQL $1 ili noviji. Vi imate $2.", - "config-mssql-old": "Zahtijeva se Microsoft SQL Server $1 ili novija verzija. Vi imate $2.", "config-sqlite-name-help": "Odaberite ime koje će predstavljati vaš wiki.\nNe koristite razmake i crte.\nOvo će se koristiti za ime datoteke SQLite-podataka.", "config-sqlite-parent-unwritable-group": "Nije moguće izraditi direktorijum $1 \njer mrežni poslužitelj ne može pisati u matični direktorijum $2.\n\nIdentificiran je korisnik pod kojim radi vaš mrežni poslužitelj.\nDa biste nastavili, namjestite da može zapisivati u direktorijum $3.\nNa Unix/Linux sistemu učinite sljedeće:\n\n
cd $2\nmkdir $3\nchgrp $4 $3\nchmod g+w $3
", "config-sqlite-parent-unwritable-nogroup": "Nije moguće izraditi direktorijum $1 \njer mrežni poslužitelj ne može pisati u matični direktorijum $2.\n\nUspostavljač nije mogao odrediti korisnika pod kojim radi vaš mrežni poslužitelj.\nDa biste nastavili, postavite toga (i druge!) da biste se globalno zapisivati u direktorijum $3.\nNa Unix/Linux sistemu učinite sljedeće:\n\n
cd $2\nmkdir $3\nchmod a+w $3
", @@ -163,11 +147,9 @@ "config-db-web-no-create-privs": "Račun koji ste naveli za uspostavu nema dovoljne privilegije za da stvori račun.\nOvdje morate navesti postojeći račun.", "config-mysql-engine": "Skladišni pogon:", "config-mysql-innodb": "InnoDB (preporučeno)", - "config-mssql-auth": "Tip potvrde identiteta:", - "config-mssql-sqlauth": "Potvrda identiteta za SQL Server", - "config-mssql-windowsauth": "Potvrda identiteta za Windows", "config-site-name": "Ime wikija:", "config-site-name-help": "Ovo će se pojaviti u naslovnoj traci pregledača i na raznim drugim mestima.", + "config-site-name-blank": "Upišite ime sajta.", "config-project-namespace": "Projektni imenski prostor:", "config-ns-generic": "Projekat", "config-ns-site-name": "Isto ime kao wikija: $1", @@ -181,6 +163,11 @@ "config-admin-password-blank": "Upišite lozinku za administratorski račun", "config-admin-password-mismatch": "Lozinke što ste upisali se ne poklapaju.", "config-admin-email": "E-mail adresa:", + "config-admin-error-bademail": "Upisali ste neispravnu adresu e-pošte", + "config-pingback": "Dijeli podatke o instalaciji s programerima MediaWikija.", + "config-almost-done": "Skoro ste gotovi!\nSada možete preskočiti preostala postavljivanja i odmah instalirati wiki.", + "config-optional-continue": "Postavi mi više pitanja.", + "config-optional-skip": "Već mi je dosadilo, daj samo instaliraj wiki.", "config-profile": "Profil korisničkih prava:", "config-profile-wiki": "Otvoren wiki", "config-profile-no-anon": "Neophodno otvaranje računa", @@ -188,6 +175,7 @@ "config-profile-private": "Privatan wiki", "config-license": "Autorska prava i licenca:", "config-license-none": "Bez podnožja za licencu", + "config-license-cc-choose": "Odaberite drugu licencu Creative Commonsa na vaš izbor", "config-email-settings": "Podešavanja e-pošte", "config-enable-email": "Omogući odlaznu e-poštu", "config-email-user": "Omogući slanje e-poruka među korisnicima", @@ -196,6 +184,8 @@ "config-email-usertalk-help": "Omogući korisnicima da primaju obaveštenja o promenama u njihovim korisničkim razgovornim stranicama ako su ih omogućili u podešavanjima.", "config-email-watchlist": "Omogući obaveštenja o spisku praćenja", "config-email-watchlist-help": "Omogući korisnicima da primaju obaveštenja o svojim nadgledanim stranicama ako su ih omogućili u podešavanjima.", + "config-email-auth": "Omogući potvrdu identiteta putem e-pošte", + "config-email-sender": "Povratna adresa e-pošte:", "config-upload-settings": "Otpremanja slika i datoteka", "config-upload-enable": "Omogući postavljanje datoteka", "config-upload-deleted": "Folder za obrisane datoteke:", @@ -212,6 +202,53 @@ "config-memcached-servers": "Memcached-serveri:", "config-memcached-help": "Lista IP adresa za uporabu u Memcached.\nTreba da se navede jednu u svaki red, kao i port što će se koristiti. Na primer:\n 127.0.0.1:11211\n 192.168.1.25:1234", "config-memcache-needservers": "Odabrali ste Memcached kao vaš tip međuspremnika (keša), ali niste naveli nijedan server.", - "mainpagetext": "MediaWiki je uspješno instaliran.", + "config-extensions": "Dodaci", + "config-skins": "Skinovi", + "config-skins-use-as-default": "Koristi kao predodređenu", + "config-skins-missing": "Nisam pronašao nijednu temu. MediaWiki će koristiti rezervnu temu dok ne instalirate druge.", + "config-skins-must-enable-some": "Će treba izabrati barem jednu temu.", + "config-skins-must-enable-default": "Tema koju ste izabrali za predodređenu mora se omogućiti.", + "config-install-step-done": "gotovo", + "config-install-step-failed": "nije uspjelo", + "config-install-extensions": "Uključujem dodatke", + "config-install-database": "Postavljam bazu podataka", + "config-install-schema": "Pravim šemu", + "config-install-pg-schema-not-exist": "PostgreSQL-šema ne postoji.", + "config-install-pg-schema-failed": "Pravljenje natabela nije uspelo.\nUvjerite se da korisnik „$1” može da zapisuje u šemi „$2”.", + "config-install-pg-commit": "Usproveđivanje promjena", + "config-install-user": "Pravim korisnika baze podataka", + "config-install-user-alreadyexists": "Korisnik \"$1\" već postoji", + "config-install-user-create-failed": "Pravljenje korisnika \"$1\" nije uspjelo: $2", + "config-install-user-grant-failed": "Dodjeljivanje dozvola korisniku \"$1\" nije uspjelo: $2", + "config-install-user-missing": "Navedeni korisnik \"$1\" ne postoji.", + "config-install-user-missing-create": "Navedeni korisnik \"$1\" ne postoji.\nAko želite da ga otvorite, štiklirajte mogućnost „napravi račun”.", + "config-install-tables": "Pravim tabele", + "config-install-tables-exist": "Upozorenje: Izgleda da MediaWiki tabele već postoje.\nPreskočim pravljenje.", + "config-install-tables-failed": "Greška: Pravljenje tabele nije uspjelo zbog sljedeće greške: $1", + "config-install-interwiki": "Popunjavam predodređene međuprojektne tabele", + "config-install-interwiki-list": "Nisam mogao pronaći datoteku interwiki.list.", + "config-install-interwiki-exists": "Upozorenje: Tabela međuwikija već ima unose.\nPreskočim podrazumevano-zadanu listu.", + "config-install-stats": "Pokrećem statistiku", + "config-install-keys": "Generisanje tajnih ključeva", + "config-install-updates": "Spriječi vršenje nepotrebnih podnova", + "config-install-updates-failed": "Greška: Umetanje podnovnih klučeva u tabele nije uspjelo, zbog sljedeće greške: $1", + "config-install-sysop": "Otvaranje korisničkog računa administratora", + "config-install-subscribe-fail": "Nije moguće Vas pretplatiti se na izvješćenje mediawiki-announce: $1", + "config-install-subscribe-notpossible": "cURL nije instaliran, a allow_url_fopen nije dostupno.", + "config-install-mainpage": "Pravim početnu stranicu sa standardnim sadržajem", + "config-install-mainpage-exists": "Početna strana već postoji. Prelazim na sljedeće.", + "config-install-extension-tables": "Izrada tabela za omogućene dodatke", + "config-install-mainpage-failed": "Nisam mogao umetnuti početnu stranu: $1", + "config-download-localsettings": "Preuzmi LocalSettings.php", + "config-help": "pomoć", + "config-help-tooltip": "kliknite da rasklopite", + "config-nofile": "Datoteka \"$1\" nije pronađena. Da nije obrisana?", + "config-extension-link": "Jeste li znali da vaš wiki podržava [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Extensions dodatke]?\n\nMožete ih pregledati [https://www.mediawiki.org/wiki/Special:MyLanguage/Category:Extensions_by_category po kategoriji]", + "config-skins-screenshots": "$1 (ekr. snimci: $2)", + "config-extensions-requires": "$1 (zahtjeva $2)", + "config-screenshot": "ekranski snimak", + "config-extension-not-found": "Nisam mogao naći datoteku registracije za dodatak „$1”", + "config-extension-dependency": "Naišao na grešku sa zavisnošću pri instaliranju dodatka „$1”: $2", + "mainpagetext": "MediaWiki je instaliran.", "mainpagedocfooter": "Za informacije o korištenju wiki softvera konzultirajte [https://meta.wikimedia.org/wiki/Help:Contents Vodič za korisnike].\n\n== Uvod u rad ==\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings Lista konfiguracije postavki]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ MediaWiki FAQ]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Lista primatelja izdanja MediaWikija]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation#Translation_resources Lokalizirajte MediaWiki za svoj jezik]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Combating_spam Saznajte kako se boriti protiv spama na svojem wikiju]" } diff --git a/includes/installer/i18n/si.json b/includes/installer/i18n/si.json index fefe7a21d2..6079246459 100644 --- a/includes/installer/i18n/si.json +++ b/includes/installer/i18n/si.json @@ -37,7 +37,6 @@ "config-db-host": "දත්ත සංචිත ධාරක:", "config-db-wiki-settings": "මෙම විකිය හඳුනා ගන්න", "config-db-name": "දත්ත සංචිතයේ නම:", - "config-db-name-oracle": "දත්ත සංචිත සංක්ෂිප්ත නිරූපණය:", "config-db-install-account": "ස්ථාපනය සඳහා පරිශීලක ගිණුම", "config-db-username": "දත්ත සංචිතයේ පරිශීලක නාමය:", "config-db-password": "දත්ත සංචිතයේ මුරපදය:", @@ -47,22 +46,17 @@ "config-db-schema": "මාධ්‍යවිකි සඳහා සංක්ෂිප්ත නිරූපණය:", "config-pg-test-error": "'''$1''' දත්ත සංචිතය වෙත සම්බන්ධ විය නොහැක: $2", "config-sqlite-dir": "SQLite දත්ත නාමවලිය:", - "config-oracle-def-ts": "සාමාන්‍ය වගු අවකාශය:", - "config-oracle-temp-ts": "තාවකාලික වගු අවකාශය:", "config-header-mysql": "MySQL සැකසුම්", "config-header-postgres": "PostgreSQL සැකසුම්", "config-header-sqlite": "SQLite සැකසුම්", - "config-header-oracle": "ඔරකල් සැකසුම්", "config-invalid-db-type": "වලංගු නොවන දත්ත සංචිත වර්ගය", "config-missing-db-name": "\"දත්ත සංචිත නාමය\" සඳහා ඔබ විසින් අගයක් දිය යුතු වේ", "config-missing-db-host": "\"දත්ත සංචිත ධාරකය\" සඳහා ඔබ විසින් අගයක් දිය යුතු වේ", - "config-missing-db-server-oracle": "\"දත්ත සංචිත TNS\" සඳහා ඔබ විසින් අගයක් දිය යුතු වේ", "config-sqlite-name-help": "ඔබගේ විකිය හදුන්වාදෙන නමක් තෝරාගන්න. \nහිස්තැන් හෝ විරාම ලක්ෂණ ‍නොයොදන්න.\nමෙය SQLite දත්ත ගොනුනාමය සදහා යොදා ගනු ඇත.", "config-regenerate": "නැවත ජනිත කරන්න LocalSettings.php →", "config-db-web-account": "ජාල ප්‍රවේශනය සඳහා දත්ත සංචිත ගිණුම", "config-mysql-engine": "ආචයන එන්ජිම:", "config-mysql-innodb": "InnoDB", - "config-mssql-windowsauth": "windows සහතික කිරීම.", "config-site-name": "විකියෙහි නම:", "config-site-name-blank": "අඩවි නාමයක් යොදන්න.", "config-project-namespace": "ව්‍යාපෘතියේ නාමඅවකාශය:", diff --git a/includes/installer/i18n/sk.json b/includes/installer/i18n/sk.json index 1dcd2c1e06..16616cf219 100644 --- a/includes/installer/i18n/sk.json +++ b/includes/installer/i18n/sk.json @@ -41,16 +41,13 @@ "config-env-hhvm": "HHVM $1 je nainštalované.", "config-db-type": "Typ databázy:", "config-db-host": "Databázový server:", - "config-db-host-oracle": "Databázové TNS:", "config-db-wiki-settings": "Identifikácia tejto wiki", "config-db-name": "Názov databázy:", - "config-db-name-oracle": "Databázová schéma:", "config-db-install-account": "Používateľský účet pre inštaláciu", "config-db-username": "Databázové používateľské meno:", "config-db-password": "Databázové heslo:", "config-missing-db-name": "Musíte zadať hodnotu pre \"{{int:config-db-name}}\".", "config-missing-db-host": "Musíte zadať hodnotu pre \"{{int:config-db-host}}\".", - "config-missing-db-server-oracle": "Musíte zadať hodnotu pre \"{{int:config-db-host-oracle}}\".", "config-site-name": "Názov wiki:", "config-site-name-blank": "Zadajte názov stránky.", "config-ns-generic": "Projekt", diff --git a/includes/installer/i18n/sl.json b/includes/installer/i18n/sl.json index 3dd70380a8..cdf7b68f3c 100644 --- a/includes/installer/i18n/sl.json +++ b/includes/installer/i18n/sl.json @@ -59,10 +59,8 @@ "config-using-uri": "Uporabljam URL strežnika \"$1$2\".", "config-db-type": "Vrsta zbirke podatkov:", "config-db-host": "Gostitelj zbirke podatkov:", - "config-db-host-oracle": "TNS zbirke podatkov:", "config-db-wiki-settings": "Prepoznaj ta wiki:", "config-db-name": "Ime zbirke podatkov (brez vezajev):", - "config-db-name-oracle": "Shema zbirke podatkov:", "config-db-install-account": "Uporabniški račun za namestitev", "config-db-username": "Uporabniško ime zbirke podatkov:", "config-db-password": "Geslo zbirke podatkov:", @@ -82,13 +80,9 @@ "config-header-mysql": "Nastavitve MariaDB/MySQL", "config-header-postgres": "Nastavitve PostgreSQL", "config-header-sqlite": "Nastavitve SQLite", - "config-header-oracle": "Nastavitve Oracle", - "config-header-mssql": "nastavitve Microsoft SQL Server", "config-invalid-db-type": "Neveljavna vrsta zbirke podatkov", "config-missing-db-name": "Vnesti morate vrednost za »{{int:config-db-name}}«", "config-missing-db-host": "Vnesti morate vrednost za »{{int:config-db-host}}«.", - "config-missing-db-server-oracle": "Vnesti morate vrednost za »{{int:config-db-host-oracle}}«.", - "config-invalid-db-server-oracle": "Neveljaven TNS zbirke podatkov »$1«.\nUporabite ali \"ime TNS\" ali niz \"Easy Connect\" ([http://docs.oracle.com/cd/E11882_01/network.112/e10836/naming.htm Načini poimenovanja Oracle])", "config-invalid-db-name": "Neveljavno ime zbirke podatkov »$1«.\nUporabljajte samo črke ASCII (a-z, A-Z), številke (0-9), podčrtaje (_) in vezaje (-).", "config-invalid-db-prefix": "Neveljavna predpona zbirke podatkov »$1«.\nUporabljajte samo črke ASCII (a-z, A-Z), številke (0-9), podčrtaje (_) in vezaje (-).", "config-connection-error": "$1.\n\nPreverite gostitelja, uporabniško ime in geslo ter poskusite znova. Če kot gostitelja zbirke podatkov uporabljate »localhost«, poskusite namesto tega uporabiti »127.0.0.1« (ali obratno).", @@ -105,7 +99,6 @@ "config-db-web-create": "Ustvari račun, če že ne obstaja", "config-mysql-engine": "Pogon skladiščenja:", "config-mysql-innodb": "InnoDB (priporočeno)", - "config-mssql-auth": "Tip avtentikacije:", "config-site-name": "Ime wikija:", "config-site-name-help": "To bo prikazano v naslovni vrstici brskalnika in na drugih različnih mestih.", "config-site-name-blank": "Vnesite ime strani.", diff --git a/includes/installer/i18n/sr-ec.json b/includes/installer/i18n/sr-ec.json index e740511c1f..92eb4c112b 100644 --- a/includes/installer/i18n/sr-ec.json +++ b/includes/installer/i18n/sr-ec.json @@ -81,11 +81,9 @@ "config-using-32bit": "Упозорење: изгледа да ваш систем ради са 32-битним целим бројевима. Ово се [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:32-bit не препоручује].", "config-db-type": "Тип базе података:", "config-db-host": "Хост базе података", - "config-db-host-oracle": "TNS базе података:", "config-db-wiki-settings": "Идентификуј овај вики", "config-db-name": "Име базе података (без цртица):", "config-db-name-help": "Одаберите име које идентификује ваш вики.\nОно не треба да садржи размаке.\n\nАко користите дељени веб-хостинг, ваш добављач услуге хостинга ће вам дати одређено име базе података за коришћење или ће вас пустити да правите базе података путем контролне табле.", - "config-db-name-oracle": "Шема базе података:", "config-db-install-account": "Кориснички налог за инсталацију", "config-db-username": "Корисничко име базе података:", "config-db-password": "Лозинка базе података:", @@ -100,37 +98,24 @@ "config-db-schema-help": "Ова шема обично ће радити добро.\nПромените је само ако знате да је то потребно.", "config-pg-test-error": "Није могуће повезати се са базом података $1: $2", "config-sqlite-dir": "Директоријум SQLite података:", - "config-oracle-def-ts": "Подразумевани табеларни простор:", - "config-oracle-temp-ts": "Привремени табеларни простор:", "config-type-mysql": "MariaDB, MySQL или компатибилан", "config-type-postgres": "PostgreSQL", "config-type-sqlite": "SQLite", - "config-type-oracle": "Oracle", - "config-type-mssql": "Microsoft SQL Server", "config-support-info": "MediaWiki подржава следеће системе база података:\n\n$1\n\nАко не видите систем који покушавате да користите на листи испод, онда пратите повезана упутства изнад како бисте омогућили подршку.", "config-dbsupport-mysql": "* [{{int:version-db-mariadb-url}} MariaDB] је примарна мета за Медијавики и најбоље је подржана. Медијавики ради и са [{{int:version-db-mysql-url}} MySQL-ом] и [{{int:version-db-percona-url}} Percona Server-ом], који су компатибилни са MariaDB-ом. ([https://www.php.net/manual/en/mysqli.installation.php Како компајлирати PHP са подршком MySQL-а])", "config-dbsupport-postgres": "* [{{int:version-db-postgres-url}} PostgreSQL] је популаран систем база података отвореног кода кaо алтернатива MySQL-у. ([https://www.php.net/manual/en/pgsql.installation.php Како компајлирати PHP са подршком PostgreSQL-а])", "config-dbsupport-sqlite": "* [{{int:version-db-sqlite-url}} SQLite] је лаган систем базе података који је веома добро подржан. ([https://www.php.net/manual/en/pdo.installation.php Како компајлирати PHP са подршком SQLite-а], користи PDO)", - "config-dbsupport-oracle": "* [{{int:version-db-oracle-url}} Oracle] је база података комерцијалних предузећа. ([https://www.php.net/manual/en/oci8.installation.php Како компајлирати PHP са подршком OCI8-а])", - "config-dbsupport-mssql": "* [{{int:version-db-mssql-url}} Microsoft SQL Server] је база података комерцијалних предузећа за Windows. ([https://www.php.net/manual/en/sqlsrv.installation.php Како компајлирати PHP са подршком SQLSRV-а])", "config-header-mysql": "Подешавања MariaDB/MySQL-а", "config-header-postgres": "Подешавања PostgreSQL-а", "config-header-sqlite": "Подешавања SQLite-а", - "config-header-oracle": "Подешавања Oracle-а", - "config-header-mssql": "Подешавања Microsoft SQL Server-а", "config-invalid-db-type": "Тип базе података није важећи.", "config-missing-db-name": "Морате да унесете вредност за „{{int:config-db-name}}”.", "config-missing-db-host": "Морате да унесете вредност за „{{int:config-db-host}}”.", - "config-missing-db-server-oracle": "Морате да унесете вредност за „{{int:config-db-host-oracle}}”.", - "config-invalid-db-server-oracle": "TNS база података „$1” није важећа.\nКористите или „TNS Name” или ниску „Easy Connect”.\n([http://docs.oracle.com/cd/E11882_01/network.112/e10836/naming.htm Oracle методи именовања]).", "config-invalid-db-name": "Име базе података „$1” није важеће.\nКористите само ASCII слова (a-z, A-Z), бројеве (0-9) и подвлаке (_).", "config-invalid-db-prefix": "Префикс базе података „$1” није важећи.\nКористите само ASCII слова (a-z, A-Z), бројеве (0-9), подвлаке (_) и цртице (-).", "config-connection-error": "$1.\n\nПроверите хост, корисничко име и лозинку, па покушајте поново.", "config-invalid-schema": "Шема за MediaWiki „$1” није важећа.\nКористите само ASCII слова (a-z, A-Z), бројеве (0-9) и подвлаке (_).", - "config-db-sys-create-oracle": "Инсталациони програм подржава само коришћење SYSDBA налога за отварање новог.", - "config-db-sys-user-exists-oracle": "Кориснички налог „$1” већ постоји. SYSDBA се само може користити за отварање новог налога!", "config-postgres-old": "Неопходан је PostgreSQL $1 или новији. Ви имате $2.", - "config-mssql-old": "Неопходан је Microsoft SQL Server $1 или новији. Ви имате $2.", "config-sqlite-name-help": "Одаберите име које идентификује ваш вики.\nНе користите размаке или цртице.\nОво ће се користити за име датотеке SQLite података.", "config-sqlite-mkdir-error": "Грешка при прављењу директоријума са подацима „$1”.\nПроверите локацију, па покушајте поново.", "config-sqlite-dir-unwritable": "Није могуће уписати у директоријум „$1”.\nПромените му дозволе, тако да веб-сервер може да уписује у њему, па покушајте поново.", @@ -152,9 +137,6 @@ "config-db-web-no-create-privs": "Налог који сте навели за инсталацију нема довољне привилегије да отвори налог.\nНалог који овде наведете већ мора да постоји.", "config-mysql-engine": "Механизам складишта:", "config-mysql-innodb": "InnoDB (препоручено)", - "config-mssql-auth": "Тип потврде идентитета:", - "config-mssql-sqlauth": "SQL Server потврда идентитета", - "config-mssql-windowsauth": "Windows потврда идентитета", "config-site-name": "Име викија:", "config-site-name-help": "Ово ће се појавити у насловној траци прегледача и на разним другим местима.", "config-site-name-blank": "Унесите име локације.", diff --git a/includes/installer/i18n/sr-el.json b/includes/installer/i18n/sr-el.json index 26d60f536a..4ce871e7a5 100644 --- a/includes/installer/i18n/sr-el.json +++ b/includes/installer/i18n/sr-el.json @@ -75,11 +75,9 @@ "config-using-32bit": "Upozorenje: izgleda da vaš sistem radi sa 32-bitnim celim brojevima. Ovo se [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:32-bit ne preporučuje].", "config-db-type": "Tip baze podataka:", "config-db-host": "Host baze podataka", - "config-db-host-oracle": "TNS baze podataka:", "config-db-wiki-settings": "Identifikuj ovaj viki", "config-db-name": "Ime baze podataka (bez crtica):", "config-db-name-help": "Odaberite ime koje identifikuje vaš viki.\nOno ne treba da sadrži razmake.\n\nAko koristite deljeni veb-hosting, vaš dobavljač usluge hostinga će vam dati određeno ime baze podataka za korišćenje ili će vas pustiti da pravite baze podataka putem kontrolne table.", - "config-db-name-oracle": "Šema baze podataka:", "config-db-install-account": "Korisnički nalog za instalaciju", "config-db-username": "Korisničko ime baze podataka:", "config-db-password": "Lozinka baze podataka:", @@ -94,37 +92,24 @@ "config-db-schema-help": "Ova šema obično će raditi dobro.\nPromenite je samo ako znate da je to potrebno.", "config-pg-test-error": "Nije moguće povezati se sa bazom podataka $1: $2", "config-sqlite-dir": "Direktorijum SQLite podataka:", - "config-oracle-def-ts": "Podrazumevani tabelarni prostor:", - "config-oracle-temp-ts": "Privremeni tabelarni prostor:", "config-type-mysql": "MariaDB, MySQL ili kompatibilan", "config-type-postgres": "PostgreSQL", "config-type-sqlite": "SQLite", - "config-type-oracle": "Oracle", - "config-type-mssql": "Microsoft SQL Server", "config-support-info": "MediaWiki podržava sledeće sisteme baza podataka:\n\n$1\n\nAko ne vidite sistem koji pokušavate da koristite na listi ispod, onda pratite povezana uputstva iznad kako biste omogućili podršku.", "config-dbsupport-mysql": "* [{{int:version-db-mariadb-url}} MariaDB] je primarna meta za MediaWiki i najbolje je podržana. MediaWiki takođe radi sa [{{int:version-db-mysql-url}} MySQL-om] i [{{int:version-db-percona-url}} Percona Server-om], koji su kompatibilni sa MariaDB-om. ([https://www.php.net/manual/en/mysqli.installation.php Kako kompajlirati PHP sa podrškom MySQL-a])", "config-dbsupport-postgres": "* [{{int:version-db-postgres-url}} PostgreSQL] je popularan sistem baza podataka otvorenog koda kao alternativa MySQL-u. ([https://www.php.net/manual/en/pgsql.installation.php Kako kompajlirati PHP sa podrškom PostgreSQL-a])", "config-dbsupport-sqlite": "* [{{int:version-db-sqlite-url}} SQLite] je lagan sistem baze podataka koji je veoma dobro podržan. ([https://www.php.net/manual/en/pdo.installation.php Kako kompajlirati PHP sa podrškom SQLite-a], koristi PDO)", - "config-dbsupport-oracle": "* [{{int:version-db-oracle-url}} Oracle] je baza podataka komercijalnih preduzeća. ([https://www.php.net/manual/en/oci8.installation.php Kako kompajlirati PHP sa podrškom OCI8-a])", - "config-dbsupport-mssql": "* [{{int:version-db-mssql-url}} Microsoft SQL Server] je baza podataka komercijalnih preduzeća za Windows. ([https://www.php.net/manual/en/sqlsrv.installation.php Kako kompajlirati PHP sa podrškom SQLSRV-a])", "config-header-mysql": "Podešavanja MariaDB/MySQL-a", "config-header-postgres": "Podešavanja PostgreSQL-a", "config-header-sqlite": "Podešavanja SQLite-a", - "config-header-oracle": "Podešavanja Oracle-a", - "config-header-mssql": "Podešavanja Microsoft SQL Server-a", "config-invalid-db-type": "Tip baze podataka nije važeći.", "config-missing-db-name": "Morate da unesete vrednost za „{{int:config-db-name}}”.", "config-missing-db-host": "Morate da unesete vrednost za „{{int:config-db-host}}”.", - "config-missing-db-server-oracle": "Morate da unesete vrednost za „{{int:config-db-host-oracle}}”.", - "config-invalid-db-server-oracle": "TNS baza podataka „$1” nije važeća.\nKoristite ili „TNS Name” ili nisku „Easy Connect”.\n([http://docs.oracle.com/cd/E11882_01/network.112/e10836/naming.htm Oracle metodi imenovanja]).", "config-invalid-db-name": "Ime baze podataka „$1” nije važeće.\nKoristite samo ASCII slova (a-z, A-Z), brojeve (0-9) i podvlake (_).", "config-invalid-db-prefix": "Prefiks baze podataka „$1” nije važeći.\nKoristite samo ASCII slova (a-z, A-Z), brojeve (0-9), podvlake (_) i crtice (-).", "config-connection-error": "$1.\n\nProverite host, korisničko ime i lozinku, pa pokušajte ponovo.", "config-invalid-schema": "Šema za MediaWiki „$1” nije važeća.\nKoristite samo ASCII slova (a-z, A-Z), brojeve (0-9) i podvlake (_).", - "config-db-sys-create-oracle": "Instalacioni program podržava samo korišćenje SYSDBA naloga za otvaranje novog.", - "config-db-sys-user-exists-oracle": "Korisnički nalog „$1” već postoji. SYSDBA se samo može koristiti za otvaranje novog naloga!", "config-postgres-old": "Neophodan je PostgreSQL $1 ili noviji. Vi imate $2.", - "config-mssql-old": "Neophodan je Microsoft SQL Server $1 ili noviji. Vi imate $2.", "config-sqlite-name-help": "Odaberite ime koje identifikuje vaš viki.\nNe koristite razmake ili crtice.\nOvo će se koristiti za ime datoteke SQLite podataka.", "config-sqlite-mkdir-error": "Greška pri pravljenju direktorijuma sa podacima „$1”.\nProverite lokaciju, pa pokušajte ponovo.", "config-sqlite-dir-unwritable": "Nije moguće upisati u direktorijum „$1”.\nPromenite mu dozvole, tako da veb-server može da upisuje u njemu, pa pokušajte ponovo.", @@ -146,9 +131,6 @@ "config-db-web-no-create-privs": "Nalog koji ste naveli za instalaciju nema dovoljne privilegije da otvori nalog.\nNalog koji ovde navedete već mora da postoji.", "config-mysql-engine": "Mehanizam skladišta:", "config-mysql-innodb": "InnoDB (preporučeno)", - "config-mssql-auth": "Tip potvrde identiteta:", - "config-mssql-sqlauth": "SQL Server potvrda identiteta", - "config-mssql-windowsauth": "Windows potvrda identiteta", "config-site-name": "Ime vikija:", "config-site-name-help": "Ovo će se pojaviti u naslovnoj traci pregledača i na raznim drugim mestima.", "config-site-name-blank": "Unesite ime lokacije.", diff --git a/includes/installer/i18n/sv.json b/includes/installer/i18n/sv.json index 45b85457ca..2c8eb7f0ba 100644 --- a/includes/installer/i18n/sv.json +++ b/includes/installer/i18n/sv.json @@ -92,13 +92,9 @@ "config-db-type": "Databastyp:", "config-db-host": "Databasvärd:", "config-db-host-help": "Om din databasserver är på en annan server, ange då värdnamnet eller IP-adressen här.\n\nOm du använder ett delat webbhotell, bör din leverantör ge dig rätt värdnamn i deras dokumentation.\n\nOm du använder MySQL, kanske \"localhost\" inte fungerar för servernamnet. Om det inte gör det försök med \"127.0.0.1\" som den lokala IP-adressen.\n\nOm du använder PostgreSQL, lämna detta fält blankt för att ansluta via en Unix-socket.", - "config-db-host-oracle": "Databas TNS:", - "config-db-host-oracle-help": "Ange ett giltigt [http://download.oracle.com/docs/cd/B28359_01/network.111/b28317/tnsnames.htm Local Connect Name]; en tnsnames.ora-fil måste vara synlig för denna installation.
Om du använder klientbibliotek 10g eller nyare kan du också använda [http://download.oracle.com/docs/cd/E11882_01/network.112/e10836/naming.htm Easy Connect] namngivningsmetoden.", "config-db-wiki-settings": "Identifiera denna wiki", "config-db-name": "Databasnamn (inga bindestreck):", "config-db-name-help": "Välj ett namn som identifierar din wiki.\nDet bör inte innehålla mellanslag.\n\nOm du använder ett delat webbhotell kan de antingen ge dig ett särskilt databasnamn att använda eller så kan de låta dig skapa en databas via kontrollpanelen.", - "config-db-name-oracle": "Databasschema:", - "config-db-account-oracle-warn": "Det finns tre stödda scenarier för installationen av Oracle som en backend-databas:\n\nOm du vill skapa ett databaskonto som en del av installationen, ange ett konto med SYSDBA-roll som databaskonto under installationen och ange de önskade autentiseringsuppgifterna för kontot med webb-åtkomst, annars kan du antingen skapa ett konto med webb-åtkomst manuellt och ange enbart detta konto (om den har behörighet att skapa schema-objekt) eller ange två olika konton, en med create-behörighet och en begränsad för webb-åtkomst.\n\nSkript för att skapa ett konto med de korrekta behörigheterna kan hittas i \"maintenance/oracle/\"-katalogen för denna installation. Tänk på att användningen av ett begränsat konto inaktiverar all underhållsmöjlighet med standardkontot.", "config-db-install-account": "Användarkonto för installation", "config-db-username": "Databas-användarnamn:", "config-db-password": "Databas-lösenord:", @@ -117,34 +113,22 @@ "config-pg-test-error": "Kan inte ansluta till databas '''$1''': $2", "config-sqlite-dir": "SQLite data-katalog:", "config-sqlite-dir-help": "SQLite lagrar all data i en enda fil.\n\nDen katalog du anger måste vara skrivbar av webbservern under installationen.\n\nDet bör inte vara tillgänglig via webben; Det är därför vi inte lägger den där dina PHP-filer är.\n\nInstallationsprogrammet kommer att skriva en .htaccess-fil tillsammans med den, men om det misslyckas kan någon få tillgång till den råa databasen.\nVlken innehåller rå användardata (e-postadresser, hashade lösenord) samt borttagna revideringar och annan begränsad data på wiki.\n\nÖverväga att lägga databasen någon helt annanstans, till exempel i /var/lib/mediawiki/yourwiki.", - "config-oracle-def-ts": "Standardtabellutrymme (tablespace):", - "config-oracle-temp-ts": "Tillfälligt tabellutrymme (tablespace):", "config-type-mysql": "MariaDB, MySQL eller kompatibelt", - "config-type-mssql": "Microsoft SQL Server", "config-support-info": "MediaWiki stöder följande databassystem:\n\n$1\n\nOm du inte ser det databassystem som du försöker använda nedanstående, följ då instruktionerna länkade ovan för aktivera stöd för det.", "config-dbsupport-mysql": "* [{{int:version-db-mariadb-url}} MariaDB] är det primära målet för MediaWiki och stöds bäst. MediaWiki fungerar även med [{{int:version-db-mysql-url}} MySQL] och [{{int:version-db-percona-url}} Percona Server], som är kompatibla med MariaDB. ([https://www.php.net/manual/en/mysqli.installation.php Hur man kompilerar PHP med stöd för MySQL])", "config-dbsupport-postgres": "* [{{int:version-db-postgres-url}} PostgreSQL] är ett populärt databassystem med öppen källkod som ett alternativ till MySQL. ([https://www.php.net/manual/en/pgsql.installation.php Hur man kompilerar PHP med PostgreSQL-stöd])", "config-dbsupport-sqlite": "* [{{int:version-db-sqlite-url}} SQLite] är en lättviktsdatabassystem med väldigt bra stöd. ([https://www.php.net/manual/en/pdo.installation.php Hur man kompilerar PHP med SQLite stöd], använder PDO)", - "config-dbsupport-oracle": "* [{{int:version-db-oracle-url}} Oracle] är en kommersiellt databas för företag. ([https://www.php.net/manual/en/oci8.installation.php Hur man kompilerar PHP med OCI8 stöd])", - "config-dbsupport-mssql": "* [{{int:version-db-mssql-url}} Microsoft SQL Server] är en kommersiellt databas för företag för Windows. ([https://www.php.net/manual/en/sqlsrv.installation.php Hur man kompilerar PHP med SQLSRV stöd])", "config-header-mysql": "MariaDB/MySQL-inställningar", "config-header-postgres": "PostgreSQL-inställningar", "config-header-sqlite": "SQLite-inställningar", - "config-header-oracle": "Oracle-inställningar", - "config-header-mssql": "Inställningar för Microsoft SQL Server", "config-invalid-db-type": "Ogiltig databastyp", "config-missing-db-name": "Du måste ange ett värde för \"{{int:config-db-name}}\".", "config-missing-db-host": "Du måste ange ett värde för \"{{int:config-db-host}}\".", - "config-missing-db-server-oracle": "Du måste ange ett värde för \"{{int:config-db-host-oracle}}\".", - "config-invalid-db-server-oracle": "Ogiltig databas-TNS \"$1\".\nAnvända antingen \"TNS Name\" eller en \"Easy Connect\"-sträng ([http://docs.oracle.com/cd/E11882_01/network.112/e10836/naming.htm Oracles namngivningsmetoder]).", "config-invalid-db-name": "\"$1\" är ett ogiltigt databasnamn.\nAnvänd bara ASCII-bokstäver (a-z, A-Z), siffror (0-9), understreck (_) och bindestreck (-).", "config-invalid-db-prefix": "\"$1\" är ett ogiltigt databasprefix.\nAnvänd bara ASCII-bokstäver (a-z, A-Z), siffror (0-9), understreck (_) och bindestreck (-).", "config-connection-error": "$1.\n\nKontrollera värd, användarnamn och lösenord och försök igen. Om du använder \"localhost\" som databasvärden, försök använda \"127.0.0.1\" istället (eller tvärtom).", "config-invalid-schema": "\"$1\" är ett ogiltigt schema för MediaWiki.\nAnvänd bara ASCII-bokstäver (a-z, A-Z), siffror (0-9), understreck (_) och bindestreck (-).", - "config-db-sys-create-oracle": "Installationsprogrammet stöder endast användningen av ett SYSDBA-konto för att skapa ett nytt konto.", - "config-db-sys-user-exists-oracle": "Användarkontot \"$1\" finns redan. SYSDBA kan endast användas för att skapa ett nytt konto!", "config-postgres-old": "PostgreSQL $1 eller senare krävs, du har $2.", - "config-mssql-old": "Microsoft SQL-server $1 eller senare krävs. Du har $2.", "config-sqlite-name-help": "Välja ett namn som identifierar din wiki.\nAnvänd inte mellanslag eller bindestreck.\nDetta kommer att användas för SQLite-data filnamnet.", "config-sqlite-parent-unwritable-group": "Kan inte skapa datakatalogen $1, då den överordnade katalogen $2 inte är skrivbar för webbservern.\n\nInstallationen har avgjort vilken användare din webbserver körs som.\nGör $3-katalogen skrivbar för den för att fortsätta.\nPå ett Unix/Linux system gör:\n\n
cd $2\nmkdir $3\nchgrp $4 $3\nchmod g+w $3
", "config-sqlite-parent-unwritable-nogroup": "Kan inte skapa datakatalogen $1, då den överordnade katalogen $2 inte är skrivbar för webbservern.\n\nInstallationen kunde inte avgöra vilken användare din webbserver körs som.\nGör $3-katalogen skrivbar för den (och andra!) för att fortsätta.\nPå ett Unix/Linux system gör:\n\n
cd $2\nmkdir $3\nchmod g+w $3
", @@ -169,11 +153,6 @@ "config-mysql-engine": "Lagringsmotor:", "config-mysql-innodb": "InnoDB (rekommenderas)", "config-mysql-engine-help": "'''InnoDB''' är nästan alltid det bästa valet eftersom den har ett bra system för samtidiga arbeten.\n\n'''MyISAM''' kan vara snabbare i enanvändarläge eller skrivskyddade installationer.\nMyISAM-databaser tenderar att bli korrupta oftare än InnoDB-databaser.", - "config-mssql-auth": "Autentiseringstyp:", - "config-mssql-install-auth": "Välj autentiseringstypen som kommer att användas för att ansluta till databasen under installationsprocessen.\nOm du väljer \"{{int:config-mssql-windowsauth}}\", kommer autentiseringsuppgifterna för den användare webbservern körs som att användas.", - "config-mssql-web-auth": "Välj autentiseringstypen som kommer att användas för att ansluta till databasen under ordinarie drift av wikin.\nOm du väljer \"{{int:config-mssql-windowsauth}}\", kommer autentiseringsuppgifterna för den användare webbservern körs som att användas.", - "config-mssql-sqlauth": "SQL Server-autentisering", - "config-mssql-windowsauth": "Windows-autentisering", "config-site-name": "Namnet på wikin:", "config-site-name-help": "Detta visas i titelfältet i webbläsaren och på flera andra platser.", "config-site-name-blank": "Ange ett webbplatsnamn.", diff --git a/includes/installer/i18n/tcy.json b/includes/installer/i18n/tcy.json index 84b5b03ec7..64e25f5c9a 100644 --- a/includes/installer/i18n/tcy.json +++ b/includes/installer/i18n/tcy.json @@ -32,7 +32,6 @@ "config-restart": "ಸರಿ,ಕುಡ ಸುರು ಮಲ್ಪುಲೆ", "config-db-type": "ದತ್ತಾಂಶಸಂಚಯ ಮಾದರಿ:", "config-db-host-help": "ಇರೆನ ದತ್ತಸಂಚಯ ಸೇವಕ ಬೇತೆ ಸೇವಕೊ(ಸರ್ವರ್)ಡು ಇತ್ತ್ಂಡ, ಆಶ್ರಯದಾತ ಪುದರು ಇಜಿಂಡ ಐಪಿ ವಿಳಾಸ ಮುಲ್ಪ ಸೇರಾಲೆ.\nಈರ್ ಪಾಲುದ ಜಾಲ ಆಶ್ರಯ ಬಳಸುನಾಂಡಾ, ಇರೆನ ಆಶ್ರಯ ದಾತೆರ್ ಅಕಲೆನ ದಾಖಲಿಕೆಡ್ ಇರೆಗ್ ಸರಿಯಾಯಿನ ಆಶ್ರಯದಾತ ನಾಮ ಕೊರೊಡು.\nಈರ್ MySQL ಬಳಸುನಾಂಡ,\"localhost\" (\"ತಲ-ಆಶ್ರಯದಾತ\")ಬಳಕೆ ಆಶ್ರಯದಾತ ಪುದರುಗು ಬೇಲೆ ಮಲ್ಪಂದ್.ಅವು ಆಯಿಜಿಡ, ತಲ ಐಪಿ ವಿಳಾಸೊಗು \"127.0.0.1\" ಪಾಡ್ದ್ ಪ್ರಯತ್ನ ಮಲ್ಪುಲೆ.\nಈರ್ PostgreSQL ಬಳಸುನಾಂಡ, ಈ ಕ್ಷೇತ್ರೊನು ಖಾಲಿ ಬುಡುದು,ಯುನಿಕ್ಸ್ ಗುರಿತ ಮೂಲಕ ಕೂಡಾಲೆ.", - "config-db-host-oracle": "ದತ್ತಾಂಶಸಂಚಯ TNS:", "config-db-wiki-settings": "ಈ ವಿಕಿಯನ್ನು ಗುರುತಿಸಾಲೆ", "config-db-name": "ಮಾಹಿತಿಕೋಶದ ಪುದರ್(ಕೂಡುಗೆರೆ ದಾಂತೆ):", "config-db-username": "ದತ್ತಾಂಶಸಂಚಯ ಪುದರ್:", diff --git a/includes/installer/i18n/te.json b/includes/installer/i18n/te.json index e8e9a75815..c95759c63e 100644 --- a/includes/installer/i18n/te.json +++ b/includes/installer/i18n/te.json @@ -62,11 +62,9 @@ "config-db-type": "డాటాబేసు రకం:", "config-db-host": "డేటాబేసు హోస్టు:", "config-db-host-help": "మీ డేటాబేసు సర్వరు వేరే సర్వరులో ఉంటే, దాని హోస్ట్ పేరు, ఐపీ చిరునామా ఇక్కడ ఇవ్వండి.\n\nమీరు షేర్‍డ్ వెబ్ హోస్టింగును వాడుతూంటే, మీ హోస్టింగు సేవను అందించేవారు తమ డాక్యుమెంటేషనులో సరైన హోస్ట్ పేరును ఇచ్చి ఉండాలి.\n\nమీరు విండోస్ సర్వరులో స్థాపిస్తూ, MySQL వాడుతూ ఉంటే, సర్వరు పేరుగా \"localhost\" పనిచెయ్యకపోవచ్చు. అపుడు, స్థానిక ఐపీ చిరునామాగా \"127.0.0.1\" వాడండి.\n\nమీరు PostgreSQL వాడుతూ ఉంటే, Unix సాకెట్ ద్వారా కనెక్టయేందుకు ఈ ఫీల్డును ఖాళీగా వదిలెయ్యండి.", - "config-db-host-oracle": "డేటాబేసు TNS:", "config-db-wiki-settings": "ఈ వికీ గుర్తింపును ఇవ్వండి", "config-db-name": "డాటాబేసు పేరు:", "config-db-name-help": "మీ వికీని సూచించే విధంగా ఓ పేరును ఎంచుకోండి.\nదానిలో స్పేసులు ఉండరాదు.\n\nమీరు షేర్‍డ్ వెబ్ హోస్టింగును వాడుతూంటే, మీకు హోస్టింగు సేవనందించేవారు మీకు ఓ డేటాబేసు పేరును గాని, లేదా కంట్రోలు ప్యానెలు ద్వారా ఓ డేటాబేసును సృష్టించుకునే వీలునుగానీ ఇస్తారు.", - "config-db-name-oracle": "డేటాబేసు స్కీమా:", "config-db-install-account": "స్థాపనకి వాడుకరి ఖాతా", "config-db-username": "డేటాబేసు వాడుకరిపేరు:", "config-db-password": "డేటాబేసు సంకేతపదం:", @@ -84,30 +82,21 @@ "config-db-schema-help": "మామూలుగా ఈ స్కీమా సరిపోతుంది.\nఅవసరమని మీకు తెలిస్తేనే మార్చండి.", "config-pg-test-error": "డేటాబేసు $1 కి కనెక్టు కాలేకపోయాం: $2", "config-sqlite-dir": "SQLite డేటా డైరెక్టరీ:", - "config-oracle-def-ts": "డిఫాల్టు టేబుల్‍స్పేసు:", - "config-oracle-temp-ts": "తాత్కాలిక టేబుల్‍స్పేసు:", "config-type-mysql": "MySQL (లేదా సరిపోయేది)", - "config-type-mssql": "Microsoft SQL Server", "config-support-info": "MediaWiki కింది డేటాబేసు వ్యవస్థలకు అనుకూలిస్తుంది:\n\n$1\n\nమీరు వాడదలచిన డేటాబేసు వ్యవస్ కింది జాబితాలో లేకపోతే, పైన లింకు ద్వారా ఇచ్చిన సూచనలను పాటించి, అనుకూలతలను సాధించండి.", "config-dbsupport-postgres": "* MySQL కు ప్రత్యామ్నాయంగా [{{int:version-db-postgres-url}} PostgreSQL] ప్రజామోదం పొందిన ఓపెన్‍సోర్సు డేటాబేసు వ్యవస్థ. దానిలో చిన్న చితకా లోపాలుండే అవకాశం ఉంది. అందుచేత దాన్ని ఉత్పాదక రంగంలో వాడవచ్చని చెప్పలేం. ([https://www.php.net/manual/en/pgsql.installation.php How to compile PHP with PostgreSQL support])", "config-dbsupport-sqlite": "* [{{int:version-db-sqlite-url}} SQLite] ఓ తేలికైన డేటాబేసు వ్యవస్థ. దానికి చక్కటి అనుకూలతలున్నాయి. ([http://www.php.net/manual/en/pdo.installation.php How to compile PHP with SQLite support], uses PDO)", - "config-dbsupport-oracle": "* [{{int:version-db-oracle-url}} Oracle] ఒక వాణిజ్యపరంగా సంస్థాగతంగా వాడదగ్గ డేటాబేసు. ([http://www.php.net/manual/en/oci8.installation.php How to compile PHP with OCI8 support])", "config-header-mysql": "MySQL అమరికలు", "config-header-postgres": "PostgreSQL అమరికలు", "config-header-sqlite": "SQLite అమరికలు", - "config-header-oracle": "Oracle అమరికలు", - "config-header-mssql": "Microsoft SQL Server అమరికలు", "config-invalid-db-type": "తప్పుడు డాటాబేసు రకం", "config-missing-db-name": "\"{{int:config-db-name}}\" ను తప్పకుండా ఇవ్వాలి", "config-missing-db-host": "\"{{int:config-db-host}}\" ను తప్పకుండా ఇవ్వాలి", - "config-missing-db-server-oracle": "\"{{int:config-db-host-oracle}}\" ను తప్పకుండా ఇవ్వాలి", "config-invalid-db-name": "డేటాబేసు పేరు సరైనది కాదు \"$1\".\nASCII అక్షరాలు (a-z, A-Z), అంకెలు (0-9), క్రీగీత (_) and హైఫన్ (-) లను మాత్రమే వాడాలి.", "config-invalid-db-prefix": "డేటాబేసు ఆదిపదం (ప్రిఫిక్స్) సరైనది కాదు \"$1\".\nASCII అక్షరాలు (a-z, A-Z), అంకెలు (0-9), క్రీగీత (_) and హైఫన్ (-) లను మాత్రమే వాడాలి.", "config-connection-error": "$1.\n\nక్రింది హోస్టు, వాడుకరిపేరు మరియు సంకేతపదాలను ఒకసారి సరిచూసుకుని అప్పుడు ప్రయత్నించండి.", "config-invalid-schema": "\"$1\" MediaWiki కోసం చెల్లని స్కీమా.\nASCII అక్షరాలు (a-z, A-Z), అంకెలు (0-9) క్రీగీత (_) లను మాత్రమే వాడాలి.", - "config-db-sys-user-exists-oracle": "వాడుకరి ఖాతా \"$1\" ఈసరికే ఉంది. కొత్త ఖాతాను సృష్టించేందుకు SYSDBA ను మాత్రమే వాడాలి!", "config-postgres-old": "PostgreSQL $1 గానీ ఆ తరువాతిది గానీ అవసరం. మీకు $2 ఉంది.", - "config-mssql-old": "మైక్రోసాఫ్ట్ SQL సర్వర్ $1 లేదీ దాని తరువాతి వర్షన్ ఉండాలి. మీ దగ్గర $2 ఉంది.", "config-sqlite-name-help": "మీ వికీని గుర్తించే పేరు ఒకదాన్ని ఎంచుకోండి.\nస్పేసులు గానీ, హైఫన్‍లు గానీ వాడకండి.\nదాన్ని SQLite డేటాఫైలు పేరు కోసంవాడతాం.", "config-sqlite-mkdir-error": "డేటా డైరెక్టరీని సృష్టించడంలో లోపం \"$1\".\nస్థానాన్ని సరిచూసి మళ్ళీ ప్రయత్నించండి.", "config-sqlite-connection-error": "$1.\n\nకింద ఉన్న డేటా డైరెక్టరీ, డేటాబేసు పేరును సరిచూసి మళ్ళీ ప్రయత్నించండి.", @@ -124,9 +113,6 @@ "config-db-web-no-create-privs": "స్థాపన కోసం మీరిచ్చిన ఖాతాకు ఓ కొత్త ఖాతాను సృష్టించే అనుమతులు లేవు.\nఇక్కడ మీరిచ్చే ఖాతా తప్పనిసరిగా ఈసరికే ఉనికిలో ఉండాలి.", "config-mysql-engine": "స్టోరేజీ ఇంజను:", "config-mysql-innodb": "InnoDB", - "config-mssql-auth": "ఆథెంటికేషన్ రకం:", - "config-mssql-sqlauth": "SQL Server ఆథెంటికేషన్", - "config-mssql-windowsauth": "విండోస్ ఆథెంటికేషన్", "config-site-name": "వికీ పేరు:", "config-site-name-help": "ఇది బ్రౌజరు టిటిలుబారు లోను, అనేక ఇతర చోట్లా కనిపిస్తుంది.", "config-site-name-blank": "ఓ సైటు పేరును ఇవ్వండి.", diff --git a/includes/installer/i18n/th.json b/includes/installer/i18n/th.json index 38aafbea5a..4512740eea 100644 --- a/includes/installer/i18n/th.json +++ b/includes/installer/i18n/th.json @@ -84,13 +84,9 @@ "config-db-type": "ชนิดฐานข้อมูล:", "config-db-host": "โฮสต์ฐานข้อมูล:", "config-db-host-help": "ถ้าเซิร์ฟเวอร์ฐานข้อมูลของคุณอยู่บนเซิร์ฟเวอร์อื่น ให้ป้อนชื่อโฮสต์หรือที่อยู่ IP ที่นี่\n\nถ้าคุณกำลังใช้งานโฮสต์เว็บที่ใช้ร่วมกัน ผู้ให้บริการโฮสต์ควรให้ชื่อโฮสต์ที่ถูกต้องแก่คุณในเอกสารคู่มือ\n\nถ้าคุณกำลังติดตั้งบนเซิร์ฟเวอร์ Windows และกำลังใช้ MySQL การใช้ \"localhost\" อาจไม่สามารถใช้ได้สำหรับชื่อเซิร์ฟเวอร์ ถ้าไม่สามารถใช้ได้ ให้ลองใช้ \"127.0.0.1\" สำหรับที่อยู่ IP เฉพาะที่", - "config-db-host-oracle": "TNS ฐานข้อมูล:", - "config-db-host-oracle-help": "ป้อน [http://download.oracle.com/docs/cd/B28359_01/network.111/b28317/tnsnames.htm Local Connect Name] ที่ถูกต้อง; ไฟล์ tnsnames.ora ต้องสามารถมองเห็นได้โดยการติดตัั้งนี้
ถ้าคุณกำลังใช้ไลบรารีไคลเอนต์ 10g หรือใหม่กว่า คุณก็สามารถใช้วิธีการตั้งชื่อแบบ [http://download.oracle.com/docs/cd/E11882_01/network.112/e10836/naming.htm Easy Connect] ได้เช่นกัน", "config-db-wiki-settings": "ระบุวิกินี้", "config-db-name": "ชื่อฐานข้อมูล:", "config-db-name-help": "เลือกชื่อที่ระบุวิกิของคุณ\nชื่อไม่ควรมีช่องว่าง\n\nถ้าคุณกำลังใช้โฮสต์เว็บที่ใช้ร่วมกัน ผู้ให้บริการโฮสต์ของคุณจะระบุชื่อฐานข้อมูลให้คุณ หรือให้คุณสร้างฐานข้อมูลโดยใช้แผงควบคุม", - "config-db-name-oracle": "แบบแผนฐานข้อมูล:", - "config-db-account-oracle-warn": "มีสถานการณ์สมมติสามสถานการณ์ที่สนับสนุนสำหรับการติดตั้ง Oracle เป็นแบ็กเอนด์ฐานข้อมูล:\n\nถ้าคุณต้องการสร้างบัญชีฐานข้อมูลเป็นส่วนหนึ่งของกระบวนการติดตั้ง โปรดจัดหาบัญชีที่มีบทบาท SYSDBA เป็นบัญชีฐานข้อมูลสำหรับการติดตั้งและระบุข้อมูลประจำตัวที่ต้องการสำหรับบัญชีการเข้าถึงเว็บ หรือคุณสามารถสร้างบัญชีการเข้าถึงเว็บด้วยตนเองและจัดหาเฉพาะบัญชีนั้น (ถ้ามีสิทธิ์ที่ต้องการในการสร้างวัตถุแบบแผน) หรือจัดหาบัญชีสองบัญชี โดยบัญชีหนึ่งใช้สร้างสิทธิ์ และบัญชีที่จำกัดอีกบัญชีหนึ่งสำหรับการเข้าถึงเว็บ\n\nสคริปต์ที่ใช้สำหรับการสร้างบัญชีพร้อมสิทธิ์ที่ต้องการสามารถพบได้ในไดเรกทอรี \"maintenance/oracle/\" ของการติดตั้งนี้\nอย่าลืมว่าการใช้บัญชีที่จำกัดจะเป็นการปิดใช้งานความสามารถในการบำรุงรักษาทั้งหมดด้วยบัญชีเริ่มต้น", "config-db-install-account": "บัญชีผู้ใช้สำหรับการติดตั้ง", "config-db-username": "ชื่อผู้ใช้ฐานข้อมูล:", "config-db-password": "รหัสผ่านฐานข้อมูล:", @@ -109,34 +105,22 @@ "config-pg-test-error": "ไม่สามารถเชื่อมต่อไปยังฐานข้อมูล $1: $2", "config-sqlite-dir": "ไดเรกทอรีข้อมูล SQLite:", "config-sqlite-dir-help": "SQLite จัดเก็บข้อมูลทั้งหมดในไฟล์เดียว\n\nไดเรกทอรีที่คุณระบุจะต้องสามารถเขียนได้โดยเว็บเซิร์ฟเวอร์ระหว่างการติดตั้ง\n\nไดเรกทอรีดังกล่าวไม่ควรสามารถเข้าถึงได้ผ่านเว็บ นี่คือเหตุผลที่เราไม่นำไฟล์ข้อมูลดังกล่าวไปไว้ในตำแหน่งที่มีไฟล์ PHP ของคุณอยู่\n\nโปรแกรมติดตั้งจะเขียนไฟล์ .htaccess ไปพร้อมกับไฟล์ข้อมูลดังกล่าว แต่ถ้าเกิดความล้มเหลว ทุกคนจะสามารถเข้าถึงฐานข้อมูลดิบของคุณได้\nซึ่งรวมถึงข้อมูลผู้ใช้ดิบ (ที่อยู่อีเมล ข้อมูลแฮช) รวมถึงรุ่นปรับปรุงที่ถูกลบไปแล้ว และข้อมูลที่ถูกจำกัดอื่นๆ บนวิกิ\n\nให้พิจารณานำฐานข้อมูลไปไว้ในตำแหน่งอื่น ตัวอย่างเช่น ใน /var/lib/mediawiki/yourwiki", - "config-oracle-def-ts": "พื้นที่ตารางเริ่มต้น:", - "config-oracle-temp-ts": "พื้นที่ตารางชั่วคราว:", "config-type-mysql": "MySQL (หรือที่เข้ากันได้)", - "config-type-mssql": "Microsoft SQL Server", "config-support-info": "MediaWiki สนับสนุนระบบฐานข้อมูลต่อไปนี้:\n\n$1\n\nถ้าคุณไม่พบระบบฐานข้อมูลที่คุณกำลังพยายามใช้ในรายการด้านล่างนี้ ให้ทำตามคำแนะนำที่เชื่อมโยงด้านบนเพื่อเปิดใช้งานการสนับสนุน", "config-dbsupport-mysql": "* [{{int:version-db-mysql-url}} MySQL] คือเป้าหมายหลักสำหรับ MediaWiki และได้รับการสนับสนุนดีที่สุด MediaWiki ยังคงสามารถใช้ได้ร่วมกับ [{{int:version-db-mariadb-url}} MariaDB] และ [{{int:version-db-percona-url}} Percona Server] ซึ่งเข้ากันได้กับ MySQL ([https://www.php.net/manual/en/mysqli.installation.php วิธีการคอมไพล์ PHP ด้วยการสนับสนุน MySQL])", "config-dbsupport-postgres": "* [{{int:version-db-postgres-url}} PostgreSQL] คือระบบฐานข้อมูลแบบโอเพนซอร์สที่ได้รับความนิยมสูงที่สามารถใช้แทน MySQL ได้ ([https://www.php.net/manual/en/pgsql.installation.php วิธีการคอมไพล์ PHP ด้วยการสนับสนุน PostgreSQL])", "config-dbsupport-sqlite": "* [{{int:version-db-sqlite-url}} SQLite] คือระบบฐานข้อมูลขนาดเล็กที่ได้รับการสนับสนุนดีมาก ([http://www.php.net/manual/en/pdo.installation.php วิธีการคอมไพล์ PHP ด้วยการสนับสนุน SQLite], ใช้ PDO)", - "config-dbsupport-oracle": "* [{{int:version-db-oracle-url}} Oracle] คือฐานข้อมูลสำหรับองค์กรพาณิชย์ ([http://www.php.net/manual/en/oci8.installation.php วิธีการคอมไพล์ PHP ด้วยการสนับสนุน OCI8])", - "config-dbsupport-mssql": "* [{{int:version-db-mssql-url}} Microsoft SQL Server] คือฐานข้อมูลสำหรับองค์กรพาณิชย์สำหรับ Windows. ([https://www.php.net/manual/en/sqlsrv.installation.php วิธีการคอมไพล์ PHP ด้วยการสนับสนุน SQLSRV])", "config-header-mysql": "การตั้งค่า MySQL", "config-header-postgres": "การตั้งค่า PostgreSQL", "config-header-sqlite": "การตั้งค่า SQLite", - "config-header-oracle": "การตั้งค่า Oracle", - "config-header-mssql": "การตั้งค่า Microsoft SQL Server", "config-invalid-db-type": "ชนิดฐานข้อมูลไม่ถูกต้อง", "config-missing-db-name": "คุณต้องป้อนค่าสำหรับ \"{{int:config-db-name}}\"", "config-missing-db-host": "คุณต้องป้อนค่าสำหรับ \"{{int:config-db-host}}\"", - "config-missing-db-server-oracle": "คุณต้องป้อนค่าสำหรับ \"{{int:config-db-host-oracle}}\"", - "config-invalid-db-server-oracle": "TNS ฐานข้อมูล \"$1\" ไม่ถูกต้อง\nให้ใช้สตริง \"ชื่อ TNS\" หรือ \"Easy Connect\"\n ([http://docs.oracle.com/cd/E11882_01/network.112/e10836/naming.htm วิธีการตั้งชื่อของ Oracle])", "config-invalid-db-name": "ชื่อฐานข้อมูล \"$1\" ไม่ถูกต้อง\nให้ใช้เฉพาะอักษร ASCII (a-z, A-Z) ตัวเลข (0-9) ขีดล่าง (_) และยัติภังค์ (-)", "config-invalid-db-prefix": "คำนำหน้าฐานข้อมูล \"$1\" ไม่ถูกต้อง\nให้ใช้เฉพาะอักษร ASCII (a-z, A-Z) ตัวเลข (0-9) ขีดล่าง (_) และยัติภังค์ (-)", "config-connection-error": "$1\n\nตรวจสอบโฮสต์ ชื่อผู้ใช้และรหัสผ่าน และลองอีกครั้ง", "config-invalid-schema": "แบบแผนสำหรับ MediaWiki \"$1\" ไม่ถูกต้อง\nให้ใช้เฉพาะอักษร ASCII (a-z, A-Z) ตัวเลข (0-9) และขีดล่าง (_)", - "config-db-sys-create-oracle": "โปรแกรมติดตั้งสนับสนุนเฉพาะการใช้บัญชี SYSDBA สำหรับการสร้างบัญชีใหม่เท่านั้น", - "config-db-sys-user-exists-oracle": "มีบัญชีผู้ใช้ \"$1\" อยู่แล้ว คุณสามารถใช้เฉพาะ SYSDBA สำหรับการสร้างบัญชีใหม่ได้เท่านั้น!", "config-postgres-old": "จำเป็นต้องใช้ PostgreSQL $1 หรือสูงกว่า คุณมี $2", - "config-mssql-old": "จำเป็นต้องใช้ Microsoft SQL Server $1 หรือสูงกว่า คุณมี $2.", "config-sqlite-name-help": "เลือกชื่อที่จะระบุวิกิของคุณ\nอย่าใช้ช่องว่างหรือยัติภังค์\nชื่อนี้จะถูกใช้สำหรับชื่อไฟล์ข้อมูล SQLite", "config-sqlite-parent-unwritable-group": "ไม่สามารถสร้างไดเรกทอรีข้อมูล $1 ได้ เนื่องจากไดเรกทอรีหลัก $2 ไม่สามารถเขียนได้โดยเว็บเซิร์ฟเวอร์\n\nโปรแกรมติดตั้งได้ทำการตรวจสอบแล้วว่าเว็บเซิร์ฟเวอร์ของคุณกำลังทำงานในฐานะผู้ใช้ใด\nทำให้ไดเรกทอรี $3 สามารถเขียนโดยผู้ใช้ดังกล่าวได้เพื่อดำเนินการต่อ\nถ้าคุณใช้ระบบ Unix/Linux ให้่ทำเช่นนี้:\n\n
cd $2\nmkdir $3\nchgrp $4 $3\nchmod g+w $3
", "config-sqlite-parent-unwritable-nogroup": "ไม่สามารถสร้างไดเรกทอรีข้อมูล $1 ได้ เนื่องจากไดเรกทอรีหลัก $2 ไม่สามารถเขียนได้โดยเว็บเซิร์ฟเวอร์\n\nโปรแกรมติดตั้งไม่สามารถทำการตรวจสอบได้ว่าเว็บเซิร์ฟเวอร์ของคุณกำลังทำงานในฐานะผู้ใช้ใด\nทำให้ไดเรกทอรี $3 สามารถเขียนโดยส่วนกลาง (ุผู้ใช้ดังกล่าว รวมถึงคนอื่นๆ ด้วย!) ได้เพื่อดำเนินการต่อ\nถ้าคุณใช้ระบบ Unix/Linux ให้่ทำเช่นนี้:\n\n
cd $2\nmkdir $3\nchmod a+w $3
", @@ -160,11 +144,6 @@ "config-mysql-engine": "กลไกที่จัดเก็บข้อมูล:", "config-mysql-innodb": "InnoDB", "config-mysql-engine-help": "InnoDB เป็นตัวเลือกที่เกือบดีที่สุดเสมอ เนื่องจากมีการสนับสนุนกระบวนการทำงานพร้อมกัน\n\nMyISAM อาจทำงานได้เร็วกว่าในการติดตั้งแบบผู้ใช้คนเดียวหรือแบบอ่านอย่างเดียว\nฐานข้อมูล MyISAM มักจะได้รับความเสียหายบ่อยมากกว่าฐานข้อมูล InnoDB", - "config-mssql-auth": "ชนิดการยืนยันตัวตน:", - "config-mssql-install-auth": "เลือกชนิดการยืนยันตัวตนที่จะใช้เชื่อมต่อไปยังฐานข้อมูลในระหว่างกระบวนการติดตั้ง\nถ้าคุณเลือก \"{{int:config-mssql-windowsauth}}\" ข้อมูลประจำตัวที่ระบุว่าเว็บเซิร์ฟเวอร์กำลังทำงานในฐานะผู้ใช้ใดจะถูกใช้", - "config-mssql-web-auth": "เลือกชนิดการยืนยันตัวตนที่จะใช้เชื่อมต่อไปยังฐานข้อมูลในระหว่างการใช้งานวิกิตามปกติ\nถ้าคุณเลือก \"{{int:config-mssql-windowsauth}}\" ข้อมูลประจำตัวที่ระบุว่าเว็บเซิร์ฟเวอร์กำลังทำงานในฐานะผู้ใช้ใดจะถูกใช้", - "config-mssql-sqlauth": "การยืนยันตัวตนโดย SQL Server", - "config-mssql-windowsauth": "การยืนยันตัวตนโดย Windows", "config-site-name": "ชื่อของวิกิ:", "config-site-name-help": "ชื่อนี้จะปรากฏในแถบชื่อเรื่องของเบราว์เซอร์และในที่อื่นๆ อีกหลายแห่ง", "config-site-name-blank": "ป้อนชื่อไซต์", diff --git a/includes/installer/i18n/tl.json b/includes/installer/i18n/tl.json index 6d63409992..1c095f7a17 100644 --- a/includes/installer/i18n/tl.json +++ b/includes/installer/i18n/tl.json @@ -81,13 +81,9 @@ "config-db-type": "Uri ng kalipunan ng datos:", "config-db-host": "Tagapagpasinaya ng kalipunan ng datos:", "config-db-host-help": "Kung ang iyong tagapaghain ng kalipunan ng dato ay nasa ibabaw ng isang ibang tagapaghain, ipasok ang pangalan ng tagapagpasinaya o tirahan ng IP dito.\n\nKung gumagamit ka ng pinagsasaluhang pagpapasinaya ng sangkasaputan, dapat ibigay sa iyo ng iyong tagapagbigay ng pagpapasinaya ang tamang pangalan ng tagapagpasinaya sa loob ng kanilang kasulatan.\n\nKapag nagluluklok ka sa ibabaw ng isang tagapaghain ng Windows at gumagamit ng MySQL, maaaring hindi gumana ang paggamit ng \"localhost\" para sa pangalan ng tagapaghain. Kung hindi, subukan ang \"127.0.0.1\" para sa katutubong tirahan ng IP.\n\nKapag gumagamit ka ng PostgreSQL, iwanang walang laman ang hanay na ito upang kumabit sa pamamagitan ng bokilya ng Unix.", - "config-db-host-oracle": "TNS ng kalipunan ng dato:", - "config-db-host-oracle-help": "Magpasok ng isang katanggap-tanggap na [http://download.oracle.com/docs/cd/B28359_01/network.111/b28317/tnsnames.htm Katutubong Pangalan ng Pagkabit]; dapat na nakikita ang isang talaksan ng tnsnames.ora sa pagluluklok na ito.
Kung gumagamit ka ng mga aklatan ng kliyente na 10g o mas bago, maaari mo ring gamitin ang pamamaraan ng pagpapangalan ng [http://download.oracle.com/docs/cd/E11882_01/network.112/e10836/naming.htm Maginhawang Pagkabit].", "config-db-wiki-settings": "Kilalanin ang wiking ito", "config-db-name": "Pangalan ng kalipunan ng dato:", "config-db-name-help": "Pumili ng isang pangalan na pangkilala sa wiki mo.\nHindi ito dapat maglaman ng mga patlang.\n\nKung gumagamit ka ng pinagsasaluhang pagpapasinaya ng sangkasaputan, ang iyong tagapagbigay ng pagpapasinaya ay maaaring bigyan ka ng isang tiyak na pangalan ng kalipunan ng datong gagamitin o papayagan kang lumikha ng mga kalipunan ng dato sa pamamagitan ng isang entrepanyong pantaban.", - "config-db-name-oracle": "Balangkas ng kalipunan ng dato:", - "config-db-account-oracle-warn": "Mayroong tatlong suportadong senaryo para sa pag-install ng Oracle bilang database backend:\n\nKung nais mong lumikha ng account ng database bilang bahagi ng proseso ng pag-install, paki magbigay ng isang account na mayroong gampanin ng SYSDBA bilang account ng database para sa pag-install at tukuyin ang ninanais na mga kredensiyal para sa account ng web-access, o di kaya ay maaaring gawing manu-mano ang paglikha ng account ng web access at ibigay lamang ang account na iyan (kung mayroong ito ng kinakailangang mga pahintulot upang malikha ang mga schema object) o magbigay ng dalawang magkaibang mga account, isang mayroong pribilehiyo ng paglikha at isang may pagbabawal para sa web access.\n\nAng script sa paglikha ng isang account na mayroon ng kinakailangang mga pribilehiyo ay matatagpuan sa loob ng directory na \"maintenance/oracle/\" ng pag-install na ito. Pakatandaan na ang paggamit ng isang account na may pagbabawal ay hindi magpapagana sa lahat ng mga kakayahang pampananatili kasama ang nakatakdang account.", "config-db-install-account": "Account ng tagagamit para sa pagluluklok", "config-db-username": "Pangalang pangtagagamit ng kalipunan ng dato:", "config-db-password": "Password sa kalipunan ng dato:", @@ -106,32 +102,23 @@ "config-pg-test-error": "Hindi makakabit sa kalipunan ng dato na '''$1''': $2", "config-sqlite-dir": "Direktoryo ng dato ng SQLite:", "config-sqlite-dir-help": "Iniimbak ng SQLite ang lahat ng dato sa loob ng isang nag-iisang file.\n\nAng ibibigay mong directory ay dapat na maging masusulatan ng tagapaghain ng kasaputan habang nag-i-install.\n\n'''Hindi''' ito dapat na mapuntahan sa pamamagitan ng web server, ito ang dahilan kung bakit hindi namin ito inilalagay sa kung nasaan ang iyong mga file ng PHP.\n\nAng installer ay magsusulat ng isang file na .htaccess na kasama ito, subalit kapag nabigo iyon mayroong isang tao na maaaring makakuha ng pagka nakakapunta sa iyong hilaw na database.\nKasama riyan ang hilaw na dato ng tagagamit (mga email address, pinaghalong mga password) pati na ang nabura nang mga pagbabago at iba pang may pagbabawal na dato ng wiki.\n\nIsaalang-alang ang paglalagay na magkakasama ang database sa ibang lugar, halimbawa na ang sa loob ng /var/lib/mediawiki/yourwiki.", - "config-oracle-def-ts": "Likas na nakatakdang puwang ng talahanayan:", - "config-oracle-temp-ts": "Pansamantalang puwang ng talahanayan:", "config-type-mysql": "MariaDB, MySQL, o katugma", "config-type-postgres": "PostgreSQL", "config-type-sqlite": "SQLite", - "config-type-oracle": "Oracle", "config-support-info": "Sinusuportahan ng MediaWiki ang sumusunod na mga sistema ng kalipunan ng dato:\n\n$1\n\nKung hindi mo makita ang sistema ng kalipunan ng dato na sinusubukan mong gamitin na nakatala sa ibaba, kung gayon ay sundi ang mga tagubilin na nakakawing sa itaas upang mapagana ang suporta,", "config-dbsupport-mysql": "* [{{int:version-db-mysql-url}} MySQL] ang pangunahing puntirya para sa MediaWiki at ang pinaka sinusuportahan. Gumagana rin ang MediaWiki [{{int:version-db-mariadb-url}} MariaDB] at sa [{{int:version-db-percona-url}} Percona Server], na tugma sa MySQL. ([https://www.php.net/manual/en/mysql.installation.php Paano magtipon ng PHP na mayroong suporta ng MySQL])", "config-dbsupport-postgres": "* Ang [{{int:version-db-postgres-url}} PostgreSQL] ay isang bantog na sistema ng kalipunan ng dato na bukas ang pinagmulan na panghalili sa MySQL. ([https://www.php.net/manual/en/pgsql.installation.php Paano magtipon ng PHP na mayroong suporta ng PostgreSQL]).", "config-dbsupport-sqlite": "* Ang [{{int:version-db-sqlite-url}} SQLite] ay isang magaan ang timbang na sistema ng kalipunan ng dato na sinusuportahan nang napaka mainam. ([http://www.php.net/manual/en/pdo.installation.php Paano magtipon ng PHP na mayroong suporta ng SQLite], gumagamit ng PDO)", - "config-dbsupport-oracle": "* Ang [{{int:version-db-oracle-url}} Oracle] ay isang kalipunan ng dato ng kasigasigang pangkalakal. ([http://www.php.net/manual/en/oci8.installation.php Paano magtipunan ng PHP na mayroong suporta ng OCI8])", "config-header-mysql": "Mga katakdaan ng MariaDB/MySQL", "config-header-postgres": "Mga katakdaan ng PostgreSQL", "config-header-sqlite": "Mga katakdaan ng SQLite", - "config-header-oracle": "Mga katakdaan ng Oracle", "config-invalid-db-type": "Hindi tanggap na uri ng kalipunan ng dato", "config-missing-db-name": "Dapat kang magpasok ng isang halaga para sa \"{{int:config-db-name}}\".", "config-missing-db-host": "Dapat kang magpasok ng isang halaga para sa \"{{int:config-db-host}}\".", - "config-missing-db-server-oracle": "Dapat kang magpasok ng isang halaga para sa \"{{int:config-db-host-oracle}}\".", - "config-invalid-db-server-oracle": "Hindi katanggap-tanggap na pangalan ng TNSng kalipunan ng dato na \"$1\".\nGumamit ng kahit na \"TNS Name\" o \"Easy Connect\" na tali ([http://docs.oracle.com/cd/E11882_01/network.112/e10836/naming.htm Paraan ng Pagpapangalan ng Oracle]).", "config-invalid-db-name": "Hindi tanggap na pangalan ng kalipunan ng dato na \"$1\".\nGumamit lamang ng mga titik ng ASCII (a-z, A-Z), mga bilang (0-9), mga salungguhit (_) at mga gitling (-).", "config-invalid-db-prefix": "Hindi tanggap na unlapi ng kalipunan ng dato na \"$1\".\nGamitin lamang ang mga titik na ASCII (a-z, A-Z), mga bilang (0-9), mga salungguhit (_) at mga gitling (-).", "config-connection-error": "$1.\n\nSuriin ang host, pangalan at password na nasa ibaba at subukan ulit.", "config-invalid-schema": "Hindi katanggap-tanggap na panukala para sa \"$1\" ng MediaWiki.\nGumamit lamang ng mga titik ng ASCII (a-z, A-Z), mga bilang (0-9), at mga salungguhit (_).", - "config-db-sys-create-oracle": "Ang installer ay sumusuporta lamang sa paggamit ng isang account ng SYSDBA para sa paglikha ng isang bagong account.", - "config-db-sys-user-exists-oracle": "Umiiral na ang account ng tagagamit na \"$1\". Magagamit lamang ang SYSDBA para sa paglikha ng isang bagong account!", "config-postgres-old": "Kailangan ang PostgreSQL $1 o mas bago, mayroon kang $2.", "config-sqlite-name-help": "Pumili ng isang pangalan na pangkilala na wiki mo.\nHuwag gumamit ng mga puwang o mga gitling.\nGagamitin ito para sa pangalan ng talaksan ng dato ng SQLite.", "config-sqlite-parent-unwritable-group": "Hindi malikha ang direktoryo ng dato na $1, sapagkat ang magulang na direktoryong $2 ay hindi masulatan ng tagapaghain ng kasaputan.\n\nNapag-alaman ng tagapagluklok kung sinong tagagamit ang kinatatakbuhan ng iyong tagapaghain ng kasaputan.\nGawing nasusulatan nito ang $3 ng direktoryo upang makapagpatuloy.\nIto ang gawin sa ibabaw ng isang sistema ng Unix/Linux:\n\n
cd $2\nmkdir $3\nchgrp $4 $3\nchmod g+w $3
", diff --git a/includes/installer/i18n/tr.json b/includes/installer/i18n/tr.json index e2a46836f8..04078d618d 100644 --- a/includes/installer/i18n/tr.json +++ b/includes/installer/i18n/tr.json @@ -97,11 +97,9 @@ "config-db-type": "Veritabanı tipi:", "config-db-host": "Veritabanı sunucusu:", "config-db-host-help": "Veritabanı sunucunuz farklı bir sunucu üzerinde ise, ana bilgisayar adını veya IP adresini buraya girin.\n\nPaylaşılan ağ barındırma hizmeti kullanıyorsanız, barındırma sağlayıcınız size doğru bir ana bilgisayar adını kendi belgelerinde vermiştir.\n\nEğer MySQL kullanan bir Windows sunucusuna yükleme yapıyorsanız, sunucu adı olarak \"localhost\" kullanırsanız çalışmayabilir. Çalışmazsa, yerel IP adresi için \"127.0.0.1\" deneyin.\n\nPostgreSQL kullanıyorsanız, bu alanı bir Unix soketi ile bağlanmak için boş bırakın.", - "config-db-host-oracle": "Veritabanı TNS:", "config-db-wiki-settings": "Bu wikiyi tanımla", "config-db-name": "Veritabanı adı (tiresiz):", "config-db-name-help": "Vikinizi tanımlayan bir isim seçin.\nBoşluk karakteri içermemelidir.\n\nPaylaşılan bir web hosting servisi kullanıyorsanız, tedarikçiniz size ya kullanmanız için bir veritabanı ismi verecek ya da bir kontrol paneli vasıtasıyla sizin oluşturmanıza izin verecektir.", - "config-db-name-oracle": "Veritabanı şeması:", "config-db-install-account": "Yükleme için kullanıcı hesabı", "config-db-username": "Veritabanı kullanıcı adı:", "config-db-password": "Veritabanı parolası:", @@ -117,26 +115,17 @@ "config-db-schema-help": "Bu şema yeterli olacaktır.\nEğer gerçekten ihtiyaç duyarsanız değiştirin.", "config-pg-test-error": "Veritabanıyla bağlantı kurulamıyor ''' $1 ''':$2", "config-sqlite-dir": "SQLite veri dizini", - "config-oracle-def-ts": "Varsayılan tablo alanı:", - "config-oracle-temp-ts": "Geçici tablo alanı:", "config-type-mysql": "MySQL (veya uyumlu)", - "config-type-mssql": "Microsoft SQL Server", "config-header-mysql": "MySQL ayarları", "config-header-postgres": "PostgreSQL ayarları", "config-header-sqlite": "SQLite ayarları", - "config-header-oracle": "Oracle ayarları", - "config-header-mssql": "Microsoft SQL Server ayarları", "config-invalid-db-type": "Geçersiz veritabanı türü", "config-missing-db-name": "\"Veritabanı adı\" için bir değer girmelisiniz", "config-missing-db-host": "\"{{int:config-db-host}}\" için bir değer girmelisiniz.", - "config-missing-db-server-oracle": "\"{{int:config-db-host-oracle}}\" için bir değer girmelisiniz", "config-invalid-db-name": "Geçersiz veritabanı adı \" $1 \".\nSadece ASCII harf (a-z, A-Z), rakamların (0-9), alt çizgi (_) ve tire (-) kullanın.", "config-connection-error": "$1.\n\nSunucuyu kontrol edin, kullanıcı adı ve parolayı denetleyin ve yeniden deneyin.", "config-invalid-schema": "Geçersiz şema MediaWiki için \" $1 \".\nYalnızca ASCII harf (a-z, A-Z), rakamların (0-9) ve alt çizgi (_) kullanın.", - "config-db-sys-create-oracle": "Kurulum yeni hesap oluştururken sadece SYSDBA hesabı kullanımını destekliyor.", - "config-db-sys-user-exists-oracle": "Kullanıcı hesabı \" $1 \" zaten var. SYSDBA sadece yeni bir hesap oluşturmak için kullanılabilir.", "config-postgres-old": "PostgreSQL $1 veya daha yenisi gerekir. Sende $2 sürümü var.", - "config-mssql-old": "Microsoft SQL Server $1 veya daha yükseği gerekli. Sizdeki sürüm: $2.", "config-sqlite-name-help": "Wiki'nizi tanımlayan bir ad seçin.\nBoşluk ya da tire kullanmayın.\nBu isim SQLite veri dosyası için kullanılacaktır.", "config-sqlite-mkdir-error": "Veri dizini oluşturulurken bir hata oluştu \" $1 \".\nKonumu denetleyin ve yeniden deneyin.", "config-sqlite-dir-unwritable": "Bu dizine yazılamadı: \"$1\"\nİzinleri değiştirerek tekrar deneyiniz.", @@ -155,10 +144,6 @@ "config-db-web-no-create-privs": "Kurulum için belirlediğiniz hesap, hesap yaratımı için gerekli izinlere sahip değil.\nBurada belirttiğiniz hesap halihazırda var olmalı.", "config-mysql-engine": "Depolama motoru:", "config-mysql-innodb": "InnoDB (önerilen)", - "config-mssql-auth": "Kimlik doğrulama türü:", - "config-mssql-install-auth": "Kurulum işlemi sırasında veritabanına bağlanmak için kullanılacak doğrulama türünü seçin.\n\"{{int:config-mssql-windowsauth}}\"'ı seçerseniz,ağ sunucusu olarak çalışan kullanıcının kimlik bilgileri kullanılacaktır.", - "config-mssql-sqlauth": "SQL Server kimlik doğrulaması", - "config-mssql-windowsauth": "Windows Kimlik Doğrulama", "config-site-name": "Wiki adı:", "config-site-name-help": "Bu tarayıcının başlık çubuğunda ve diğer yerlerde görünecek.", "config-site-name-blank": "Bir site adı girin.", diff --git a/includes/installer/i18n/tt-cyrl.json b/includes/installer/i18n/tt-cyrl.json index d9c615d9e0..5d976e461b 100644 --- a/includes/installer/i18n/tt-cyrl.json +++ b/includes/installer/i18n/tt-cyrl.json @@ -49,30 +49,22 @@ "config-using-uri": "«$1$2» URL исемле сервер файдаланыла.", "config-db-type": "Мәгълүматлар базасы төре:", "config-db-host": "Мәгълүматлар базасы хосты:", - "config-db-host-oracle": "TNS мәгълүмат базасы:", "config-db-wiki-settings": "Бу вики тәңгәлләштерү", "config-db-name": "Мәгълүматлар базасы исеме (сызыкчасыз):", - "config-db-name-oracle": "Мәгълүматлар базасы төзелеше:", "config-db-username": "Мәгълүмат базасын кулланучы исеме:", "config-db-password": "Мәгълүмат базасының серсүзе:", "config-db-port": "Мәгълүматлар базасы порты:", "config-db-schema": "MediaWiki өчен (сызыкчасыз) төзелеш:", "config-type-mysql": "MariaDB, MySQL яки ярашлы", - "config-type-mssql": "Microsoft SQL Server", "config-header-mysql": "MariaDB/MySQL көйләнмәләре", "config-header-postgres": "PostgreSQL көйләнмәләре", "config-header-sqlite": "SQLite көйләнмәләре", - "config-header-oracle": "Oracle көйләнмәләре", - "config-header-mssql": "Microsoft SQL Server көйләнмәләре", "config-invalid-db-type": "Мәгълүматлар базасы ялгыш төре.", "config-upgrade-done-no-regenerate": "Яңартү тәмамланды.\n\nХәзер сез [$1 вики] белән эшли аласыз.", "config-regenerate": "LocalSettings.php яңадан төзү →", "config-show-table-status": "«SHOW TABLE STATUS» таләбе эшләнмәде!", "config-mysql-engine": "Саклау системасы:", "config-mysql-innodb": "InnoDB (киңәш ителә)", - "config-mssql-auth": "Аутентификация төре:", - "config-mssql-sqlauth": "SQL Server чынлыгын раслау", - "config-mssql-windowsauth": "Windows чынлыгын раслау", "config-site-name": "Вики исеме:", "config-site-name-blank": "Сайт исемен языгыз.", "config-project-namespace": "Проектның исемнәр киңлеге:", diff --git a/includes/installer/i18n/uk.json b/includes/installer/i18n/uk.json index 3ae74e747b..938f1993bb 100644 --- a/includes/installer/i18n/uk.json +++ b/includes/installer/i18n/uk.json @@ -98,13 +98,9 @@ "config-db-type": "Тип бази даних:", "config-db-host": "Хост бази даних:", "config-db-host-help": "Якщо сервер бази даних знаходиться на іншому сервері, введіть тут ім'я хосту і IP-адресу.\n\nЯкщо Ви використовуєте спільний веб-хостинг, Ваш хостинг-провайдер має надати Вам правильне ім'я хосту у його документації.\n\nЯкщо Ви використовуєте MySQL, можливість «localhost» може не працювати для серверного імені. Якщо не працює, використайте «127.0.0.1» як локальну IP-адресу.\n\nЯкщо Ви використовуєте PostgreSQL, залиште це поле пустим, щоб під'єднатись через сокет Unix.", - "config-db-host-oracle": "TNS бази даних:", - "config-db-host-oracle-help": "Введіть допустиме [http://download.oracle.com/docs/cd/B28359_01/network.111/b28317/tnsnames.htm Local Connect Name]; файл tnsnames.ora має бути видимим для цієї інсталяції.
Якщо Ви використовуєте бібліотеки 10g чи новіші, можна також використовувати метод іменування [http://download.oracle.com/docs/cd/E11882_01/network.112/e10836/naming.htm Easy Connect].", "config-db-wiki-settings": "Ідентифікувати цю вікі", "config-db-name": "Назва бази даних (без дефісів):", "config-db-name-help": "Виберіть назву, що ідентифікує Вашу вікі.\nВона не повинна містити пробілів.\n\nЯкщо Ви використовуєте віртуальний хостинг, Ваш хостинг-провайдер або надасть Вам конкретну назву бази даних, або дозволить створювати бази даних з допомогою панелі управління.", - "config-db-name-oracle": "Схема бази даних:", - "config-db-account-oracle-warn": "Є три підтримувані сценарії установки Oracle:\n\nЯкщо Ви хочете створити обліковий запис бази даних у процесі встановлення, будь ласка, вкажіть обліковий запис ролі SYSDBA для установки і бажані повноваження для облікового запису з веб-доступом. В протилежному випадку Ви можете або створити обліковий запис з веб-доступом вручну і вказати тільки цей обліковий запис (якщо він має необхідні дозволи на створення об'єктів-схем), або вказати два різні облікові записи, з яких в одного будуть права на створення, а в другого, обмеженого — права веб-доступу.\n\nСкрипт для створення облікового запису з необхідними повноваженнями можна знайти у папці \"maintenance/oracle/\" цієї інсталяції. Майте на увазі, що використання обмеженого облікового запису вимкне можливість використання технічного обслуговування з облікового запису за замовчуванням.", "config-db-install-account": "Обліковий запис користувача для встановлення", "config-db-username": "Ім'я користувача бази даних:", "config-db-password": "Пароль бази даних:", @@ -123,34 +119,22 @@ "config-pg-test-error": "Не вдається підключитися до бази даних '''$1''': $2", "config-sqlite-dir": "Папка даних SQLite:", "config-sqlite-dir-help": "SQLite зберігає усі дані в єдиному файлі.\n\nПапка, яку Ви вказуєте, має бути доступна серверу для запису під час встановлення.\n\nВона '''не''' повинна бути доступна через інтернет, тому ми і не поміщуємо її туди, де Ваші файли PHP.\n\nІнсталятор пропише у неї файл .htaccess, але якщо це не спрацює, хтось може отримати доступ до Вашої вихідної бази даних, яка містить вихідні дані користувача (адреси електронної пошти, хеші паролів), а також видалені версії та інші обмежені дані на вікі.\n\nЗа можливості розташуйте базу даних десь окремо, наприклад в /var/lib/mediawiki/yourwiki.", - "config-oracle-def-ts": "Простір таблиць за замовчуванням:", - "config-oracle-temp-ts": "Тимчасовий простір таблиць:", "config-type-mysql": "\nMariaDB, MySQL (або сумісні)", - "config-type-mssql": "Microsoft SQL Server", "config-support-info": "MediaWiki підтримує такі системи баз даних:\n\n$1\n\nЯкщо Ви не бачите серед перерахованих систему баз даних, яку використовуєте, виконайте вказівки, вказані вище, щоб увімкнути підтримку.", "config-dbsupport-mysql": "* [{{int:version-db-mariadb-url}} MariaDB] є основною ціллю для MediaWiki і найкраще підтримується. MediaWiki також працює з [{{int:version-db-mysql-url}} MySQL] та [{{int:version-db-percona-url}} Percona Server], які сумісні з MariaDB. ([https://www.php.net/manual/en/mysqli.installation.php Як зібрати PHP з підтримкою MySQL])", "config-dbsupport-postgres": "* [{{int:version-db-postgres-url}} PostgreSQL] — популярна відкрита СУБД, альтернатива MySQL. ([https://www.php.net/manual/en/pgsql.installation.php як зібрати PHP з допомогою PostgreSQL]).", "config-dbsupport-sqlite": "* [{{int:version-db-sqlite-url}} SQLite] — легка система баз даних, яка дуже добре підтримується. ([http://www.php.net/manual/en/pdo.installation.php Як зібрати PHP з допомогою SQLite], використовує PDO)", - "config-dbsupport-oracle": "* [{{int:version-db-oracle-url}} Oracle] — комерційна база даних масштабу підприємства. ([http://www.php.net/manual/en/oci8.installation.php Як зібрати PHP з підтримкою OCI8])", - "config-dbsupport-mssql": "* [{{int:version-db-mssql-url}} Microsoft SQL Server] — це комерційна база даних для Windows масштабу підприємства. ([https://www.php.net/manual/en/sqlsrv.installation.php Як зібрати PHP з підтримкою SQLSRV])", "config-header-mysql": "Налаштування MariaDB/MySQL", "config-header-postgres": "Налаштування PostgreSQL", "config-header-sqlite": "Налаштування SQLite", - "config-header-oracle": "Налаштування Oracle", - "config-header-mssql": "Параметри Microsoft SQL Server", "config-invalid-db-type": "Невірний тип бази даних", "config-missing-db-name": "Ви повинні ввести значення параметра «{{int:config-db-name}}».", "config-missing-db-host": "Ви повинні ввести значення параметра «{{int:config-db-host}}».", - "config-missing-db-server-oracle": "Ви повинні ввести значення параметра «{{int:config-db-host-oracle}}».", - "config-invalid-db-server-oracle": "Неприпустиме TNS бази даних \"$1\".\nВикористовуйте \"TNS Name\" або рядок \"Easy Connect\" ([http://docs.oracle.com/cd/E11882_01/network.112/e10836/naming.htm Методи найменування Oracle])", "config-invalid-db-name": "Неприпустима назва бази даних \"$1\".\nВикористовуйте тільки ASCII букви (a-z, A-Z), цифри (0-9), знаки підкреслення (_) і дефіси (-).", "config-invalid-db-prefix": "Неприпустимий префікс бази даних \"$1\".\nВикористовуйте тільки ASCII букви (a-z, A-Z), цифри (0-9), знаки підкреслення (_) і дефіси (-).", "config-connection-error": "$1.\n\nПеревірте нижченаведений хост, ім'я користувача та пароль і спробуйте ще раз. Якщо Ви використовуєте «localhost» як хост бази даних, замініть його на «127.0.0.1» (або навпаки)", "config-invalid-schema": "Неприпустима схема для MediaWiki \"$1\".\nВикористовуйте тільки ASCII букви (a-z, A-Z), цифри (0-9) і знаки підкреслення(_).", - "config-db-sys-create-oracle": "Інсталятор підтримує лише використання облікового запису SYSDBA для створення нового облікового запису.", - "config-db-sys-user-exists-oracle": "Обліковий запис користувача \"$1\" уже існує. SYSDBA використовується лише для створення новий облікових записів!", "config-postgres-old": "Необхідна PostgreSQL $1 або пізніша, а у Вас $2.", - "config-mssql-old": "Вимагається Microsoft SQL Server версії $1 або більш пізнішої. У вас установлена версія $2.", "config-sqlite-name-help": "Виберіть назву, що ідентифікує Вашу вікі.\nНе використовуйте пробіли і дефіси.\nЦе буде використовуватись у назві файлу даних SQLite.", "config-sqlite-parent-unwritable-group": "Не можна створити папку даних $1, оскільки батьківська папка $2 не доступна веб-серверу для запису.\n\nІнсталятор виявив, під яким користувачем працює Ваш сервер.\nЗробіть папку $3 доступною для запису, щоб продовжити.\nВ ОС Unix/Linux виконайте:\n\n
cd $2\nmkdir $3\nchgrp $4 $3\nchmod g+w $3
", "config-sqlite-parent-unwritable-nogroup": "Не можна створити папку даних $1, оскільки батьківська папка $2 не доступна веб-серверу для запису.\n\nІнсталятор не зміг виявити, під яким користувачем працює Ваш сервер.\nЗробіть папку $3 доступною для запису серверу (і всім!) глобально, щоб продовжити.\nВ ОС Unix/Linux виконайте:\n\n
cd $2\nmkdir $3\nchmod a+w $3
", @@ -175,11 +159,6 @@ "config-mysql-engine": "Двигун бази даних:", "config-mysql-innodb": "InnoDB (рекомендовано)", "config-mysql-engine-help": "'''InnoDB''' є завжди кращим вибором, оскільки краще підтримує паралельний доступ.\n\n'''MyISAM''' може бути швидшим для одного користувача або в інсталяціях read-only.\nБази даних MyISAM схильні псуватись частіше, ніж бази InnoDB.", - "config-mssql-auth": "Тип автентифікації:", - "config-mssql-install-auth": "Виберіть тип перевірки автентичності, який буде використовуватися для підключення до бази даних під час процесу установки. \nЯкщо ви оберете \"{{int:config-mssql-windowsauth}}\", будуть використовуватися облікові дані користувача, під яким працює веб-сервер.", - "config-mssql-web-auth": "Виберіть тип перевірки автентичності, який веб-сервер буде використовувати для підключення до сервера бази даних під час звичайного функціонування вікі. \nЯкщо ви оберете \"{{int:config-mssql-windowsauth}}\", будуть використовуватися облікові дані користувача, під яким працює веб-сервер.", - "config-mssql-sqlauth": "Автентифікація сервера SQL", - "config-mssql-windowsauth": "Перевірка Достовірності Windows", "config-site-name": "Назва вікі:", "config-site-name-help": "Це буде відображатись у заголовку вікна браузера та у деяких інших місцях.", "config-site-name-blank": "Введіть назву сайту.", diff --git a/includes/installer/i18n/vi.json b/includes/installer/i18n/vi.json index 0929c1b5b1..eb71a4837b 100644 --- a/includes/installer/i18n/vi.json +++ b/includes/installer/i18n/vi.json @@ -92,13 +92,9 @@ "config-db-type": "Kiểu cơ sở dữ liệu:", "config-db-host": "Máy chủ của cơ sở dữ liệu:", "config-db-host-help": "Nếu máy chủ cơ sở dữ liệu của bạn nằm trên máy chủ khác, hãy điền tên hoặc địa chỉ IP của máy chủ vào đây.\n\nNếu bạn đang dùng Web hosting chia sẻ, tài liệu của nhà cung cấp hosting của bạn sẽ có tên chính xác của máy chủ.\n\nNếu bạn đang sử dụng MySQL, việc dùng “localhost” có thể không hợp với tên máy chủ. Nếu bị như vậy, hãy thử “127.0.0.1” tức địa chỉ IP địa phương.\n\nNếu bạn đang dùng PostgreSQL, hãy để trống mục này để kết nối với một ổ cắm Unix.", - "config-db-host-oracle": "TNS cơ sở dữ liệu:", - "config-db-host-oracle-help": "Nhập một [http://download.oracle.com/docs/cd/B28359_01/network.111/b28317/tnsnames.htm Tên Kết nối Địa phương] hợp lệ; một tập tin tnsnames.ora phải được hiển thị đối với cài đặt này.
Nếu bạn đang sử dụng các thư viện trình khách 10g trở lên, bạn cũng có thể sử dụng phương pháp đặt tên [http://download.oracle.com/docs/cd/E11882_01/network.112/e10836/naming.htm Easy Connect].", "config-db-wiki-settings": "Dữ liệu để nhận ra wiki này", "config-db-name": "Tên cơ sở dữ liệu (không có dấu gạch ngang):", "config-db-name-help": "Chọn một tên để chỉ thị wiki của bạn.\nKhông nên đưa dấu cách vào tên này.\n\nNếu bạn đang sử dụng Web hosting chia sẻ, nhà cung cấp hosting của bạn hoặc là sẽ cung cấp cho bạn một tên cơ sở dữ liệu cụ thể để sử dụng hoặc là sẽ cho phép bạn tạo ra các cơ sở dữ liệu thông qua một bảng điều khiển.", - "config-db-name-oracle": "Giản đồ cơ sở dữ liệu:", - "config-db-account-oracle-warn": "Có ba trường hợp được hỗ trợ để cài đặt Oracle làm cơ sở dữ liệu phía sau:\n\nNếu bạn muốn tạo tài khoản cơ sở dữ liệu trong quá trình cài đặt, xin vui lòng cung cấp một tài khoản với vai trò SYSDBA là tài khoản cơ sở dữ liệu để cài đặt và xác định định danh mong muốn cho tài khoản truy cập Web, nếu không bạn có thể tạo tài khoản truy cập Web thủ công và chỉ cung cấp tài khoản đó (nếu nó có các quyền yêu cầu để tạo ra các đối tượng giản đồ) hoặc cung cấp hai tài khoản riêng, một có quyền tạo ra và một bị hạn chế có quyền truy cập Web.\n\nMột kịch bản để tạo một tài khoản với quyền yêu cầu có sẵn trong thư mục cài đặt “maintenance/oracle/”. Hãy nhớ rằng việc sử dụng một tài khoản bị hạn chế sẽ vô hiệu hóa tất cả các khả năng bảo trì với tài khoản mặc định.", "config-db-install-account": "Tài khoản người dùng để cài đặt", "config-db-username": "Tên người dùng cơ sở dữ liệu:", "config-db-password": "Mật khẩu cơ sở dữ liệu:", @@ -117,34 +113,22 @@ "config-pg-test-error": "Không thể kết nối với cơ sở dữ liệu '''$1''': $2", "config-sqlite-dir": "Thư mục dữ liệu SQLite:", "config-sqlite-dir-help": "SQLite lưu tất cả các dữ liệu trong một tập tin duy nhất.\n\nThư mục mà bạn cung cấp phải cho phép máy chủ Web ghi vào khi cài đặt.\n\nKhông nên làm cho nó truy cập được qua Web; đây là lý do chúng tôi không đặt nó vào cùng thư mục với các tập tin PHP của bạn.\n\nTrình cài đặt sẽ ghi một tập tin .htaccess đi kèm, nhưng nếu thất bại người nào đó có thể truy cập vào cơ sở dữ liệu thô của bạn.\nĐiều đó bao gồm dữ liệu người dùng thô (địa chỉ thư điện tử, mật khẩu được băm) cũng như các phiên bản bị xóa và dữ liệu bị hạn chế khác trên wiki.\n\nXem xét đặt cơ sở dữ liệu tại nơi nào khác hẳn, ví dụ trong /var/lib/mediawiki/wiki_cua_ban.", - "config-oracle-def-ts": "Không gian bảng mặc định:", - "config-oracle-temp-ts": "Không gian bảng tạm:", "config-type-mysql": "MariaDB, MySQL, hoặc tương hợp", - "config-type-mssql": "Microsoft SQL Server", "config-support-info": "MediaWiki hỗ trợ các hệ thống cơ sở dữ liệu sau đây:\n\n$1\n\nNếu bạn không thấy hệ thống cơ sở dữ liệu mà bạn đang muốn sử dụng được liệt kê dưới đây, thì hãy theo chỉ dẫn được liên kết ở trên để kích hoạt tính năng hỗ trợ.", "config-dbsupport-mysql": "* [{{int:version-db-mariadb-url}} MariaDB] là mục tiêu chính cho MediaWiki và được hỗ trợ tốt nhất. MediaWiki cũng làm việc với [{{int:version-db-mysql-url}} MySQL] và [{{int:version-db-percona-url}} Percona Server], là những cơ sở dữ liệu tương thích với MariaDB. ([https://www.php.net/manual/en/mysqli.installation.php Làm thế nào để biên dịch PHP với sự hỗ trợ của MySQL])", "config-dbsupport-postgres": "* [{{int:version-db-postgres-url}} PostgreSQL] là một hệ thống cơ sở dữ liệu mã nguồn mở phổ biến như là một thay thế cho MySQL. ([https://www.php.net/manual/en/pgsql.installation.php Làm thế nào để biên dịch PHP với sự hỗ trợ của PostgreSQL])", "config-dbsupport-sqlite": "* [{{int:version-db-sqlite-url}} SQLite] là một hệ thống cơ sở dữ liệu dung lượng nhẹ được hỗ trợ rất tốt. ([https://www.php.net/manual/en/pdo.installation.php Làm thế nào để biên dịch PHP với sự hỗ trợ của SQLite], sử dụng PDO)", - "config-dbsupport-oracle": "* [{{int:version-db-oracle-url}} Oracle] là một cơ sở dữ liệu doanh nghiệp thương mại. ([https://www.php.net/manual/en/oci8.installation.php Làm thế nào để biên dịch PHP với sự hỗ trợ của OCI8])", - "config-dbsupport-mssql": "* [{{int:version-db-mssql-url}} Microsoft SQL Server] là một cơ sở dữ liệu doanh nghiệp thương mại cho Windows. ([https://www.php.net/manual/en/sqlsrv.installation.php Làm thế nào để biên dịch PHP với sự hỗ trợ của SQLSRV])", "config-header-mysql": "Thiết lập MariaDB/MySQL", "config-header-postgres": "Thiết lập PostgreSQL", "config-header-sqlite": "Thiết lập SQLite", - "config-header-oracle": "Thiết lập Oracle", - "config-header-mssql": "Thiết lập Microsoft SQL Server", "config-invalid-db-type": "Loại cơ sở dữ liệu không hợp lệ", "config-missing-db-name": "Bạn phải nhập một giá trị cho “{{int:config-db-name}}”", "config-missing-db-host": "Bạn phải nhập một giá trị cho “{{int:config-db-host}}”", - "config-missing-db-server-oracle": "Bạn phải nhập một giá trị cho “{{int:config-db-host-oracle}}”", - "config-invalid-db-server-oracle": "Cơ sở dữ liệu TNS không hợp lệ “$1”.\nHoặc sử dụng “TNS Name” hoặc một chuỗi “Easy Connect” ([http://docs.oracle.com/cd/E11882_01/network.112/e10836/naming.htm Phương pháp đặt tên Oracle]).", "config-invalid-db-name": "Tên cơ sở dữ liệu không hợp lệ “$1”.\nChỉ sử dụng các chữ cái ASCII (a–z, A–Z), số (0–9), dấu gạch dưới (_) và dấu gạch ngang (-).", "config-invalid-db-prefix": "Tiền tố cơ sở dữ liệu không hợp lệ “$1”.\nChỉ sử dụng các chữ cái ASCII (a–z, A–Z), số (0–9), dấu gạch dưới (_) và dấu gạch ngang (-).", "config-connection-error": "$1.\n\nKiểm tra máy chủ, tên người dùng, và mật khẩu và thử lại lần nữa. Nếu sử dụng “localhost” làm máy chủ cơ sở dữ liệu, hãy thử sử dụng “127.0.0.1” thay thế (hoặc ngược lại).", "config-invalid-schema": "Giản đồ “$1” không hợp lệ cho MediaWiki.\nHãy chỉ sử dụng các chữ cái ASCII (a–z, A–Z), chữ số (0–9), và dấu gạch dưới (_).", - "config-db-sys-create-oracle": "Trình cài đặt chỉ hỗ trợ sử dụng một tài khoản SYSDBA để tạo một tài khoản mới.", - "config-db-sys-user-exists-oracle": "Tài khoản người dùng “$1” đã tồn tại. SYSDBA chỉ có thể được sử dụng để tạo một tài khoản mới!", "config-postgres-old": "Cần PostgreSQL $1 trở lên; bạn có $2.", - "config-mssql-old": "Cần Microsoft SQL Server $1 trở lên. Bạn có $2.", "config-sqlite-name-help": "Chọn một tên để chỉ thị wiki của bạn.\nKhông sử dụng các dấu cách ( ) hoặc dấu gạch nối (-).\nTên này sẽ được sử dụng cho tên tập tin dữ liệu SQLite.", "config-sqlite-parent-unwritable-group": "Không thể tạo ra thư mục dữ liệu $1, bởi vì thư mục cha $2 không cho phép máy chủ Web ghi vào.\n\nTrình cài đặt đã xác định người dùng mà máy chủ Web của bạn đang chạy.\n\nHãy thiết lập để thư mục $3 có thể ghi được bởi nó để tiếp tục.\nTrong một hệ thống Unix/Linux làm theo như sau:\n\n
cd $2\nmkdir $3\nchgrp $4 $3\nchmod g+w $3
", "config-sqlite-parent-unwritable-nogroup": "Không thể tạo ra thư mục dữ liệu $1, bởi vì thư mục cha $2 không cho phép máy chủ Web ghi vào.\n\nTrình cài đặt không thể xác định người sử dụng mà máy chủ web của bạn đang chạy.\nThiết lập thư mục $3 có thể ghi toàn cục bởi nó (và những người khác!) để tiếp tục.\nTrong một hệ thống Unix/Linux hãy đánh các dòng lệnh sau:\n\n
cd $2\nmkdir $3\nchmod a+w $3
", @@ -169,11 +153,6 @@ "config-mysql-engine": "Máy lưu trữ:", "config-mysql-innodb": "InnoDB (khuyến khích)", "config-mysql-engine-help": "InnoDB hầu như luôn là tùy chọn tốt nhất, vì nó có hỗ trợ đồng thời rất tốt.\n\nMyISAM có thể nhanh hơn trong chế độ một người dùng hoặc các cài đặt chỉ-đọc (read-only).\nCơ sở dữ liệu MyISAM có xu hướng thường xuyên bị hỏng hóc hơn so với cơ sở dữ liệu InnoDB.", - "config-mssql-auth": "Kiểu xác thực:", - "config-mssql-install-auth": "Chọn loại xác thực sẽ được sử dụng để kết nối với cơ sở dữ liệu trong quá trình cài đặt.\nNếu bạn chọn “{{int:config-mssql-windowsauth}}”, thông tin của bất cứ người sử dụng nào mà máy chủ web đang chạy sẽ được sử dụng.", - "config-mssql-web-auth": "Chọn kiểu xác thực mà máy chủ web sẽ sử dụng để kết nối đến máy chủ cơ sở dữ liệu, trong quá trình hoạt động bình thường của wiki.\nNếu bạn chọn “{{int:config-mssql-windowsauth}}”, thông tin của bất cứ người sử dụng nào mà máy chủ web đang hoạt động sẽ được sử dụng.", - "config-mssql-sqlauth": "Xác thực SQL Server", - "config-mssql-windowsauth": "Xác thực Windows", "config-site-name": "Tên wiki:", "config-site-name-help": "Điều này sẽ xuất hiện trên thanh tiêu đề của trình duyệt và ở những nơi khác.", "config-site-name-blank": "Nhập tên của trang Web.", diff --git a/includes/installer/i18n/war.json b/includes/installer/i18n/war.json index f53d856f22..a4b5bf356e 100644 --- a/includes/installer/i18n/war.json +++ b/includes/installer/i18n/war.json @@ -50,12 +50,10 @@ "config-pcre-old": "Nangangarat-an: Nagkikinahanglan hin PCRE $1 o mas urhi pa.\nAn imo PHP nga binaryo in nakasumpay hin PCRE $2. [https://www.mediawiki.org/wiki/Manual:Errors_and_symptoms/PCRE More information].", "config-db-name": "Ngaran han database:", "config-db-name-help": "Pagpili hin ngaran nga natudlok ha imo wiki.\nDapat waray ini mga espasyo.\n\nKun ikaw in nagamit hin shared web hosting, an imo hosting provider in mahatag diri ngani an specific database name para paggamit, matugot ha imo paghimo hin mga database pinaagi han control panel.", - "config-db-name-oracle": "Schema han database:", "config-db-username": "Agnay-gumaramit para ha database:", "config-db-password": "Password para ha database:", "config-db-port": "Database port:", "config-type-mysql": "MySQL (o compatible)", - "config-type-mssql": "Microsoft SQL Server", "config-sqlite-readonly": "An file nga $1 in diri writeable.", "config-sqlite-cant-create-db": "Diri nakakahimo hin database file nga $1.", "config-db-web-account": "Database account para han web access", diff --git a/includes/installer/i18n/yi.json b/includes/installer/i18n/yi.json index 8877f2d98f..58c31a0f9b 100644 --- a/includes/installer/i18n/yi.json +++ b/includes/installer/i18n/yi.json @@ -46,17 +46,14 @@ "config-using-uri": "באניצן סארווער־אדרעס \"$1$2\".", "config-db-type": "דאטנבאזע טיפ:", "config-db-host": "דאטנבאזע־סארווער:", - "config-db-host-oracle": "דאטנבאזע־TNS:", "config-db-wiki-settings": "אידענטיפיצירן די דאזיקע וויקי", "config-db-name": "דאטנבאזע נאָמען (קיין מקף):", - "config-db-name-oracle": "דאטנבאזע סכעמע:", "config-db-install-account": "באניצער־קאנטע פאר אינסטאלאציע", "config-db-username": "דאטנבאזע באניצער־נאָמען:", "config-db-password": "דאטנבאזע־פאסווארט:", "config-invalid-db-type": "אומגילטיגער דאטנבאזע־טיפ", "config-missing-db-name": "איר דארפט איינגעבן א ווערט פאר \"{{int:config-db-name}}\".", "config-missing-db-host": "איר דארפט איינגעבן א ווערט פאר \"{{int:config-db-host}}\".", - "config-missing-db-server-oracle": "איר דארפט איינגעבן א ווערט פאר \"{{int:config-db-host-oracle}}\".", "config-project-namespace": "פראיעקט נאָמענטייל:", "config-ns-generic": "פראיעקט", "config-admin-name": "אײַער באַניצער־נאָמען:", diff --git a/includes/installer/i18n/yue.json b/includes/installer/i18n/yue.json index 49d534a184..9f054dfb84 100644 --- a/includes/installer/i18n/yue.json +++ b/includes/installer/i18n/yue.json @@ -32,8 +32,8 @@ "config-env-php": "安裝咗PHP$1", "config-env-hhvm": "安裝咗HHVM$1", "config-outdated-sqlite": "警告:你安裝咗SQLite $1,但係佢嘅版本低過最低要求版本 $2。你將會用毋到SQLite。", - "config-apc": "[https://secure.php.net/apc APC]安裝咗", - "config-apcu": "[https://secure.php.net/apcu APCu]安裝咗", + "config-apc": "[https://www.php.net/apc APC]安裝咗", + "config-apcu": "[https://www.php.net/apcu APCu]安裝咗", "config-wincache": "[https://www.iis.net/downloads/microsoft/wincache-extension WinCache]安裝咗", "config-diff3-bad": "搵毋到GNU diff3。", "config-db-type": "資料庫類型:", diff --git a/includes/installer/i18n/zh-hans.json b/includes/installer/i18n/zh-hans.json index ecbfe026bd..4800cbe924 100644 --- a/includes/installer/i18n/zh-hans.json +++ b/includes/installer/i18n/zh-hans.json @@ -104,13 +104,9 @@ "config-db-type": "数据库类型:", "config-db-host": "数据库主机:", "config-db-host-help": "如果您的数据库在别的服务器上,请在这里输入其域名或IP地址。\n\n如果您在使用共享网站套餐,您的网站商应该已在他们的控制面板中给您数据库信息了。\n\n如果您使用MySQL,“localhost”可能无效。如果确实无效,请输入“127.0.0.1”作为IP地址。\n\n如果您在使用PostgreSQL,并且要用Unix socket来连接,请留空。", - "config-db-host-oracle": "数据库透明网络底层(TNS):", - "config-db-host-oracle-help": "请输入合法的[http://download.oracle.com/docs/cd/B28359_01/network.111/b28317/tnsnames.htm 本地连接名],并确保tnsnames.ora文件对本安装程序可见。
如果您使用的客户端库为10g或更新的版本,您还可以使用[http://download.oracle.com/docs/cd/E11882_01/network.112/e10836/naming.htm 简单连接名方法](easy connect naming method)。", "config-db-wiki-settings": "标识本wiki", "config-db-name": "数据库名称(不带连字号):", "config-db-name-help": "请输入一个可以标识您的wiki的名称。请勿使用空格。\n\n如果您正在使用共享web主机,您的主机提供商或会给您指定一个数据库名称,或会让您通过控制面板创建数据库。", - "config-db-name-oracle": "数据库模式:", - "config-db-account-oracle-warn": "现有三种已支持方案可以将Oracle设置为后端数据库:\n\n如果您希望在安装过程中创建数据库帐户,请为安装程序提供具有SYSDBA角色的数据库帐户,并为web访问帐户指定所需身份证明;否则您可以手动创建web访问的账户并仅须提供该帐户(确保帐户已有创建方案对象(schema object)的所需权限);或提供两个不同的帐户,其一具有创建权限,另一则被限制为web访问。\n\n具有所需权限账户的创建脚本存放于本程序的“maintenance/oracle/”目录下。请注意,使用受限制的帐户将禁用默认帐户的所有维护性功能。", "config-db-install-account": "用于安装的用户帐号", "config-db-username": "数据库用户名:", "config-db-password": "数据库密码:", @@ -129,34 +125,22 @@ "config-pg-test-error": "无法连接到数据库$1:$2", "config-sqlite-dir": "SQLite数据目录:", "config-sqlite-dir-help": "SQLite会将所有的数据存储于单一文件中。\n\n您所提供的目录必须在安装过程中对网页服务器可写。\n\n该目录不应允许通过web访问,因此我们不会将数据文件和PHP文件放在一起。\n\n安装程序在创建数据文件时,亦会在相同目录下创建.htaccess以控制权限。假若此等控制失效,则可能会将您的数据文件暴露于公共空间,让他人可以获取用户数据(电子邮件地址、杂凑后的密码)、被删除的版本以及其他在wiki上被限制访问的数据。\n\n请考虑将数据库统一放置在某处,如/var/lib/mediawiki/yourwiki下。", - "config-oracle-def-ts": "默认表空间:", - "config-oracle-temp-ts": "临时表空间:", "config-type-mysql": "MariaDB、MySQL或兼容程序", - "config-type-mssql": "微软SQL服务器", "config-support-info": "MediaWiki支持以下数据库系统:\n\n$1\n\n如果您在下面列出的数据库系统中没有找到您希望使用的系统,请根据上方链向的指引启用支持。", "config-dbsupport-mysql": "* [{{int:version-db-mariadb-url}} MariaDB]是用于MediaWiki的主要数据库,对它的支持最为完备。MediaWiki也可以在[{{int:version-db-mysql-url}} MySQL]和[{{int:version-db-percona-url}} Percona Server]下工作,它们与MariaDB兼容。([https://www.php.net/manual/en/mysqli.installation.php 如何将对MySQL的支持编译进PHP中])", "config-dbsupport-postgres": "* [{{int:version-db-postgres-url}} PostgreSQL]是一种流行的开源数据库系统,可作为MySQL的替代。([https://www.php.net/manual/en/pgsql.installation.php 如何将对PostgreSQL的支持编译进PHP中])", "config-dbsupport-sqlite": "* [{{int:version-db-sqlite-url}} SQLite]是一种轻量级的数据库系统,能被良好地支持。([https://www.php.net/manual/en/pdo.installation.php 如何将对SQLite的支持编译进PHP中],须使用PDO)", - "config-dbsupport-oracle": "* [{{int:version-db-oracle-url}} Oracle]是一种商用企业级的数据库。([https://www.php.net/manual/en/oci8.installation.php 如何将对OCI8的支持编译进PHP中])", - "config-dbsupport-mssql": "* [{{int:version-db-mssql-url}} Microsoft SQL Server]是一个适用于Windows的商业性企业数据库。([https://www.php.net/manual/en/sqlsrv.installation.php 如何编译带有SQLSRV支持的PHP])", "config-header-mysql": "MariaDB/MySQL设置", "config-header-postgres": "PostgreSQL设置", "config-header-sqlite": "SQLite设置", - "config-header-oracle": "Oracle设置", - "config-header-mssql": "Microsoft SQL Server设置", "config-invalid-db-type": "无效的数据库类型", "config-missing-db-name": "您必须为“{{int:config-db-name}}”输入一个值。", "config-missing-db-host": "您必须为“{{int:config-db-host}}”输入一个值。", - "config-missing-db-server-oracle": "您必须为“{{int:config-db-host-oracle}}”输入一个值。", - "config-invalid-db-server-oracle": "无效的数据库TNS“$1”。请使用“TNS 名称”或者一个“轻松连接”字符串([http://docs.oracle.com/cd/E11882_01/network.112/e10836/naming.htm Oracle 命名方法])", "config-invalid-db-name": "无效的数据库名称“$1”。请只使用ASCII字母(a-z、A-Z)、数字(0-9)、下划线(_)和连字号(-)。", "config-invalid-db-prefix": "无效的数据库前缀“$1”。请只使用ASCII字母(a-z、A-Z)、数字(0-9)、下划线(_)和连字号(-)。", "config-connection-error": "$1。\n\n请检查下列的主机、用户名和密码设置后重试。若使用\"localhost\"作为数据库主机,请尝试\"127.0.0.1\"(反之亦然)。", "config-invalid-schema": "无效的MediaWiki数据库模式“$1”。请只使用ASCII字母(a-z、A-Z)、数字(0-9)和下划线(_)。", - "config-db-sys-create-oracle": "安装程序仅支持使用SYSDBA帐户创建新帐户。", - "config-db-sys-user-exists-oracle": "用户帐户“$1”已经存在。SYSDBA仅可用于创建新帐户!", "config-postgres-old": "需要PostgreSQL $1或更新的版本,您的版本为$2。", - "config-mssql-old": "需要 Microsoft SQL Server $1 或者更高版本。您的版本是 $2。", "config-sqlite-name-help": "请为您的wiki指定一个用于标识的名称。请勿使用空格或连字号,该名称将被用作SQLite的数据文件名。", "config-sqlite-parent-unwritable-group": "由于父目录$2对网页服务器不可写,无法创建数据目录$1。\n\n安装程序已确定您网页服务器所使用的用户。请将$3目录设为对该用户可写以继续安装过程。在Unix/Linux系统中,您可以逐行输入下列命令:\n\n
cd $2\nmkdir $3\nchgrp $4 $3\nchmod g+w $3
", "config-sqlite-parent-unwritable-nogroup": "由于父目录$2对网页服务器不可写,无法创建数据目录$1。\n\n安装程序无法确定您网页服务器所使用的用户。请将$3目录设为全局可写(对所有用户)以继续安装过程。在Unix/Linux系统中,您可以逐行输入下列命令:\n\n
cd $2\nmkdir $3\nchmod a+w $3
", @@ -180,11 +164,6 @@ "config-mysql-engine": "存储引擎:", "config-mysql-innodb": "InnoDB(推荐)", "config-mysql-engine-help": "InnoDB通常是最佳选项,因为它对并发操作有着良好的支持。\n\nMyISAM在单用户或只读环境下可能会有更快的性能表现。但MyISAM数据库出错的概率一般要大于InnoDB数据库。", - "config-mssql-auth": "身份验证类型:", - "config-mssql-install-auth": "选择安装过程中链接数据库时将采用的身份验证方式。如果您选择“{{int:config-mssql-windowsauth}}”,将使用运行服务器的用户的身份凭据。", - "config-mssql-web-auth": "选择Web服务器在通常wiki操作期间用来连接数据库服务器的身份验证方式。如果您选择“{{int:config-mssql-windowsauth}}”,将使用运行Web服务器的用户的凭据。", - "config-mssql-sqlauth": "SQL Server 身份验证", - "config-mssql-windowsauth": "Windows 身份验证", "config-site-name": "wiki的名称:", "config-site-name-help": "填入的内容会出现在浏览器的标题栏以及其他多处位置中。", "config-site-name-blank": "输入网站的名称。", diff --git a/includes/installer/i18n/zh-hant.json b/includes/installer/i18n/zh-hant.json index 28774f80e1..3669178a04 100644 --- a/includes/installer/i18n/zh-hant.json +++ b/includes/installer/i18n/zh-hant.json @@ -105,13 +105,9 @@ "config-db-type": "資料庫類型:", "config-db-host": "資料庫主機:", "config-db-host-help": "如果您的資料庫安裝在其他伺服器上,請在此輸入該主機的名稱或 IP 位址。\n\n如果您使用共用的網頁主機,您的主機提供商應會在說明文件上告訴您正確的主機名稱。\n\n如果您使用 MySQL,伺服器名稱可能無法使用 \"localhost\"。若確實無法使用,請改嘗試使用本機的 IP 位址 \"127.0.0.1\"。\n\n如果您使用 PostgreSQL,將此欄位空白以使用 Unix socket 來連線。", - "config-db-host-oracle": "資料庫的 TNS:", - "config-db-host-oracle-help": "請輸入有效的 [http://download.oracle.com/docs/cd/B28359_01/network.111/b28317/tnsnames.htm 本地連線名稱],並確認安裝程式可以讀取 tnsnames.ora 檔案。
如果您使用的客戶端程式庫為 10g 或者更新的版本,您也可使用 [http://download.oracle.com/docs/cd/E11882_01/network.112/e10836/naming.htm 簡易連線] 的命名方法進行連線。", "config-db-wiki-settings": "識別此 wiki", "config-db-name": "資料庫名稱(不帶連字號):", "config-db-name-help": "請輸入一個可以辨識您的 Wiki 的名稱,\n請勿包含空格。\n\n如果您使用的是共用的網頁主機,您的主機提供商會給您一個指定的資料庫名稱,或者讓您透過管理介面建立資料庫。", - "config-db-name-oracle": "資料庫 Schema:", - "config-db-account-oracle-warn": "目前有三種支援 Oracle 做為後端資料庫的方案:\n\n如果您希望在安裝的過程中自動建立新的資料庫,請提供具有 SYSDBA 權限的帳號並且提供未來要給網頁存取使用的資料庫帳號及密碼。或者您可以手動建立給網頁存取使用的資料庫帳號 (請確保該帳號有建立 Schema Object 的權限),再不然您可以提供兩組不同的帳號,一組用來建立權限,而另一組用來做為網頁存取使用。\n\n本次安裝建立的帳號以及權限所需要的 Script,可以在 \"maintenance/oracle/\" 中找到。\n請注意,若您使用有限制的帳號將會預設關閉所有維護性功能。", "config-db-install-account": "安裝程式使用的使用者帳號", "config-db-username": "資料庫使用者名稱:", "config-db-password": "資料庫密碼:", @@ -130,34 +126,22 @@ "config-pg-test-error": "無法連線到資料庫 $1:$2", "config-sqlite-dir": "SQLite 的資料目錄:", "config-sqlite-dir-help": "SQLite 會將所有的資料存儲於單一檔案中。\n\n您所提供的目錄在安裝過程中必須開啟給網頁伺服器的寫入權限。\n\n該目錄 不應 可以被透過網頁所開啟,這也是為什麼我們不將資料與 PHP 檔案放在一起。\n\n安裝程式在建立資料庫檔案時,會同時在目錄下建立 .htaccess 以控制網頁伺服器權限。若此設定失效,則會導致任何人可以直接存取您的原始資料檔案,而資料庫的內容包含原始的使用者資料 (電子郵件地址、加密後的密碼)、刪除後的修訂及其他在 Wiki 上被限制存取的資料。\n\n請考慮將資料庫統一放置在某處,如 /var/lib/mediawiki/yourwiki 底下。", - "config-oracle-def-ts": "預設資料表空間:", - "config-oracle-temp-ts": "臨時資料表空間:", "config-type-mysql": "MariaDB、MySQL、或與其相容的套件", - "config-type-mssql": "Microsoft SQL Server", "config-support-info": "MediaWiki 支援以下資料庫系統:\n\n$1\n\n如果您下方沒有看到您要使用的資料庫系統,請根據上方連結指示開啟資料庫的支援。", "config-dbsupport-mysql": "* [{{int:version-db-mariadb-url}} MariaDB] 是 MediaWiki 主要支援的資料庫系統。MediaWiki 也同時可運作與於 [{{int:version-db-mysql-url}} MySQL] 和 [{{int:version-db-percona-url}} Percona 伺服器],上述這些與 MariaDB 相容的資料庫系統。([https://www.php.net/manual/en/mysqli.installation.php 如何編譯支援 MySQL 的 PHP])", "config-dbsupport-postgres": "* [{{int:version-db-postgres-url}} PostgreSQL] 是一套受歡迎的開源資料庫系統,可用來替代 MySQL。([https://www.php.net/manual/en/pgsql.installation.php 如何編譯支援 PostgreSQL 的 PHP])。", "config-dbsupport-sqlite": "* [{{int:version-db-sqlite-url}} SQLite] 是一套輕量級的資料庫系統,MediaWiki 可在此資料庫系統上良好的運作。([https://www.php.net/manual/en/pdo.installation.php 如何編譯支援 SQLite 的 PHP],須透過 PDO)", - "config-dbsupport-oracle": "* [{{int:version-db-oracle-url}} Oracle] 是一套商用企業級的資料庫。([https://www.php.net/manual/en/oci8.installation.php 如何編譯支援 OCI8 的 PHP])", - "config-dbsupport-mssql": "* [{{int:version-db-mssql-url}} Microsoft SQL Server] 是一套 Windows 專用的商用企業級的資料庫。 ([https://www.php.net/manual/en/sqlsrv.installation.php 如何編譯支援 SQLSRV 的 PHP])", "config-header-mysql": "MariaDB/MySQL 設定", "config-header-postgres": "PostgreSQL 設定", "config-header-sqlite": "SQLite 設定", - "config-header-oracle": "Oracle 設定", - "config-header-mssql": "Microsoft SQL Server 設定", "config-invalid-db-type": "無效的資料庫類型。", "config-missing-db-name": "您必須輸入 \"{{int:config-db-name}}\" 欄位的內容。", "config-missing-db-host": "您必須輸入 \"{{int:config-db-host}}\" 欄位的內容。", - "config-missing-db-server-oracle": "您必須輸入 \"{{int:config-db-host-oracle}}\" 欄位的內容。", - "config-invalid-db-server-oracle": "無效的資料庫 TNS \"$1\"。\n請使用符合 \"TNS 名稱\" 或 \"簡易連線\" 規則的字串([http://docs.oracle.com/cd/E11882_01/network.112/e10836/naming.htm Oracle命名規則])", "config-invalid-db-name": "無效的資料庫名稱 \"$1\"。\n僅允許使用 ASCII 字母(a-z、A-Z)、數字(0-9)、底線(_)與連字號(-)。", "config-invalid-db-prefix": "無效的資料庫字首 \"$1\"。\n僅允許使用 ASCII 字母(a-z、A-Z)、數字(0-9)、底線(_)與連字號(-)。", "config-connection-error": "$1。\n\n請檢查主機、使用者名稱和密碼設定,然後重試。如果是使用 \"localhost\" 來作為資料庫主機,請嘗試改用 \"127.0.0.1\"(反之亦然)。", "config-invalid-schema": "無效的資料庫 Schema \"$1\"。\n僅允許使用 ASCII 字母(a-z、A-Z)、數字(0-9)、底線(_)與連字號(-)。", - "config-db-sys-create-oracle": "安裝程式只支援使用 SYSDBA 帳號建立新帳號。", - "config-db-sys-user-exists-oracle": "使用者帳號 \"$1\" 已存在。 SYSDBA 只可用來建立新的帳號!", "config-postgres-old": "需要使用 PostgreSQL $1 或更新的版本,您的版本為 $2。", - "config-mssql-old": "需要使用 Microsoft SQL Server $1 或更新的版本,您的版本為 $2。", "config-sqlite-name-help": "請為您的 Wiki 設定一個用來辨識的名稱。\n請勿使用空格或連字號,\n該名稱會被用來做為 SQLite 資料檔的名稱。", "config-sqlite-parent-unwritable-group": "無法建立資料目錄 $1
,因網頁伺服器對該目錄所在的上層目錄 $2 沒有寫入權限。\n\n安裝程序所使用的身份依據您用來執行網頁伺服器的身份而定,\n請開啟網頁伺服器對 $3 的寫入權以繼續安裝,\n在 Unix/Linux 系統可以執行以下指令:\n\n
cd $2\nmkdir $3\nchgrp $4 $3\nchmod g+w $3
", "config-sqlite-parent-unwritable-nogroup": "無法建立資料目錄 $1
,因網頁伺服器對該目錄所在的上層目錄 $2 沒有寫入權限。\n\n安裝程序所使用的身份依據您用來執行網頁伺服器的身份而定,\n請開啟全部人對 $3 的寫入權以繼續安裝,\n在 Unix/Linux 系統可以執行以下指令:\n\n
cd $2\nmkdir $3\nchmod a+w $3
", @@ -182,11 +166,6 @@ "config-mysql-engine": "儲存引擎:", "config-mysql-innodb": "InnoDB(推薦)", "config-mysql-engine-help": "由於對同時連線有較好的處理能力,InnoDB 通常是最佳的選項。\n\nMyISAM 只在單人使用或者唯讀作業的情況之下才可能有較快的處理能力。\n相較於 InnoDB,MyISAM 也較容易出現資料損毀的情況。", - "config-mssql-auth": "身份驗證類型:", - "config-mssql-install-auth": "請選擇安裝程序中要用來連線資料庫使用的身份驗證類型。\n若您選擇 \"{{int:config-mssql-windowsauth}}\",不論網頁伺服器是使用何種身份執行都會使用這組驗證資料。", - "config-mssql-web-auth": "請選擇一般操作中要用來連線資料庫使用的身份驗證類型。\n若您選擇 \"{{int:config-mssql-windowsauth}}\",不論網頁伺服器是使用何種身份執行都會使用這組驗證資料。", - "config-mssql-sqlauth": "SQL Server 身份驗證", - "config-mssql-windowsauth": "Windows 身份驗證", "config-site-name": "wiki 的名稱:", "config-site-name-help": "您所填入的內容會出現在瀏覽器的標題列以及各種其他地方。", "config-site-name-blank": "請輸入網站名稱。", diff --git a/includes/jobqueue/Job.php b/includes/jobqueue/Job.php index c87dedc722..29086e69a1 100644 --- a/includes/jobqueue/Job.php +++ b/includes/jobqueue/Job.php @@ -135,7 +135,7 @@ abstract class Job implements RunnableJob { // When constructing this class for submitting to the queue, // normalise the $title arg of old job classes as part of $params. $params['namespace'] = $title->getNamespace(); - $params['title'] = $title->getDBKey(); + $params['title'] = $title->getDBkey(); } $this->command = $command; diff --git a/includes/jobqueue/JobQueueDB.php b/includes/jobqueue/JobQueueDB.php index d449e8a259..4be41a20e2 100644 --- a/includes/jobqueue/JobQueueDB.php +++ b/includes/jobqueue/JobQueueDB.php @@ -424,7 +424,7 @@ class JobQueueDB extends JobQueue { if ( $dbw->getType() === 'mysql' ) { // Per https://bugs.mysql.com/bug.php?id=6980, we can't use subqueries on the // same table being changed in an UPDATE query in MySQL (gives Error: 1093). - // Oracle and Postgre have no such limitation. However, MySQL offers an + // Postgres has no such limitation. However, MySQL offers an // alternative here by supporting ORDER BY + LIMIT for UPDATE queries. $dbw->query( "UPDATE {$dbw->tableName( 'job' )} " . "SET " . diff --git a/includes/jobqueue/JobQueueGroup.php b/includes/jobqueue/JobQueueGroup.php index 06cd04ce6d..7bc97d82fe 100644 --- a/includes/jobqueue/JobQueueGroup.php +++ b/includes/jobqueue/JobQueueGroup.php @@ -397,7 +397,8 @@ class JobQueueGroup { } /** - * @return JobQueue[] + * @return array[] + * @phan-return array}> */ protected function getCoalescedQueues() { global $wgJobTypeConf; diff --git a/includes/jobqueue/JobRunner.php b/includes/jobqueue/JobRunner.php index 21d8c7e8af..709a67b01f 100644 --- a/includes/jobqueue/JobRunner.php +++ b/includes/jobqueue/JobRunner.php @@ -279,16 +279,26 @@ class JobRunner implements LoggerAwareInterface { ] ); $this->debugCallback( $msg ); + // Clear out title cache data from prior snapshots + // (e.g. from before JobRunner was invoked in this process) + MediaWikiServices::getInstance()->getLinkCache()->clear(); + // Run the job... $rssStart = $this->getMaxRssKb(); $jobStartTime = microtime( true ); try { $fnameTrxOwner = get_class( $job ) . '::run'; // give run() outer scope - if ( !$job->hasExecutionFlag( $job::JOB_NO_EXPLICIT_TRX_ROUND ) ) { - $lbFactory->beginMasterChanges( $fnameTrxOwner ); + // Flush any pending changes left over from an implicit transaction round + if ( $job->hasExecutionFlag( $job::JOB_NO_EXPLICIT_TRX_ROUND ) ) { + $lbFactory->commitMasterChanges( $fnameTrxOwner ); // new implicit round + } else { + $lbFactory->beginMasterChanges( $fnameTrxOwner ); // new explicit round } + // Clear any stale REPEATABLE-READ snapshots from replica DB connections + $lbFactory->flushReplicaSnapshots( $fnameTrxOwner ); $status = $job->run(); $error = $job->getLastError(); + // Commit all pending changes from this job $this->commitMasterChanges( $lbFactory, $job, $fnameTrxOwner ); // Run any deferred update tasks; doUpdates() manages transactions itself DeferredUpdates::doUpdates(); @@ -299,17 +309,11 @@ class JobRunner implements LoggerAwareInterface { } // Always attempt to call teardown() even if Job throws exception. try { - $job->teardown( $status ); + $job->tearDown( $status ); } catch ( Exception $e ) { MWExceptionHandler::logException( $e ); } - // Commit all outstanding connections that are in a transaction - // to get a fresh repeatable read snapshot on every connection. - // Note that jobs are still responsible for handling replica DB lag. - $lbFactory->flushReplicaSnapshots( __METHOD__ ); - // Clear out title cache data from prior snapshots - MediaWikiServices::getInstance()->getLinkCache()->clear(); $timeMs = intval( ( microtime( true ) - $jobStartTime ) * 1000 ); $rssEnd = $this->getMaxRssKb(); diff --git a/includes/jobqueue/jobs/ActivityUpdateJob.php b/includes/jobqueue/jobs/ActivityUpdateJob.php index 4de72a9b12..d27056dc02 100644 --- a/includes/jobqueue/jobs/ActivityUpdateJob.php +++ b/includes/jobqueue/jobs/ActivityUpdateJob.php @@ -42,7 +42,7 @@ class ActivityUpdateJob extends Job { static $required = [ 'type', 'userid', 'notifTime', 'curTime' ]; $missing = implode( ', ', array_diff( $required, array_keys( $this->params ) ) ); if ( $missing != '' ) { - throw new InvalidArgumentException( "Missing paramter(s) $missing" ); + throw new InvalidArgumentException( "Missing parameter(s) $missing" ); } $this->removeDuplicates = true; diff --git a/includes/jobqueue/jobs/RecentChangesUpdateJob.php b/includes/jobqueue/jobs/RecentChangesUpdateJob.php index 2d4ce34c83..63e6da47ca 100644 --- a/includes/jobqueue/jobs/RecentChangesUpdateJob.php +++ b/includes/jobqueue/jobs/RecentChangesUpdateJob.php @@ -168,7 +168,7 @@ class RecentChangesUpdateJob extends Job { ], __METHOD__, [ - 'GROUP BY' => [ 'rc_user_text' ], + 'GROUP BY' => [ $actorQuery['fields']['rc_user_text'] ], 'ORDER BY' => 'NULL' // avoid filesort ], $actorQuery['joins'] diff --git a/includes/jobqueue/jobs/RefreshLinksJob.php b/includes/jobqueue/jobs/RefreshLinksJob.php index b4046a61bc..33b05b8571 100644 --- a/includes/jobqueue/jobs/RefreshLinksJob.php +++ b/includes/jobqueue/jobs/RefreshLinksJob.php @@ -276,8 +276,8 @@ class RefreshLinksJob extends Job { $title = $page->getTitle(); // Get the latest ID since acquirePageLock() in runForTitle() flushed the transaction. // This is used to detect edits/moves after loadPageData() but before the scope lock. - // The works around the chicken/egg problem of determining the scope lock key name. - $latest = $title->getLatestRevID( Title::GAID_FOR_UPDATE ); + // The works around the chicken/egg problem of determining the scope lock key name + $latest = $title->getLatestRevID( Title::READ_LATEST ); $triggeringRevisionId = $this->params['triggeringRevisionId'] ?? null; if ( $triggeringRevisionId && $triggeringRevisionId !== $latest ) { diff --git a/includes/jobqueue/jobs/ThumbnailRenderJob.php b/includes/jobqueue/jobs/ThumbnailRenderJob.php index 85e3af9d2d..28e6433193 100644 --- a/includes/jobqueue/jobs/ThumbnailRenderJob.php +++ b/includes/jobqueue/jobs/ThumbnailRenderJob.php @@ -108,7 +108,8 @@ class ThumbnailRenderJob extends Job { // T203135 We don't wait for the request to complete, as this is mostly fire & forget. // Looking at the HTTP status of requests that take less than 1s is a sanity check. - $request = MWHttpRequest::factory( $thumbUrl, + $request = MediaWikiServices::getInstance()->getHttpRequestFactory()->create( + $thumbUrl, [ 'method' => 'HEAD', 'followRedirects' => true, 'timeout' => 1 ], __METHOD__ ); diff --git a/includes/language/ConverterRule.php b/includes/language/ConverterRule.php new file mode 100644 index 0000000000..4a330ad6ac --- /dev/null +++ b/includes/language/ConverterRule.php @@ -0,0 +1,498 @@ +, PhiLiP + */ +class ConverterRule { + public $mText; // original text in -{text}- + public $mConverter; // LanguageConverter object + public $mRuleDisplay = ''; + public $mRuleTitle = false; + public $mRules = ''; // string : the text of the rules + public $mRulesAction = 'none'; + public $mFlags = []; + public $mVariantFlags = []; + public $mConvTable = []; + public $mBidtable = []; // array of the translation in each variant + public $mUnidtable = []; // array of the translation in each variant + + /** + * @param string $text The text between -{ and }- + * @param LanguageConverter $converter + */ + public function __construct( $text, $converter ) { + $this->mText = $text; + $this->mConverter = $converter; + } + + /** + * Check if variants array in convert array. + * + * @param array|string $variants Variant language code + * @return string Translated text + */ + public function getTextInBidtable( $variants ) { + $variants = (array)$variants; + if ( !$variants ) { + return false; + } + foreach ( $variants as $variant ) { + if ( isset( $this->mBidtable[$variant] ) ) { + return $this->mBidtable[$variant]; + } + } + return false; + } + + /** + * Parse flags with syntax -{FLAG| ... }- + * @private + */ + function parseFlags() { + $text = $this->mText; + $flags = []; + $variantFlags = []; + + $sepPos = strpos( $text, '|' ); + if ( $sepPos !== false ) { + $validFlags = $this->mConverter->mFlags; + $f = StringUtils::explode( ';', substr( $text, 0, $sepPos ) ); + foreach ( $f as $ff ) { + $ff = trim( $ff ); + if ( isset( $validFlags[$ff] ) ) { + $flags[$validFlags[$ff]] = true; + } + } + $text = strval( substr( $text, $sepPos + 1 ) ); + } + + if ( !$flags ) { + $flags['S'] = true; + } elseif ( isset( $flags['R'] ) ) { + $flags = [ 'R' => true ];// remove other flags + } elseif ( isset( $flags['N'] ) ) { + $flags = [ 'N' => true ];// remove other flags + } elseif ( isset( $flags['-'] ) ) { + $flags = [ '-' => true ];// remove other flags + } elseif ( count( $flags ) == 1 && isset( $flags['T'] ) ) { + $flags['H'] = true; + } elseif ( isset( $flags['H'] ) ) { + // replace A flag, and remove other flags except T + $temp = [ '+' => true, 'H' => true ]; + if ( isset( $flags['T'] ) ) { + $temp['T'] = true; + } + if ( isset( $flags['D'] ) ) { + $temp['D'] = true; + } + $flags = $temp; + } else { + if ( isset( $flags['A'] ) ) { + $flags['+'] = true; + $flags['S'] = true; + } + if ( isset( $flags['D'] ) ) { + unset( $flags['S'] ); + } + // try to find flags like "zh-hans", "zh-hant" + // allow syntaxes like "-{zh-hans;zh-hant|XXXX}-" + $variantFlags = array_intersect( array_keys( $flags ), $this->mConverter->mVariants ); + if ( $variantFlags ) { + $variantFlags = array_flip( $variantFlags ); + $flags = []; + } + } + $this->mVariantFlags = $variantFlags; + $this->mRules = $text; + $this->mFlags = $flags; + } + + /** + * Generate conversion table. + * @private + */ + function parseRules() { + $rules = $this->mRules; + $bidtable = []; + $unidtable = []; + $variants = $this->mConverter->mVariants; + $varsep_pattern = $this->mConverter->getVarSeparatorPattern(); + + // Split according to $varsep_pattern, but ignore semicolons from HTML entities + $rules = preg_replace( '/(&[#a-zA-Z0-9]+);/', "$1\x01", $rules ); + $choice = preg_split( $varsep_pattern, $rules ); + $choice = str_replace( "\x01", ';', $choice ); + + foreach ( $choice as $c ) { + $v = explode( ':', $c, 2 ); + if ( count( $v ) != 2 ) { + // syntax error, skip + continue; + } + $to = trim( $v[1] ); + $v = trim( $v[0] ); + $u = explode( '=>', $v, 2 ); + $vv = $this->mConverter->validateVariant( $v ); + // if $to is empty (which is also used as $from in bidtable), + // strtr() could return a wrong result. + if ( count( $u ) == 1 && $to !== '' && $vv ) { + $bidtable[$vv] = $to; + } elseif ( count( $u ) == 2 ) { + $from = trim( $u[0] ); + $v = trim( $u[1] ); + $vv = $this->mConverter->validateVariant( $v ); + // if $from is empty, strtr() could return a wrong result. + if ( array_key_exists( $vv, $unidtable ) + && !is_array( $unidtable[$vv] ) + && $from !== '' + && $vv ) { + $unidtable[$vv] = [ $from => $to ]; + } elseif ( $from !== '' && $vv ) { + $unidtable[$vv][$from] = $to; + } + } + // syntax error, pass + if ( !isset( $this->mConverter->mVariantNames[$vv] ) ) { + $bidtable = []; + $unidtable = []; + break; + } + } + $this->mBidtable = $bidtable; + $this->mUnidtable = $unidtable; + } + + /** + * @private + * + * @return string + */ + function getRulesDesc() { + $codesep = $this->mConverter->mDescCodeSep; + $varsep = $this->mConverter->mDescVarSep; + $text = ''; + foreach ( $this->mBidtable as $k => $v ) { + $text .= $this->mConverter->mVariantNames[$k] . "$codesep$v$varsep"; + } + foreach ( $this->mUnidtable as $k => $a ) { + foreach ( $a as $from => $to ) { + $text .= $from . '⇒' . $this->mConverter->mVariantNames[$k] . + "$codesep$to$varsep"; + } + } + return $text; + } + + /** + * Parse rules conversion. + * @private + * + * @param string $variant + * + * @return string + */ + function getRuleConvertedStr( $variant ) { + $bidtable = $this->mBidtable; + $unidtable = $this->mUnidtable; + + if ( count( $bidtable ) + count( $unidtable ) == 0 ) { + return $this->mRules; + } else { + // display current variant in bidirectional array + $disp = $this->getTextInBidtable( $variant ); + // or display current variant in fallbacks + if ( $disp === false ) { + $disp = $this->getTextInBidtable( + $this->mConverter->getVariantFallbacks( $variant ) ); + } + // or display current variant in unidirectional array + if ( $disp === false && array_key_exists( $variant, $unidtable ) ) { + $disp = array_values( $unidtable[$variant] )[0]; + } + // or display first text under disable manual convert + if ( $disp === false && $this->mConverter->mManualLevel[$variant] == 'disable' ) { + if ( count( $bidtable ) > 0 ) { + $disp = array_values( $bidtable )[0]; + } else { + $disp = array_values( array_values( $unidtable )[0] )[0]; + } + } + return $disp; + } + } + + /** + * Similar to getRuleConvertedStr(), but this prefers to use original + * page title if $variant === $this->mConverter->mMainLanguageCode + * and may return false in this case (so this title conversion rule + * will be ignored and the original title is shown). + * + * @since 1.22 + * @param string $variant The variant code to display page title in + * @return string|bool The converted title or false if just page name + */ + function getRuleConvertedTitle( $variant ) { + if ( $variant === $this->mConverter->mMainLanguageCode ) { + // If a string targeting exactly this variant is set, + // use it. Otherwise, just return false, so the real + // page name can be shown (and because variant === main, + // there'll be no further automatic conversion). + $disp = $this->getTextInBidtable( $variant ); + if ( $disp ) { + return $disp; + } + if ( array_key_exists( $variant, $this->mUnidtable ) ) { + $disp = array_values( $this->mUnidtable[$variant] )[0]; + } + // Assigned above or still false. + return $disp; + } else { + return $this->getRuleConvertedStr( $variant ); + } + } + + /** + * Generate conversion table for all text. + * @private + */ + function generateConvTable() { + // Special case optimisation + if ( !$this->mBidtable && !$this->mUnidtable ) { + $this->mConvTable = []; + return; + } + + $bidtable = $this->mBidtable; + $unidtable = $this->mUnidtable; + $manLevel = $this->mConverter->mManualLevel; + + $vmarked = []; + foreach ( $this->mConverter->mVariants as $v ) { + /* for bidirectional array + fill in the missing variants, if any, + with fallbacks */ + if ( !isset( $bidtable[$v] ) ) { + $variantFallbacks = + $this->mConverter->getVariantFallbacks( $v ); + $vf = $this->getTextInBidtable( $variantFallbacks ); + if ( $vf ) { + $bidtable[$v] = $vf; + } + } + + if ( isset( $bidtable[$v] ) ) { + foreach ( $vmarked as $vo ) { + // use syntax: -{A|zh:WordZh;zh-tw:WordTw}- + // or -{H|zh:WordZh;zh-tw:WordTw}- + // or -{-|zh:WordZh;zh-tw:WordTw}- + // to introduce a custom mapping between + // words WordZh and WordTw in the whole text + if ( $manLevel[$v] == 'bidirectional' ) { + $this->mConvTable[$v][$bidtable[$vo]] = $bidtable[$v]; + } + if ( $manLevel[$vo] == 'bidirectional' ) { + $this->mConvTable[$vo][$bidtable[$v]] = $bidtable[$vo]; + } + } + $vmarked[] = $v; + } + /* for unidirectional array fill to convert tables */ + if ( ( $manLevel[$v] == 'bidirectional' || $manLevel[$v] == 'unidirectional' ) + && isset( $unidtable[$v] ) + ) { + if ( isset( $this->mConvTable[$v] ) ) { + $this->mConvTable[$v] = $unidtable[$v] + $this->mConvTable[$v]; + } else { + $this->mConvTable[$v] = $unidtable[$v]; + } + } + } + } + + /** + * Parse rules and flags. + * @param string|null $variant Variant language code + */ + public function parse( $variant = null ) { + if ( !$variant ) { + $variant = $this->mConverter->getPreferredVariant(); + } + + $this->parseFlags(); + $flags = $this->mFlags; + + // convert to specified variant + // syntax: -{zh-hans;zh-hant[;...]|}- + if ( $this->mVariantFlags ) { + // check if current variant in flags + if ( isset( $this->mVariantFlags[$variant] ) ) { + // then convert to current language + $this->mRules = $this->mConverter->autoConvert( $this->mRules, + $variant ); + } else { + // if current variant no in flags, + // then we check its fallback variants. + $variantFallbacks = + $this->mConverter->getVariantFallbacks( $variant ); + if ( is_array( $variantFallbacks ) ) { + foreach ( $variantFallbacks as $variantFallback ) { + // if current variant's fallback exist in flags + if ( isset( $this->mVariantFlags[$variantFallback] ) ) { + // then convert to fallback language + $this->mRules = + $this->mConverter->autoConvert( $this->mRules, + $variantFallback ); + break; + } + } + } + } + $this->mFlags = $flags = [ 'R' => true ]; + } + + if ( !isset( $flags['R'] ) && !isset( $flags['N'] ) ) { + // decode => HTML entities modified by Sanitizer::removeHTMLtags + $this->mRules = str_replace( '=>', '=>', $this->mRules ); + $this->parseRules(); + } + $rules = $this->mRules; + + if ( !$this->mBidtable && !$this->mUnidtable ) { + if ( isset( $flags['+'] ) || isset( $flags['-'] ) ) { + // fill all variants if text in -{A/H/-|text}- is non-empty but without rules + if ( $rules !== '' ) { + foreach ( $this->mConverter->mVariants as $v ) { + $this->mBidtable[$v] = $rules; + } + } + } elseif ( !isset( $flags['N'] ) && !isset( $flags['T'] ) ) { + $this->mFlags = $flags = [ 'R' => true ]; + } + } + + $this->mRuleDisplay = false; + foreach ( $flags as $flag => $unused ) { + switch ( $flag ) { + case 'R': + // if we don't do content convert, still strip the -{}- tags + $this->mRuleDisplay = $rules; + break; + case 'N': + // process N flag: output current variant name + $ruleVar = trim( $rules ); + $this->mRuleDisplay = $this->mConverter->mVariantNames[$ruleVar] ?? ''; + break; + case 'D': + // process D flag: output rules description + $this->mRuleDisplay = $this->getRulesDesc(); + break; + case 'H': + // process H,- flag or T only: output nothing + $this->mRuleDisplay = ''; + break; + case '-': + $this->mRulesAction = 'remove'; + $this->mRuleDisplay = ''; + break; + case '+': + $this->mRulesAction = 'add'; + $this->mRuleDisplay = ''; + break; + case 'S': + $this->mRuleDisplay = $this->getRuleConvertedStr( $variant ); + break; + case 'T': + $this->mRuleTitle = $this->getRuleConvertedTitle( $variant ); + $this->mRuleDisplay = ''; + break; + default: + // ignore unknown flags (but see error case below) + } + } + if ( $this->mRuleDisplay === false ) { + $this->mRuleDisplay = '' + . wfMessage( 'converter-manual-rule-error' )->inContentLanguage()->escaped() + . ''; + } + + $this->generateConvTable(); + } + + /** + * Checks if there are conversion rules. + * @return bool + */ + public function hasRules() { + return $this->mRules !== ''; + } + + /** + * Get display text on markup -{...}- + * @return string + */ + public function getDisplay() { + return $this->mRuleDisplay; + } + + /** + * Get converted title. + * @return string + */ + public function getTitle() { + return $this->mRuleTitle; + } + + /** + * Return how deal with conversion rules. + * @return string + */ + public function getRulesAction() { + return $this->mRulesAction; + } + + /** + * Get conversion table. (bidirectional and unidirectional + * conversion table) + * @return array + */ + public function getConvTable() { + return $this->mConvTable; + } + + /** + * Get conversion rules string. + * @return string + */ + public function getRules() { + return $this->mRules; + } + + /** + * Get conversion flags. + * @return array + */ + public function getFlags() { + return $this->mFlags; + } +} diff --git a/includes/language/Message.php b/includes/language/Message.php index 12007faf87..35cc34881d 100644 --- a/includes/language/Message.php +++ b/includes/language/Message.php @@ -158,6 +158,8 @@ use MediaWiki\MediaWikiServices; * @see https://www.mediawiki.org/wiki/Localisation * * @since 1.17 + * @phan-file-suppress PhanCommentParamOnEmptyParamList Cannot make variadic due to HHVM bug, + * T191668#5263929 */ class Message implements MessageSpecifier, Serializable { /** Use message text as-is */ @@ -404,6 +406,7 @@ class Message implements MessageSpecifier, Serializable { * * @param string|string[]|MessageSpecifier $key * @param mixed $param,... Parameters as strings. + * @suppress PhanCommentParamWithoutRealParam HHVM bug T228695#5450847 * * @return Message */ diff --git a/includes/language/MessageLocalizer.php b/includes/language/MessageLocalizer.php index 9a1796b140..fc514390dd 100644 --- a/includes/language/MessageLocalizer.php +++ b/includes/language/MessageLocalizer.php @@ -36,6 +36,7 @@ interface MessageLocalizer { * @param string|string[]|MessageSpecifier $key Message key, or array of keys, * or a MessageSpecifier. * @param mixed $params,... Normal message parameters + * @suppress PhanCommentParamWithoutRealParam HHVM bug T228695#5450847 * @return Message */ public function msg( $key /*...*/ ); diff --git a/includes/libs/ExplodeIterator.php b/includes/libs/ExplodeIterator.php index d4abdc86e8..7d2c5d62c8 100644 --- a/includes/libs/ExplodeIterator.php +++ b/includes/libs/ExplodeIterator.php @@ -89,7 +89,7 @@ class ExplodeIterator implements Iterator { } /** - * @return string + * @return void */ public function next() { if ( $this->endPos === false ) { @@ -103,8 +103,6 @@ class ExplodeIterator implements Iterator { } } $this->refreshCurrent(); - - return $this->current; } /** diff --git a/includes/libs/GenericArrayObject.php b/includes/libs/GenericArrayObject.php index a9b26ac913..2e0a2afe8c 100644 --- a/includes/libs/GenericArrayObject.php +++ b/includes/libs/GenericArrayObject.php @@ -211,6 +211,7 @@ abstract class GenericArrayObject extends ArrayObject { * @param string $serialization * * @return array + * @suppress PhanParamSignatureMismatchInternal The stub appears to be wrong */ public function unserialize( $serialization ) { $serializationData = unserialize( $serialization ); diff --git a/includes/libs/HashRing.php b/includes/libs/HashRing.php index 251fa88bae..94413c2be5 100644 --- a/includes/libs/HashRing.php +++ b/includes/libs/HashRing.php @@ -44,9 +44,9 @@ class HashRing implements Serializable { /** @var int[] Map of (location => UNIX timestamp) */ protected $ejectExpiryByLocation; - /** @var array[] Non-empty list of (float, node name, location name) */ + /** @var array[] Non-empty position-ordered list of (position, location name) */ protected $baseRing; - /** @var array[] Non-empty list of (float, node name, location name) */ + /** @var array[] Non-empty position-ordered list of (position, location name) */ protected $liveRing; /** @var float Number of positions on the ring */ @@ -96,7 +96,7 @@ class HashRing implements Serializable { $this->algo = $algo; $this->weightByLocation = $weightByLocation; $this->ejectExpiryByLocation = $ejections; - $this->baseRing = $this->buildLocationRing( $this->weightByLocation, $this->algo ); + $this->baseRing = $this->buildLocationRing( $this->weightByLocation ); } /** @@ -111,12 +111,13 @@ class HashRing implements Serializable { } /** - * Get the location of an item on the ring, as well as the next locations + * Get the location of an item on the ring followed by the next ring locations * * @param string $item * @param int $limit Maximum number of locations to return * @param int $from One of the RING_* class constants * @return string[] List of locations + * @throws InvalidArgumentException * @throws UnexpectedValueException */ public function getLocations( $item, $limit, $from = self::RING_ALL ) { @@ -128,22 +129,31 @@ class HashRing implements Serializable { throw new InvalidArgumentException( "Invalid ring source specified." ); } - // Locate this item's position on the hash ring - $position = $this->getItemPosition( $item ); - $itemNodeIndex = $this->findNodeIndexForPosition( $position, $ring ); + // Short-circuit for the common single-location case. Note that if there was only one + // location and it was ejected from the live ring, getLiveRing() would have error out. + if ( count( $this->weightByLocation ) == 1 ) { + return ( $limit > 0 ) ? [ $ring[0][self::KEY_LOCATION] ] : []; + } + + // Locate the node index for this item's position on the hash ring + $itemIndex = $this->findNodeIndexForPosition( $this->getItemPosition( $item ), $ring ); $locations = []; - $currentIndex = $itemNodeIndex; + $currentIndex = null; while ( count( $locations ) < $limit ) { + if ( $currentIndex === null ) { + $currentIndex = $itemIndex; + } else { + $currentIndex = $this->getNextClockwiseNodeIndex( $currentIndex, $ring ); + if ( $currentIndex === $itemIndex ) { + break; // all nodes visited + } + } $nodeLocation = $ring[$currentIndex][self::KEY_LOCATION]; if ( !in_array( $nodeLocation, $locations, true ) ) { // Ignore other nodes for the same locations already added $locations[] = $nodeLocation; } - $currentIndex = $this->getNextClockwiseNodeIndex( $currentIndex, $ring ); - if ( $currentIndex === $itemNodeIndex ) { - break; // all nodes visited - } } return $locations; @@ -159,18 +169,22 @@ class HashRing implements Serializable { if ( $count === 0 ) { return null; } + + $index = null; $lowPos = 0; $highPos = $count; while ( true ) { - $midPos = intval( ( $lowPos + $highPos ) / 2 ); + $midPos = (int)( ( $lowPos + $highPos ) / 2 ); if ( $midPos === $count ) { - return 0; + $index = 0; + break; } - $midVal = $ring[$midPos][self::KEY_POS]; - $midMinusOneVal = $midPos === 0 ? 0 : $ring[$midPos - 1][self::KEY_POS]; + $midVal = $ring[$midPos][self::KEY_POS]; + $midMinusOneVal = ( $midPos === 0 ) ? 0 : $ring[$midPos - 1][self::KEY_POS]; if ( $position <= $midVal && $position > $midMinusOneVal ) { - return $midPos; + $index = $midPos; + break; } if ( $midVal < $position ) { @@ -180,9 +194,12 @@ class HashRing implements Serializable { } if ( $lowPos > $highPos ) { - return 0; + $index = 0; + break; } } + + return $index; } /** @@ -260,10 +277,9 @@ class HashRing implements Serializable { /** * @param int[] $weightByLocation - * @param string $algo Hashing algorithm * @return array[] */ - private function buildLocationRing( array $weightByLocation, $algo ) { + private function buildLocationRing( array $weightByLocation ) { $locationCount = count( $weightByLocation ); $totalWeight = array_sum( $weightByLocation ); @@ -323,7 +339,14 @@ class HashRing implements Serializable { throw new UnexpectedValueException( __METHOD__ . ": {$this->algo} is < 32 bits." ); } - return (float)sprintf( '%u', unpack( 'V', $octets )[1] ); + $pos = unpack( 'V', $octets )[1]; + if ( $pos < 0 ) { + // Most-significant-bit is set, causing unpack() to return a negative integer due + // to the fact that it returns a signed int. Cast it to an unsigned integer string. + $pos = sprintf( '%u', $pos ); + } + + return (float)$pos; } /** diff --git a/includes/libs/MWMessagePack.php b/includes/libs/MWMessagePack.php index 107672e361..cb9e647f49 100644 --- a/includes/libs/MWMessagePack.php +++ b/includes/libs/MWMessagePack.php @@ -134,6 +134,7 @@ class MWMessagePack { // int64 // pack() does not support 64-bit ints either so pack into two 32-bits $p1 = pack( 'l', $value & 0xFFFFFFFF ); + // @phan-suppress-next-line PhanTypeInvalidLeftOperandOfIntegerOp $p2 = pack( 'l', ( $value >> 32 ) & 0xFFFFFFFF ); return self::$bigendian ? pack( 'Ca4a4', 0xD3, $p1, $p2 ) diff --git a/includes/libs/MappedIterator.php b/includes/libs/MappedIterator.php index 4a62e72909..0e53038d54 100644 --- a/includes/libs/MappedIterator.php +++ b/includes/libs/MappedIterator.php @@ -45,9 +45,10 @@ class MappedIterator extends FilterIterator { * the base iterator (post-callback) and will return true if that value should be * included in iteration of the MappedIterator (otherwise it will be filtered out). * - * @param Iterator|Array $iter + * @param Iterator|array $iter * @param callable $vCallback Value transformation callback * @param array $options Options map (includes "accept") (since 1.22) + * @phan-param array{accept?:callable} $options * @throws UnexpectedValueException */ public function __construct( $iter, $vCallback, array $options = [] ) { diff --git a/includes/libs/MemoizedCallable.php b/includes/libs/MemoizedCallable.php index 5e7485c279..f5112295fc 100644 --- a/includes/libs/MemoizedCallable.php +++ b/includes/libs/MemoizedCallable.php @@ -48,6 +48,9 @@ class MemoizedCallable { /** @var string Unique name of callable; used for cache keys. */ private $callableName; + /** @var int */ + private $ttl; + /** * @throws InvalidArgumentException if $callable is not a callable. * @param callable $callable Function or method to memoize. diff --git a/includes/libs/Message/IMessageFormatterFactory.php b/includes/libs/Message/IMessageFormatterFactory.php new file mode 100644 index 0000000000..337ea82436 --- /dev/null +++ b/includes/libs/Message/IMessageFormatterFactory.php @@ -0,0 +1,18 @@ +type = ParamType::LIST; + $this->listType = $listType; + $this->value = []; + foreach ( $elements as $element ) { + if ( $element instanceof MessageParam ) { + $this->value[] = $element; + } elseif ( is_scalar( $element ) || $element instanceof MessageValue ) { + $this->value[] = new ScalarParam( ParamType::TEXT, $element ); + } else { + throw new \InvalidArgumentException( + 'ListParam elements must be MessageParam or scalar' ); + } + } + } + + /** + * Get the type of the list + * + * @return string One of the ListType constants + */ + public function getListType() { + return $this->listType; + } + + public function dump() { + $contents = ''; + foreach ( $this->value as $element ) { + $contents .= $element->dump(); + } + return "<{$this->type} listType=\"{$this->listType}\">$contentstype}>"; + } +} diff --git a/includes/libs/Message/ListType.php b/includes/libs/Message/ListType.php new file mode 100644 index 0000000000..f846464daa --- /dev/null +++ b/includes/libs/Message/ListType.php @@ -0,0 +1,21 @@ +type; + } + + /** + * Get the input value of the parameter + * + * @return mixed + */ + public function getValue() { + return $this->value; + } + + /** + * Dump the object for testing/debugging + * + * @return string + */ + abstract public function dump(); +} diff --git a/includes/libs/Message/MessageValue.php b/includes/libs/Message/MessageValue.php new file mode 100644 index 0000000000..1d80d603fd --- /dev/null +++ b/includes/libs/Message/MessageValue.php @@ -0,0 +1,273 @@ +key = $key; + $this->params = []; + $this->params( ...$params ); + } + + /** + * Get the message key + * + * @return string + */ + public function getKey() { + return $this->key; + } + + /** + * Get the parameter array + * + * @return MessageParam[] + */ + public function getParams() { + return $this->params; + } + + /** + * Chainable mutator which adds text parameters and MessageParam parameters + * + * @param MessageParam|MessageValue|string|int|float ...$values + * @return MessageValue + */ + public function params( ...$values ) { + foreach ( $values as $value ) { + if ( $value instanceof MessageParam ) { + $this->params[] = $value; + } else { + $this->params[] = new ScalarParam( ParamType::TEXT, $value ); + } + } + return $this; + } + + /** + * Chainable mutator which adds text parameters with a common type + * + * @param string $type One of the ParamType constants + * @param MessageValue|string|int|float ...$values Scalar values + * @return MessageValue + */ + public function textParamsOfType( $type, ...$values ) { + foreach ( $values as $value ) { + $this->params[] = new ScalarParam( $type, $value ); + } + return $this; + } + + /** + * Chainable mutator which adds list parameters with a common type + * + * @param string $listType One of the ListType constants + * @param (MessageParam|MessageValue|string|int|float)[] ...$values Each value + * is an array of items suitable to pass as $params to ListParam::__construct() + * @return MessageValue + */ + public function listParamsOfType( $listType, ...$values ) { + foreach ( $values as $value ) { + $this->params[] = new ListParam( $listType, $value ); + } + return $this; + } + + /** + * Chainable mutator which adds parameters of type text (ParamType::TEXT). + * + * @param MessageValue|string|int|float ...$values + * @return MessageValue + */ + public function textParams( ...$values ) { + return $this->textParamsOfType( ParamType::TEXT, ...$values ); + } + + /** + * Chainable mutator which adds numeric parameters (ParamType::NUM). + * + * @param int|float ...$values + * @return MessageValue + */ + public function numParams( ...$values ) { + return $this->textParamsOfType( ParamType::NUM, ...$values ); + } + + /** + * Chainable mutator which adds parameters which are a duration specified + * in seconds (ParamType::DURATION_LONG). + * + * This is similar to shorDurationParams() except that the result will be + * more verbose. + * + * @param int|float ...$values + * @return MessageValue + */ + public function longDurationParams( ...$values ) { + return $this->textParamsOfType( ParamType::DURATION_LONG, ...$values ); + } + + /** + * Chainable mutator which adds parameters which are a duration specified + * in seconds (ParamType::DURATION_SHORT). + * + * This is similar to longDurationParams() except that the result will be more + * compact. + * + * @param int|float ...$values + * @return MessageValue + */ + public function shortDurationParams( ...$values ) { + return $this->textParamsOfType( ParamType::DURATION_SHORT, ...$values ); + } + + /** + * Chainable mutator which adds parameters which are an expiry timestamp (ParamType::EXPIRY). + * + * @param string ...$values Timestamp as accepted by the Wikimedia\Timestamp library, + * or "infinity" + * @return MessageValue + */ + public function expiryParams( ...$values ) { + return $this->textParamsOfType( ParamType::EXPIRY, ...$values ); + } + + /** + * Chainable mutator which adds parameters which are a number of bytes (ParamType::SIZE). + * + * @param int ...$values + * @return MessageValue + */ + public function sizeParams( ...$values ) { + return $this->textParamsOfType( ParamType::SIZE, ...$values ); + } + + /** + * Chainable mutator which adds parameters which are a number of bits per + * second (ParamType::BITRATE). + * + * @param int|float ...$values + * @return MessageValue + */ + public function bitrateParams( ...$values ) { + return $this->textParamsOfType( ParamType::BITRATE, ...$values ); + } + + /** + * Chainable mutator which adds "raw" parameters (ParamType::RAW). + * + * Raw parameters are substituted after formatter processing. The caller is responsible + * for ensuring that the value will be safe for the intended output format, and + * documenting what that intended output format is. + * + * @param string ...$values + * @return MessageValue + */ + public function rawParams( ...$values ) { + return $this->textParamsOfType( ParamType::RAW, ...$values ); + } + + /** + * Chainable mutator which adds plaintext parameters (ParamType::PLAINTEXT). + * + * Plaintext parameters are substituted after formatter processing. The value + * will be escaped by the formatter as appropriate for the target output format + * so as to be represented as plain text rather than as any sort of markup. + * + * @param string ...$values + * @return MessageValue + */ + public function plaintextParams( ...$values ) { + return $this->textParamsOfType( ParamType::PLAINTEXT, ...$values ); + } + + /** + * Chainable mutator which adds comma lists (ListType::COMMA). + * + * The list parameters thus created are formatted as a comma-separated list, + * or some local equivalent. + * + * @param (MessageParam|MessageValue|string|int|float)[] ...$values Each value + * is an array of items suitable to pass as $params to ListParam::__construct() + * @return MessageValue + */ + public function commaListParams( ...$values ) { + return $this->listParamsOfType( ListType::COMMA, ...$values ); + } + + /** + * Chainable mutator which adds semicolon lists (ListType::SEMICOLON). + * + * The list parameters thus created are formatted as a semicolon-separated + * list, or some local equivalent. + * + * @param (MessageParam|MessageValue|string|int|float)[] ...$values Each value + * is an array of items suitable to pass as $params to ListParam::__construct() + * @return MessageValue + */ + public function semicolonListParams( ...$values ) { + return $this->listParamsOfType( ListType::SEMICOLON, ...$values ); + } + + /** + * Chainable mutator which adds pipe lists (ListType::PIPE). + * + * The list parameters thus created are formatted as a pipe ("|") -separated + * list, or some local equivalent. + * + * @param (MessageParam|MessageValue|string|int|float)[] ...$values Each value + * is an array of items suitable to pass as $params to ListParam::__construct() + * @return MessageValue + */ + public function pipeListParams( ...$values ) { + return $this->listParamsOfType( ListType::PIPE, ...$values ); + } + + /** + * Chainable mutator which adds natural-language lists (ListType::AND). + * + * The list parameters thus created, when formatted, are joined as in natural + * language. In English, this means a comma-separated list, with the last + * two elements joined with "and". + * + * @param (MessageParam|string)[] ...$values + * @return MessageValue + */ + public function textListParams( ...$values ) { + return $this->listParamsOfType( ListType::AND, ...$values ); + } + + /** + * Dump the object for testing/debugging + * + * @return string + */ + public function dump() { + $contents = ''; + foreach ( $this->params as $param ) { + $contents .= $param->dump(); + } + return '' . + $contents . ''; + } +} diff --git a/includes/libs/Message/ParamType.php b/includes/libs/Message/ParamType.php new file mode 100644 index 0000000000..4db7112342 --- /dev/null +++ b/includes/libs/Message/ParamType.php @@ -0,0 +1,65 @@ + +use Wikimedia\Message\MessageValue; +use Wikimedia\Message\MessageParam; +use Wikimedia\Message\ParamType; + +// Constructor interface +$message = new MessageValue( 'message-key', [ + 'parameter', + new MessageValue( 'another-message' ), + new MessageParam( ParamType::NUM, 12345 ), +] ); + +// Fluent interface +$message = ( new MessageValue( 'message-key' ) ) + ->params( 'parameter', new MessageValue( 'another-message' ) ) + ->numParams( 12345 ); + +// Formatting +$messageFormatter = $serviceContainter->get( 'MessageFormatterFactory' )->getTextFormatter( 'de' ); +$output = $messageFormatter->format( $message ); + + +Class Overview +-------------- + +### Messages + +Messages and their parameters are represented by newable value objects. + +**MessageValue** represents an instance of a message, holding the key and any +parameters. It is mutable in that parameters can be added to the object after +creation. + +**MessageParam** is an abstract value class representing a parameter to a message. +It has a type (using constants defined in the **ParamType** class) and a value. It +has two implementations: + +- **ScalarParam** represents a single-valued parameter, such as a text string, a + number, or another message. +- **ListParam** represents a list of values, which will be joined together with + appropriate separators. It has a "list type" (using constants defined in the + **ListType** class) defining the desired separators. + +### Formatters + +A formatter for a particular language is obtained from an implementation of +**IMessageFormatterFactory**. No implementation of this interface is provided by +this library. If an environment needs its formatters to vary behavior on things +other than the language code, for example selecting among multiple sources of +messages or markup language used for processing message texts, it should define +a MessageFormatterFactoryFactory of some sort to provide appropriate +IMessageFormatterFactory implementations. + +There is no one base interface for all formatters; the intent is that type +hinting will ensure that the formatter being used will produce output in the +expected output format. The defined output formats are: + +- **ITextFormatter** produces plain text output. + +No implementation of these interfaces are provided by this library. + +Formatter implementations are expected to perform the following procedure to +generate the output string: + +1. Fetch the message's translation in the formatter's language. Details of this + fetching are unspecified here. + - If no translation is found in the formatter's language, it should attempt + to fall back to appropriate other languages. Details of the fallback are + unspecified here. + - If no translation can be found in any fallback language, a string should + be returned that indicates at minimum the message key that was unable to + be found. +2. Replace placeholders with parameter values. + - Note that placeholders must not be replaced recursively. That is, if a + parameter's value contains text that looks like a placeholder, it must not + be replaced as if it really were a placeholder. + - Certain types of parameters are not substituted directly at this stage. + Instead their placeholders must be replaced with an opaque representation + that will not be misinterpreted during later stages. + - Parameters of type RAW or PLAINTEXT + - TEXT parameters with a MessageValue as the value + - LIST parameters with any late-substituted value as one of their values. +3. Process any formatting commands. +4. Process the source markup language to produce a string in the desired output + format. This may be a no-op, and may be combined with the previous step if + the markup language implements compatible formatting commands. +5. Replace any opaque representations from step 2 with the actual values of + the corresponding parameters. + +Guidelines for Interoperability +------------------------------- + +Besides allowing for libraries to safely supply their own translations for +every app using them, and apps to easily use libraries' translations instead of +having to retranslate everything, following these guidelines will also help +open source projects use [translatewiki.net] for crowdsourced volunteer +translation into many languages. + +### Language codes + +[BCP 47] language tags should be used for language codes. If a supplied +language tag is not recognized, at minimum the corresponding tag with all +optional subtags stripped should be tried as a fallback. + +All messages must have a translation in English (code "en"). All languages +should fall back to English as a last resort. + +The English translations should use `{{PLURAL:...}}` and `{{GENDER:...}}` even +when English doesn't make a grammatical distinction, to signal to translators +that plural/gender support is available. + +Language code "qqq" is reserved for documenting messages. Documentation should +describe the context in which the message is used and the values of all +parameters used with the message. Generally this is written in English. +Attempting to obtain a message formatter for "qqq" should return one for "en" +instead. + +Language code "qqx" is reserved for debugging. Rather than retrieving +translations from some underlying storage, every key should act as if it were +translated as something `(key-name: $1, $2, $3)` with the number of +placeholders depending on how many parameters are included in the +MessageValue. + +### Message keys + +Message keys intended for use with external implementations should follow +certain guidelines for interoperability: + +- Keys should be restricted to the regular expression `/^[a-z][a-z0-9-]*$/`. + That is, it should consist of lowercase ASCII letters, numbers, and hyphen + only, and should begin with a letter. +- Keys should be prefixed to help avoid collisions. For example, a library + named "ApplePicker" should prefix its message keys with "applepicker-". +- Common values needing translation, such as names of months and weekdays, + should not be prefixed by each library. Libraries needing these should use + keys from the [Common Locale Data Repository][CLDR] and document this + requirement, and environments should provide these messages. + +### Message format + +Placeholders are represented by `$1`, `$2`, `$3`, and so on. Text like `$100` +is interpreted as a placeholder for parameter 100 if 100 or more parameters +were supplied, as a placeholder for parameter 10 followed by text "0" if +between ten and 99 parameters were supplied, and as a placeholder for parameter +1 followed by text "00" if between one and nine parameters were supplied. + +All formatting commands look like `{{NAME:$value1|$value2|$value3|...}}`. Braces +are to be balanced, e.g. `{{NAME:foo|{{bar|baz}}}}` has $value1 as "foo" and +$value2 as "{{bar|baz}}". The name is always case-insensitive. + +Anything syntactically resembling a placeholder or formatting command that does +not correspond to an actual paramter or known command should be left unchanged +for processing by the markup language processor. + +Libraries providing messages for use by externally-defined formatters should +generally assume no markup language will be applied, and should avoid +constructs used by common markup languages unless they also make sense when +read as plain text. + +### Formatting commands + +The following formatting commands should be supported. + +#### PLURAL + +`{{PLURAL:$count|$formA|$formB|...}}` is used to produce plurals. + +$count is a number, which may have been formatted with ParamType::NUM. + +The number of forms and which count corresponds to which form depend on the +language, for example English uses `{{PLURAL:$1|one|other}}` while Arabic uses +`{{PLURAL:$1|zero|one|two|few|many|other}}`. Details are defined in +[CLDR][CLDR plurals]. + +It is not possible to "skip" positions while still suppling later ones. If too +few values are supplied, the final form is repeated for subsequent positions. + +If there is an explicit plural form to be given for a specific number, it may +be specified with syntax like `{{PLURAL:$1|one egg|$1 eggs|12=a dozen eggs}}`. + +#### GENDER + +`{{GENDER:$name|$masculine|$feminine|$unspecified}}` is used to handle +grammatical gender, typically when messages refer to user accounts. + +This supports three grammatical genders: "male", "female", and a third option +for cases where the gender is unspecified, unknown, or neither male nor female. +It does not attempt to handle animate-inanimate or [T-V] distinctions. + +$name is a user account name or other similar identifier. If the name given +does not correspond to any known user account, it should probably use the +$unspecified gender. + +If $feminine and/or $unspecified is not specified, the value of $masculine +is normally used in its place. + +#### GRAMMAR + +`{{GRAMMAR:$form|$term}}` converts a term to an appropriate grammatical form. + +If no mapping for $term to $form exists, $term should be returned unchanged. + +See [jQuery.i18n § Grammar][jQuery.i18n grammar] for details. + +#### BIDI + +`{{BIDI:$text}}` applies directional isolation to the wrapped text, to attempt +to avoid errors where directionally-neutral characters are wrongly displayed +when between LTR and RTL content. + +This should output U+202A (left-to-right embedding) or U+202B (right-to-left +embedding) before the text, depending on the directionality of the first +strongly-directional character in $text, and U+202C (pop directional +formatting) after, or do something equivalent for the target output format. + +### Supplying translations + +Code intending its messages to be used by externally-defined formatters should +supply the translations as described by +[jQuery.i18n § Message File Format][jQuery.i18n file format]. + +In brief, the base directory of the library should contain a directory named +"i18n". This directory should contain JSON files named by code such as +"en.json", "de.json", "qqq.json", each with contents like: + +```json +{ + "@metadata": { + "authors": [ + "Alice", + "Bob", + "Carol", + "David" + ], + "last-updated": "2012-09-21" + }, + "appname-title": "Example Application", + "appname-sub-title": "An example application", + "appname-header-introduction": "Introduction", + "appname-about": "About this application", + "appname-footer": "Footer text" +} +``` + +Formatter implementations should be able to consume message data supplied in +this format, either directly via registration of i18n directories to check or +by providing tooling to incorporate it during a build step. + + +--- +[jQuery.i18n]: https://github.com/wikimedia/jquery.i18n +[BCP 47]: https://tools.ietf.org/rfc/bcp/bcp47.txt +[CLDR]: http://cldr.unicode.org/ +[CLDR plurals]: https://www.unicode.org/cldr/charts/latest/supplemental/language_plural_rules.html +[jQuery.i18n grammar]: https://github.com/wikimedia/jquery.i18n#grammar +[jQuery.i18n file format]: https://github.com/wikimedia/jquery.i18n#message-file-format +[translatewiki.net]: https://translatewiki.net/wiki/Translating:New_project +[T-V]: https://en.wikipedia.org/wiki/T%E2%80%93V_distinction diff --git a/includes/libs/Message/ScalarParam.php b/includes/libs/Message/ScalarParam.php new file mode 100644 index 0000000000..c17bc7f149 --- /dev/null +++ b/includes/libs/Message/ScalarParam.php @@ -0,0 +1,30 @@ +type = $type; + $this->value = $value; + } + + public function dump() { + if ( $this->value instanceof MessageValue ) { + $contents = $this->value->dump(); + } else { + $contents = htmlspecialchars( $this->value ); + } + return "<{$this->type}>" . $contents . "type}>"; + } +} diff --git a/includes/libs/StatusValue.php b/includes/libs/StatusValue.php index 71a0e348dd..4b381f8de1 100644 --- a/includes/libs/StatusValue.php +++ b/includes/libs/StatusValue.php @@ -93,7 +93,7 @@ class StatusValue { * 1 => object(StatusValue) # The StatusValue with warning messages, only * ] * - * @return StatusValue[] + * @return static[] */ public function splitByErrorType() { $errorsOnlyStatusValue = clone $this; diff --git a/includes/libs/StringUtils.php b/includes/libs/StringUtils.php index 51d108168a..19dd8fe4d3 100644 --- a/includes/libs/StringUtils.php +++ b/includes/libs/StringUtils.php @@ -1,4 +1,7 @@ whether a warning happened) */ + private $warningTrapStack = []; /** * @see FileBackendStore::__construct() @@ -102,11 +102,9 @@ class FSFileBackend extends FileBackendStore { $this->basePath = null; // none; containers must have explicit paths } - if ( isset( $config['containerPaths'] ) ) { - $this->containerPaths = (array)$config['containerPaths']; - foreach ( $this->containerPaths as &$path ) { - $path = rtrim( $path, '/' ); // remove trailing slash - } + $this->containerPaths = []; + foreach ( ( $config['containerPaths'] ?? [] ) as $container => $path ) { + $this->containerPaths[$container] = rtrim( $path, '/' ); // remove trailing slash } $this->fileMode = $config['fileMode'] ?? 0644; @@ -124,7 +122,7 @@ class FSFileBackend extends FileBackendStore { // See https://www.php.net/manual/en/migration71.windows-support.php return 0; } else { - return FileBackend::ATTR_UNICODE_PATHS; + return self::ATTR_UNICODE_PATHS; } } @@ -137,7 +135,7 @@ class FSFileBackend extends FileBackendStore { } } - return null; + return null; // invalid } /** @@ -228,20 +226,12 @@ class FSFileBackend extends FileBackendStore { } if ( !empty( $params['async'] ) ) { // deferred - $tempFile = TempFSFile::factory( 'create_', 'tmp', $this->tmpDirectory ); + $tempFile = $this->stageContentAsTempFile( $params ); if ( !$tempFile ) { $status->fatal( 'backend-fail-create', $params['dst'] ); return $status; } - $this->trapWarnings(); - $bytes = file_put_contents( $tempFile->getPath(), $params['content'] ); - $this->untrapWarnings(); - if ( $bytes === false ) { - $status->fatal( 'backend-fail-create', $params['dst'] ); - - return $status; - } $cmd = implode( ' ', [ $this->isWindows ? 'COPY /B /Y' : 'cp', // (binary, overwrite) escapeshellarg( $this->cleanPathSlashes( $tempFile->getPath() ) ), @@ -376,34 +366,38 @@ class FSFileBackend extends FileBackendStore { protected function doMoveInternal( array $params ) { $status = $this->newStatus(); - $source = $this->resolveToFSPath( $params['src'] ); - if ( $source === null ) { + $fsSrcPath = $this->resolveToFSPath( $params['src'] ); + if ( $fsSrcPath === null ) { $status->fatal( 'backend-fail-invalidpath', $params['src'] ); return $status; } - $dest = $this->resolveToFSPath( $params['dst'] ); - if ( $dest === null ) { + $fsDstPath = $this->resolveToFSPath( $params['dst'] ); + if ( $fsDstPath === null ) { $status->fatal( 'backend-fail-invalidpath', $params['dst'] ); return $status; } - if ( !is_file( $source ) ) { - if ( empty( $params['ignoreMissingSource'] ) ) { - $status->fatal( 'backend-fail-move', $params['src'] ); - } - - return $status; // do nothing; either OK or bad status + if ( $fsSrcPath === $fsDstPath ) { + return $status; // no-op } + $ignoreMissing = !empty( $params['ignoreMissingSource'] ); + if ( !empty( $params['async'] ) ) { // deferred - $cmd = implode( ' ', [ - $this->isWindows ? 'MOVE /Y' : 'mv', // (overwrite) - escapeshellarg( $this->cleanPathSlashes( $source ) ), - escapeshellarg( $this->cleanPathSlashes( $dest ) ) - ] ); + // https://manpages.debian.org/buster/coreutils/mv.1.en.html + // https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/move + $encSrc = escapeshellarg( $this->cleanPathSlashes( $fsSrcPath ) ); + $encDst = escapeshellarg( $this->cleanPathSlashes( $fsDstPath ) ); + if ( $this->isWindows ) { + $writeCmd = "MOVE /Y $encSrc $encDst"; + $cmd = $ignoreMissing ? "IF EXIST $encSrc $writeCmd" : $writeCmd; + } else { + $writeCmd = "mv -f $encSrc $encDst"; + $cmd = $ignoreMissing ? "test -f $encSrc && $writeCmd" : $writeCmd; + } $handler = function ( $errors, StatusValue $status, array $params, $cmd ) { if ( $errors !== '' && !( $this->isWindows && $errors[0] === " " ) ) { $status->fatal( 'backend-fail-move', $params['src'], $params['dst'] ); @@ -412,11 +406,13 @@ class FSFileBackend extends FileBackendStore { }; $status->value = new FSFileOpHandle( $this, $params, $handler, $cmd ); } else { // immediate write - $this->trapWarnings(); - $ok = ( $source === $dest ) ? true : rename( $source, $dest ); - $this->untrapWarnings(); - clearstatcache(); // file no longer at source - if ( !$ok ) { + // Use rename() here since (a) this clears xattrs, (b) any threads still reading the + // old inode are unaffected since it writes to a new inode, and (c) this is fast and + // atomic within a file system volume (as is normally the case) + $this->trapWarnings( '/: No such file or directory$/' ); + $moved = rename( $fsSrcPath, $fsDstPath ); + $hadError = $this->untrapWarnings(); + if ( $hadError || ( !$moved && !$ignoreMissing ) ) { $status->fatal( 'backend-fail-move', $params['src'], $params['dst'] ); return $status; @@ -429,26 +425,25 @@ class FSFileBackend extends FileBackendStore { protected function doDeleteInternal( array $params ) { $status = $this->newStatus(); - $source = $this->resolveToFSPath( $params['src'] ); - if ( $source === null ) { + $fsSrcPath = $this->resolveToFSPath( $params['src'] ); + if ( $fsSrcPath === null ) { $status->fatal( 'backend-fail-invalidpath', $params['src'] ); return $status; } - if ( !is_file( $source ) ) { - if ( empty( $params['ignoreMissingSource'] ) ) { - $status->fatal( 'backend-fail-delete', $params['src'] ); - } - - return $status; // do nothing; either OK or bad status - } + $ignoreMissing = !empty( $params['ignoreMissingSource'] ); if ( !empty( $params['async'] ) ) { // deferred - $cmd = implode( ' ', [ - $this->isWindows ? 'DEL' : 'unlink', - escapeshellarg( $this->cleanPathSlashes( $source ) ) - ] ); + // https://manpages.debian.org/buster/coreutils/rm.1.en.html + // https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/del + $encSrc = escapeshellarg( $this->cleanPathSlashes( $fsSrcPath ) ); + if ( $this->isWindows ) { + $writeCmd = "DEL /Q $encSrc"; + $cmd = $ignoreMissing ? "IF EXIST $encSrc $writeCmd" : $writeCmd; + } else { + $cmd = $ignoreMissing ? "rm -f $encSrc" : "rm $encSrc"; + } $handler = function ( $errors, StatusValue $status, array $params, $cmd ) { if ( $errors !== '' && !( $this->isWindows && $errors[0] === " " ) ) { $status->fatal( 'backend-fail-delete', $params['src'] ); @@ -457,10 +452,10 @@ class FSFileBackend extends FileBackendStore { }; $status->value = new FSFileOpHandle( $this, $params, $handler, $cmd ); } else { // immediate write - $this->trapWarnings(); - $ok = unlink( $source ); - $this->untrapWarnings(); - if ( !$ok ) { + $this->trapWarnings( '/: No such file or directory$/' ); + $deleted = unlink( $fsSrcPath ); + $hadError = $this->untrapWarnings(); + if ( $hadError || ( !$deleted && !$ignoreMissing ) ) { $status->fatal( 'backend-fail-delete', $params['src'] ); return $status; @@ -483,7 +478,7 @@ class FSFileBackend extends FileBackendStore { $dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot; $existed = is_dir( $dir ); // already there? // Create the directory and its parents as needed... - $this->trapWarnings(); + AtEase::suppressWarnings(); if ( !$existed && !mkdir( $dir, $this->dirMode, true ) && !is_dir( $dir ) ) { $this->logger->error( __METHOD__ . ": cannot create directory $dir" ); $status->fatal( 'directorycreateerror', $params['dir'] ); // fails on races @@ -494,7 +489,7 @@ class FSFileBackend extends FileBackendStore { $this->logger->error( __METHOD__ . ": directory $dir is not readable" ); $status->fatal( 'directorynotreadableerror', $params['dir'] ); } - $this->untrapWarnings(); + AtEase::restoreWarnings(); // Respect any 'noAccess' or 'noListing' flags... if ( is_dir( $dir ) && !$existed ) { $status->merge( $this->doSecureInternal( $fullCont, $dirRel, $params ) ); @@ -519,9 +514,9 @@ class FSFileBackend extends FileBackendStore { } // Add a .htaccess file to the root of the container... if ( !empty( $params['noAccess'] ) && !file_exists( "{$contRoot}/.htaccess" ) ) { - $this->trapWarnings(); + AtEase::suppressWarnings(); $bytes = file_put_contents( "{$contRoot}/.htaccess", $this->htaccessPrivate() ); - $this->untrapWarnings(); + AtEase::restoreWarnings(); if ( $bytes === false ) { $storeDir = "mwstore://{$this->name}/{$shortCont}"; $status->fatal( 'backend-fail-create', "{$storeDir}/.htaccess" ); @@ -539,21 +534,17 @@ class FSFileBackend extends FileBackendStore { // Unseed new directories with a blank index.html, to allow crawling... if ( !empty( $params['listing'] ) && is_file( "{$dir}/index.html" ) ) { $exists = ( file_get_contents( "{$dir}/index.html" ) === $this->indexHtmlPrivate() ); - $this->trapWarnings(); - if ( $exists && !unlink( "{$dir}/index.html" ) ) { // reverse secure() + if ( $exists && !$this->unlink( "{$dir}/index.html" ) ) { // reverse secure() $status->fatal( 'backend-fail-delete', $params['dir'] . '/index.html' ); } - $this->untrapWarnings(); } // Remove the .htaccess file from the root of the container... if ( !empty( $params['access'] ) && is_file( "{$contRoot}/.htaccess" ) ) { $exists = ( file_get_contents( "{$contRoot}/.htaccess" ) === $this->htaccessPrivate() ); - $this->trapWarnings(); - if ( $exists && !unlink( "{$contRoot}/.htaccess" ) ) { // reverse secure() + if ( $exists && !$this->unlink( "{$contRoot}/.htaccess" ) ) { // reverse secure() $storeDir = "mwstore://{$this->name}/{$shortCont}"; $status->fatal( 'backend-fail-delete', "{$storeDir}/.htaccess" ); } - $this->untrapWarnings(); } return $status; @@ -564,11 +555,9 @@ class FSFileBackend extends FileBackendStore { list( , $shortCont, ) = FileBackend::splitStoragePath( $params['dir'] ); $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid $dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot; - $this->trapWarnings(); - if ( is_dir( $dir ) ) { - rmdir( $dir ); // remove directory if empty - } - $this->untrapWarnings(); + AtEase::suppressWarnings(); + rmdir( $dir ); // remove directory if empty + AtEase::restoreWarnings(); return $status; } @@ -576,25 +565,23 @@ class FSFileBackend extends FileBackendStore { protected function doGetFileStat( array $params ) { $source = $this->resolveToFSPath( $params['src'] ); if ( $source === null ) { - return false; // invalid storage path + return self::$RES_ERROR; // invalid storage path } $this->trapWarnings(); // don't trust 'false' if there were errors $stat = is_file( $source ) ? stat( $source ) : false; // regular files only $hadError = $this->untrapWarnings(); - if ( $stat ) { + if ( is_array( $stat ) ) { $ct = new ConvertibleTimestamp( $stat['mtime'] ); return [ 'mtime' => $ct->getTimestamp( TS_MW ), 'size' => $stat['size'] ]; - } elseif ( !$hadError ) { - return false; // file does not exist - } else { - return null; // failure } + + return $hadError ? self::$RES_ERROR : self::$RES_ABSENT; } protected function doClearCache( array $paths = null ) { @@ -610,7 +597,7 @@ class FSFileBackend extends FileBackendStore { $exists = is_dir( $dir ); $hadError = $this->untrapWarnings(); - return $hadError ? null : $exists; + return $hadError ? self::$RES_ERROR : $exists; } /** @@ -624,18 +611,27 @@ class FSFileBackend extends FileBackendStore { list( , $shortCont, ) = FileBackend::splitStoragePath( $params['dir'] ); $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid $dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot; + + $this->trapWarnings(); // don't trust 'false' if there were errors $exists = is_dir( $dir ); - if ( !$exists ) { - $this->logger->warning( __METHOD__ . "() given directory does not exist: '$dir'\n" ); + $isReadable = $exists ? is_readable( $dir ) : false; + $hadError = $this->untrapWarnings(); - return []; // nothing under this dir - } elseif ( !is_readable( $dir ) ) { - $this->logger->warning( __METHOD__ . "() given directory is unreadable: '$dir'\n" ); + if ( $isReadable ) { + return new FSFileBackendDirList( $dir, $params ); + } elseif ( $exists ) { + $this->logger->warning( __METHOD__ . ": given directory is unreadable: '$dir'" ); - return null; // bad permissions? - } + return self::$RES_ERROR; // bad permissions? + } elseif ( $hadError ) { + $this->logger->warning( __METHOD__ . ": given directory was unreachable: '$dir'" ); + + return self::$RES_ERROR; + } else { + $this->logger->info( __METHOD__ . ": given directory does not exist: '$dir'" ); - return new FSFileBackendDirList( $dir, $params ); + return []; // nothing under this dir + } } /** @@ -649,18 +645,27 @@ class FSFileBackend extends FileBackendStore { list( , $shortCont, ) = FileBackend::splitStoragePath( $params['dir'] ); $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid $dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot; + + $this->trapWarnings(); // don't trust 'false' if there were errors $exists = is_dir( $dir ); - if ( !$exists ) { - $this->logger->warning( __METHOD__ . "() given directory does not exist: '$dir'\n" ); + $isReadable = $exists ? is_readable( $dir ) : false; + $hadError = $this->untrapWarnings(); - return []; // nothing under this dir - } elseif ( !is_readable( $dir ) ) { - $this->logger->warning( __METHOD__ . "() given directory is unreadable: '$dir'\n" ); + if ( $exists && $isReadable ) { + return new FSFileBackendFileList( $dir, $params ); + } elseif ( $exists ) { + $this->logger->warning( __METHOD__ . ": given directory is unreadable: '$dir'\n" ); - return null; // bad permissions? - } + return self::$RES_ERROR; // bad permissions? + } elseif ( $hadError ) { + $this->logger->warning( __METHOD__ . ": given directory was unreachable: '$dir'\n" ); - return new FSFileBackendFileList( $dir, $params ); + return self::$RES_ERROR; + } else { + $this->logger->info( __METHOD__ . ": given directory does not exist: '$dir'\n" ); + + return []; // nothing under this dir + } } protected function doGetLocalReferenceMulti( array $params ) { @@ -668,10 +673,21 @@ class FSFileBackend extends FileBackendStore { foreach ( $params['srcs'] as $src ) { $source = $this->resolveToFSPath( $src ); - if ( $source === null || !is_file( $source ) ) { - $fsFiles[$src] = null; // invalid path or file does not exist - } else { + if ( $source === null ) { + $fsFiles[$src] = self::$RES_ERROR; // invalid path + continue; + } + + $this->trapWarnings(); // don't trust 'false' if there were errors + $isFile = is_file( $source ); // regular files only + $hadError = $this->untrapWarnings(); + + if ( $isFile ) { $fsFiles[$src] = new FSFile( $source ); + } elseif ( $hadError ) { + $fsFiles[$src] = self::$RES_ERROR; + } else { + $fsFiles[$src] = self::$RES_ABSENT; } } @@ -684,26 +700,31 @@ class FSFileBackend extends FileBackendStore { foreach ( $params['srcs'] as $src ) { $source = $this->resolveToFSPath( $src ); if ( $source === null ) { - $tmpFiles[$src] = null; // invalid path + $tmpFiles[$src] = self::$RES_ERROR; // invalid path + continue; + } + // Create a new temporary file with the same extension... + $ext = FileBackend::extensionFromPath( $src ); + $tmpFile = $this->tmpFileFactory->newTempFSFile( 'localcopy_', $ext ); + if ( !$tmpFile ) { + $tmpFiles[$src] = self::$RES_ERROR; + continue; + } + + $tmpPath = $tmpFile->getPath(); + // Copy the source file over the temp file + $this->trapWarnings(); // don't trust 'false' if there were errors + $isFile = is_file( $source ); // regular files only + $copySuccess = $isFile ? copy( $source, $tmpPath ) : false; + $hadError = $this->untrapWarnings(); + + if ( $copySuccess ) { + $this->chmod( $tmpPath ); + $tmpFiles[$src] = $tmpFile; + } elseif ( $hadError ) { + $tmpFiles[$src] = self::$RES_ERROR; // copy failed } else { - // Create a new temporary file with the same extension... - $ext = FileBackend::extensionFromPath( $src ); - $tmpFile = TempFSFile::factory( 'localcopy_', $ext, $this->tmpDirectory ); - if ( !$tmpFile ) { - $tmpFiles[$src] = null; - } else { - $tmpPath = $tmpFile->getPath(); - // Copy the source file over the temp file - $this->trapWarnings(); - $ok = copy( $source, $tmpPath ); - $this->untrapWarnings(); - if ( !$ok ) { - $tmpFiles[$src] = null; - } else { - $this->chmod( $tmpPath ); - $tmpFiles[$src] = $tmpFile; - } - } + $tmpFiles[$src] = self::$RES_ABSENT; } } @@ -723,8 +744,19 @@ class FSFileBackend extends FileBackendStore { $statuses = []; $pipes = []; + $octalPermissions = '0' . decoct( $this->fileMode ); foreach ( $fileOpHandles as $index => $fileOpHandle ) { - $pipes[$index] = popen( "{$fileOpHandle->cmd} 2>&1", 'r' ); + $cmd = "{$fileOpHandle->cmd} 2>&1"; + // Add a post-operation chmod command for permissions cleanup if applicable + if ( + !$this->isWindows && + $fileOpHandle->chmodPath !== null && + strlen( $octalPermissions ) == 4 + ) { + $encPath = escapeshellarg( $fileOpHandle->chmodPath ); + $cmd .= " && chmod $octalPermissions $encPath 2>/dev/null"; + } + $pipes[$index] = popen( $cmd, 'r' ); } $errs = []; @@ -740,12 +772,10 @@ class FSFileBackend extends FileBackendStore { $function = $fileOpHandle->call; $function( $errs[$index], $status, $fileOpHandle->params, $fileOpHandle->cmd ); $statuses[$index] = $status; - if ( $status->isOK() && $fileOpHandle->chmodPath ) { - $this->chmod( $fileOpHandle->chmodPath ); - } } clearstatcache(); // files changed + return $statuses; } @@ -756,13 +786,52 @@ class FSFileBackend extends FileBackendStore { * @return bool Success */ protected function chmod( $path ) { - $this->trapWarnings(); + if ( $this->isWindows ) { + return true; + } + + AtEase::suppressWarnings(); $ok = chmod( $path, $this->fileMode ); - $this->untrapWarnings(); + AtEase::restoreWarnings(); return $ok; } + /** + * Unlink a file, suppressing the warnings + * + * @param string $path Absolute file system path + * @return bool Success + */ + protected function unlink( $path ) { + AtEase::suppressWarnings(); + $ok = unlink( $path ); + AtEase::restoreWarnings(); + + return $ok; + } + + /** + * @param array $params Operation parameters with 'content' and 'headers' fields + * @return TempFSFile|null + */ + protected function stageContentAsTempFile( array $params ) { + $content = $params['content']; + $tempFile = $this->tmpFileFactory->newTempFSFile( 'create_', 'tmp' ); + if ( !$tempFile ) { + return null; + } + + AtEase::suppressWarnings(); + $tmpPath = $tempFile->getPath(); + if ( file_put_contents( $tmpPath, $content ) === false ) { + $tempFile = null; + } + AtEase::restoreWarnings(); + + return $tempFile; + } + /** * Return the text of an index.html file to hide directory listings * @@ -792,33 +861,29 @@ class FSFileBackend extends FileBackendStore { } /** - * Listen for E_WARNING errors and track whether any happen + * Listen for E_WARNING errors and track whether any that happen + * + * @param string|null $regexIgnore Optional regex of errors to ignore */ - protected function trapWarnings() { - $this->hadWarningErrors[] = false; // push to stack - set_error_handler( [ $this, 'handleWarning' ], E_WARNING ); + protected function trapWarnings( $regexIgnore = null ) { + $this->warningTrapStack[] = false; + set_error_handler( function ( $errno, $errstr ) use ( $regexIgnore ) { + if ( $regexIgnore === null || !preg_match( $regexIgnore, $errstr ) ) { + $this->logger->error( $errstr ); + $this->warningTrapStack[count( $this->warningTrapStack ) - 1] = true; + } + return true; // suppress from PHP handler + }, E_WARNING ); } /** - * Stop listening for E_WARNING errors and return true if any happened + * Stop listening for E_WARNING errors and get whether any happened * - * @return bool + * @return bool Whether any warnings happened */ protected function untrapWarnings() { - restore_error_handler(); // restore previous handler - return array_pop( $this->hadWarningErrors ); // pop from stack - } - - /** - * @param int $errno - * @param string $errstr - * @return bool - * @private - */ - public function handleWarning( $errno, $errstr ) { - $this->logger->error( $errstr ); // more detailed error logging - $this->hadWarningErrors[count( $this->hadWarningErrors ) - 1] = true; + restore_error_handler(); - return true; // suppress from PHP handler + return array_pop( $this->warningTrapStack ); } } diff --git a/includes/libs/filebackend/FileBackend.php b/includes/libs/filebackend/FileBackend.php index f65619fd8c..6ab1707be6 100644 --- a/includes/libs/filebackend/FileBackend.php +++ b/includes/libs/filebackend/FileBackend.php @@ -27,6 +27,7 @@ * @file * @ingroup FileBackend */ +use MediaWiki\FileBackend\FSFile\TempFSFileFactory; use Psr\Log\LoggerAwareInterface; use Psr\Log\LoggerInterface; use Wikimedia\ScopedCallback; @@ -106,8 +107,8 @@ abstract class FileBackend implements LoggerAwareInterface { /** @var int How many operations can be done in parallel */ protected $concurrency; - /** @var string Temporary file directory */ - protected $tmpDirectory; + /** @var TempFSFileFactory */ + protected $tmpFileFactory; /** @var LockManager */ protected $lockManager; @@ -130,6 +131,29 @@ abstract class FileBackend implements LoggerAwareInterface { const ATTR_METADATA = 2; // files can be stored with metadata key/values const ATTR_UNICODE_PATHS = 4; // files can have Unicode paths (not just ASCII) + /** @var false Idiom for "no info; non-existant file" (since 1.34) */ + const STAT_ABSENT = false; + + /** @var null Idiom for "no info; I/O errors" (since 1.34) */ + const STAT_ERROR = null; + /** @var null Idiom for "no file/directory list; I/O errors" (since 1.34) */ + const LIST_ERROR = null; + /** @var null Idiom for "no temp URL; not supported or I/O errors" (since 1.34) */ + const TEMPURL_ERROR = null; + /** @var null Idiom for "existence unknown; I/O errors" (since 1.34) */ + const EXISTENCE_ERROR = null; + + /** @var false Idiom for "no timestamp; missing file or I/O errors" (since 1.34) */ + const TIMESTAMP_FAIL = false; + /** @var false Idiom for "no content; missing file or I/O errors" (since 1.34) */ + const CONTENT_FAIL = false; + /** @var false Idiom for "no metadata; missing file or I/O errors" (since 1.34) */ + const XATTRS_FAIL = false; + /** @var false Idiom for "no size; missing file or I/O errors" (since 1.34) */ + const SIZE_FAIL = false; + /** @var false Idiom for "no SHA1 hash; missing file or I/O errors" (since 1.34) */ + const SHA1_FAIL = false; + /** * Create a new backend instance from configuration. * This should only be called from within FileBackendGroup. @@ -152,8 +176,10 @@ abstract class FileBackend implements LoggerAwareInterface { * - parallelize : When to do file operations in parallel (when possible). * Allowed values are "implicit", "explicit" and "off". * - concurrency : How many file operations can be done in parallel. - * - tmpDirectory : Directory to use for temporary files. If this is not set or null, - * then the backend will try to discover a usable temporary directory. + * - tmpDirectory : Directory to use for temporary files. + * - tmpFileFactory : Optional TempFSFileFactory object. Only has an effect if + * tmpDirectory is not set. If both are unset or null, then the backend will + * try to discover a usable temporary directory. * - obResetFunc : alternative callback to clear the output buffer * - streamMimeFunc : alternative method to determine the content type from the path * - logger : Optional PSR logger object. @@ -193,7 +219,12 @@ abstract class FileBackend implements LoggerAwareInterface { } $this->logger = $config['logger'] ?? new NullLogger(); $this->statusWrapper = $config['statusWrapper'] ?? null; - $this->tmpDirectory = $config['tmpDirectory'] ?? null; + // tmpDirectory gets precedence for backward compatibility + if ( isset( $config['tmpDirectory'] ) ) { + $this->tmpFileFactory = new TempFSFileFactory( $config['tmpDirectory'] ); + } else { + $this->tmpFileFactory = $config['tmpFileFactory'] ?? new TempFSFileFactory(); + } } public function setLogger( LoggerInterface $logger ) { @@ -201,7 +232,8 @@ abstract class FileBackend implements LoggerAwareInterface { } /** - * Get the unique backend name. + * Get the unique backend name + * * We may have multiple different backends of the same type. * For example, we can have two Swift backends using different proxies. * @@ -223,6 +255,7 @@ abstract class FileBackend implements LoggerAwareInterface { /** * Alias to getDomainId() + * * @return string * @since 1.20 * @deprecated Since 1.34 Use getDomainId() @@ -412,10 +445,14 @@ abstract class FileBackend implements LoggerAwareInterface { * * The StatusValue will be "OK" unless: * - a) unexpected operation errors occurred (network partitions, disk full...) - * - b) significant operation errors occurred and 'force' was not set + * - b) predicted operation errors occurred and 'force' was not set * * @param array $ops List of operations to execute in order + * @codingStandardsIgnoreStart + * @phan-param array{ignoreMissingSource?:bool,overwrite?:bool,overwriteSame?:bool,headers?:bool} $ops * @param array $opts Batch operation options + * @phan-param array{force?:bool,nonLocking?:bool,nonJournaled?:bool,parallelize?:bool,bypassReadOnly?:bool,preserveCache?:bool} $opts + * @codingStandardsIgnoreEnd * @return StatusValue */ final public function doOperations( array $ops, array $opts = [] ) { @@ -653,7 +690,9 @@ abstract class FileBackend implements LoggerAwareInterface { * considered "OK" as long as no fatal errors occurred. * * @param array $ops Set of operations to execute + * @phan-param array{ignoreMissingSource?:bool,headers?:bool} $ops * @param array $opts Batch operation options + * @phan-param array{bypassReadOnly?:bool} $opts * @return StatusValue * @since 1.20 */ @@ -927,20 +966,29 @@ abstract class FileBackend implements LoggerAwareInterface { * Check if a file exists at a storage path in the backend. * This returns false if only a directory exists at the path. * + * Callers that only care if a file is readily accessible can use non-strict + * comparisons on the result. If "does not exist" and "existence is unknown" + * must be distinguished, then strict comparisons to true/null should be used. + * + * @see FileBackend::EXISTENCE_ERROR + * @see FileBackend::directoryExists() + * * @param array $params Parameters include: * - src : source storage path * - latest : use the latest available data - * @return bool|null Returns null on failure + * @return bool|null Whether the file exists or null (I/O error) */ abstract public function fileExists( array $params ); /** * Get the last-modified timestamp of the file at a storage path. * + * @see FileBackend::TIMESTAMP_FAIL + * * @param array $params Parameters include: * - src : source storage path * - latest : use the latest available data - * @return string|bool TS_MW timestamp or false on failure + * @return string|false TS_MW timestamp or false (missing file or I/O error) */ abstract public function getFileTimestamp( array $params ); @@ -948,22 +996,22 @@ abstract class FileBackend implements LoggerAwareInterface { * Get the contents of a file at a storage path in the backend. * This should be avoided for potentially large files. * + * @see FileBackend::CONTENT_FAIL + * * @param array $params Parameters include: * - src : source storage path * - latest : use the latest available data - * @return string|bool Returns false on failure + * @return string|false Content string or false (missing file or I/O error) */ final public function getFileContents( array $params ) { - $contents = $this->getFileContentsMulti( - [ 'srcs' => [ $params['src'] ] ] + $params ); + $contents = $this->getFileContentsMulti( [ 'srcs' => [ $params['src'] ] ] + $params ); return $contents[$params['src']]; } /** * Like getFileContents() except it takes an array of storage paths - * and returns a map of storage paths to strings (or null on failure). - * The map keys (paths) are in the same order as the provided list of paths. + * and returns an order preserved map of storage paths to their content. * * @see FileBackend::getFileContents() * @@ -971,7 +1019,7 @@ abstract class FileBackend implements LoggerAwareInterface { * - srcs : list of source storage paths * - latest : use the latest available data * - parallelize : try to do operations in parallel when possible - * @return array Map of (path name => string or false on failure) + * @return string[]|false[] Map of (path name => file content or false on failure) * @since 1.20 */ abstract public function getFileContentsMulti( array $params ); @@ -987,11 +1035,13 @@ abstract class FileBackend implements LoggerAwareInterface { * * Use FileBackend::hasFeatures() to check how well this is supported. * + * @see FileBackend::XATTRS_FAIL + * * @param array $params * $params include: * - src : source storage path * - latest : use the latest available data - * @return array|bool Returns false on failure + * @return array|false File metadata array or false (missing file or I/O error) * @since 1.23 */ abstract public function getFileXAttributes( array $params ); @@ -999,10 +1049,12 @@ abstract class FileBackend implements LoggerAwareInterface { /** * Get the size (bytes) of a file at a storage path in the backend. * + * @see FileBackend::SIZE_FAIL + * * @param array $params Parameters include: * - src : source storage path * - latest : use the latest available data - * @return int|bool Returns false on failure + * @return int|false File size in bytes or false (missing file or I/O error) */ abstract public function getFileSize( array $params ); @@ -1014,36 +1066,41 @@ abstract class FileBackend implements LoggerAwareInterface { * - size : the file size (bytes) * Additional values may be included for internal use only. * + * @see FileBackend::STAT_ABSENT + * @see FileBackend::STAT_ERROR + * * @param array $params Parameters include: * - src : source storage path * - latest : use the latest available data - * @return array|bool|null Returns null on failure + * @return array|false|null Attribute map, false (missing file), or null (I/O error) */ abstract public function getFileStat( array $params ); /** - * Get a SHA-1 hash of the file at a storage path in the backend. + * Get a SHA-1 hash of the content of the file at a storage path in the backend. + * + * @see FileBackend::SHA1_FAIL * * @param array $params Parameters include: * - src : source storage path * - latest : use the latest available data - * @return string|bool Hash string or false on failure + * @return string|false Hash string or false (missing file or I/O error) */ abstract public function getFileSha1Base36( array $params ); /** - * Get the properties of the file at a storage path in the backend. + * Get the properties of the content of the file at a storage path in the backend. * This gives the result of FSFile::getProps() on a local copy of the file. * * @param array $params Parameters include: * - src : source storage path * - latest : use the latest available data - * @return array Returns FSFile::placeholderProps() on failure + * @return array Properties map; FSFile::placeholderProps() if file missing or on I/O error */ abstract public function getFileProps( array $params ); /** - * Stream the file at a storage path in the backend. + * Stream the content of the file at a storage path in the backend. * * If the file does not exists, an HTTP 404 error will be given. * Appropriate HTTP headers (Status, Content-Type, Content-Length) @@ -1064,34 +1121,36 @@ abstract class FileBackend implements LoggerAwareInterface { abstract public function streamFile( array $params ); /** - * Returns a file system file, identical to the file at a storage path. + * Returns a file system file, identical in content to the file at a storage path. * The file returned is either: - * - a) A local copy of the file at a storage path in the backend. + * - a) A TempFSFile local copy of the file at a storage path in the backend. * The temporary copy will have the same extension as the source. - * - b) An original of the file at a storage path in the backend. - * Temporary files may be purged when the file object falls out of scope. + * Temporary files may be purged when the file object falls out of scope. + * - b) An FSFile pointing to the original file at a storage path in the backend. + * This is applicable for backends layered directly on top of file systems. * - * Write operations should *never* be done on this file as some backends - * may do internal tracking or may be instances of FileBackendMultiWrite. - * In that latter case, there are copies of the file that must stay in sync. - * Additionally, further calls to this function may return the same file. + * Never modify the returned file since it might be the original, it might be shared + * among multiple callers of this method, or the backend might internally keep FSFile + * references for deferred operations. * * @param array $params Parameters include: * - src : source storage path * - latest : use the latest available data - * @return FSFile|null Returns null on failure + * @return FSFile|null Local file copy or null (missing file or I/O error) */ final public function getLocalReference( array $params ) { - $fsFiles = $this->getLocalReferenceMulti( - [ 'srcs' => [ $params['src'] ] ] + $params ); + $fsFiles = $this->getLocalReferenceMulti( [ 'srcs' => [ $params['src'] ] ] + $params ); return $fsFiles[$params['src']]; } /** - * Like getLocalReference() except it takes an array of storage paths - * and returns a map of storage paths to FSFile objects (or null on failure). - * The map keys (paths) are in the same order as the provided list of paths. + * Like getLocalReference() except it takes an array of storage paths and + * yields an order-preserved map of storage paths to temporary local file copies. + * + * Never modify the returned files since they might be originals, they might be shared + * among multiple callers of this method, or the backend might internally keep FSFile + * references for deferred operations. * * @see FileBackend::getLocalReference() * @@ -1109,22 +1168,24 @@ abstract class FileBackend implements LoggerAwareInterface { * The temporary copy will have the same file extension as the source. * Temporary files may be purged when the file object falls out of scope. * + * Multiple calls to this method for the same path will create new copies. + * * @param array $params Parameters include: * - src : source storage path * - latest : use the latest available data - * @return TempFSFile|null Returns null on failure + * @return TempFSFile|null Temporary local file copy or null (missing file or I/O error) */ final public function getLocalCopy( array $params ) { - $tmpFiles = $this->getLocalCopyMulti( - [ 'srcs' => [ $params['src'] ] ] + $params ); + $tmpFiles = $this->getLocalCopyMulti( [ 'srcs' => [ $params['src'] ] ] + $params ); return $tmpFiles[$params['src']]; } /** - * Like getLocalCopy() except it takes an array of storage paths and - * returns a map of storage paths to TempFSFile objects (or null on failure). - * The map keys (paths) are in the same order as the provided list of paths. + * Like getLocalCopy() except it takes an array of storage paths and yields + * an order preserved-map of storage paths to temporary local file copies. + * + * Multiple calls to this method for the same path will create new copies. * * @see FileBackend::getLocalCopy() * @@ -1147,30 +1208,46 @@ abstract class FileBackend implements LoggerAwareInterface { * Otherwise, one would need to use getLocalReference(), which involves loading * the entire file on to local disk. * + * @see FileBackend::TEMPURL_ERROR + * * @param array $params Parameters include: * - src : source storage path * - ttl : lifetime (seconds) if pre-authenticated; default is 1 day - * @return string|null + * @return string|null URL or null (not supported or I/O error) * @since 1.21 */ abstract public function getFileHttpUrl( array $params ); /** - * Check if a directory exists at a given storage path. - * Backends using key/value stores will check if the path is a - * virtual directory, meaning there are files under the given directory. + * Check if a directory exists at a given storage path + * + * For backends using key/value stores, a directory is said to exist whenever + * there exist any files with paths using the given directory path as a prefix + * followed by a forward slash. For example, if there is a file called + * "mwstore://backend/container/dir/path.svg" then directories are said to exist + * at "mwstore://backend/container" and "mwstore://backend/container/dir". These + * can be thought of as "virtual" directories. + * + * Backends that directly use a filesystem layer might enumerate empty directories. + * The clean() method should always be used when files are deleted or moved if this + * is a concern. This is a trade-off to avoid write amplication/contention on file + * changes or read amplification when calling this method. * * Storage backends with eventual consistency might return stale data. * + * @see FileBackend::EXISTENCE_ERROR + * @see FileBackend::clean() + * * @param array $params Parameters include: * - dir : storage directory - * @return bool|null Returns null on failure + * @return bool|null Whether a directory exists or null (I/O error) * @since 1.20 */ abstract public function directoryExists( array $params ); /** - * Get an iterator to list *all* directories under a storage directory. + * Get an iterator to list *all* directories under a storage directory + * * If the directory is of the form "mwstore://backend/container", * then all directories in the container will be listed. * If the directory is of form "mwstore://backend/container/dir", @@ -1181,10 +1258,13 @@ abstract class FileBackend implements LoggerAwareInterface { * * Failures during iteration can result in FileBackendError exceptions (since 1.22). * + * @see FileBackend::LIST_ERROR + * @see FileBackend::directoryExists() + * * @param array $params Parameters include: * - dir : storage directory * - topOnly : only return direct child dirs of the directory - * @return Traversable|array|null Returns null on failure + * @return Traversable|array|null Directory list enumerator or null (initial I/O error) * @since 1.20 */ abstract public function getDirectoryList( array $params ); @@ -1197,9 +1277,12 @@ abstract class FileBackend implements LoggerAwareInterface { * * Failures during iteration can result in FileBackendError exceptions (since 1.22). * + * @see FileBackend::LIST_ERROR + * @see FileBackend::directoryExists() + * * @param array $params Parameters include: * - dir : storage directory - * @return Traversable|array|null Returns null on failure + * @return Traversable|array|null Directory list enumerator or null (initial I/O error) * @since 1.20 */ final public function getTopDirectoryList( array $params ) { @@ -1207,22 +1290,24 @@ abstract class FileBackend implements LoggerAwareInterface { } /** - * Get an iterator to list *all* stored files under a storage directory. - * If the directory is of the form "mwstore://backend/container", - * then all files in the container will be listed. - * If the directory is of form "mwstore://backend/container/dir", - * then all files under that directory will be listed. - * Results will be storage paths relative to the given directory. + * Get an iterator to list *all* stored files under a storage directory + * + * If the directory is of the form "mwstore://backend/container", then all + * files in the container will be listed. If the directory is of form + * "mwstore://backend/container/dir", then all files under that directory will + * be listed. Results will be storage paths relative to the given directory. * * Storage backends with eventual consistency might return stale data. * * Failures during iteration can result in FileBackendError exceptions (since 1.22). * + * @see FileBackend::LIST_ERROR + * * @param array $params Parameters include: * - dir : storage directory * - topOnly : only return direct child files of the directory (since 1.20) * - adviseStat : set to true if stat requests will be made on the files (since 1.22) - * @return Traversable|array|null Returns null on failure + * @return Traversable|array|null File list enumerator or null (initial I/O error) */ abstract public function getFileList( array $params ); @@ -1234,10 +1319,12 @@ abstract class FileBackend implements LoggerAwareInterface { * * Failures during iteration can result in FileBackendError exceptions (since 1.22). * + * @see FileBackend::LIST_ERROR + * * @param array $params Parameters include: * - dir : storage directory * - adviseStat : set to true if stat requests will be made on the files (since 1.22) - * @return Traversable|array|null Returns null on failure + * @return Traversable|array|null File list enumerator or null on failure * @since 1.20 */ final public function getTopFileList( array $params ) { @@ -1275,7 +1362,7 @@ abstract class FileBackend implements LoggerAwareInterface { * @param array $params Parameters include: * - srcs : list of source storage paths * - latest : use the latest available data - * @return bool All requests proceeded without I/O errors (since 1.24) + * @return bool Whether all requests proceeded without I/O errors (since 1.24) * @since 1.23 */ abstract public function preloadFileStat( array $params ); @@ -1324,7 +1411,7 @@ abstract class FileBackend implements LoggerAwareInterface { * @param int|string $type LockManager::LOCK_* constant or "mixed" * @param StatusValue $status StatusValue to update on lock/unlock * @param int $timeout Timeout in seconds (0 means non-blocking) (since 1.24) - * @return ScopedLock|null Returns null on failure + * @return ScopedLock|null RAII-style self-unlocking lock or null on failure */ final public function getScopedFileLocks( array $paths, $type, StatusValue $status, $timeout = 0 @@ -1353,7 +1440,7 @@ abstract class FileBackend implements LoggerAwareInterface { * * @param array $ops List of file operations to FileBackend::doOperations() * @param StatusValue $status StatusValue to update on lock/unlock - * @return ScopedLock|null + * @return ScopedLock|null RAII-style self-unlocking lock or null on failure * @since 1.20 */ abstract public function getScopedLocksForOps( array $ops, StatusValue $status ); @@ -1451,7 +1538,7 @@ abstract class FileBackend implements LoggerAwareInterface { * Returns null if the path is not of the format of a valid storage path. * * @param string $storagePath - * @return string|null + * @return string|null Normalized storage path or null on failure */ final public static function normalizeStoragePath( $storagePath ) { list( $backend, $container, $relPath ) = self::splitStoragePath( $storagePath ); @@ -1473,7 +1560,7 @@ abstract class FileBackend implements LoggerAwareInterface { * "mwstore://backend/container/...", or null if there is no parent. * * @param string $storagePath - * @return string|null + * @return string|null Parent storage path or null on failure */ final public static function parentStoragePath( $storagePath ) { $storagePath = dirname( $storagePath ); @@ -1518,7 +1605,7 @@ abstract class FileBackend implements LoggerAwareInterface { * * @param string $type One of (attachment, inline) * @param string $filename Suggested file name (should not contain slashes) - * @throws FileBackendError + * @throws InvalidArgumentException * @return string * @since 1.20 */ @@ -1546,7 +1633,7 @@ abstract class FileBackend implements LoggerAwareInterface { * This uses the same traversal protection as Title::secureAndSplit(). * * @param string $path Storage path relative to a container - * @return string|null + * @return string|null Normalized container path or null on failure */ final protected static function normalizeContainerPath( $path ) { // Normalize directory separators diff --git a/includes/libs/filebackend/FileBackendMultiWrite.php b/includes/libs/filebackend/FileBackendMultiWrite.php index f92d5fa0a9..cbfd76e407 100644 --- a/includes/libs/filebackend/FileBackendMultiWrite.php +++ b/includes/libs/filebackend/FileBackendMultiWrite.php @@ -21,6 +21,8 @@ * @ingroup FileBackend */ +use Wikimedia\Timestamp\ConvertibleTimestamp; + /** * @brief Proxy backend that mirrors writes to several internal backends. * @@ -57,9 +59,11 @@ class FileBackendMultiWrite extends FileBackend { /** @var bool */ protected $asyncWrites = false; - /* Possible internal backend consistency checks */ + /** @var int Compare file sizes among backends */ const CHECK_SIZE = 1; + /** @var int Compare file mtimes among backends */ const CHECK_TIME = 2; + /** @var int Compare file hashes among backends */ const CHECK_SHA1 = 4; /** @@ -88,7 +92,7 @@ class FileBackendMultiWrite extends FileBackend { * any checks from "syncChecks" are still synchronous. * * @param array $config - * @throws FileBackendError + * @throws LogicException */ public function __construct( array $config ) { parent::__construct( $config ); @@ -139,10 +143,8 @@ class FileBackendMultiWrite extends FileBackend { $mbe = $this->backends[$this->masterIndex]; // convenience - // Try to lock those files for the scope of this function... - $scopeLock = null; + // Acquire any locks as needed if ( empty( $opts['nonLocking'] ) ) { - // Try to lock those files for the scope of this function... /** @noinspection PhpUnusedLocalVariableInspection */ $scopeLock = $this->getScopedLocksForOps( $ops, $status ); if ( !$status->isOK() ) { @@ -152,35 +154,36 @@ class FileBackendMultiWrite extends FileBackend { // Clear any cache entries (after locks acquired) $this->clearCache(); $opts['preserveCache'] = true; // only locked files are cached - // Get the list of paths to read/write... + // Get the list of paths to read/write $relevantPaths = $this->fileStoragePathsForOps( $ops ); - // Check if the paths are valid and accessible on all backends... + // Check if the paths are valid and accessible on all backends $status->merge( $this->accessibilityCheck( $relevantPaths ) ); if ( !$status->isOK() ) { return $status; // abort } - // Do a consistency check to see if the backends are consistent... + // Do a consistency check to see if the backends are consistent $syncStatus = $this->consistencyCheck( $relevantPaths ); if ( !$syncStatus->isOK() ) { - wfDebugLog( 'FileOperation', static::class . - " failed sync check: " . FormatJson::encode( $relevantPaths ) ); - // Try to resync the clone backends to the master on the spot... - if ( $this->autoResync === false - || !$this->resyncFiles( $relevantPaths, $this->autoResync )->isOK() + $this->logger->error( + __METHOD__ . ": failed sync check: " . FormatJson::encode( $relevantPaths ) + ); + // Try to resync the clone backends to the master on the spot + if ( + $this->autoResync === false || + !$this->resyncFiles( $relevantPaths, $this->autoResync )->isOK() ) { $status->merge( $syncStatus ); return $status; // abort } } - // Actually attempt the operation batch on the master backend... + // Actually attempt the operation batch on the master backend $realOps = $this->substOpBatchPaths( $ops, $mbe ); $masterStatus = $mbe->doOperations( $realOps, $opts ); $status->merge( $masterStatus ); // Propagate the operations to the clone backends if there were no unexpected errors - // and if there were either no expected errors or if the 'force' option was used. - // However, if nothing succeeded at all, then don't replicate any of the operations. - // If $ops only had one operation, this might avoid backend sync inconsistencies. + // and everything didn't fail due to predicted errors. If $ops only had one operation, + // this might avoid backend sync inconsistencies. if ( $masterStatus->isOK() && $masterStatus->successCount > 0 ) { foreach ( $this->backends as $index => $backend ) { if ( $index === $this->masterIndex ) { @@ -192,16 +195,18 @@ class FileBackendMultiWrite extends FileBackend { // Bind $scopeLock to the callback to preserve locks DeferredUpdates::addCallableUpdate( function () use ( $backend, $realOps, $opts, $scopeLock, $relevantPaths ) { - wfDebugLog( 'FileOperationReplication', + $this->logger->error( "'{$backend->getName()}' async replication; paths: " . - FormatJson::encode( $relevantPaths ) ); + FormatJson::encode( $relevantPaths ) + ); $backend->doOperations( $realOps, $opts ); } ); } else { - wfDebugLog( 'FileOperationReplication', + $this->logger->error( "'{$backend->getName()}' sync replication; paths: " . - FormatJson::encode( $relevantPaths ) ); + FormatJson::encode( $relevantPaths ) + ); $status->merge( $backend->doOperations( $realOps, $opts ) ); } } @@ -219,6 +224,9 @@ class FileBackendMultiWrite extends FileBackend { /** * Check that a set of files are consistent across all internal backends * + * This method should only be called if the files are locked or the backend + * is in read-only mode + * * @param array $paths List of storage paths * @return StatusValue */ @@ -228,55 +236,75 @@ class FileBackendMultiWrite extends FileBackend { return $status; // skip checks } - // Preload all of the stat info in as few round trips as possible... + // Preload all of the stat info in as few round trips as possible foreach ( $this->backends as $backend ) { $realPaths = $this->substPaths( $paths, $backend ); $backend->preloadFileStat( [ 'srcs' => $realPaths, 'latest' => true ] ); } - $mBackend = $this->backends[$this->masterIndex]; foreach ( $paths as $path ) { $params = [ 'src' => $path, 'latest' => true ]; - $mParams = $this->substOpPaths( $params, $mBackend ); - // Stat the file on the 'master' backend - $mStat = $mBackend->getFileStat( $mParams ); + // Get the state of the file on the master backend + $masterBackend = $this->backends[$this->masterIndex]; + $masterParams = $this->substOpPaths( $params, $masterBackend ); + $masterStat = $masterBackend->getFileStat( $masterParams ); + if ( $masterStat === self::STAT_ERROR ) { + $status->fatal( 'backend-fail-stat', $path ); + continue; + } if ( $this->syncChecks & self::CHECK_SHA1 ) { - $mSha1 = $mBackend->getFileSha1Base36( $mParams ); + $masterSha1 = $masterBackend->getFileSha1Base36( $masterParams ); + if ( ( $masterSha1 !== false ) !== (bool)$masterStat ) { + $status->fatal( 'backend-fail-hash', $path ); + continue; + } } else { - $mSha1 = false; + $masterSha1 = null; // unused } + // Check if all clone backends agree with the master... - foreach ( $this->backends as $index => $cBackend ) { + foreach ( $this->backends as $index => $cloneBackend ) { if ( $index === $this->masterIndex ) { continue; // master } - $cParams = $this->substOpPaths( $params, $cBackend ); - $cStat = $cBackend->getFileStat( $cParams ); - if ( $mStat ) { // file is in master - if ( !$cStat ) { // file should exist + + // Get the state of the file on the clone backend + $cloneParams = $this->substOpPaths( $params, $cloneBackend ); + $cloneStat = $cloneBackend->getFileStat( $cloneParams ); + + if ( $masterStat ) { + // File exists in the master backend + if ( !$cloneStat ) { + // File is missing from the clone backend $status->fatal( 'backend-fail-synced', $path ); - continue; - } - if ( ( $this->syncChecks & self::CHECK_SIZE ) - && $cStat['size'] != $mStat['size'] - ) { // wrong size + } elseif ( + ( $this->syncChecks & self::CHECK_SIZE ) && + $cloneStat['size'] !== $masterStat['size'] + ) { + // File in the clone backend is different + $status->fatal( 'backend-fail-synced', $path ); + } elseif ( + ( $this->syncChecks & self::CHECK_TIME ) && + abs( + ConvertibleTimestamp::convert( TS_UNIX, $masterStat['mtime'] ) - + ConvertibleTimestamp::convert( TS_UNIX, $cloneStat['mtime'] ) + ) > 30 + ) { + // File in the clone backend is significantly newer or older + $status->fatal( 'backend-fail-synced', $path ); + } elseif ( + ( $this->syncChecks & self::CHECK_SHA1 ) && + $cloneBackend->getFileSha1Base36( $cloneParams ) !== $masterSha1 + ) { + // File in the clone backend is different $status->fatal( 'backend-fail-synced', $path ); - continue; - } - if ( $this->syncChecks & self::CHECK_TIME ) { - $mTs = wfTimestamp( TS_UNIX, $mStat['mtime'] ); - $cTs = wfTimestamp( TS_UNIX, $cStat['mtime'] ); - if ( abs( $mTs - $cTs ) > 30 ) { // outdated file somewhere - $status->fatal( 'backend-fail-synced', $path ); - continue; - } } - if ( ( $this->syncChecks & self::CHECK_SHA1 ) && $cBackend->getFileSha1Base36( $cParams ) !== $mSha1 ) { // wrong SHA1 + } else { + // File does not exist in the master backend + if ( $cloneStat ) { + // Stray file exists in the clone backend $status->fatal( 'backend-fail-synced', $path ); - continue; } - } elseif ( $cStat ) { // file is not in master; file should not exist - $status->fatal( 'backend-fail-synced', $path ); } } } @@ -312,6 +340,8 @@ class FileBackendMultiWrite extends FileBackend { * Check that a set of files are consistent across all internal backends * and re-synchronize those files against the "multi master" if needed. * + * This method should only be called if the files are locked + * * @param array $paths List of storage paths * @param string|bool $resyncMode False, True, or "conservative"; see __construct() * @return StatusValue @@ -319,58 +349,83 @@ class FileBackendMultiWrite extends FileBackend { public function resyncFiles( array $paths, $resyncMode = true ) { $status = $this->newStatus(); - $mBackend = $this->backends[$this->masterIndex]; + $fname = __METHOD__; foreach ( $paths as $path ) { - $mPath = $this->substPaths( $path, $mBackend ); - $mSha1 = $mBackend->getFileSha1Base36( [ 'src' => $mPath, 'latest' => true ] ); - $mStat = $mBackend->getFileStat( [ 'src' => $mPath, 'latest' => true ] ); - if ( $mStat === null || ( $mSha1 !== false && !$mStat ) ) { // sanity - $status->fatal( 'backend-fail-internal', $this->name ); - wfDebugLog( 'FileOperation', __METHOD__ - . ': File is not available on the master backend' ); - continue; // file is not available on the master backend... + $params = [ 'src' => $path, 'latest' => true ]; + // Get the state of the file on the master backend + $masterBackend = $this->backends[$this->masterIndex]; + $masterParams = $this->substOpPaths( $params, $masterBackend ); + $masterPath = $masterParams['src']; + $masterStat = $masterBackend->getFileStat( $masterParams ); + if ( $masterStat === self::STAT_ERROR ) { + $status->fatal( 'backend-fail-stat', $path ); + $this->logger->error( "$fname: file '$masterPath' is not available" ); + continue; + } + $masterSha1 = $masterBackend->getFileSha1Base36( $masterParams ); + if ( ( $masterSha1 !== false ) !== (bool)$masterStat ) { + $status->fatal( 'backend-fail-hash', $path ); + $this->logger->error( "$fname: file '$masterPath' hash does not match stat" ); + continue; } + // Check of all clone backends agree with the master... - foreach ( $this->backends as $index => $cBackend ) { + foreach ( $this->backends as $index => $cloneBackend ) { if ( $index === $this->masterIndex ) { continue; // master } - $cPath = $this->substPaths( $path, $cBackend ); - $cSha1 = $cBackend->getFileSha1Base36( [ 'src' => $cPath, 'latest' => true ] ); - $cStat = $cBackend->getFileStat( [ 'src' => $cPath, 'latest' => true ] ); - if ( $cStat === null || ( $cSha1 !== false && !$cStat ) ) { // sanity - $status->fatal( 'backend-fail-internal', $cBackend->getName() ); - wfDebugLog( 'FileOperation', __METHOD__ . - ': File is not available on the clone backend' ); - continue; // file is not available on the clone backend... + + // Get the state of the file on the clone backend + $cloneParams = $this->substOpPaths( $params, $cloneBackend ); + $clonePath = $cloneParams['src']; + $cloneStat = $cloneBackend->getFileStat( $cloneParams ); + if ( $cloneStat === self::STAT_ERROR ) { + $status->fatal( 'backend-fail-stat', $path ); + $this->logger->error( "$fname: file '$clonePath' is not available" ); + continue; + } + $cloneSha1 = $cloneBackend->getFileSha1Base36( $cloneParams ); + if ( ( $cloneSha1 !== false ) !== (bool)$cloneStat ) { + $status->fatal( 'backend-fail-hash', $path ); + $this->logger->error( "$fname: file '$clonePath' hash does not match stat" ); + continue; } - if ( $mSha1 === $cSha1 ) { - // already synced; nothing to do - } elseif ( $mSha1 !== false ) { // file is in master - if ( $resyncMode === 'conservative' - && $cStat && $cStat['mtime'] > $mStat['mtime'] + + if ( $masterSha1 === $cloneSha1 ) { + // File is either the same in both backends or absent from both backends + $this->logger->debug( "$fname: file '$clonePath' matches '$masterPath'" ); + } elseif ( $masterSha1 !== false ) { + // File is either missing from or different in the clone backend + if ( + $resyncMode === 'conservative' && + $cloneStat && + $cloneStat['mtime'] > $masterStat['mtime'] ) { + // Do not replace files with older ones; reduces the risk of data loss $status->fatal( 'backend-fail-synced', $path ); - continue; // don't rollback data + } else { + // Copy the master backend file to the clone backend in overwrite mode + $fsFile = $masterBackend->getLocalReference( $masterParams ); + $status->merge( $cloneBackend->quickStore( [ + 'src' => $fsFile, + 'dst' => $clonePath + ] ) ); } - $fsFile = $mBackend->getLocalReference( - [ 'src' => $mPath, 'latest' => true ] ); - $status->merge( $cBackend->quickStore( - [ 'src' => $fsFile->getPath(), 'dst' => $cPath ] - ) ); - } elseif ( $mStat === false ) { // file is not in master + } elseif ( $masterStat === false ) { + // Stray file exists in the clone backend if ( $resyncMode === 'conservative' ) { + // Do not delete stray files; reduces the risk of data loss $status->fatal( 'backend-fail-synced', $path ); - continue; // don't delete data + } else { + // Delete the stay file from the clone backend + $status->merge( $cloneBackend->quickDelete( [ 'src' => $clonePath ] ) ); } - $status->merge( $cBackend->quickDelete( [ 'src' => $cPath ] ) ); } } } if ( !$status->isOK() ) { - wfDebugLog( 'FileOperation', static::class . - " failed to resync: " . FormatJson::encode( $paths ) ); + $this->logger->error( "$fname: failed to resync: " . FormatJson::encode( $paths ) ); } return $status; @@ -446,7 +501,7 @@ class FileBackendMultiWrite extends FileBackend { * * @param array|string $paths List of paths or single string path * @param FileBackendStore $backend - * @return array|string + * @return string[]|string */ protected function substPaths( $paths, FileBackendStore $backend ) { return preg_replace( @@ -460,12 +515,13 @@ class FileBackendMultiWrite extends FileBackend { * Substitute the backend of internal storage paths with the proxy backend's name * * @param array|string $paths List of paths or single string path - * @return array|string + * @param FileBackendStore $backend internal storage backend + * @return string[]|string */ - protected function unsubstPaths( $paths ) { + protected function unsubstPaths( $paths, FileBackendStore $backend ) { return preg_replace( - '!^mwstore://([^/]+)!', - StringUtils::escapeRegexReplacement( "mwstore://{$this->name}" ), + '!^mwstore://' . preg_quote( $backend->getName(), '!' ) . '/!', + StringUtils::escapeRegexReplacement( "mwstore://{$this->name}/" ), $paths // string or array ); } @@ -486,7 +542,7 @@ class FileBackendMultiWrite extends FileBackend { protected function doQuickOperationsInternal( array $ops ) { $status = $this->newStatus(); - // Do the operations on the master backend; setting StatusValue fields... + // Do the operations on the master backend; setting StatusValue fields $realOps = $this->substOpBatchPaths( $ops, $this->backends[$this->masterIndex] ); $masterStatus = $this->backends[$this->masterIndex]->doQuickOperations( $realOps ); $status->merge( $masterStatus ); @@ -619,7 +675,7 @@ class FileBackendMultiWrite extends FileBackend { $contents = []; // (path => FSFile) mapping using the proxy backend's name foreach ( $contentsM as $path => $data ) { - $contents[$this->unsubstPaths( $path )] = $data; + $contents[$this->unsubstPaths( $path, $this->backends[$index] )] = $data; } return $contents; @@ -654,7 +710,7 @@ class FileBackendMultiWrite extends FileBackend { $fsFiles = []; // (path => FSFile) mapping using the proxy backend's name foreach ( $fsFilesM as $path => $fsFile ) { - $fsFiles[$this->unsubstPaths( $path )] = $fsFile; + $fsFiles[$this->unsubstPaths( $path, $this->backends[$index] )] = $fsFile; } return $fsFiles; @@ -668,7 +724,7 @@ class FileBackendMultiWrite extends FileBackend { $tempFiles = []; // (path => TempFSFile) mapping using the proxy backend's name foreach ( $tempFilesM as $path => $tempFile ) { - $tempFiles[$this->unsubstPaths( $path )] = $tempFile; + $tempFiles[$this->unsubstPaths( $path, $this->backends[$index] )] = $tempFile; } return $tempFiles; @@ -729,8 +785,14 @@ class FileBackendMultiWrite extends FileBackend { $paths = $this->backends[$this->masterIndex]->getPathsToLockForOpsInternal( $fileOps ); // Get the paths under the proxy backend's name $pbPaths = [ - LockManager::LOCK_UW => $this->unsubstPaths( $paths[LockManager::LOCK_UW] ), - LockManager::LOCK_EX => $this->unsubstPaths( $paths[LockManager::LOCK_EX] ) + LockManager::LOCK_UW => $this->unsubstPaths( + $paths[LockManager::LOCK_UW], + $this->backends[$this->masterIndex] + ), + LockManager::LOCK_EX => $this->unsubstPaths( + $paths[LockManager::LOCK_EX], + $this->backends[$this->masterIndex] + ) ]; // Actually acquire the locks diff --git a/includes/libs/filebackend/FileBackendStore.php b/includes/libs/filebackend/FileBackendStore.php index e2a25fcd51..d7d428ebff 100644 --- a/includes/libs/filebackend/FileBackendStore.php +++ b/includes/libs/filebackend/FileBackendStore.php @@ -20,6 +20,8 @@ * @file * @ingroup FileBackend */ + +use Wikimedia\AtEase\AtEase; use Wikimedia\Timestamp\ConvertibleTimestamp; /** @@ -57,6 +59,16 @@ abstract class FileBackendStore extends FileBackend { const CACHE_CHEAP_SIZE = 500; // integer; max entries in "cheap cache" const CACHE_EXPENSIVE_SIZE = 5; // integer; max entries in "expensive cache" + /** @var false Idiom for "no result due to missing file" (since 1.34) */ + protected static $RES_ABSENT = false; + /** @var null Idiom for "no result due to I/O errors" (since 1.34) */ + protected static $RES_ERROR = null; + + /** @var string File does not exist according to a normal stat query */ + protected static $ABSENT_NORMAL = 'FNE-N'; + /** @var string File does not exist according to a "latest"-mode stat query */ + protected static $ABSENT_LATEST = 'FNE-L'; + /** * @see FileBackend::__construct() * Additional $config params include: @@ -89,9 +101,10 @@ abstract class FileBackendStore extends FileBackend { } /** - * Check if a file can be created or changed at a given storage path. - * FS backends should check if the parent directory exists, files can be - * written under it, and that any file already there is writable. + * Check if a file can be created or changed at a given storage path in the backend + * + * FS backends should check that the parent directory exists, files can be written + * under it, and that any file already there is both readable and writable. * Backends using key/value stores should check if the container exists. * * @param string $storagePath @@ -118,7 +131,9 @@ abstract class FileBackendStore extends FileBackend { * @return StatusValue */ final public function createInternal( array $params ) { + /** @noinspection PhpUnusedLocalVariableInspection */ $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" ); + if ( strlen( $params['content'] ) > $this->maxFileSizeInternal() ) { $status = $this->newStatus( 'backend-fail-maxsize', $params['dst'], $this->maxFileSizeInternal() ); @@ -159,7 +174,9 @@ abstract class FileBackendStore extends FileBackend { * @return StatusValue */ final public function storeInternal( array $params ) { + /** @noinspection PhpUnusedLocalVariableInspection */ $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" ); + if ( filesize( $params['src'] ) > $this->maxFileSizeInternal() ) { $status = $this->newStatus( 'backend-fail-maxsize', $params['dst'], $this->maxFileSizeInternal() ); @@ -201,7 +218,9 @@ abstract class FileBackendStore extends FileBackend { * @return StatusValue */ final public function copyInternal( array $params ) { + /** @noinspection PhpUnusedLocalVariableInspection */ $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" ); + $status = $this->doCopyInternal( $params ); $this->clearCache( [ $params['dst'] ] ); if ( !isset( $params['dstExists'] ) || $params['dstExists'] ) { @@ -233,7 +252,9 @@ abstract class FileBackendStore extends FileBackend { * @return StatusValue */ final public function deleteInternal( array $params ) { + /** @noinspection PhpUnusedLocalVariableInspection */ $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" ); + $status = $this->doDeleteInternal( $params ); $this->clearCache( [ $params['src'] ] ); $this->deleteFileCache( $params['src'] ); // persistent cache @@ -267,7 +288,9 @@ abstract class FileBackendStore extends FileBackend { * @return StatusValue */ final public function moveInternal( array $params ) { + /** @noinspection PhpUnusedLocalVariableInspection */ $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" ); + $status = $this->doMoveInternal( $params ); $this->clearCache( [ $params['src'], $params['dst'] ] ); $this->deleteFileCache( $params['src'] ); // persistent cache @@ -313,7 +336,9 @@ abstract class FileBackendStore extends FileBackend { * @return StatusValue */ final public function describeInternal( array $params ) { + /** @noinspection PhpUnusedLocalVariableInspection */ $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" ); + if ( count( $params['headers'] ) ) { $status = $this->doDescribeInternal( $params ); $this->clearCache( [ $params['src'] ] ); @@ -346,10 +371,12 @@ abstract class FileBackendStore extends FileBackend { } final public function concatenate( array $params ) { + /** @noinspection PhpUnusedLocalVariableInspection */ $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" ); $status = $this->newStatus(); // Try to lock the source files for the scope of this function + /** @noinspection PhpUnusedLocalVariableInspection */ $scopeLockS = $this->getScopedFileLocks( $params['srcs'], LockManager::LOCK_UW, $status ); if ( $status->isOK() ) { // Actually do the file concatenation... @@ -376,9 +403,9 @@ abstract class FileBackendStore extends FileBackend { unset( $params['latest'] ); // sanity // Check that the specified temp file is valid... - Wikimedia\suppressWarnings(); + AtEase::suppressWarnings(); $ok = ( is_file( $tmpPath ) && filesize( $tmpPath ) == 0 ); - Wikimedia\restoreWarnings(); + AtEase::restoreWarnings(); if ( !$ok ) { // not present or not empty $status->fatal( 'backend-fail-opentemp', $tmpPath ); @@ -439,6 +466,7 @@ abstract class FileBackendStore extends FileBackend { } final protected function doPrepare( array $params ) { + /** @noinspection PhpUnusedLocalVariableInspection */ $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" ); $status = $this->newStatus(); @@ -474,6 +502,7 @@ abstract class FileBackendStore extends FileBackend { } final protected function doSecure( array $params ) { + /** @noinspection PhpUnusedLocalVariableInspection */ $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" ); $status = $this->newStatus(); @@ -509,6 +538,7 @@ abstract class FileBackendStore extends FileBackend { } final protected function doPublish( array $params ) { + /** @noinspection PhpUnusedLocalVariableInspection */ $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" ); $status = $this->newStatus(); @@ -544,6 +574,7 @@ abstract class FileBackendStore extends FileBackend { } final protected function doClean( array $params ) { + /** @noinspection PhpUnusedLocalVariableInspection */ $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" ); $status = $this->newStatus(); @@ -568,6 +599,7 @@ abstract class FileBackendStore extends FileBackend { // Attempt to lock this directory... $filesLockEx = [ $params['dir'] ]; + /** @noinspection PhpUnusedLocalVariableInspection */ $scopedLockE = $this->getScopedFileLocks( $filesLockEx, LockManager::LOCK_EX, $status ); if ( !$status->isOK() ) { return $status; // abort @@ -600,52 +632,73 @@ abstract class FileBackendStore extends FileBackend { } final public function fileExists( array $params ) { + /** @noinspection PhpUnusedLocalVariableInspection */ $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" ); + $stat = $this->getFileStat( $params ); + if ( is_array( $stat ) ) { + return true; + } - return ( $stat === null ) ? null : (bool)$stat; // null => failure + return ( $stat === self::$RES_ABSENT ) ? false : self::EXISTENCE_ERROR; } final public function getFileTimestamp( array $params ) { + /** @noinspection PhpUnusedLocalVariableInspection */ $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" ); + $stat = $this->getFileStat( $params ); + if ( is_array( $stat ) ) { + return $stat['mtime']; + } - return $stat ? $stat['mtime'] : false; + return self::TIMESTAMP_FAIL; // all failure cases } final public function getFileSize( array $params ) { + /** @noinspection PhpUnusedLocalVariableInspection */ $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" ); + $stat = $this->getFileStat( $params ); + if ( is_array( $stat ) ) { + return $stat['size']; + } - return $stat ? $stat['size'] : false; + return self::SIZE_FAIL; // all failure cases } final public function getFileStat( array $params ) { + /** @noinspection PhpUnusedLocalVariableInspection */ + $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" ); + $path = self::normalizeStoragePath( $params['src'] ); if ( $path === null ) { - return false; // invalid storage path + return self::STAT_ERROR; // invalid storage path } - $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" ); - $latest = !empty( $params['latest'] ); // use latest data? - $requireSHA1 = !empty( $params['requireSHA1'] ); // require SHA-1 if file exists? + // Whether to bypass cache except for process cache entries loaded directly from + // high consistency backend queries (caller handles any cache flushing and locking) + $latest = !empty( $params['latest'] ); + // Whether to ignore cache entries missing the SHA-1 field for existing files + $requireSHA1 = !empty( $params['requireSHA1'] ); + $stat = $this->cheapCache->getField( $path, 'stat', self::CACHE_TTL ); + // Load the persistent stat cache into process cache if needed if ( !$latest ) { - $stat = $this->cheapCache->getField( $path, 'stat', self::CACHE_TTL ); - // Note that some backends, like SwiftFileBackend, sometimes set file stat process - // cache entries from mass object listings that do not include the SHA-1. In that - // case, loading the persistent stat cache will likely yield the SHA-1. if ( + // File stat is not in process cache $stat === null || + // Key/value store backends might opportunistically set file stat process + // cache entries from object listings that do not include the SHA-1. In that + // case, loading the persistent stat cache will likely yield the SHA-1. ( $requireSHA1 && is_array( $stat ) && !isset( $stat['sha1'] ) ) ) { - $this->primeFileCache( [ $path ] ); // check persistent cache + $this->primeFileCache( [ $path ] ); + // Get any newly process-cached entry + $stat = $this->cheapCache->getField( $path, 'stat', self::CACHE_TTL ); } } - $stat = $this->cheapCache->getField( $path, 'stat', self::CACHE_TTL ); - // If we want the latest data, check that this cached - // value was in fact fetched with the latest available data. if ( is_array( $stat ) ) { if ( ( !$latest || $stat['latest'] ) && @@ -653,42 +706,90 @@ abstract class FileBackendStore extends FileBackend { ) { return $stat; } - } elseif ( in_array( $stat, [ 'NOT_EXIST', 'NOT_EXIST_LATEST' ], true ) ) { - if ( !$latest || $stat === 'NOT_EXIST_LATEST' ) { - return false; + } elseif ( $stat === self::$ABSENT_LATEST ) { + return self::STAT_ABSENT; + } elseif ( $stat === self::$ABSENT_NORMAL ) { + if ( !$latest ) { + return self::STAT_ABSENT; } } + // Load the file stat from the backend and update caches $stat = $this->doGetFileStat( $params ); + $this->ingestFreshFileStats( [ $path => $stat ], $latest ); - if ( is_array( $stat ) ) { // file exists - // Strongly consistent backends can automatically set "latest" - $stat['latest'] = $stat['latest'] ?? $latest; - $this->cheapCache->setField( $path, 'stat', $stat ); - $this->setFileCache( $path, $stat ); // update persistent cache - if ( isset( $stat['sha1'] ) ) { // some backends store SHA-1 as metadata - $this->cheapCache->setField( $path, 'sha1', - [ 'hash' => $stat['sha1'], 'latest' => $latest ] ); - } - if ( isset( $stat['xattr'] ) ) { // some backends store headers/metadata - $stat['xattr'] = self::normalizeXAttributes( $stat['xattr'] ); - $this->cheapCache->setField( $path, 'xattr', - [ 'map' => $stat['xattr'], 'latest' => $latest ] ); + if ( is_array( $stat ) ) { + return $stat; + } + + return ( $stat === self::$RES_ERROR ) ? self::STAT_ERROR : self::STAT_ABSENT; + } + + /** + * Ingest file stat entries that just came from querying the backend (not cache) + * + * @param array[]|bool[]|null[] $stats Map of (path => doGetFileStat() stype result) + * @param bool $latest Whether doGetFileStat()/doGetFileStatMulti() had the 'latest' flag + * @return bool Whether all files have non-error stat replies + */ + final protected function ingestFreshFileStats( array $stats, $latest ) { + $success = true; + + foreach ( $stats as $path => $stat ) { + if ( is_array( $stat ) ) { + // Strongly consistent backends might automatically set this flag + $stat['latest'] = $stat['latest'] ?? $latest; + + $this->cheapCache->setField( $path, 'stat', $stat ); + if ( isset( $stat['sha1'] ) ) { + // Some backends store the SHA-1 hash as metadata + $this->cheapCache->setField( + $path, + 'sha1', + [ 'hash' => $stat['sha1'], 'latest' => $latest ] + ); + } + if ( isset( $stat['xattr'] ) ) { + // Some backends store custom headers/metadata + $stat['xattr'] = self::normalizeXAttributes( $stat['xattr'] ); + $this->cheapCache->setField( + $path, + 'xattr', + [ 'map' => $stat['xattr'], 'latest' => $latest ] + ); + } + // Update persistent cache (@TODO: set all entries in one batch) + $this->setFileCache( $path, $stat ); + } elseif ( $stat === self::$RES_ABSENT ) { + $this->cheapCache->setField( + $path, + 'stat', + $latest ? self::$ABSENT_LATEST : self::$ABSENT_NORMAL + ); + $this->cheapCache->setField( + $path, + 'xattr', + [ 'map' => self::XATTRS_FAIL, 'latest' => $latest ] + ); + $this->cheapCache->setField( + $path, + 'sha1', + [ 'hash' => self::SHA1_FAIL, 'latest' => $latest ] + ); + $this->logger->debug( + __METHOD__ . ': File {path} does not exist', + [ 'path' => $path ] + ); + } else { + $success = false; + $this->logger->error( + __METHOD__ . ': Could not stat file {path}', + [ 'path' => $path ] + ); } - } elseif ( $stat === false ) { // file does not exist - $this->cheapCache->setField( $path, 'stat', $latest ? 'NOT_EXIST_LATEST' : 'NOT_EXIST' ); - $this->cheapCache->setField( $path, 'xattr', [ 'map' => false, 'latest' => $latest ] ); - $this->cheapCache->setField( $path, 'sha1', [ 'hash' => false, 'latest' => $latest ] ); - $this->logger->debug( __METHOD__ . ': File {path} does not exist', [ - 'path' => $path, - ] ); - } else { // an error occurred - $this->logger->warning( __METHOD__ . ': Could not stat file {path}', [ - 'path' => $path, - ] ); } - return $stat; + return $success; } /** @@ -698,10 +799,16 @@ abstract class FileBackendStore extends FileBackend { abstract protected function doGetFileStat( array $params ); public function getFileContentsMulti( array $params ) { + /** @noinspection PhpUnusedLocalVariableInspection */ $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" ); $params = $this->setConcurrencyFlags( $params ); $contents = $this->doGetFileContentsMulti( $params ); + foreach ( $contents as $path => $content ) { + if ( !is_string( $content ) ) { + $contents[$path] = self::CONTENT_FAIL; // used for all failure cases + } + } return $contents; } @@ -709,25 +816,34 @@ abstract class FileBackendStore extends FileBackend { /** * @see FileBackendStore::getFileContentsMulti() * @param array $params - * @return array + * @return string[]|bool[]|null[] Map of (path => string, false (missing), or null (error)) */ protected function doGetFileContentsMulti( array $params ) { $contents = []; foreach ( $this->doGetLocalReferenceMulti( $params ) as $path => $fsFile ) { - Wikimedia\suppressWarnings(); - $contents[$path] = $fsFile ? file_get_contents( $fsFile->getPath() ) : false; - Wikimedia\restoreWarnings(); + if ( $fsFile instanceof FSFile ) { + AtEase::suppressWarnings(); + $content = file_get_contents( $fsFile->getPath() ); + AtEase::restoreWarnings(); + $contents[$path] = is_string( $content ) ? $content : self::$RES_ERROR; + } elseif ( $fsFile === self::$RES_ABSENT ) { + $contents[$path] = self::$RES_ABSENT; + } else { + $contents[$path] = self::$RES_ERROR; + } } return $contents; } final public function getFileXAttributes( array $params ) { + /** @noinspection PhpUnusedLocalVariableInspection */ + $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" ); + $path = self::normalizeStoragePath( $params['src'] ); if ( $path === null ) { - return false; // invalid storage path + return self::XATTRS_FAIL; // invalid storage path } - $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" ); $latest = !empty( $params['latest'] ); // use latest data? if ( $this->cheapCache->hasField( $path, 'xattr', self::CACHE_TTL ) ) { $stat = $this->cheapCache->getField( $path, 'xattr' ); @@ -738,8 +854,22 @@ abstract class FileBackendStore extends FileBackend { } } $fields = $this->doGetFileXAttributes( $params ); - $fields = is_array( $fields ) ? self::normalizeXAttributes( $fields ) : false; - $this->cheapCache->setField( $path, 'xattr', [ 'map' => $fields, 'latest' => $latest ] ); + if ( is_array( $fields ) ) { + $fields = self::normalizeXAttributes( $fields ); + $this->cheapCache->setField( + $path, + 'xattr', + [ 'map' => $fields, 'latest' => $latest ] + ); + } elseif ( $fields === self::$RES_ABSENT ) { + $this->cheapCache->setField( + $path, + 'xattr', + [ 'map' => self::XATTRS_FAIL, 'latest' => $latest ] + ); + } else { + $fields = self::XATTRS_FAIL; // used for all failure cases + } return $fields; } @@ -747,18 +877,20 @@ abstract class FileBackendStore extends FileBackend { /** * @see FileBackendStore::getFileXAttributes() * @param array $params - * @return array[][]|false + * @return array[][]|false|null Attributes, false (missing file), or null (error) */ protected function doGetFileXAttributes( array $params ) { return [ 'headers' => [], 'metadata' => [] ]; // not supported } final public function getFileSha1Base36( array $params ) { + /** @noinspection PhpUnusedLocalVariableInspection */ + $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" ); + $path = self::normalizeStoragePath( $params['src'] ); if ( $path === null ) { - return false; // invalid storage path + return self::SHA1_FAIL; // invalid storage path } - $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" ); $latest = !empty( $params['latest'] ); // use latest data? if ( $this->cheapCache->hasField( $path, 'sha1', self::CACHE_TTL ) ) { $stat = $this->cheapCache->getField( $path, 'sha1' ); @@ -768,35 +900,53 @@ abstract class FileBackendStore extends FileBackend { return $stat['hash']; } } - $hash = $this->doGetFileSha1Base36( $params ); - $this->cheapCache->setField( $path, 'sha1', [ 'hash' => $hash, 'latest' => $latest ] ); + $sha1 = $this->doGetFileSha1Base36( $params ); + if ( is_string( $sha1 ) ) { + $this->cheapCache->setField( + $path, + 'sha1', + [ 'hash' => $sha1, 'latest' => $latest ] + ); + } elseif ( $sha1 === self::$RES_ABSENT ) { + $this->cheapCache->setField( + $path, + 'sha1', + [ 'hash' => self::SHA1_FAIL, 'latest' => $latest ] + ); + } else { + $sha1 = self::SHA1_FAIL; // used for all failure cases + } - return $hash; + return $sha1; } /** * @see FileBackendStore::getFileSha1Base36() * @param array $params - * @return bool|string + * @return bool|string|null SHA1, false (missing file), or null (error) */ protected function doGetFileSha1Base36( array $params ) { $fsFile = $this->getLocalReference( $params ); - if ( !$fsFile ) { - return false; - } else { - return $fsFile->getSha1Base36(); + if ( $fsFile instanceof FSFile ) { + $sha1 = $fsFile->getSha1Base36(); + + return is_string( $sha1 ) ? $sha1 : self::$RES_ERROR; } + + return ( $fsFile === self::$RES_ERROR ) ? self::$RES_ERROR : self::$RES_ABSENT; } final public function getFileProps( array $params ) { + /** @noinspection PhpUnusedLocalVariableInspection */ $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" ); + $fsFile = $this->getLocalReference( $params ); - $props = $fsFile ? $fsFile->getProps() : FSFile::placeholderProps(); - return $props; + return $fsFile ? $fsFile->getProps() : FSFile::placeholderProps(); } final public function getLocalReferenceMulti( array $params ) { + /** @noinspection PhpUnusedLocalVariableInspection */ $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" ); $params = $this->setConcurrencyFlags( $params ); @@ -820,10 +970,15 @@ abstract class FileBackendStore extends FileBackend { // Fetch local references of any remaning files... $params['srcs'] = array_diff( $params['srcs'], array_keys( $fsFiles ) ); foreach ( $this->doGetLocalReferenceMulti( $params ) as $path => $fsFile ) { - $fsFiles[$path] = $fsFile; - if ( $fsFile ) { // update the process cache... - $this->expensiveCache->setField( $path, 'localRef', - [ 'object' => $fsFile, 'latest' => $latest ] ); + if ( $fsFile instanceof FSFile ) { + $fsFiles[$path] = $fsFile; + $this->expensiveCache->setField( + $path, + 'localRef', + [ 'object' => $fsFile, 'latest' => $latest ] + ); + } else { + $fsFiles[$path] = null; // used for all failure cases } } @@ -833,17 +988,23 @@ abstract class FileBackendStore extends FileBackend { /** * @see FileBackendStore::getLocalReferenceMulti() * @param array $params - * @return array + * @return string[]|bool[]|null[] Map of (path => FSFile, false (missing), or null (error)) */ protected function doGetLocalReferenceMulti( array $params ) { return $this->doGetLocalCopyMulti( $params ); } final public function getLocalCopyMulti( array $params ) { + /** @noinspection PhpUnusedLocalVariableInspection */ $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" ); $params = $this->setConcurrencyFlags( $params ); $tmpFiles = $this->doGetLocalCopyMulti( $params ); + foreach ( $tmpFiles as $path => $tmpFile ) { + if ( !$tmpFile ) { + $tmpFiles[$path] = null; // used for all failure cases + } + } return $tmpFiles; } @@ -851,7 +1012,7 @@ abstract class FileBackendStore extends FileBackend { /** * @see FileBackendStore::getLocalCopyMulti() * @param array $params - * @return array + * @return string[]|bool[]|null[] Map of (path => TempFSFile, false (missing), or null (error)) */ abstract protected function doGetLocalCopyMulti( array $params ); @@ -861,10 +1022,11 @@ abstract class FileBackendStore extends FileBackend { * @return string|null */ public function getFileHttpUrl( array $params ) { - return null; // not supported + return self::TEMPURL_ERROR; // not supported } final public function streamFile( array $params ) { + /** @noinspection PhpUnusedLocalVariableInspection */ $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" ); $status = $this->newStatus(); @@ -921,7 +1083,7 @@ abstract class FileBackendStore extends FileBackend { final public function directoryExists( array $params ) { list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] ); if ( $dir === null ) { - return false; // invalid storage path + return self::EXISTENCE_ERROR; // invalid storage path } if ( $shard !== null ) { // confined to a single container/shard return $this->doDirectoryExists( $fullCont, $dir, $params ); @@ -931,11 +1093,11 @@ abstract class FileBackendStore extends FileBackend { $res = false; // response foreach ( $this->getContainerSuffixes( $shortCont ) as $suffix ) { $exists = $this->doDirectoryExists( "{$fullCont}{$suffix}", $dir, $params ); - if ( $exists ) { + if ( $exists === true ) { $res = true; break; // found one! - } elseif ( $exists === null ) { // error? - $res = null; // if we don't find anything, it is indeterminate + } elseif ( $exists === self::$RES_ERROR ) { + $res = self::EXISTENCE_ERROR; } } @@ -955,8 +1117,8 @@ abstract class FileBackendStore extends FileBackend { final public function getDirectoryList( array $params ) { list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] ); - if ( $dir === null ) { // invalid storage path - return null; + if ( $dir === null ) { + return self::EXISTENCE_ERROR; // invalid storage path } if ( $shard !== null ) { // File listing is confined to a single container/shard @@ -979,14 +1141,14 @@ abstract class FileBackendStore extends FileBackend { * @param string $container Resolved container name * @param string $dir Resolved path relative to container * @param array $params - * @return Traversable|array|null Returns null on failure + * @return Traversable|array|null Iterable list or null (error) */ abstract public function getDirectoryListInternal( $container, $dir, array $params ); final public function getFileList( array $params ) { list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] ); - if ( $dir === null ) { // invalid storage path - return null; + if ( $dir === null ) { + return self::LIST_ERROR; // invalid storage path } if ( $shard !== null ) { // File listing is confined to a single container/shard @@ -1009,7 +1171,7 @@ abstract class FileBackendStore extends FileBackend { * @param string $container Resolved container name * @param string $dir Resolved path relative to container * @param array $params - * @return Traversable|string[]|null Returns null on failure + * @return Traversable|string[]|null Iterable list or null (error) */ abstract public function getFileListInternal( $container, $dir, array $params ); @@ -1088,6 +1250,7 @@ abstract class FileBackendStore extends FileBackend { } final protected function doOperationsInternal( array $ops, array $opts ) { + /** @noinspection PhpUnusedLocalVariableInspection */ $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" ); $status = $this->newStatus(); @@ -1102,7 +1265,7 @@ abstract class FileBackendStore extends FileBackend { // Build up a list of files to lock... $paths = $this->getPathsToLockForOpsInternal( $performOps ); // Try to lock those files for the scope of this function... - + /** @noinspection PhpUnusedLocalVariableInspection */ $scopeLock = $this->getScopedFileLocks( $paths, 'mixed', $status ); if ( !$status->isOK() ) { return $status; // abort @@ -1155,6 +1318,7 @@ abstract class FileBackendStore extends FileBackend { } final protected function doQuickOperationsInternal( array $ops ) { + /** @noinspection PhpUnusedLocalVariableInspection */ $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" ); $status = $this->newStatus(); @@ -1221,6 +1385,7 @@ abstract class FileBackendStore extends FileBackend { * @throws FileBackendError */ final public function executeOpHandlesInternal( array $fileOpHandles ) { + /** @noinspection PhpUnusedLocalVariableInspection */ $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" ); foreach ( $fileOpHandles as $fileOpHandle ) { @@ -1231,12 +1396,12 @@ abstract class FileBackendStore extends FileBackend { } } - $res = $this->doExecuteOpHandlesInternal( $fileOpHandles ); + $statuses = $this->doExecuteOpHandlesInternal( $fileOpHandles ); foreach ( $fileOpHandles as $fileOpHandle ) { $fileOpHandle->closeResources(); } - return $res; + return $statuses; } /** @@ -1249,7 +1414,7 @@ abstract class FileBackendStore extends FileBackend { */ protected function doExecuteOpHandlesInternal( array $fileOpHandles ) { if ( count( $fileOpHandles ) ) { - throw new LogicException( "Backend does not support asynchronous operations." ); + throw new FileBackendError( "Backend does not support asynchronous operations." ); } return []; @@ -1325,8 +1490,8 @@ abstract class FileBackendStore extends FileBackend { } final public function preloadFileStat( array $params ) { + /** @noinspection PhpUnusedLocalVariableInspection */ $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" ); - $success = true; // no network errors $params['concurrency'] = ( $this->parallelize !== 'off' ) ? $this->concurrency : 1; $stats = $this->doGetFileStatMulti( $params ); @@ -1334,45 +1499,10 @@ abstract class FileBackendStore extends FileBackend { return true; // not supported } - $latest = !empty( $params['latest'] ); // use latest data? - foreach ( $stats as $path => $stat ) { - $path = FileBackend::normalizeStoragePath( $path ); - if ( $path === null ) { - continue; // this shouldn't happen - } - if ( is_array( $stat ) ) { // file exists - // Strongly consistent backends can automatically set "latest" - $stat['latest'] = $stat['latest'] ?? $latest; - $this->cheapCache->setField( $path, 'stat', $stat ); - $this->setFileCache( $path, $stat ); // update persistent cache - if ( isset( $stat['sha1'] ) ) { // some backends store SHA-1 as metadata - $this->cheapCache->setField( $path, 'sha1', - [ 'hash' => $stat['sha1'], 'latest' => $latest ] ); - } - if ( isset( $stat['xattr'] ) ) { // some backends store headers/metadata - $stat['xattr'] = self::normalizeXAttributes( $stat['xattr'] ); - $this->cheapCache->setField( $path, 'xattr', - [ 'map' => $stat['xattr'], 'latest' => $latest ] ); - } - } elseif ( $stat === false ) { // file does not exist - $this->cheapCache->setField( $path, 'stat', - $latest ? 'NOT_EXIST_LATEST' : 'NOT_EXIST' ); - $this->cheapCache->setField( $path, 'xattr', - [ 'map' => false, 'latest' => $latest ] ); - $this->cheapCache->setField( $path, 'sha1', - [ 'hash' => false, 'latest' => $latest ] ); - $this->logger->debug( __METHOD__ . ': File {path} does not exist', [ - 'path' => $path, - ] ); - } else { // an error occurred - $success = false; - $this->logger->warning( __METHOD__ . ': Could not stat file {path}', [ - 'path' => $path, - ] ); - } - } + // Whether this queried the backend in high consistency mode + $latest = !empty( $params['latest'] ); - return $success; + return $this->ingestFreshFileStats( $stats, $latest ); } /** @@ -1457,7 +1587,7 @@ abstract class FileBackendStore extends FileBackend { // Validate and sanitize the relative path (backend-specific) $relPath = $this->resolveContainerPath( $shortCont, $relPath ); if ( $relPath !== null ) { - // Prepend any wiki ID prefix to the container name + // Prepend any domain ID prefix to the container name $container = $this->fullContainerName( $shortCont ); if ( self::isValidContainerName( $container ) ) { // Validate and sanitize the container name (backend-specific) @@ -1592,7 +1722,7 @@ abstract class FileBackendStore extends FileBackend { } /** - * Get the full container name, including the wiki ID prefix + * Get the full container name, including the domain ID prefix * * @param string $container * @return string @@ -1671,6 +1801,7 @@ abstract class FileBackendStore extends FileBackend { * @param array $items */ final protected function primeContainerCache( array $items ) { + /** @noinspection PhpUnusedLocalVariableInspection */ $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" ); $paths = []; // list of storage paths @@ -1768,6 +1899,7 @@ abstract class FileBackendStore extends FileBackend { * @param array $items List of storage paths */ final protected function primeFileCache( array $items ) { + /** @noinspection PhpUnusedLocalVariableInspection */ $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" ); $paths = []; // list of storage paths @@ -1778,7 +1910,7 @@ abstract class FileBackendStore extends FileBackend { $paths[] = FileBackend::normalizeStoragePath( $item ); } } - // Get rid of any paths that failed normalization... + // Get rid of any paths that failed normalization $paths = array_filter( $paths, 'strlen' ); // remove nulls // Get all the corresponding cache keys for paths... foreach ( $paths as $path ) { @@ -1787,22 +1919,33 @@ abstract class FileBackendStore extends FileBackend { $pathNames[$this->fileCacheKey( $path )] = $path; } } - // Get all cache entries for these file cache keys... + // Get all cache entries for these file cache keys. + // Note that negatives are not cached by getFileStat()/preloadFileStat(). $values = $this->memCache->getMulti( array_keys( $pathNames ) ); - foreach ( $values as $cacheKey => $val ) { + // Load all of the results into process cache... + foreach ( array_filter( $values, 'is_array' ) as $cacheKey => $stat ) { $path = $pathNames[$cacheKey]; - if ( is_array( $val ) ) { - $val['latest'] = false; // never completely trust cache - $this->cheapCache->setField( $path, 'stat', $val ); - if ( isset( $val['sha1'] ) ) { // some backends store SHA-1 as metadata - $this->cheapCache->setField( $path, 'sha1', - [ 'hash' => $val['sha1'], 'latest' => false ] ); - } - if ( isset( $val['xattr'] ) ) { // some backends store headers/metadata - $val['xattr'] = self::normalizeXAttributes( $val['xattr'] ); - $this->cheapCache->setField( $path, 'xattr', - [ 'map' => $val['xattr'], 'latest' => false ] ); - } + // Sanity; this flag only applies to stat info loaded directly + // from a high consistency backend query to the process cache + unset( $stat['latest'] ); + + $this->cheapCache->setField( $path, 'stat', $stat ); + if ( isset( $stat['sha1'] ) && strlen( $stat['sha1'] ) == 31 ) { + // Some backends store SHA-1 as metadata + $this->cheapCache->setField( + $path, + 'sha1', + [ 'hash' => $stat['sha1'], 'latest' => false ] + ); + } + if ( isset( $stat['xattr'] ) && is_array( $stat['xattr'] ) ) { + // Some backends store custom headers/metadata + $stat['xattr'] = self::normalizeXAttributes( $stat['xattr'] ); + $this->cheapCache->setField( + $path, + 'xattr', + [ 'map' => $stat['xattr'], 'latest' => false ] + ); } } } diff --git a/includes/libs/filebackend/FileOpBatch.php b/includes/libs/filebackend/FileOpBatch.php index 540961edd1..bbcda085c0 100644 --- a/includes/libs/filebackend/FileOpBatch.php +++ b/includes/libs/filebackend/FileOpBatch.php @@ -46,7 +46,7 @@ class FileOpBatch { * * The resulting StatusValue will be "OK" unless: * - a) unexpected operation errors occurred (network partitions, disk full...) - * - b) significant operation errors occurred and 'force' was not set + * - b) predicted operation errors occurred and 'force' was not set * * @param FileOp[] $performOps List of FileOp operations * @param array $opts Batch operation options diff --git a/includes/libs/filebackend/HTTPFileStreamer.php b/includes/libs/filebackend/HTTPFileStreamer.php index 7a11aebf7e..653a102cc4 100644 --- a/includes/libs/filebackend/HTTPFileStreamer.php +++ b/includes/libs/filebackend/HTTPFileStreamer.php @@ -19,6 +19,8 @@ * * @file */ + +use Wikimedia\AtEase\AtEase; use Wikimedia\Timestamp\ConvertibleTimestamp; /** @@ -100,9 +102,9 @@ class HTTPFileStreamer { is_int( $header ) ? HttpStatus::header( $header ) : header( $header ); }; - Wikimedia\suppressWarnings(); + AtEase::suppressWarnings(); $info = stat( $this->path ); - Wikimedia\restoreWarnings(); + AtEase::restoreWarnings(); if ( !is_array( $info ) ) { if ( $sendErrors ) { diff --git a/includes/libs/filebackend/MemoryFileBackend.php b/includes/libs/filebackend/MemoryFileBackend.php index 548c85c85d..82f196263f 100644 --- a/includes/libs/filebackend/MemoryFileBackend.php +++ b/includes/libs/filebackend/MemoryFileBackend.php @@ -21,6 +21,9 @@ * @ingroup FileBackend */ +use Wikimedia\AtEase\AtEase; +use Wikimedia\Timestamp\ConvertibleTimestamp; + /** * Simulation of a backend storage in memory. * @@ -39,7 +42,7 @@ class MemoryFileBackend extends FileBackendStore { } public function isPathUsableInternal( $storagePath ) { - return true; + return ( $this->resolveHashKey( $storagePath ) !== null ); } protected function doCreateInternal( array $params ) { @@ -54,7 +57,7 @@ class MemoryFileBackend extends FileBackendStore { $this->files[$dst] = [ 'data' => $params['content'], - 'mtime' => wfTimestamp( TS_MW, time() ) + 'mtime' => ConvertibleTimestamp::convert( TS_MW, time() ) ]; return $status; @@ -70,9 +73,9 @@ class MemoryFileBackend extends FileBackendStore { return $status; } - Wikimedia\suppressWarnings(); + AtEase::suppressWarnings(); $data = file_get_contents( $params['src'] ); - Wikimedia\restoreWarnings(); + AtEase::restoreWarnings(); if ( $data === false ) { // source doesn't exist? $status->fatal( 'backend-fail-store', $params['src'], $params['dst'] ); @@ -81,7 +84,7 @@ class MemoryFileBackend extends FileBackendStore { $this->files[$dst] = [ 'data' => $data, - 'mtime' => wfTimestamp( TS_MW, time() ) + 'mtime' => ConvertibleTimestamp::convert( TS_MW, time() ) ]; return $status; @@ -114,7 +117,7 @@ class MemoryFileBackend extends FileBackendStore { $this->files[$dst] = [ 'data' => $this->files[$src]['data'], - 'mtime' => wfTimestamp( TS_MW, time() ) + 'mtime' => ConvertibleTimestamp::convert( TS_MW, time() ) ]; return $status; @@ -146,7 +149,7 @@ class MemoryFileBackend extends FileBackendStore { protected function doGetFileStat( array $params ) { $src = $this->resolveHashKey( $params['src'] ); if ( $src === null ) { - return null; + return self::$RES_ERROR; // invalid path } if ( isset( $this->files[$src] ) ) { @@ -156,23 +159,25 @@ class MemoryFileBackend extends FileBackendStore { ]; } - return false; + return self::$RES_ABSENT; } protected function doGetLocalCopyMulti( array $params ) { $tmpFiles = []; // (path => TempFSFile) foreach ( $params['srcs'] as $srcPath ) { $src = $this->resolveHashKey( $srcPath ); - if ( $src === null || !isset( $this->files[$src] ) ) { - $fsFile = null; + if ( $src === null ) { + $fsFile = self::$RES_ERROR; + } elseif ( !isset( $this->files[$src] ) ) { + $fsFile = self::$RES_ABSENT; } else { // Create a new temporary file with the same extension... $ext = FileBackend::extensionFromPath( $src ); - $fsFile = TempFSFile::factory( 'localcopy_', $ext, $this->tmpDirectory ); + $fsFile = $this->tmpFileFactory->newTempFSFile( 'localcopy_', $ext ); if ( $fsFile ) { $bytes = file_put_contents( $fsFile->getPath(), $this->files[$src]['data'] ); if ( $bytes !== strlen( $this->files[$src]['data'] ) ) { - $fsFile = null; + $fsFile = self::$RES_ERROR; } } } diff --git a/includes/libs/filebackend/SwiftFileBackend.php b/includes/libs/filebackend/SwiftFileBackend.php index a1b2460df6..ce1e99f5da 100644 --- a/includes/libs/filebackend/SwiftFileBackend.php +++ b/includes/libs/filebackend/SwiftFileBackend.php @@ -22,6 +22,8 @@ * @author Russ Nelson */ +use Wikimedia\AtEase\AtEase; + /** * @brief Class for an OpenStack Swift (or Ceph RGW) based file backend. * @@ -143,8 +145,11 @@ class SwiftFileBackend extends FileBackendStore { } public function getFeatures() { - return ( FileBackend::ATTR_UNICODE_PATHS | - FileBackend::ATTR_HEADERS | FileBackend::ATTR_METADATA ); + return ( + self::ATTR_UNICODE_PATHS | + self::ATTR_HEADERS | + self::ATTR_METADATA + ); } protected function resolveContainerPath( $container, $relStoragePath ) { @@ -167,64 +172,33 @@ class SwiftFileBackend extends FileBackendStore { } /** - * Sanitize and filter the custom headers from a $params array. - * Only allows certain "standard" Content- and X-Content- headers. - * - * @param array $params - * @return array Sanitized value of 'headers' field in $params - */ - protected function sanitizeHdrsStrict( array $params ) { - if ( !isset( $params['headers'] ) ) { - return []; - } - - $headers = $this->getCustomHeaders( $params['headers'] ); - unset( $headers[ 'content-type' ] ); - - return $headers; - } - - /** - * Sanitize and filter the custom headers from a $params array. - * Only allows certain "standard" Content- and X-Content- headers. + * Filter/normalize a header map to only include mutable "content-"/"x-content-" headers * - * When POSTing data, libcurl adds Content-Type: application/x-www-form-urlencoded - * if Content-Type is not set, which overwrites the stored Content-Type header - * in Swift - therefore for POSTing data do not strip the Content-Type header (the - * previously-stored header that has been already read back from swift is sent) + * Mutable headers can be changed via HTTP POST even if the file content is the same * - * @param array $params - * @return array Sanitized value of 'headers' field in $params + * @see https://docs.openstack.org/api-ref/object-store + * @param string[] $headers Map of (header => value) for a swift object + * @return string[] Map of (header => value) for Content-* headers mutable via POST */ - protected function sanitizeHdrs( array $params ) { - return isset( $params['headers'] ) - ? $this->getCustomHeaders( $params['headers'] ) - : []; - } - - /** - * @param array $rawHeaders - * @return array Custom non-metadata HTTP headers - */ - protected function getCustomHeaders( array $rawHeaders ) { - $headers = []; - + protected function extractMutableContentHeaders( array $headers ) { + $contentHeaders = []; // Normalize casing, and strip out illegal headers - foreach ( $rawHeaders as $name => $value ) { + foreach ( $headers as $name => $value ) { $name = strtolower( $name ); - if ( preg_match( '/^content-length$/', $name ) ) { - continue; // blacklisted - } elseif ( preg_match( '/^(x-)?content-/', $name ) ) { - $headers[$name] = $value; // allowed - } elseif ( preg_match( '/^content-(disposition)/', $name ) ) { - $headers[$name] = $value; // allowed + if ( !preg_match( '/^(x-)?content-(?!length$)/', $name ) ) { + // Only allow content-* and x-content-* headers (but not content-length) + continue; + } elseif ( $name === 'content-type' && !strlen( $value ) ) { + // This header can be set to a value but not unset for sanity + continue; } + $contentHeaders[$name] = $value; } // By default, Swift has annoyingly low maximum header value limits - if ( isset( $headers['content-disposition'] ) ) { + if ( isset( $contentHeaders['content-disposition'] ) ) { $disposition = ''; // @note: assume FileBackend::makeContentDisposition() already used - foreach ( explode( ';', $headers['content-disposition'] ) as $part ) { + foreach ( explode( ';', $contentHeaders['content-disposition'] ) as $part ) { $part = trim( $part ); $new = ( $disposition === '' ) ? $part : "{$disposition};{$part}"; if ( strlen( $new ) <= 255 ) { @@ -233,36 +207,40 @@ class SwiftFileBackend extends FileBackendStore { break; // too long; sigh } } - $headers['content-disposition'] = $disposition; + $contentHeaders['content-disposition'] = $disposition; } - return $headers; + return $contentHeaders; } /** - * @param array $rawHeaders - * @return array Custom metadata headers + * @see https://docs.openstack.org/api-ref/object-store + * @param string[] $headers Map of (header => value) for a swift object + * @return string[] Map of (metadata header name => metadata value) */ - protected function getMetadataHeaders( array $rawHeaders ) { - $headers = []; - foreach ( $rawHeaders as $name => $value ) { + protected function extractMetadataHeaders( array $headers ) { + $metadataHeaders = []; + foreach ( $headers as $name => $value ) { $name = strtolower( $name ); if ( strpos( $name, 'x-object-meta-' ) === 0 ) { - $headers[$name] = $value; + $metadataHeaders[$name] = $value; } } - return $headers; + return $metadataHeaders; } /** - * @param array $rawHeaders - * @return array Custom metadata headers with prefix removed + * @see https://docs.openstack.org/api-ref/object-store + * @param string[] $headers Map of (header => value) for a swift object + * @return string[] Map of (metadata key name => metadata value) */ - protected function getMetadata( array $rawHeaders ) { + protected function getMetadataFromHeaders( array $headers ) { + $prefixLen = strlen( 'x-object-meta-' ); + $metadata = []; - foreach ( $this->getMetadataHeaders( $rawHeaders ) as $name => $value ) { - $metadata[substr( $name, strlen( 'x-object-meta-' ) )] = $value; + foreach ( $this->extractMetadataHeaders( $headers ) as $name => $value ) { + $metadata[substr( $name, $prefixLen )] = $value; } return $metadata; @@ -278,19 +256,24 @@ class SwiftFileBackend extends FileBackendStore { return $status; } - $sha1Hash = Wikimedia\base_convert( sha1( $params['content'] ), 16, 36, 31 ); - $contentType = $params['headers']['content-type'] + // Headers that are not strictly a function of the file content + $mutableHeaders = $this->extractMutableContentHeaders( $params['headers'] ?? [] ); + // Make sure that the "content-type" header is set to something sensible + $mutableHeaders['content-type'] = $mutableHeaders['content-type'] ?? $this->getContentType( $params['dst'], $params['content'], null ); $reqs = [ [ 'method' => 'PUT', 'url' => [ $dstCont, $dstRel ], - 'headers' => [ - 'content-length' => strlen( $params['content'] ), - 'etag' => md5( $params['content'] ), - 'content-type' => $contentType, - 'x-object-meta-sha1base36' => $sha1Hash - ] + $this->sanitizeHdrsStrict( $params ), + 'headers' => array_merge( + $mutableHeaders, + [ + 'etag' => md5( $params['content'] ), + 'content-length' => strlen( $params['content'] ), + 'x-object-meta-sha1base36' => + Wikimedia\base_convert( sha1( $params['content'] ), 16, 36, 31 ) + ] + ), 'body' => $params['content'] ] ]; @@ -304,6 +287,8 @@ class SwiftFileBackend extends FileBackendStore { } else { $this->onError( $status, $method, $params, $rerr, $rcode, $rdesc ); } + + return SwiftFileOpHandle::CONTINUE_IF_OK; }; $opHandle = new SwiftFileOpHandle( $this, $handler, $reqs ); @@ -326,35 +311,57 @@ class SwiftFileBackend extends FileBackendStore { return $status; } - Wikimedia\suppressWarnings(); - $sha1Hash = sha1_file( $params['src'] ); - Wikimedia\restoreWarnings(); - if ( $sha1Hash === false ) { // source doesn't exist? - $status->fatal( 'backend-fail-store', $params['src'], $params['dst'] ); + // Open a handle to the source file so that it can be streamed. The size and hash + // will be computed using the handle. In the off chance that the source file changes + // during this operation, the PUT will fail due to an ETag mismatch and be aborted. + AtEase::suppressWarnings(); + $srcHandle = fopen( $params['src'], 'rb' ); + AtEase::restoreWarnings(); + if ( $srcHandle === false ) { // source doesn't exist? + $status->fatal( 'backend-fail-notexists', $params['src'] ); return $status; } - $sha1Hash = Wikimedia\base_convert( $sha1Hash, 16, 36, 31 ); - $contentType = $params['headers']['content-type'] - ?? $this->getContentType( $params['dst'], null, $params['src'] ); - $handle = fopen( $params['src'], 'rb' ); - if ( $handle === false ) { // source doesn't exist? - $status->fatal( 'backend-fail-store', $params['src'], $params['dst'] ); + // Compute the MD5 and SHA-1 hashes in one pass + $srcSize = fstat( $srcHandle )['size']; + $md5Context = hash_init( 'md5' ); + $sha1Context = hash_init( 'sha1' ); + $hashDigestSize = 0; + while ( !feof( $srcHandle ) ) { + $buffer = (string)fread( $srcHandle, 131072 ); // 128 KiB + hash_update( $md5Context, $buffer ); + hash_update( $sha1Context, $buffer ); + $hashDigestSize += strlen( $buffer ); + } + // Reset the handle back to the beginning so that it can be streamed + rewind( $srcHandle ); + + if ( $hashDigestSize !== $srcSize ) { + $status->fatal( 'backend-fail-hash', $params['src'] ); return $status; } + // Headers that are not strictly a function of the file content + $mutableHeaders = $this->extractMutableContentHeaders( $params['headers'] ?? [] ); + // Make sure that the "content-type" header is set to something sensible + $mutableHeaders['content-type'] = $mutableHeaders['content-type'] + ?? $this->getContentType( $params['dst'], null, $params['src'] ); + $reqs = [ [ 'method' => 'PUT', 'url' => [ $dstCont, $dstRel ], - 'headers' => [ - 'content-length' => filesize( $params['src'] ), - 'etag' => md5_file( $params['src'] ), - 'content-type' => $contentType, - 'x-object-meta-sha1base36' => $sha1Hash - ] + $this->sanitizeHdrsStrict( $params ), - 'body' => $handle // resource + 'headers' => array_merge( + $mutableHeaders, + [ + 'content-length' => $srcSize, + 'etag' => hash_final( $md5Context ), + 'x-object-meta-sha1base36' => + Wikimedia\base_convert( hash_final( $sha1Context ), 16, 36, 31 ) + ] + ), + 'body' => $srcHandle // resource ] ]; $method = __METHOD__; @@ -367,10 +374,12 @@ class SwiftFileBackend extends FileBackendStore { } else { $this->onError( $status, $method, $params, $rerr, $rcode, $rdesc ); } + + return SwiftFileOpHandle::CONTINUE_IF_OK; }; $opHandle = new SwiftFileOpHandle( $this, $handler, $reqs ); - $opHandle->resourcesToClose[] = $handle; + $opHandle->resourcesToClose[] = $srcHandle; if ( !empty( $params['async'] ) ) { // deferred $status->value = $opHandle; @@ -401,10 +410,13 @@ class SwiftFileBackend extends FileBackendStore { $reqs = [ [ 'method' => 'PUT', 'url' => [ $dstCont, $dstRel ], - 'headers' => [ - 'x-copy-from' => '/' . rawurlencode( $srcCont ) . - '/' . str_replace( "%2F", "/", rawurlencode( $srcRel ) ) - ] + $this->sanitizeHdrsStrict( $params ), // extra headers merged into object + 'headers' => array_merge( + $this->extractMutableContentHeaders( $params['headers'] ?? [] ), + [ + 'x-copy-from' => '/' . rawurlencode( $srcCont ) . '/' . + str_replace( "%2F", "/", rawurlencode( $srcRel ) ) + ] + ) ] ]; $method = __METHOD__; @@ -413,10 +425,14 @@ class SwiftFileBackend extends FileBackendStore { if ( $rcode === 201 ) { // good } elseif ( $rcode === 404 ) { - $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] ); + if ( empty( $params['ignoreMissingSource'] ) ) { + $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] ); + } } else { $this->onError( $status, $method, $params, $rerr, $rcode, $rdesc ); } + + return SwiftFileOpHandle::CONTINUE_IF_OK; }; $opHandle = new SwiftFileOpHandle( $this, $handler, $reqs ); @@ -446,16 +462,17 @@ class SwiftFileBackend extends FileBackendStore { return $status; } - $reqs = [ - [ - 'method' => 'PUT', - 'url' => [ $dstCont, $dstRel ], - 'headers' => [ - 'x-copy-from' => '/' . rawurlencode( $srcCont ) . - '/' . str_replace( "%2F", "/", rawurlencode( $srcRel ) ) - ] + $this->sanitizeHdrsStrict( $params ) // extra headers merged into object - ] - ]; + $reqs = [ [ + 'method' => 'PUT', + 'url' => [ $dstCont, $dstRel ], + 'headers' => array_merge( + $this->extractMutableContentHeaders( $params['headers'] ?? [] ), + [ + 'x-copy-from' => '/' . rawurlencode( $srcCont ) . '/' . + str_replace( "%2F", "/", rawurlencode( $srcRel ) ) + ] + ) + ] ]; if ( "{$srcCont}/{$srcRel}" !== "{$dstCont}/{$dstRel}" ) { $reqs[] = [ 'method' => 'DELETE', @@ -472,10 +489,17 @@ class SwiftFileBackend extends FileBackendStore { } elseif ( $request['method'] === 'DELETE' && $rcode === 204 ) { // good } elseif ( $rcode === 404 ) { - $status->fatal( 'backend-fail-move', $params['src'], $params['dst'] ); + if ( empty( $params['ignoreMissingSource'] ) ) { + $status->fatal( 'backend-fail-move', $params['src'], $params['dst'] ); + } else { + // Leave Status as OK but skip the DELETE request + return SwiftFileOpHandle::CONTINUE_NO; + } } else { $this->onError( $status, $method, $params, $rerr, $rcode, $rdesc ); } + + return SwiftFileOpHandle::CONTINUE_IF_OK; }; $opHandle = new SwiftFileOpHandle( $this, $handler, $reqs ); @@ -516,6 +540,8 @@ class SwiftFileBackend extends FileBackendStore { } else { $this->onError( $status, $method, $params, $rerr, $rcode, $rdesc ); } + + return SwiftFileOpHandle::CONTINUE_IF_OK; }; $opHandle = new SwiftFileOpHandle( $this, $handler, $reqs ); @@ -549,17 +575,20 @@ class SwiftFileBackend extends FileBackendStore { return $status; } - // POST clears prior headers, so we need to merge the changes in to the old ones - $metaHdrs = []; + // Swift object POST clears any prior headers, so merge the new and old headers here. + // Also, during, POST, libcurl adds "Content-Type: application/x-www-form-urlencoded" + // if "Content-Type" is not set, which would clobber the header value for the object. + $oldMetadataHeaders = []; foreach ( $stat['xattr']['metadata'] as $name => $value ) { - $metaHdrs["x-object-meta-$name"] = $value; + $oldMetadataHeaders["x-object-meta-$name"] = $value; } - $customHdrs = $this->sanitizeHdrs( $params ) + $stat['xattr']['headers']; + $newContentHeaders = $this->extractMutableContentHeaders( $params['headers'] ?? [] ); + $oldContentHeaders = $stat['xattr']['headers']; $reqs = [ [ 'method' => 'POST', 'url' => [ $srcCont, $srcRel ], - 'headers' => $metaHdrs + $customHdrs + 'headers' => $oldMetadataHeaders + $newContentHeaders + $oldContentHeaders ] ]; $method = __METHOD__; @@ -591,7 +620,7 @@ class SwiftFileBackend extends FileBackendStore { $stat = $this->getContainerStat( $fullCont ); if ( is_array( $stat ) ) { return $status; // already there - } elseif ( $stat === null ) { + } elseif ( $stat === self::$RES_ERROR ) { $status->fatal( 'backend-fail-internal', $this->name ); $this->logger->error( __METHOD__ . ': cannot get container stat' ); @@ -738,9 +767,9 @@ class SwiftFileBackend extends FileBackendStore { } // Find prior custom HTTP headers - $postHeaders = $this->getCustomHeaders( $objHdrs ); + $postHeaders = $this->extractMutableContentHeaders( $objHdrs ); // Find prior metadata headers - $postHeaders += $this->getMetadataHeaders( $objHdrs ); + $postHeaders += $this->extractMetadataHeaders( $objHdrs ); $status = $this->newStatus(); /** @noinspection PhpUnusedLocalVariableInspection */ @@ -775,8 +804,6 @@ class SwiftFileBackend extends FileBackendStore { } protected function doGetFileContentsMulti( array $params ) { - $contents = []; - $auth = $this->getAuthentication(); $ep = array_diff_key( $params, [ 'srcs' => 1 ] ); // for error logging @@ -784,11 +811,12 @@ class SwiftFileBackend extends FileBackendStore { // if the file does not exist. Do not waste time doing file stats here. $reqs = []; // (path => op) + // Initial dummy values to preserve path order + $contents = array_fill_keys( $params['srcs'], self::$RES_ERROR ); foreach ( $params['srcs'] as $path ) { // each path in this concurrent batch list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $path ); if ( $srcRel === null || !$auth ) { - $contents[$path] = false; - continue; + continue; // invalid storage path or auth error } // Create a new temporary memory file... $handle = fopen( 'php://temp', 'wb' ); @@ -801,7 +829,6 @@ class SwiftFileBackend extends FileBackendStore { 'stream' => $handle, ]; } - $contents[$path] = false; } $opts = [ 'maxConnsPerHost' => $params['concurrency'] ]; @@ -810,10 +837,21 @@ class SwiftFileBackend extends FileBackendStore { list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $op['response']; if ( $rcode >= 200 && $rcode <= 299 ) { rewind( $op['stream'] ); // start from the beginning - $contents[$path] = stream_get_contents( $op['stream'] ); + $content = (string)stream_get_contents( $op['stream'] ); + $size = strlen( $content ); + // Make sure that stream finished + if ( $size === (int)$rhdrs['content-length'] ) { + $contents[$path] = $content; + } else { + $contents[$path] = self::$RES_ERROR; + $rerr = "Got {$size}/{$rhdrs['content-length']} bytes"; + $this->onError( null, __METHOD__, + [ 'src' => $path ] + $ep, $rerr, $rcode, $rdesc ); + } } elseif ( $rcode === 404 ) { - $contents[$path] = false; + $contents[$path] = self::$RES_ABSENT; } else { + $contents[$path] = self::$RES_ERROR; $this->onError( null, __METHOD__, [ 'src' => $path ] + $ep, $rerr, $rcode, $rdesc ); } @@ -830,7 +868,7 @@ class SwiftFileBackend extends FileBackendStore { return ( count( $status->value ) ) > 0; } - return null; // error + return self::$RES_ERROR; } /** @@ -872,6 +910,7 @@ class SwiftFileBackend extends FileBackendStore { return $dirs; // nothing more } + /** @noinspection PhpUnusedLocalVariableInspection */ $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" ); $prefix = ( $dir == '' ) ? null : "{$dir}/"; @@ -882,6 +921,7 @@ class SwiftFileBackend extends FileBackendStore { throw new FileBackendError( "Iterator page I/O error." ); } $objects = $status->value; + // @phan-suppress-next-line PhanTypeSuspiciousNonTraversableForeach foreach ( $objects as $object ) { // files and directories if ( substr( $object, -1 ) === '/' ) { $dirs[] = $object; // directories end in '/' @@ -903,6 +943,7 @@ class SwiftFileBackend extends FileBackendStore { $objects = $status->value; + // @phan-suppress-next-line PhanTypeSuspiciousNonTraversableForeach foreach ( $objects as $object ) { // files $objectDir = $getParentDir( $object ); // directory of object @@ -952,10 +993,11 @@ class SwiftFileBackend extends FileBackendStore { return $files; // nothing more } + /** @noinspection PhpUnusedLocalVariableInspection */ $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" ); $prefix = ( $dir == '' ) ? null : "{$dir}/"; - // $objects will contain a list of unfiltered names or CF_Object items + // $objects will contain a list of unfiltered names or stdClass items // Non-recursive: only list files right under $dir if ( !empty( $params['topOnly'] ) ) { if ( !empty( $params['adviseStat'] ) ) { @@ -978,7 +1020,7 @@ class SwiftFileBackend extends FileBackendStore { } $objects = $status->value; - $files = $this->buildFileObjectListing( $params, $dir, $objects ); + $files = $this->buildFileObjectListing( $objects ); // Page on the unfiltered object listing (what is returned may be filtered) if ( count( $objects ) < $limit ) { @@ -993,14 +1035,12 @@ class SwiftFileBackend extends FileBackendStore { /** * Build a list of file objects, filtering out any directories - * and extracting any stat info if provided in $objects (for CF_Objects) + * and extracting any stat info if provided in $objects * - * @param array $params Parameters for getDirectoryList() - * @param string $dir Resolved container directory path - * @param array $objects List of CF_Object items or object names + * @param stdClass[]|string[] $objects List of stdClass items or object names * @return array List of (names,stat array or null) entries */ - private function buildFileObjectListing( array $params, $dir, array $objects ) { + private function buildFileObjectListing( array $objects ) { $names = []; foreach ( $objects as $object ) { if ( is_object( $object ) ) { @@ -1038,17 +1078,17 @@ class SwiftFileBackend extends FileBackendStore { protected function doGetFileXAttributes( array $params ) { $stat = $this->getFileStat( $params ); - if ( $stat ) { - if ( !isset( $stat['xattr'] ) ) { - // Stat entries filled by file listings don't include metadata/headers - $this->clearCache( [ $params['src'] ] ); - $stat = $this->getFileStat( $params ); - } + // Stat entries filled by file listings don't include metadata/headers + if ( is_array( $stat ) && !isset( $stat['xattr'] ) ) { + $this->clearCache( [ $params['src'] ] ); + $stat = $this->getFileStat( $params ); + } + if ( is_array( $stat ) ) { return $stat['xattr']; - } else { - return false; } + + return ( $stat === self::$RES_ERROR ) ? self::$RES_ERROR : self::$RES_ABSENT; } protected function doGetFileSha1base36( array $params ) { @@ -1057,21 +1097,21 @@ class SwiftFileBackend extends FileBackendStore { $params['requireSHA1'] = true; $stat = $this->getFileStat( $params ); - if ( $stat ) { + if ( is_array( $stat ) ) { return $stat['sha1']; - } else { - return false; } + + return ( $stat === self::$RES_ERROR ) ? self::$RES_ERROR : self::$RES_ABSENT; } protected function doStreamFile( array $params ) { $status = $this->newStatus(); - $flags = !empty( $params['headless'] ) ? StreamFile::STREAM_HEADLESS : 0; + $flags = !empty( $params['headless'] ) ? HTTPFileStreamer::STREAM_HEADLESS : 0; list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] ); if ( $srcRel === null ) { - StreamFile::send404Message( $params['src'], $flags ); + HTTPFileStreamer::send404Message( $params['src'], $flags ); $status->fatal( 'backend-fail-invalidpath', $params['src'] ); return $status; @@ -1079,7 +1119,7 @@ class SwiftFileBackend extends FileBackendStore { $auth = $this->getAuthentication(); if ( !$auth || !is_array( $this->getContainerStat( $srcCont ) ) ) { - StreamFile::send404Message( $params['src'], $flags ); + HTTPFileStreamer::send404Message( $params['src'], $flags ); $status->fatal( 'backend-fail-stream', $params['src'] ); return $status; @@ -1088,7 +1128,7 @@ class SwiftFileBackend extends FileBackendStore { // If "headers" is set, we only want to send them if the file is there. // Do not bother checking if the file exists if headers are not set though. if ( $params['headers'] && !$this->fileExists( $params ) ) { - StreamFile::send404Message( $params['src'], $flags ); + HTTPFileStreamer::send404Message( $params['src'], $flags ); $status->fatal( 'backend-fail-stream', $params['src'] ); return $status; @@ -1131,9 +1171,6 @@ class SwiftFileBackend extends FileBackendStore { } protected function doGetLocalCopyMulti( array $params ) { - /** @var TempFSFile[] $tmpFiles */ - $tmpFiles = []; - $auth = $this->getAuthentication(); $ep = array_diff_key( $params, [ 'srcs' => 1 ] ); // for error logging @@ -1141,56 +1178,62 @@ class SwiftFileBackend extends FileBackendStore { // if the file does not exist. Do not waste time doing file stats here. $reqs = []; // (path => op) + // Initial dummy values to preserve path order + $tmpFiles = array_fill_keys( $params['srcs'], self::$RES_ERROR ); foreach ( $params['srcs'] as $path ) { // each path in this concurrent batch list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $path ); if ( $srcRel === null || !$auth ) { - $tmpFiles[$path] = null; - continue; + continue; // invalid storage path or auth error } // Get source file extension $ext = FileBackend::extensionFromPath( $path ); // Create a new temporary file... - $tmpFile = TempFSFile::factory( 'localcopy_', $ext, $this->tmpDirectory ); - if ( $tmpFile ) { - $handle = fopen( $tmpFile->getPath(), 'wb' ); - if ( $handle ) { - $reqs[$path] = [ - 'method' => 'GET', - 'url' => $this->storageUrl( $auth, $srcCont, $srcRel ), - 'headers' => $this->authTokenHeaders( $auth ) - + $this->headersFromParams( $params ), - 'stream' => $handle, - ]; - } else { - $tmpFile = null; - } + $tmpFile = $this->tmpFileFactory->newTempFSFile( 'localcopy_', $ext ); + $handle = $tmpFile ? fopen( $tmpFile->getPath(), 'wb' ) : false; + if ( $handle ) { + $reqs[$path] = [ + 'method' => 'GET', + 'url' => $this->storageUrl( $auth, $srcCont, $srcRel ), + 'headers' => $this->authTokenHeaders( $auth ) + + $this->headersFromParams( $params ), + 'stream' => $handle, + ]; + $tmpFiles[$path] = $tmpFile; } - $tmpFiles[$path] = $tmpFile; } - $isLatest = ( $this->isRGW || !empty( $params['latest'] ) ); + // Ceph RADOS Gateway is in use (strong consistency) or X-Newest will be used + $latest = ( $this->isRGW || !empty( $params['latest'] ) ); + $opts = [ 'maxConnsPerHost' => $params['concurrency'] ]; $reqs = $this->http->runMulti( $reqs, $opts ); foreach ( $reqs as $path => $op ) { list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $op['response']; fclose( $op['stream'] ); // close open handle if ( $rcode >= 200 && $rcode <= 299 ) { - $size = $tmpFiles[$path] ? $tmpFiles[$path]->getSize() : 0; - // Double check that the disk is not full/broken - if ( $size != $rhdrs['content-length'] ) { - $tmpFiles[$path] = null; + /** @var TempFSFile $tmpFile */ + $tmpFile = $tmpFiles[$path]; + // Make sure that the stream finished and fully wrote to disk + $size = $tmpFile->getSize(); + if ( $size !== (int)$rhdrs['content-length'] ) { + $tmpFiles[$path] = self::$RES_ERROR; $rerr = "Got {$size}/{$rhdrs['content-length']} bytes"; $this->onError( null, __METHOD__, [ 'src' => $path ] + $ep, $rerr, $rcode, $rdesc ); } // Set the file stat process cache in passing $stat = $this->getStatFromHeaders( $rhdrs ); - $stat['latest'] = $isLatest; + $stat['latest'] = $latest; $this->cheapCache->setField( $path, 'stat', $stat ); } elseif ( $rcode === 404 ) { - $tmpFiles[$path] = false; + $tmpFiles[$path] = self::$RES_ABSENT; + $this->cheapCache->setField( + $path, + 'stat', + $latest ? self::$ABSENT_LATEST : self::$ABSENT_NORMAL + ); } else { - $tmpFiles[$path] = null; + $tmpFiles[$path] = self::$RES_ERROR; $this->onError( null, __METHOD__, [ 'src' => $path ] + $ep, $rerr, $rcode, $rdesc ); } @@ -1205,12 +1248,12 @@ class SwiftFileBackend extends FileBackendStore { ) { list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] ); if ( $srcRel === null ) { - return null; // invalid path + return self::TEMPURL_ERROR; // invalid path } $auth = $this->getAuthentication(); if ( !$auth ) { - return null; + return self::TEMPURL_ERROR; } $ttl = $params['ttl'] ?? 86400; @@ -1250,7 +1293,7 @@ class SwiftFileBackend extends FileBackendStore { } } - return null; + return self::TEMPURL_ERROR; } protected function directoriesAreVirtual() { @@ -1274,12 +1317,10 @@ class SwiftFileBackend extends FileBackendStore { return $hdrs; } - /** - * @param FileBackendStoreOpHandle[] $fileOpHandles - * - * @return StatusValue[] - */ protected function doExecuteOpHandlesInternal( array $fileOpHandles ) { + /** @var SwiftFileOpHandle[] $fileOpHandles */ + '@phan-var SwiftFileOpHandle[] $fileOpHandles'; + /** @var StatusValue[] $statuses */ $statuses = []; @@ -1295,7 +1336,6 @@ class SwiftFileBackend extends FileBackendStore { // Split the HTTP requests into stages that can be done concurrently $httpReqsByStage = []; // map of (stage => index => HTTP request) foreach ( $fileOpHandles as $index => $fileOpHandle ) { - /** @var SwiftFileOpHandle $fileOpHandle */ $reqs = $fileOpHandle->httpOp; // Convert the 'url' parameter to an actual URL using $auth foreach ( $reqs as $stage => &$req ) { @@ -1313,13 +1353,18 @@ class SwiftFileBackend extends FileBackendStore { for ( $stage = 0; $stage < $reqCount; ++$stage ) { $httpReqs = $this->http->runMulti( $httpReqsByStage[$stage] ); foreach ( $httpReqs as $index => $httpReq ) { + /** @var SwiftFileOpHandle $fileOpHandle */ + $fileOpHandle = $fileOpHandles[$index]; // Run the callback for each request of this operation - $callback = $fileOpHandles[$index]->callback; - $callback( $httpReq, $statuses[$index] ); - // On failure, abort all remaining requests for this operation - // (e.g. abort the DELETE request if the COPY request fails for a move) - if ( !$statuses[$index]->isOK() ) { - $stages = count( $fileOpHandles[$index]->httpOp ); + $status = $statuses[$index]; + ( $fileOpHandle->callback )( $httpReq, $status ); + // On failure, abort all remaining requests for this operation. This is used + // in "move" operations to abort the DELETE request if the PUT request fails. + if ( + !$status->isOK() || + $fileOpHandle->state === $fileOpHandle::CONTINUE_NO + ) { + $stages = count( $fileOpHandle->httpOp ); for ( $s = ( $stage + 1 ); $s < $stages; ++$s ) { unset( $httpReqsByStage[$s][$index] ); } @@ -1389,6 +1434,7 @@ class SwiftFileBackend extends FileBackendStore { * @return array|bool|null False on 404, null on failure */ protected function getContainerStat( $container, $bypassCache = false ) { + /** @noinspection PhpUnusedLocalVariableInspection */ $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" ); if ( $bypassCache ) { // purge cache @@ -1399,7 +1445,7 @@ class SwiftFileBackend extends FileBackendStore { if ( !$this->containerStatCache->hasField( $container, 'stat' ) ) { $auth = $this->getAuthentication(); if ( !$auth ) { - return null; + return self::$RES_ERROR; } list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $this->http->run( [ @@ -1420,12 +1466,12 @@ class SwiftFileBackend extends FileBackendStore { $this->setContainerCache( $container, $stat ); // update persistent cache } } elseif ( $rcode === 404 ) { - return false; + return self::$RES_ABSENT; } else { $this->onError( null, __METHOD__, [ 'cont' => $container ], $rerr, $rcode, $rdesc ); - return null; + return self::$RES_ERROR; } } @@ -1590,24 +1636,21 @@ class SwiftFileBackend extends FileBackendStore { $auth = $this->getAuthentication(); - $reqs = []; + $reqs = []; // (path => op) + // (a) Check the containers of the paths... foreach ( $params['srcs'] as $path ) { list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $path ); - if ( $srcRel === null ) { - $stats[$path] = false; - continue; // invalid storage path - } elseif ( !$auth ) { - $stats[$path] = null; - continue; + if ( $srcRel === null || !$auth ) { + $stats[$path] = self::$RES_ERROR; + continue; // invalid storage path or auth error } - // (a) Check the container $cstat = $this->getContainerStat( $srcCont ); - if ( $cstat === false ) { - $stats[$path] = false; + if ( $cstat === self::$RES_ABSENT ) { + $stats[$path] = self::$RES_ABSENT; continue; // ok, nothing to do } elseif ( !is_array( $cstat ) ) { - $stats[$path] = null; + $stats[$path] = self::$RES_ERROR; continue; } @@ -1618,15 +1661,11 @@ class SwiftFileBackend extends FileBackendStore { ]; } + // (b) Check the files themselves... $opts = [ 'maxConnsPerHost' => $params['concurrency'] ]; $reqs = $this->http->runMulti( $reqs, $opts ); - - foreach ( $params['srcs'] as $path ) { - if ( array_key_exists( $path, $stats ) ) { - continue; // some sort of failure above - } - // (b) Check the file - list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $reqs[$path]['response']; + foreach ( $reqs as $path => $op ) { + list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $op['response']; if ( $rcode === 200 || $rcode === 204 ) { // Update the object if it is missing some headers if ( !empty( $params['requireSHA1'] ) ) { @@ -1638,9 +1677,9 @@ class SwiftFileBackend extends FileBackendStore { $stat['latest'] = true; // strong consistency } } elseif ( $rcode === 404 ) { - $stat = false; + $stat = self::$RES_ABSENT; } else { - $stat = null; + $stat = self::$RES_ERROR; $this->onError( null, __METHOD__, $params, $rerr, $rcode, $rdesc ); } $stats[$path] = $stat; @@ -1655,9 +1694,9 @@ class SwiftFileBackend extends FileBackendStore { */ protected function getStatFromHeaders( array $rhdrs ) { // Fetch all of the custom metadata headers - $metadata = $this->getMetadata( $rhdrs ); + $metadata = $this->getMetadataFromHeaders( $rhdrs ); // Fetch all of the custom raw HTTP headers - $headers = $this->sanitizeHdrs( [ 'headers' => $rhdrs ] ); + $headers = $this->extractMutableContentHeaders( $rhdrs ); return [ // Convert various random Swift dates to TS_MW diff --git a/includes/libs/filebackend/fileiteration/SwiftFileBackendList.php b/includes/libs/filebackend/fileiteration/SwiftFileBackendList.php index bcde8d90a9..92105c350e 100644 --- a/includes/libs/filebackend/fileiteration/SwiftFileBackendList.php +++ b/includes/libs/filebackend/fileiteration/SwiftFileBackendList.php @@ -33,7 +33,7 @@ abstract class SwiftFileBackendList implements Iterator { /** @var array List of path or (path,stat array) entries */ protected $bufferIter = []; - /** @var string List items *after* this path */ + /** @var string|null List items *after* this path */ protected $bufferAfter = null; /** @var int */ @@ -108,6 +108,7 @@ abstract class SwiftFileBackendList implements Iterator { $this->pos = 0; $this->bufferAfter = null; $this->bufferIter = $this->pageFromList( + // @phan-suppress-next-line PhanTypeMismatchArgumentPropertyReferenceReal $this->container, $this->dir, $this->bufferAfter, self::PAGE_SIZE, $this->params ); // updates $this->bufferAfter } diff --git a/includes/libs/filebackend/filejournal/FileJournal.php b/includes/libs/filebackend/filejournal/FileJournal.php index dc007a0cec..c256d72b1d 100644 --- a/includes/libs/filebackend/filejournal/FileJournal.php +++ b/includes/libs/filebackend/filejournal/FileJournal.php @@ -26,6 +26,7 @@ * @ingroup FileJournal */ +use Wikimedia\ObjectFactory; use Wikimedia\Timestamp\ConvertibleTimestamp; /** @@ -39,16 +40,16 @@ use Wikimedia\Timestamp\ConvertibleTimestamp; abstract class FileJournal { /** @var string */ protected $backend; - /** @var int */ + /** @var int|false */ protected $ttlDays; /** - * Construct a new instance from configuration. + * Construct a new instance from configuration. Do not call this directly, use factory(). * * @param array $config Includes: * 'ttlDays' : days to keep log entries around (false means "forever") */ - protected function __construct( array $config ) { + public function __construct( array $config ) { $this->ttlDays = $config['ttlDays'] ?? false; } @@ -61,11 +62,10 @@ abstract class FileJournal { * @return FileJournal */ final public static function factory( array $config, $backend ) { - $class = $config['class']; - $jrn = new $class( $config ); - if ( !$jrn instanceof self ) { - throw new InvalidArgumentException( "$class is not an instance of " . __CLASS__ ); - } + $jrn = ObjectFactory::getObjectFromSpec( + $config, + [ 'specIsArg' => true, 'assertClass' => __CLASS__ ] + ); $jrn->backend = $backend; return $jrn; @@ -153,7 +153,7 @@ abstract class FileJournal { * A starting change ID and/or limit can be specified. * * @param int|null $start Starting change ID or null - * @param int $limit Maximum number of items to return + * @param int $limit Maximum number of items to return (0 = unlimited) * @param string|null &$next Updated to the ID of the next entry. * @return array List of associative arrays, each having: * id : unique, monotonic, ID for this change diff --git a/includes/libs/filebackend/fileop/CopyFileOp.php b/includes/libs/filebackend/fileop/CopyFileOp.php index 527de6a5e4..59af94473b 100644 --- a/includes/libs/filebackend/fileop/CopyFileOp.php +++ b/includes/libs/filebackend/fileop/CopyFileOp.php @@ -36,8 +36,10 @@ class CopyFileOp extends FileOp { protected function doPrecheck( array &$predicates ) { $status = StatusValue::newGood(); - // Check if the source file exists - if ( !$this->fileExists( $this->params['src'], $predicates ) ) { + + // Check source file existence + $srcExists = $this->fileExists( $this->params['src'], $predicates ); + if ( $srcExists === false ) { if ( $this->getParam( 'ignoreMissingSource' ) ) { $this->doOperation = false; // no-op // Update file existence predicates (cache 404s) @@ -50,10 +52,8 @@ class CopyFileOp extends FileOp { return $status; } - // Check if a file can be placed/changed at the destination - } elseif ( !$this->backend->isPathUsableInternal( $this->params['dst'] ) ) { - $status->fatal( 'backend-fail-usable', $this->params['dst'] ); - $status->fatal( 'backend-fail-copy', $this->params['src'], $this->params['dst'] ); + } elseif ( $srcExists === FileBackend::EXISTENCE_ERROR ) { + $status->fatal( 'backend-fail-stat', $this->params['src'] ); return $status; } diff --git a/includes/libs/filebackend/fileop/CreateFileOp.php b/includes/libs/filebackend/fileop/CreateFileOp.php index f45b055cae..b68b98f669 100644 --- a/includes/libs/filebackend/fileop/CreateFileOp.php +++ b/includes/libs/filebackend/fileop/CreateFileOp.php @@ -34,21 +34,15 @@ class CreateFileOp extends FileOp { protected function doPrecheck( array &$predicates ) { $status = StatusValue::newGood(); - // Check if the source data is too big - if ( strlen( $this->getParam( 'content' ) ) > $this->backend->maxFileSizeInternal() ) { - $status->fatal( 'backend-fail-maxsize', - $this->params['dst'], $this->backend->maxFileSizeInternal() ); - $status->fatal( 'backend-fail-create', $this->params['dst'] ); - return $status; - // Check if a file can be placed/changed at the destination - } elseif ( !$this->backend->isPathUsableInternal( $this->params['dst'] ) ) { - $status->fatal( 'backend-fail-usable', $this->params['dst'] ); - $status->fatal( 'backend-fail-create', $this->params['dst'] ); + // Check if the source data is too big + $maxBytes = $this->backend->maxFileSizeInternal(); + if ( strlen( $this->getParam( 'content' ) ) > $maxBytes ) { + $status->fatal( 'backend-fail-maxsize', $this->params['dst'], $maxBytes ); return $status; } - // Check if destination file exists + // Check if an incompatible destination file exists $status->merge( $this->precheckDestExistence( $predicates ) ); $this->params['dstExists'] = $this->destExists; // see FileBackendStore::setFileCache() if ( $status->isOK() ) { @@ -61,12 +55,14 @@ class CreateFileOp extends FileOp { } protected function doAttempt() { - if ( !$this->overwriteSameCase ) { + if ( $this->overwriteSameCase ) { + $status = StatusValue::newGood(); // nothing to do + } else { // Create the file at the destination - return $this->backend->createInternal( $this->setFlags( $this->params ) ); + $status = $this->backend->createInternal( $this->setFlags( $this->params ) ); } - return StatusValue::newGood(); + return $status; } protected function getSourceSha1Base36() { diff --git a/includes/libs/filebackend/fileop/DeleteFileOp.php b/includes/libs/filebackend/fileop/DeleteFileOp.php index 1047a985ec..3b48881e72 100644 --- a/includes/libs/filebackend/fileop/DeleteFileOp.php +++ b/includes/libs/filebackend/fileop/DeleteFileOp.php @@ -32,8 +32,10 @@ class DeleteFileOp extends FileOp { protected function doPrecheck( array &$predicates ) { $status = StatusValue::newGood(); - // Check if the source file exists - if ( !$this->fileExists( $this->params['src'], $predicates ) ) { + + // Check source file existence + $srcExists = $this->fileExists( $this->params['src'], $predicates ); + if ( $srcExists === false ) { if ( $this->getParam( 'ignoreMissingSource' ) ) { $this->doOperation = false; // no-op // Update file existence predicates (cache 404s) @@ -46,10 +48,8 @@ class DeleteFileOp extends FileOp { return $status; } - // Check if a file can be placed/changed at the source - } elseif ( !$this->backend->isPathUsableInternal( $this->params['src'] ) ) { - $status->fatal( 'backend-fail-usable', $this->params['src'] ); - $status->fatal( 'backend-fail-delete', $this->params['src'] ); + } elseif ( $srcExists === FileBackend::EXISTENCE_ERROR ) { + $status->fatal( 'backend-fail-stat', $this->params['src'] ); return $status; } diff --git a/includes/libs/filebackend/fileop/DescribeFileOp.php b/includes/libs/filebackend/fileop/DescribeFileOp.php index 0d1e553265..3604b26ae5 100644 --- a/includes/libs/filebackend/fileop/DescribeFileOp.php +++ b/includes/libs/filebackend/fileop/DescribeFileOp.php @@ -32,21 +32,20 @@ class DescribeFileOp extends FileOp { protected function doPrecheck( array &$predicates ) { $status = StatusValue::newGood(); - // Check if the source file exists - if ( !$this->fileExists( $this->params['src'], $predicates ) ) { + + // Check source file existence + $srcExists = $this->fileExists( $this->params['src'], $predicates ); + if ( $srcExists === false ) { $status->fatal( 'backend-fail-notexists', $this->params['src'] ); return $status; - // Check if a file can be placed/changed at the source - } elseif ( !$this->backend->isPathUsableInternal( $this->params['src'] ) ) { - $status->fatal( 'backend-fail-usable', $this->params['src'] ); - $status->fatal( 'backend-fail-describe', $this->params['src'] ); + } elseif ( $srcExists === FileBackend::EXISTENCE_ERROR ) { + $status->fatal( 'backend-fail-stat', $this->params['src'] ); return $status; } // Update file existence predicates - $predicates['exists'][$this->params['src']] = - $this->fileExists( $this->params['src'], $predicates ); + $predicates['exists'][$this->params['src']] = $srcExists; $predicates['sha1'][$this->params['src']] = $this->fileSha1( $this->params['src'], $predicates ); diff --git a/includes/libs/filebackend/fileop/FileOp.php b/includes/libs/filebackend/fileop/FileOp.php index 206048bd71..a0465880e8 100644 --- a/includes/libs/filebackend/fileop/FileOp.php +++ b/includes/libs/filebackend/fileop/FileOp.php @@ -77,7 +77,7 @@ abstract class FileOp { * @param FileBackendStore $backend * @param array $params * @param LoggerInterface $logger PSR logger instance - * @throws FileBackendError + * @throws InvalidArgumentException */ final public function __construct( FileBackendStore $backend, array $params, LoggerInterface $logger @@ -255,6 +255,18 @@ abstract class FileOp { return StatusValue::newFatal( 'fileop-fail-state', self::STATE_NEW, $this->state ); } $this->state = self::STATE_CHECKED; + + $status = StatusValue::newGood(); + $storagePaths = array_merge( $this->storagePathsRead(), $this->storagePathsChanged() ); + foreach ( array_unique( $storagePaths ) as $storagePath ) { + if ( !$this->backend->isPathUsableInternal( $storagePath ) ) { + $status->fatal( 'backend-fail-usable', $storagePath ); + } + } + if ( !$status->isOK() ) { + return $status; + } + $status = $this->doPrecheck( $predicates ); if ( !$status->isOK() ) { $this->failed = true; @@ -391,6 +403,8 @@ abstract class FileOp { return $status; } + } elseif ( $this->destExists === FileBackend::EXISTENCE_ERROR ) { + $status->fatal( 'backend-fail-stat', $this->params['dst'] ); } return $status; @@ -409,9 +423,12 @@ abstract class FileOp { /** * Check if a file will exist in storage when this operation is attempted * + * Ideally, the file stat entry should already be preloaded via preloadFileStat(). + * Otherwise, this will query the backend. + * * @param string $source Storage path * @param array $predicates - * @return bool + * @return bool|null Whether the file will exist or null on error */ final protected function fileExists( $source, array $predicates ) { if ( isset( $predicates['exists'][$source] ) ) { @@ -424,11 +441,14 @@ abstract class FileOp { } /** - * Get the SHA-1 of a file in storage when this operation is attempted + * Get the SHA-1 hash a file in storage will have when this operation is attempted + * + * Ideally, file the stat entry should already be preloaded via preloadFileStat() and + * the backend tracks hashes as extended attributes. Otherwise, this will query the backend. * * @param string $source Storage path * @param array $predicates - * @return string|bool False on failure + * @return string|bool The SHA-1 hash the file will have or false if non-existent or on error */ final protected function fileSha1( $source, array $predicates ) { if ( isset( $predicates['sha1'][$source] ) ) { diff --git a/includes/libs/filebackend/fileop/MoveFileOp.php b/includes/libs/filebackend/fileop/MoveFileOp.php index 55dca516cd..0a83370db9 100644 --- a/includes/libs/filebackend/fileop/MoveFileOp.php +++ b/includes/libs/filebackend/fileop/MoveFileOp.php @@ -36,8 +36,10 @@ class MoveFileOp extends FileOp { protected function doPrecheck( array &$predicates ) { $status = StatusValue::newGood(); - // Check if the source file exists - if ( !$this->fileExists( $this->params['src'], $predicates ) ) { + + // Check source file existence + $srcExists = $this->fileExists( $this->params['src'], $predicates ); + if ( $srcExists === false ) { if ( $this->getParam( 'ignoreMissingSource' ) ) { $this->doOperation = false; // no-op // Update file existence predicates (cache 404s) @@ -50,14 +52,12 @@ class MoveFileOp extends FileOp { return $status; } - // Check if a file can be placed/changed at the destination - } elseif ( !$this->backend->isPathUsableInternal( $this->params['dst'] ) ) { - $status->fatal( 'backend-fail-usable', $this->params['dst'] ); - $status->fatal( 'backend-fail-move', $this->params['src'], $this->params['dst'] ); + } elseif ( $srcExists === FileBackend::EXISTENCE_ERROR ) { + $status->fatal( 'backend-fail-stat', $this->params['src'] ); return $status; } - // Check if destination file exists + // Check if an incompatible destination file exists $status->merge( $this->precheckDestExistence( $predicates ) ); $this->params['dstExists'] = $this->destExists; // see FileBackendStore::setFileCache() if ( $status->isOK() ) { diff --git a/includes/libs/filebackend/fileop/StoreFileOp.php b/includes/libs/filebackend/fileop/StoreFileOp.php index 69ae47f428..b8cfbf6bdf 100644 --- a/includes/libs/filebackend/fileop/StoreFileOp.php +++ b/includes/libs/filebackend/fileop/StoreFileOp.php @@ -21,6 +21,8 @@ * @ingroup FileBackend */ +use Wikimedia\AtEase\AtEase; + /** * Store a file into the backend from a file on the file system. * Parameters for this operation are outlined in FileBackend::doOperations(). @@ -36,26 +38,21 @@ class StoreFileOp extends FileOp { protected function doPrecheck( array &$predicates ) { $status = StatusValue::newGood(); - // Check if the source file exists on the file system + + // Check if the source file exists in the file system and is not too big if ( !is_file( $this->params['src'] ) ) { $status->fatal( 'backend-fail-notexists', $this->params['src'] ); return $status; - // Check if the source file is too big - } elseif ( filesize( $this->params['src'] ) > $this->backend->maxFileSizeInternal() ) { - $status->fatal( 'backend-fail-maxsize', - $this->params['dst'], $this->backend->maxFileSizeInternal() ); - $status->fatal( 'backend-fail-store', $this->params['src'], $this->params['dst'] ); - - return $status; - // Check if a file can be placed/changed at the destination - } elseif ( !$this->backend->isPathUsableInternal( $this->params['dst'] ) ) { - $status->fatal( 'backend-fail-usable', $this->params['dst'] ); - $status->fatal( 'backend-fail-store', $this->params['src'], $this->params['dst'] ); + } + // Check if the source file is too big + $maxBytes = $this->backend->maxFileSizeInternal(); + if ( filesize( $this->params['src'] ) > $maxBytes ) { + $status->fatal( 'backend-fail-maxsize', $this->params['dst'], $maxBytes ); return $status; } - // Check if destination file exists + // Check if an incompatible destination file exists $status->merge( $this->precheckDestExistence( $predicates ) ); $this->params['dstExists'] = $this->destExists; // see FileBackendStore::setFileCache() if ( $status->isOK() ) { @@ -68,18 +65,20 @@ class StoreFileOp extends FileOp { } protected function doAttempt() { - if ( !$this->overwriteSameCase ) { + if ( $this->overwriteSameCase ) { + $status = StatusValue::newGood(); // nothing to do + } else { // Store the file at the destination - return $this->backend->storeInternal( $this->setFlags( $this->params ) ); + $status = $this->backend->storeInternal( $this->setFlags( $this->params ) ); } - return StatusValue::newGood(); + return $status; } protected function getSourceSha1Base36() { - Wikimedia\suppressWarnings(); + AtEase::suppressWarnings(); $hash = sha1_file( $this->params['src'] ); - Wikimedia\restoreWarnings(); + AtEase::restoreWarnings(); if ( $hash !== false ) { $hash = Wikimedia\base_convert( $hash, 16, 36, 31 ); } diff --git a/includes/libs/filebackend/fileophandle/FileBackendStoreOpHandle.php b/includes/libs/filebackend/fileophandle/FileBackendStoreOpHandle.php index c366a0fff7..8697f9f0b6 100644 --- a/includes/libs/filebackend/fileophandle/FileBackendStoreOpHandle.php +++ b/includes/libs/filebackend/fileophandle/FileBackendStoreOpHandle.php @@ -34,8 +34,8 @@ abstract class FileBackendStoreOpHandle { public $backend; /** @var array */ public $resourcesToClose = []; - - public $call; // string; name that identifies the function called + /** @var callable name that identifies the function called */ + public $call; /** * Close all open file handles diff --git a/includes/libs/filebackend/fileophandle/SwiftFileOpHandle.php b/includes/libs/filebackend/fileophandle/SwiftFileOpHandle.php index 1119867800..70ed46a241 100644 --- a/includes/libs/filebackend/fileophandle/SwiftFileOpHandle.php +++ b/includes/libs/filebackend/fileophandle/SwiftFileOpHandle.php @@ -26,14 +26,28 @@ * @see FileBackendStoreOpHandle */ class SwiftFileOpHandle extends FileBackendStoreOpHandle { - /** @var array List of Requests for MultiHttpClient */ + /** @var array[] List of HTTP request maps for MultiHttpClient */ public $httpOp; - /** @var Closure */ + /** @var Closure Function to run after each HTTP request finishes */ public $callback; + /** @var int Class CONTINUE_* constant */ + public $state = self::CONTINUE_IF_OK; + + /** @var int Continue with the next requests stages if no errors occured */ + const CONTINUE_IF_OK = 0; + /** @var int Cancel the next requests stages */ + const CONTINUE_NO = 1; + /** + * Construct a handle to be use with SwiftFileOpHandle::doExecuteOpHandlesInternal() + * + * The callback returns a class CONTINUE_* constant and takes the following parameters: + * - An HTTP request map array with 'response' filled + * - A StatusValue instance to be updated as needed + * * @param SwiftFileBackend $backend - * @param Closure $callback Function that takes (HTTP request array, status) + * @param Closure $callback * @param array $httpOp MultiHttpClient op */ public function __construct( SwiftFileBackend $backend, Closure $callback, array $httpOp ) { diff --git a/includes/libs/filebackend/fsfile/FSFile.php b/includes/libs/filebackend/fsfile/FSFile.php index 553c9aaf17..cce32baccc 100644 --- a/includes/libs/filebackend/fsfile/FSFile.php +++ b/includes/libs/filebackend/fsfile/FSFile.php @@ -21,6 +21,9 @@ * @ingroup FileBackend */ +use Wikimedia\AtEase\AtEase; +use Wikimedia\Timestamp\ConvertibleTimestamp; + /** * Class representing a non-directory file on the file system * @@ -66,7 +69,11 @@ class FSFile { * @return int|bool */ public function getSize() { - return filesize( $this->path ); + AtEase::suppressWarnings(); + $size = filesize( $this->path ); + AtEase::restoreWarnings(); + + return $size; } /** @@ -75,11 +82,11 @@ class FSFile { * @return string|bool TS_MW timestamp or false on failure */ public function getTimestamp() { - Wikimedia\suppressWarnings(); + AtEase::suppressWarnings(); $timestamp = filemtime( $this->path ); - Wikimedia\restoreWarnings(); + AtEase::restoreWarnings(); if ( $timestamp !== false ) { - $timestamp = wfTimestamp( TS_MW, $timestamp ); + $timestamp = ConvertibleTimestamp::convert( TS_MW, $timestamp ); } return $timestamp; @@ -168,9 +175,9 @@ class FSFile { return $this->sha1Base36; } - Wikimedia\suppressWarnings(); + AtEase::suppressWarnings(); $this->sha1Base36 = sha1_file( $this->path ); - Wikimedia\restoreWarnings(); + AtEase::restoreWarnings(); if ( $this->sha1Base36 !== false ) { $this->sha1Base36 = Wikimedia\base_convert( $this->sha1Base36, 16, 36, 31 ); diff --git a/includes/libs/filebackend/fsfile/TempFSFile.php b/includes/libs/filebackend/fsfile/TempFSFile.php index b993626fda..46fa5e1333 100644 --- a/includes/libs/filebackend/fsfile/TempFSFile.php +++ b/includes/libs/filebackend/fsfile/TempFSFile.php @@ -1,4 +1,7 @@ 1) for paths to delete on shutdown */ protected static $pathsCollect = null; + /** + * Do not call directly. Use TempFSFileFactory + * + * @param string $path + */ public function __construct( $path ) { parent::__construct( $path ); if ( self::$pathsCollect === null ) { + // @codeCoverageIgnoreStart self::$pathsCollect = []; register_shutdown_function( [ __CLASS__, 'purgeAllOnShutdown' ] ); + // @codeCoverageIgnoreEnd } } @@ -47,40 +59,23 @@ class TempFSFile extends FSFile { * Make a new temporary file on the file system. * Temporary files may be purged when the file object falls out of scope. * + * @deprecated since 1.34, use TempFSFileFactory directly + * * @param string $prefix * @param string $extension Optional file extension * @param string|null $tmpDirectory Optional parent directory * @return TempFSFile|null */ public static function factory( $prefix, $extension = '', $tmpDirectory = null ) { - $ext = ( $extension != '' ) ? ".{$extension}" : ''; - - $attempts = 5; - while ( $attempts-- ) { - $hex = sprintf( '%06x%06x', mt_rand( 0, 0xffffff ), mt_rand( 0, 0xffffff ) ); - if ( !is_string( $tmpDirectory ) ) { - $tmpDirectory = self::getUsableTempDirectory(); - } - $path = $tmpDirectory . '/' . $prefix . $hex . $ext; - Wikimedia\suppressWarnings(); - $newFileHandle = fopen( $path, 'x' ); - Wikimedia\restoreWarnings(); - if ( $newFileHandle ) { - fclose( $newFileHandle ); - $tmpFile = new self( $path ); - $tmpFile->autocollect(); - // Safely instantiated, end loop. - return $tmpFile; - } - } - - // Give up - return null; + return ( new TempFSFileFactory( $tmpDirectory ) )->newTempFSFile( $prefix, $extension ); } /** + * @todo Is there any useful way to test this? Would it be useful to make this non-static on + * TempFSFileFactory? + * * @return string Filesystem path to a temporary directory - * @throws RuntimeException + * @throws RuntimeException if no writable temporary directory can be found */ public static function getUsableTempDirectory() { $tmpDir = array_map( 'getenv', [ 'TMPDIR', 'TMP', 'TEMP' ] ); @@ -119,9 +114,9 @@ class TempFSFile extends FSFile { */ public function purge() { $this->canDelete = false; // done - Wikimedia\suppressWarnings(); + AtEase::suppressWarnings(); $ok = unlink( $this->path ); - Wikimedia\restoreWarnings(); + AtEase::restoreWarnings(); unset( self::$pathsCollect[$this->path] ); @@ -176,12 +171,14 @@ class TempFSFile extends FSFile { * Try to make sure that all files are purged on error * * This method should only be called internally + * + * @codeCoverageIgnore */ public static function purgeAllOnShutdown() { foreach ( self::$pathsCollect as $path => $unused ) { - Wikimedia\suppressWarnings(); + AtEase::suppressWarnings(); unlink( $path ); - Wikimedia\restoreWarnings(); + AtEase::restoreWarnings(); } } diff --git a/includes/libs/filebackend/fsfile/TempFSFileFactory.php b/includes/libs/filebackend/fsfile/TempFSFileFactory.php new file mode 100644 index 0000000000..1120973803 --- /dev/null +++ b/includes/libs/filebackend/fsfile/TempFSFileFactory.php @@ -0,0 +1,56 @@ +tmpDirectory = $tmpDirectory; + } + + /** + * Make a new temporary file on the file system. + * Temporary files may be purged when the file object falls out of scope. + * + * @param string $prefix + * @param string $extension Optional file extension + * @return TempFSFile|null + */ + public function newTempFSFile( $prefix, $extension = '' ) { + $ext = ( $extension != '' ) ? ".{$extension}" : ''; + $tmpDirectory = $this->tmpDirectory; + if ( !is_string( $tmpDirectory ) ) { + $tmpDirectory = TempFSFile::getUsableTempDirectory(); + } + + $attempts = 5; + while ( $attempts-- ) { + $hex = sprintf( '%06x%06x', mt_rand( 0, 0xffffff ), mt_rand( 0, 0xffffff ) ); + $path = "$tmpDirectory/$prefix$hex$ext"; + \Wikimedia\suppressWarnings(); + $newFileHandle = fopen( $path, 'x' ); + \Wikimedia\restoreWarnings(); + if ( $newFileHandle ) { + fclose( $newFileHandle ); + $tmpFile = new TempFSFile( $path ); + $tmpFile->autocollect(); + // Safely instantiated, end loop. + return $tmpFile; + } + } + + // Give up + return null; // @codeCoverageIgnore + } +} diff --git a/includes/libs/http/MultiHttpClient.php b/includes/libs/http/MultiHttpClient.php index 2e418b96b9..b195a085ef 100644 --- a/includes/libs/http/MultiHttpClient.php +++ b/includes/libs/http/MultiHttpClient.php @@ -150,7 +150,7 @@ class MultiHttpClient implements LoggerAwareInterface { * 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[] $reqs Map of HTTP request arrays * @param array $opts * - connTimeout : connection timeout per request (seconds) * - reqTimeout : post-connection timeout per request (seconds) @@ -161,6 +161,8 @@ class MultiHttpClient implements LoggerAwareInterface { */ public function runMulti( array $reqs, array $opts = [] ) { $this->normalizeRequests( $reqs ); + $opts += [ 'connTimeout' => $this->connTimeout, 'reqTimeout' => $this->reqTimeout ]; + if ( $this->isCurlEnabled() ) { return $this->runMultiCurl( $reqs, $opts ); } else { @@ -182,16 +184,20 @@ class MultiHttpClient implements LoggerAwareInterface { * * @see MultiHttpClient::runMulti() * - * @param array $reqs Map of HTTP request arrays + * @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) + * @codingStandardsIgnoreStart + * @phan-param array{connTimeout?:int,reqTimeout?:int,usePipelining?:bool,maxConnsPerHost?:int} $opts + * @codingStandardsIgnoreEnd * @return array $reqs With response array populated for each * @throws Exception + * @suppress PhanTypeInvalidDimOffset */ - private function runMultiCurl( array $reqs, array $opts = [] ) { + private function runMultiCurl( array $reqs, array $opts ) { $chm = $this->getCurlMulti(); $selectTimeout = $this->getSelectTimeout( $opts ); @@ -288,20 +294,21 @@ class MultiHttpClient implements LoggerAwareInterface { /** * @param array &$req HTTP request map + * @codingStandardsIgnoreStart + * @phan-param array{url:string,proxy?:?string,query:mixed,method:string,body:string|resource,headers:string[],stream?:resource,flags:array} $req + * @codingStandardsIgnoreEnd * @param array $opts - * - connTimeout : default connection timeout - * - reqTimeout : default request timeout + * - connTimeout : default connection timeout + * - reqTimeout : default request timeout * @return resource * @throws Exception */ - protected function getCurlHandle( array &$req, array $opts = [] ) { + 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_CONNECTTIMEOUT_MS, intval( $opts['connTimeout'] * 1e3 ) ); + curl_setopt( $ch, CURLOPT_TIMEOUT_MS, intval( $opts['reqTimeout'] * 1e3 ) ); curl_setopt( $ch, CURLOPT_FOLLOWLOCATION, 1 ); curl_setopt( $ch, CURLOPT_MAXREDIRS, 4 ); curl_setopt( $ch, CURLOPT_HEADER, 0 ); @@ -317,11 +324,8 @@ class MultiHttpClient implements LoggerAwareInterface { $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 ); - } + curl_setopt( $ch, CURLOPT_NOBODY, ( $req['method'] === 'HEAD' ) ); if ( $req['method'] === 'PUT' ) { curl_setopt( $ch, CURLOPT_PUT, 1 ); @@ -348,17 +352,11 @@ class MultiHttpClient implements LoggerAwareInterface { } curl_setopt( $ch, CURLOPT_READFUNCTION, function ( $ch, $fd, $length ) { - $data = fread( $fd, $length ); - $len = strlen( $data ); - return $data; + return (string)fread( $fd, $length ); } ); } 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'] !== '' ) { @@ -407,23 +405,20 @@ class MultiHttpClient implements LoggerAwareInterface { } ); - 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 ) { + // This works with both file and php://temp handles (unlike CURLOPT_FILE) + $hasOutputStream = isset( $req['stream'] ); + curl_setopt( $ch, CURLOPT_WRITEFUNCTION, + function ( $ch, $data ) use ( &$req, $hasOutputStream ) { + if ( $hasOutputStream ) { return fwrite( $req['stream'], $data ); - } - ); - } else { - curl_setopt( $ch, CURLOPT_WRITEFUNCTION, - function ( $ch, $data ) use ( &$req ) { + } else { + // @phan-suppress-next-line PhanTypeArraySuspiciousNullable $req['response']['body'] .= $data; + return strlen( $data ); } - ); - } + } + ); return $ch; } @@ -498,7 +493,7 @@ class MultiHttpClient implements LoggerAwareInterface { 'error' => '', ]; - if ( !$sv->isOk() ) { + if ( !$sv->isOK() ) { $svErrors = $sv->getErrors(); if ( isset( $svErrors[0] ) ) { $req['response']['error'] = $svErrors[0]['message']; @@ -507,6 +502,7 @@ class MultiHttpClient implements LoggerAwareInterface { if ( isset( $svErrors[0]['params'][0] ) ) { if ( is_numeric( $svErrors[0]['params'][0] ) ) { if ( isset( $svErrors[0]['params'][1] ) ) { + // @phan-suppress-next-line PhanTypeInvalidDimOffset $req['response']['reason'] = $svErrors[0]['params'][1]; } } else { @@ -529,7 +525,7 @@ class MultiHttpClient implements LoggerAwareInterface { /** * Normalize request information * - * @param array $reqs the requests to normalize + * @param array[] $reqs the requests to normalize */ private function normalizeRequests( array &$reqs ) { foreach ( $reqs as &$req ) { diff --git a/includes/libs/lockmanager/PostgreSqlLockManager.php b/includes/libs/lockmanager/PostgreSqlLockManager.php index fd3ffa5cbc..5530eedba9 100644 --- a/includes/libs/lockmanager/PostgreSqlLockManager.php +++ b/includes/libs/lockmanager/PostgreSqlLockManager.php @@ -7,6 +7,8 @@ use Wikimedia\Rdbms\DBError; * All locks are non-blocking, which avoids deadlocks. * * @ingroup LockManager + * @phan-file-suppress PhanUndeclaredConstant Phan doesn't read constants in LockManager + * when accessed via self:: */ class PostgreSqlLockManager extends DBLockManager { /** @var array Mapping of lock types to the type actually used */ diff --git a/includes/libs/lockmanager/QuorumLockManager.php b/includes/libs/lockmanager/QuorumLockManager.php index 950b283670..83dcc6be56 100644 --- a/includes/libs/lockmanager/QuorumLockManager.php +++ b/includes/libs/lockmanager/QuorumLockManager.php @@ -38,7 +38,7 @@ abstract class QuorumLockManager extends LockManager { final protected function doLockByType( array $pathsByType ) { $status = StatusValue::newGood(); - $pathsToLock = []; // (bucket => type => paths) + $pathsByTypeByBucket = []; // (bucket => type => paths) // Get locks that need to be acquired (buckets => locks)... foreach ( $pathsByType as $type => $paths ) { foreach ( $paths as $path ) { @@ -46,23 +46,27 @@ abstract class QuorumLockManager extends LockManager { ++$this->locksHeld[$path][$type]; } else { $bucket = $this->getBucketFromPath( $path ); - $pathsToLock[$bucket][$type][] = $path; + $pathsByTypeByBucket[$bucket][$type][] = $path; } } } + // Acquire locks in each bucket in bucket order to reduce contention. Any blocking + // mutexes during the acquisition step will not involve circular waiting on buckets. + ksort( $pathsByTypeByBucket ); + $lockedPaths = []; // files locked in this attempt (type => paths) // Attempt to acquire these locks... - foreach ( $pathsToLock as $bucket => $pathsToLockByType ) { + foreach ( $pathsByTypeByBucket as $bucket => $bucketPathsByType ) { // Try to acquire the locks for this bucket - $status->merge( $this->doLockingRequestBucket( $bucket, $pathsToLockByType ) ); + $status->merge( $this->doLockingRequestBucket( $bucket, $bucketPathsByType ) ); if ( !$status->isOK() ) { $status->merge( $this->doUnlockByType( $lockedPaths ) ); return $status; } // Record these locks as active - foreach ( $pathsToLockByType as $type => $paths ) { + foreach ( $bucketPathsByType as $type => $paths ) { foreach ( $paths as $path ) { $this->locksHeld[$path][$type] = 1; // locked // Keep track of what locks were made in this attempt @@ -77,7 +81,7 @@ abstract class QuorumLockManager extends LockManager { protected function doUnlockByType( array $pathsByType ) { $status = StatusValue::newGood(); - $pathsToUnlock = []; // (bucket => type => paths) + $pathsByTypeByBucket = []; // (bucket => type => paths) foreach ( $pathsByType as $type => $paths ) { foreach ( $paths as $path ) { if ( !isset( $this->locksHeld[$path][$type] ) ) { @@ -88,7 +92,7 @@ abstract class QuorumLockManager extends LockManager { if ( $this->locksHeld[$path][$type] <= 0 ) { unset( $this->locksHeld[$path][$type] ); $bucket = $this->getBucketFromPath( $path ); - $pathsToUnlock[$bucket][$type][] = $path; + $pathsByTypeByBucket[$bucket][$type][] = $path; } if ( $this->locksHeld[$path] === [] ) { unset( $this->locksHeld[$path] ); // no SH or EX locks left for key @@ -99,8 +103,8 @@ abstract class QuorumLockManager extends LockManager { // Remove these specific locks if possible, or at least release // all locks once this process is currently not holding any locks. - foreach ( $pathsToUnlock as $bucket => $pathsToUnlockByType ) { - $status->merge( $this->doUnlockingRequestBucket( $bucket, $pathsToUnlockByType ) ); + foreach ( $pathsByTypeByBucket as $bucket => $bucketPathsByType ) { + $status->merge( $this->doUnlockingRequestBucket( $bucket, $bucketPathsByType ) ); } if ( $this->locksHeld === [] ) { $status->merge( $this->releaseAllLocks() ); @@ -148,7 +152,7 @@ abstract class QuorumLockManager extends LockManager { * This is all or nothing; if any key is already pledged then this totally fails. * * @param int $bucket - * @param callable $callback Pledge method taking a server name and yeilding a StatusValue + * @param callable $callback Pledge method taking a server name and yielding a StatusValue * @return StatusValue */ final protected function collectPledgeQuorum( $bucket, callable $callback ) { @@ -190,7 +194,7 @@ abstract class QuorumLockManager extends LockManager { * Attempt to release pledges with the peers for a bucket * * @param int $bucket - * @param callable $callback Pledge method taking a server name and yeilding a StatusValue + * @param callable $callback Pledge method taking a server name and yielding a StatusValue * @return StatusValue */ final protected function releasePledges( $bucket, callable $callback ) { diff --git a/includes/libs/mime/MSCompoundFileReader.php b/includes/libs/mime/MSCompoundFileReader.php index 8afaa38e02..34d612abbd 100644 --- a/includes/libs/mime/MSCompoundFileReader.php +++ b/includes/libs/mime/MSCompoundFileReader.php @@ -149,6 +149,7 @@ class MSCompoundFileReader { $this->error( 'invalid signature: ' . bin2hex( $this->header['header_signature'] ), self::ERROR_INVALID_SIGNATURE ); } + // @phan-suppress-next-line PhanTypeInvalidRightOperandOfIntegerOp $this->sectorLength = 1 << $this->header['sector_shift']; $this->readDifat(); $this->readDirectory(); @@ -177,16 +178,22 @@ class MSCompoundFileReader { ); } + /** + * @param int $offset + * @param int[] $struct + * @return array + */ private function unpackOffset( $offset, $struct ) { $block = $this->readOffset( $offset, array_sum( $struct ) ); return $this->unpack( $block, 0, $struct ); } - private function unpackSector( $sectorNumber, $struct ) { - $offset = $this->sectorOffset( $sectorNumber ); - return $this->unpackOffset( $offset, array_sum( $struct ) ); - } - + /** + * @param string $block + * @param int $offset + * @param int[] $struct + * @return array + */ private function unpack( $block, $offset, $struct ) { $data = []; foreach ( $struct as $key => $length ) { @@ -225,6 +232,7 @@ class MSCompoundFileReader { } private function readSector( $sectorId ) { + // @phan-suppress-next-line PhanTypeInvalidRightOperandOfIntegerOp return $this->readOffset( $this->sectorOffset( $sectorId ), 1 << $this->header['sector_shift'] ); } diff --git a/includes/libs/mime/MimeAnalyzer.php b/includes/libs/mime/MimeAnalyzer.php index bafe5e3098..6fca0438f5 100644 --- a/includes/libs/mime/MimeAnalyzer.php +++ b/includes/libs/mime/MimeAnalyzer.php @@ -1089,7 +1089,7 @@ EOT; // Special code for ogg - detect if it's video (theora), // else label it as sound. - if ( $mime == 'application/ogg' && file_exists( $path ) ) { + if ( $mime == 'application/ogg' && is_string( $path ) && file_exists( $path ) ) { // Read a chunk of the file $f = fopen( $path, "rt" ); if ( !$f ) { diff --git a/includes/libs/mime/XmlTypeCheck.php b/includes/libs/mime/XmlTypeCheck.php index 65cc54bdff..16cd5ca9ca 100644 --- a/includes/libs/mime/XmlTypeCheck.php +++ b/includes/libs/mime/XmlTypeCheck.php @@ -68,6 +68,9 @@ class XmlTypeCheck { */ protected $stackDepth = 0; + /** @var callable|null */ + protected $filterCallback; + /** * @var array Additional parsing options */ @@ -150,7 +153,8 @@ class XmlTypeCheck { } /** - * @param string $fname the filename + * @param string $xml + * @param bool $isFile */ private function validateFromInput( $xml, $isFile ) { $reader = new XMLReader(); @@ -422,7 +426,7 @@ class XmlTypeCheck { * * Only contains entity definitions (e.g. No 255 bytes). diff --git a/includes/libs/objectcache/APCBagOStuff.php b/includes/libs/objectcache/APCBagOStuff.php index 0954ac8061..e9bd7bec90 100644 --- a/includes/libs/objectcache/APCBagOStuff.php +++ b/includes/libs/objectcache/APCBagOStuff.php @@ -73,7 +73,7 @@ class APCBagOStuff extends MediumSpecificBagOStuff { return true; } - public function add( $key, $value, $exptime = 0, $flags = 0 ) { + protected function doAdd( $key, $value, $exptime = 0, $flags = 0 ) { return apc_add( $key . self::KEY_SUFFIX, $this->nativeSerialize ? $value : $this->serialize( $value ), @@ -87,11 +87,11 @@ class APCBagOStuff extends MediumSpecificBagOStuff { return true; } - public function incr( $key, $value = 1 ) { + public function incr( $key, $value = 1, $flags = 0 ) { return apc_inc( $key . self::KEY_SUFFIX, $value ); } - public function decr( $key, $value = 1 ) { + public function decr( $key, $value = 1, $flags = 0 ) { return apc_dec( $key . self::KEY_SUFFIX, $value ); } } diff --git a/includes/libs/objectcache/APCUBagOStuff.php b/includes/libs/objectcache/APCUBagOStuff.php index 021cdf7b76..2b26500597 100644 --- a/includes/libs/objectcache/APCUBagOStuff.php +++ b/includes/libs/objectcache/APCUBagOStuff.php @@ -71,7 +71,7 @@ class APCUBagOStuff extends MediumSpecificBagOStuff { ); } - public function add( $key, $value, $exptime = 0, $flags = 0 ) { + protected function doAdd( $key, $value, $exptime = 0, $flags = 0 ) { return apcu_add( $key . self::KEY_SUFFIX, $this->nativeSerialize ? $value : $this->serialize( $value ), @@ -85,7 +85,7 @@ class APCUBagOStuff extends MediumSpecificBagOStuff { return true; } - public function incr( $key, $value = 1 ) { + public function incr( $key, $value = 1, $flags = 0 ) { // https://github.com/krakjoe/apcu/issues/166 if ( apcu_exists( $key . self::KEY_SUFFIX ) ) { return apcu_inc( $key . self::KEY_SUFFIX, $value ); @@ -94,7 +94,7 @@ class APCUBagOStuff extends MediumSpecificBagOStuff { } } - public function decr( $key, $value = 1 ) { + public function decr( $key, $value = 1, $flags = 0 ) { // https://github.com/krakjoe/apcu/issues/166 if ( apcu_exists( $key . self::KEY_SUFFIX ) ) { return apcu_dec( $key . self::KEY_SUFFIX, $value ); diff --git a/includes/libs/objectcache/BagOStuff.php b/includes/libs/objectcache/BagOStuff.php index da60c01959..ad3f681153 100644 --- a/includes/libs/objectcache/BagOStuff.php +++ b/includes/libs/objectcache/BagOStuff.php @@ -91,6 +91,7 @@ abstract class BagOStuff implements IExpiringStore, IStoreKeyEncoder, LoggerAwar * - asyncHandler: Callable to use for scheduling tasks after the web request ends. * In CLI mode, it should run the task immediately. * @param array $params + * @phan-param array{logger?:Psr\Log\LoggerInterface,asyncHandler?:callable} $params */ public function __construct( array $params = [] ) { $this->setLogger( $params['logger'] ?? new NullLogger() ); @@ -362,31 +363,36 @@ abstract class BagOStuff implements IExpiringStore, IStoreKeyEncoder, LoggerAwar * Increase stored value of $key by $value while preserving its TTL * @param string $key Key to increase * @param int $value Value to add to $key (default: 1) [optional] + * @param int $flags Bit field of class WRITE_* constants [optional] * @return int|bool New value or false on failure */ - abstract public function incr( $key, $value = 1 ); + abstract public function incr( $key, $value = 1, $flags = 0 ); /** * Decrease stored value of $key by $value while preserving its TTL * @param string $key * @param int $value Value to subtract from $key (default: 1) [optional] + * @param int $flags Bit field of class WRITE_* constants [optional] * @return int|bool New value or false on failure */ - abstract public function decr( $key, $value = 1 ); + abstract public function decr( $key, $value = 1, $flags = 0 ); /** - * Increase stored value of $key by $value while preserving its TTL + * Increase the value of the given key (no TTL change) if it exists or create it otherwise * - * This will create the key with value $init and TTL $ttl instead if not present + * This will create the key with the value $init and TTL $ttl instead if not present. + * Callers should make sure that both ($init - $value) and $ttl are invariants for all + * operations to any given key. The value of $init should be at least that of $value. * - * @param string $key - * @param int $ttl - * @param int $value - * @param int $init + * @param string $key Key built via makeKey() or makeGlobalKey() + * @param int $exptime Time-to-live (in seconds) or a UNIX timestamp expiration + * @param int $value Amount to increase the key value by [default: 1] + * @param int|null $init Value to initialize the key to if it does not exist [default: $value] + * @param int $flags Bit field of class WRITE_* constants [optional] * @return int|bool New value or false on failure * @since 1.24 */ - abstract public function incrWithInit( $key, $ttl, $value = 1, $init = 1 ); + abstract public function incrWithInit( $key, $exptime, $value = 1, $init = null, $flags = 0 ); /** * Get the "last error" registered; clearLastError() should be called manually @@ -478,6 +484,16 @@ abstract class BagOStuff implements IExpiringStore, IStoreKeyEncoder, LoggerAwar return INF; } + /** + * @param int $field + * @param int $flags + * @return bool + * @since 1.34 + */ + final protected function fieldHasFlags( $field, $flags ) { + return ( ( $field & $flags ) === $flags ); + } + /** * Merge the flag maps of one or more BagOStuff objects into a "lowest common denominator" map * diff --git a/includes/libs/objectcache/CachedBagOStuff.php b/includes/libs/objectcache/CachedBagOStuff.php index 0ab26c9520..8b4c9c686f 100644 --- a/includes/libs/objectcache/CachedBagOStuff.php +++ b/includes/libs/objectcache/CachedBagOStuff.php @@ -79,7 +79,7 @@ class CachedBagOStuff extends BagOStuff { } } - $valuesByKeyFetched = $this->backend->getMulti( $keys, $flags ); + $valuesByKeyFetched = $this->backend->getMulti( $keysMissing, $flags ); $this->setMulti( $valuesByKeyFetched, self::TTL_INDEFINITE, self::WRITE_CACHE_ONLY ); return $valuesByKeyCached + $valuesByKeyFetched; @@ -87,7 +87,8 @@ class CachedBagOStuff extends BagOStuff { public function set( $key, $value, $exptime = 0, $flags = 0 ) { $this->procCache->set( $key, $value, $exptime, $flags ); - if ( ( $flags & self::WRITE_CACHE_ONLY ) != self::WRITE_CACHE_ONLY ) { + + if ( !$this->fieldHasFlags( $flags, self::WRITE_CACHE_ONLY ) ) { $this->backend->set( $key, $value, $exptime, $flags ); } @@ -96,7 +97,8 @@ class CachedBagOStuff extends BagOStuff { public function delete( $key, $flags = 0 ) { $this->procCache->delete( $key, $flags ); - if ( ( $flags & self::WRITE_CACHE_ONLY ) != self::WRITE_CACHE_ONLY ) { + + if ( !$this->fieldHasFlags( $flags, self::WRITE_CACHE_ONLY ) ) { $this->backend->delete( $key, $flags ); } @@ -166,7 +168,8 @@ class CachedBagOStuff extends BagOStuff { public function setMulti( array $data, $exptime = 0, $flags = 0 ) { $this->procCache->setMulti( $data, $exptime, $flags ); - if ( ( $flags & self::WRITE_CACHE_ONLY ) != self::WRITE_CACHE_ONLY ) { + + if ( !$this->fieldHasFlags( $flags, self::WRITE_CACHE_ONLY ) ) { return $this->backend->setMulti( $data, $exptime, $flags ); } @@ -175,7 +178,8 @@ class CachedBagOStuff extends BagOStuff { public function deleteMulti( array $keys, $flags = 0 ) { $this->procCache->deleteMulti( $keys, $flags ); - if ( ( $flags & self::WRITE_CACHE_ONLY ) != self::WRITE_CACHE_ONLY ) { + + if ( !$this->fieldHasFlags( $flags, self::WRITE_CACHE_ONLY ) ) { return $this->backend->deleteMulti( $keys, $flags ); } @@ -184,29 +188,30 @@ class CachedBagOStuff extends BagOStuff { public function changeTTLMulti( array $keys, $exptime, $flags = 0 ) { $this->procCache->changeTTLMulti( $keys, $exptime, $flags ); - if ( ( $flags & self::WRITE_CACHE_ONLY ) != self::WRITE_CACHE_ONLY ) { + + if ( !$this->fieldHasFlags( $flags, self::WRITE_CACHE_ONLY ) ) { return $this->backend->changeTTLMulti( $keys, $exptime, $flags ); } return true; } - public function incr( $key, $value = 1 ) { + public function incr( $key, $value = 1, $flags = 0 ) { $this->procCache->delete( $key ); - return $this->backend->incr( $key, $value ); + return $this->backend->incr( $key, $value, $flags ); } - public function decr( $key, $value = 1 ) { + public function decr( $key, $value = 1, $flags = 0 ) { $this->procCache->delete( $key ); - return $this->backend->decr( $key, $value ); + return $this->backend->decr( $key, $value, $flags ); } - public function incrWithInit( $key, $ttl, $value = 1, $init = 1 ) { + public function incrWithInit( $key, $exptime, $value = 1, $init = null, $flags = 0 ) { $this->procCache->delete( $key ); - return $this->backend->incrWithInit( $key, $ttl, $value, $init ); + return $this->backend->incrWithInit( $key, $exptime, $value, $init, $flags ); } public function addBusyCallback( callable $workCallback ) { diff --git a/includes/libs/objectcache/EmptyBagOStuff.php b/includes/libs/objectcache/EmptyBagOStuff.php index dab8ba1d35..9723cadd97 100644 --- a/includes/libs/objectcache/EmptyBagOStuff.php +++ b/includes/libs/objectcache/EmptyBagOStuff.php @@ -41,15 +41,19 @@ class EmptyBagOStuff extends MediumSpecificBagOStuff { return true; } - public function add( $key, $value, $exptime = 0, $flags = 0 ) { + protected function doAdd( $key, $value, $exptime = 0, $flags = 0 ) { return true; } - public function incr( $key, $value = 1 ) { + public function incr( $key, $value = 1, $flags = 0 ) { return false; } - public function incrWithInit( $key, $ttl, $value = 1, $init = 1 ) { + public function decr( $key, $value = 1, $flags = 0 ) { + return false; + } + + public function incrWithInit( $key, $exptime, $value = 1, $init = null, $flags = 0 ) { return false; // faster } diff --git a/includes/libs/objectcache/HashBagOStuff.php b/includes/libs/objectcache/HashBagOStuff.php index 1cfa0c7921..348f300c07 100644 --- a/includes/libs/objectcache/HashBagOStuff.php +++ b/includes/libs/objectcache/HashBagOStuff.php @@ -47,6 +47,9 @@ class HashBagOStuff extends MediumSpecificBagOStuff { /** * @param array $params Additional parameters include: * - maxKeys : only allow this many keys (using oldest-first eviction) + * @codingStandardsIgnoreStart + * @phan-param array{logger?:Psr\Log\LoggerInterface,asyncHandler?:callable,keyspace?:string,reportDupes?:bool,syncTimeout?:int,segmentationSize?:int,segmentedValueMaxSize?:int,maxKeys?:int} $params + * @codingStandardsIgnoreEnd */ function __construct( $params = [] ) { $params['segmentationSize'] = $params['segmentationSize'] ?? INF; @@ -94,7 +97,7 @@ class HashBagOStuff extends MediumSpecificBagOStuff { return true; } - public function add( $key, $value, $exptime = 0, $flags = 0 ) { + protected function doAdd( $key, $value, $exptime = 0, $flags = 0 ) { if ( $this->hasKey( $key ) && !$this->expire( $key ) ) { return false; // key already set } @@ -108,10 +111,10 @@ class HashBagOStuff extends MediumSpecificBagOStuff { return true; } - public function incr( $key, $value = 1 ) { + public function incr( $key, $value = 1, $flags = 0 ) { $n = $this->get( $key ); if ( $this->isInteger( $n ) ) { - $n = max( $n + intval( $value ), 0 ); + $n = max( $n + (int)$value, 0 ); $this->bag[$key][self::KEY_VAL] = $n; return $n; @@ -120,6 +123,10 @@ class HashBagOStuff extends MediumSpecificBagOStuff { return false; } + public function decr( $key, $value = 1, $flags = 0 ) { + return $this->incr( $key, -$value, $flags ); + } + /** * Clear all values in cache */ diff --git a/includes/libs/objectcache/MediumSpecificBagOStuff.php b/includes/libs/objectcache/MediumSpecificBagOStuff.php index 62a8aec967..252c08975e 100644 --- a/includes/libs/objectcache/MediumSpecificBagOStuff.php +++ b/includes/libs/objectcache/MediumSpecificBagOStuff.php @@ -73,6 +73,9 @@ abstract class MediumSpecificBagOStuff extends BagOStuff { * This should be configured to a reasonable size give the site traffic and the * amount of I/O between application and cache servers that the network can handle. * @param array $params + * @codingStandardsIgnoreStart + * @phan-param array{logger?:Psr\Log\LoggerInterface,asyncHandler?:callable,keyspace?:string,reportDupes?:bool,syncTimeout?:int,segmentationSize?:int,segmentedValueMaxSize?:int} $params + * @codingStandardsIgnoreEnd */ public function __construct( array $params = [] ) { parent::__construct( $params ); @@ -160,57 +163,9 @@ abstract class MediumSpecificBagOStuff extends BagOStuff { * @return bool Success */ public function set( $key, $value, $exptime = 0, $flags = 0 ) { - if ( - is_int( $value ) || // avoid breaking incr()/decr() - ( $flags & self::WRITE_ALLOW_SEGMENTS ) != self::WRITE_ALLOW_SEGMENTS || - is_infinite( $this->segmentationSize ) - ) { - return $this->doSet( $key, $value, $exptime, $flags ); - } - - $serialized = $this->serialize( $value ); - $segmentSize = $this->getSegmentationSize(); - $maxTotalSize = $this->getSegmentedValueMaxSize(); - - $size = strlen( $serialized ); - if ( $size <= $segmentSize ) { - // Since the work of serializing it was already done, just use it inline - return $this->doSet( - $key, - SerializedValueContainer::newUnified( $serialized ), - $exptime, - $flags - ); - } elseif ( $size > $maxTotalSize ) { - $this->setLastError( "Key $key exceeded $maxTotalSize bytes." ); - - return false; - } - - $chunksByKey = []; - $segmentHashes = []; - $count = intdiv( $size, $segmentSize ) + ( ( $size % $segmentSize ) ? 1 : 0 ); - for ( $i = 0; $i < $count; ++$i ) { - $segment = substr( $serialized, $i * $segmentSize, $segmentSize ); - $hash = sha1( $segment ); - $chunkKey = $this->makeGlobalKey( self::SEGMENT_COMPONENT, $key, $hash ); - $chunksByKey[$chunkKey] = $segment; - $segmentHashes[] = $hash; - } - - $flags &= ~self::WRITE_ALLOW_SEGMENTS; // sanity - $ok = $this->setMulti( $chunksByKey, $exptime, $flags ); - if ( $ok ) { - // Only when all segments are stored should the main key be changed - $ok = $this->doSet( - $key, - SerializedValueContainer::newSegmented( $segmentHashes ), - $exptime, - $flags - ); - } - - return $ok; + list( $entry, $usable ) = $this->makeValueOrSegmentList( $key, $value, $exptime, $flags ); + // Only when all segments (if any) are stored should the main key be changed + return $usable ? $this->doSet( $key, $entry, $exptime, $flags ) : false; } /** @@ -236,7 +191,7 @@ abstract class MediumSpecificBagOStuff extends BagOStuff { * @return bool True if the item was deleted or not found, false on failure */ public function delete( $key, $flags = 0 ) { - if ( ( $flags & self::WRITE_PRUNE_SEGMENTS ) != self::WRITE_PRUNE_SEGMENTS ) { + if ( !$this->fieldHasFlags( $flags, self::WRITE_PRUNE_SEGMENTS ) ) { return $this->doDelete( $key, $flags ); } @@ -256,7 +211,7 @@ abstract class MediumSpecificBagOStuff extends BagOStuff { $mainValue->{SerializedValueContainer::SEGMENTED_HASHES} ); - return $this->deleteMulti( $orderedKeys, $flags ); + return $this->deleteMulti( $orderedKeys, $flags & ~self::WRITE_PRUNE_SEGMENTS ); } /** @@ -268,6 +223,23 @@ abstract class MediumSpecificBagOStuff extends BagOStuff { */ abstract protected function doDelete( $key, $flags = 0 ); + public function add( $key, $value, $exptime = 0, $flags = 0 ) { + list( $entry, $usable ) = $this->makeValueOrSegmentList( $key, $value, $exptime, $flags ); + // Only when all segments (if any) are stored should the main key be changed + return $usable ? $this->doAdd( $key, $entry, $exptime, $flags ) : false; + } + + /** + * Insert an item if it does not already exist + * + * @param string $key + * @param mixed $value + * @param int $exptime + * @param int $flags Bitfield of BagOStuff::WRITE_* constants (since 1.33) + * @return bool Success + */ + abstract protected function doAdd( $key, $value, $exptime = 0, $flags = 0 ); + /** * Merge changes into the existing cache value (possibly creating a new one) * @@ -283,7 +255,6 @@ abstract class MediumSpecificBagOStuff extends BagOStuff { * @param int $attempts The amount of times to attempt a merge in case of failure * @param int $flags Bitfield of BagOStuff::WRITE_* constants * @return bool Success - * @throws InvalidArgumentException */ public function merge( $key, callable $callback, $exptime = 0, $attempts = 10, $flags = 0 ) { return $this->mergeViaCas( $key, $callback, $exptime, $attempts, $flags ); @@ -297,51 +268,56 @@ abstract class MediumSpecificBagOStuff extends BagOStuff { * @param int $flags Bitfield of BagOStuff::WRITE_* constants * @return bool Success * @see BagOStuff::merge() - * */ final protected function mergeViaCas( $key, callable $callback, $exptime, $attempts, $flags ) { + $attemptsLeft = $attempts; do { - $casToken = null; // passed by reference + $token = null; // passed by reference // Get the old value and CAS token from cache $this->clearLastError(); $currentValue = $this->resolveSegments( $key, - $this->doGet( $key, self::READ_LATEST, $casToken ) + $this->doGet( $key, $flags, $token ) ); if ( $this->getLastError() ) { + // Don't spam slow retries due to network problems (retry only on races) $this->logger->warning( - __METHOD__ . ' failed due to I/O error on get() for {key}.', + __METHOD__ . ' failed due to read I/O error on get() for {key}.', [ 'key' => $key ] ); - - return false; // don't spam retries (retry only on races) + $success = false; + break; } // Derive the new value from the old value $value = call_user_func( $callback, $this, $key, $currentValue, $exptime ); - $hadNoCurrentValue = ( $currentValue === false ); + $keyWasNonexistant = ( $currentValue === false ); + $valueMatchesOldValue = ( $value === $currentValue ); unset( $currentValue ); // free RAM in case the value is large $this->clearLastError(); - if ( $value === false ) { + if ( $value === false || $exptime < 0 ) { $success = true; // do nothing - } elseif ( $hadNoCurrentValue ) { + } elseif ( $valueMatchesOldValue && $attemptsLeft !== $attempts ) { + $success = true; // recently set by another thread to the same value + } elseif ( $keyWasNonexistant ) { // Try to create the key, failing if it gets created in the meantime $success = $this->add( $key, $value, $exptime, $flags ); } else { // Try to update the key, failing if it gets changed in the meantime - $success = $this->cas( $casToken, $key, $value, $exptime, $flags ); + $success = $this->cas( $token, $key, $value, $exptime, $flags ); } if ( $this->getLastError() ) { + // Don't spam slow retries due to network problems (retry only on races) $this->logger->warning( - __METHOD__ . ' failed due to I/O error for {key}.', + __METHOD__ . ' failed due to write I/O error for {key}.', [ 'key' => $key ] ); - - return false; // IO error; don't spam retries + $success = false; + break; } - } while ( !$success && --$attempts ); + } while ( !$success && --$attemptsLeft ); return $success; } @@ -357,21 +333,58 @@ abstract class MediumSpecificBagOStuff extends BagOStuff { * @return bool Success */ protected function cas( $casToken, $key, $value, $exptime = 0, $flags = 0 ) { + if ( $casToken === null ) { + $this->logger->warning( + __METHOD__ . ' got empty CAS token for {key}.', + [ 'key' => $key ] + ); + + return false; // caller may have meant to use add()? + } + + list( $entry, $usable ) = $this->makeValueOrSegmentList( $key, $value, $exptime, $flags ); + // Only when all segments (if any) are stored should the main key be changed + return $usable ? $this->doCas( $casToken, $key, $entry, $exptime, $flags ) : false; + } + + /** + * Check and set an item + * + * @param mixed $casToken + * @param string $key + * @param mixed $value + * @param int $exptime Either an interval in seconds or a unix timestamp for expiry + * @param int $flags Bitfield of BagOStuff::WRITE_* constants + * @return bool Success + */ + protected function doCas( $casToken, $key, $value, $exptime = 0, $flags = 0 ) { + // @TODO: the lock() call assumes that all other relavent sets() use one if ( !$this->lock( $key, 0 ) ) { return false; // non-blocking } $curCasToken = null; // passed by reference + $this->clearLastError(); $this->doGet( $key, self::READ_LATEST, $curCasToken ); - if ( $casToken === $curCasToken ) { - $success = $this->set( $key, $value, $exptime, $flags ); + if ( is_object( $curCasToken ) ) { + // Using === does not work with objects since it checks for instance identity + throw new UnexpectedValueException( "CAS token cannot be an object" ); + } + if ( $this->getLastError() ) { + // Fail if the old CAS token could not be read + $success = false; + $this->logger->warning( + __METHOD__ . ' failed due to write I/O error for {key}.', + [ 'key' => $key ] + ); + } elseif ( $casToken === $curCasToken ) { + $success = $this->doSet( $key, $value, $exptime, $flags ); } else { + $success = false; // mismatched or failed $this->logger->info( __METHOD__ . ' failed due to race condition for {key}.', [ 'key' => $key ] ); - - $success = false; // mismatched or failed } $this->unlock( $key ); @@ -588,9 +601,10 @@ abstract class MediumSpecificBagOStuff extends BagOStuff { * @since 1.24 */ public function setMulti( array $data, $exptime = 0, $flags = 0 ) { - if ( ( $flags & self::WRITE_ALLOW_SEGMENTS ) === self::WRITE_ALLOW_SEGMENTS ) { + if ( $this->fieldHasFlags( $flags, self::WRITE_ALLOW_SEGMENTS ) ) { throw new InvalidArgumentException( __METHOD__ . ' got WRITE_ALLOW_SEGMENTS' ); } + return $this->doSetMulti( $data, $exptime, $flags ); } @@ -605,6 +619,7 @@ abstract class MediumSpecificBagOStuff extends BagOStuff { foreach ( $data as $key => $value ) { $res = $this->doSet( $key, $value, $exptime, $flags ) && $res; } + return $res; } @@ -619,9 +634,10 @@ abstract class MediumSpecificBagOStuff extends BagOStuff { * @since 1.33 */ public function deleteMulti( array $keys, $flags = 0 ) { - if ( ( $flags & self::WRITE_ALLOW_SEGMENTS ) === self::WRITE_ALLOW_SEGMENTS ) { - throw new InvalidArgumentException( __METHOD__ . ' got WRITE_ALLOW_SEGMENTS' ); + if ( $this->fieldHasFlags( $flags, self::WRITE_PRUNE_SEGMENTS ) ) { + throw new InvalidArgumentException( __METHOD__ . ' got WRITE_PRUNE_SEGMENTS' ); } + return $this->doDeleteMulti( $keys, $flags ); } @@ -658,37 +674,16 @@ abstract class MediumSpecificBagOStuff extends BagOStuff { return $res; } - /** - * Decrease stored value of $key by $value while preserving its TTL - * @param string $key - * @param int $value Value to subtract from $key (default: 1) [optional] - * @return int|bool New value or false on failure - */ - public function decr( $key, $value = 1 ) { - return $this->incr( $key, -$value ); - } - - /** - * Increase stored value of $key by $value while preserving its TTL - * - * This will create the key with value $init and TTL $ttl instead if not present - * - * @param string $key - * @param int $ttl - * @param int $value - * @param int $init - * @return int|bool New value or false on failure - * @since 1.24 - */ - public function incrWithInit( $key, $ttl, $value = 1, $init = 1 ) { + public function incrWithInit( $key, $exptime, $value = 1, $init = null, $flags = 0 ) { + $init = is_int( $init ) ? $init : $value; $this->clearLastError(); - $newValue = $this->incr( $key, $value ); + $newValue = $this->incr( $key, $value, $flags ); if ( $newValue === false && !$this->getLastError() ) { // No key set; initialize - $newValue = $this->add( $key, (int)$init, $ttl ) ? $init : false; + $newValue = $this->add( $key, (int)$init, $exptime, $flags ) ? $init : false; if ( $newValue === false && !$this->getLastError() ) { // Raced out initializing; increment - $newValue = $this->incr( $key, $value ); + $newValue = $this->incr( $key, $value, $flags ); } } @@ -758,28 +753,61 @@ abstract class MediumSpecificBagOStuff extends BagOStuff { $this->lastError = $err; } + final public function addBusyCallback( callable $workCallback ) { + $this->busyCallbacks[] = $workCallback; + } + /** - * Let a callback be run to avoid wasting time on special blocking calls - * - * The callbacks may or may not be called ever, in any particular order. - * They are likely to be invoked when something WRITE_SYNC is used used. - * They should follow a caching pattern as shown below, so that any code - * using the work will get it's result no matter what happens. - * @code - * $result = null; - * $workCallback = function () use ( &$result ) { - * if ( !$result ) { - * $result = .... - * } - * return $result; - * } - * @endcode + * Determine the entry (inline or segment list) to store under a key to save the value * - * @param callable $workCallback - * @since 1.28 + * @param string $key + * @param mixed $value + * @param int $exptime + * @param int $flags + * @return array (inline value or segment list, whether the entry is usable) + * @since 1.34 */ - final public function addBusyCallback( callable $workCallback ) { - $this->busyCallbacks[] = $workCallback; + final protected function makeValueOrSegmentList( $key, $value, $exptime, $flags ) { + $entry = $value; + $usable = true; + + if ( + $this->fieldHasFlags( $flags, self::WRITE_ALLOW_SEGMENTS ) && + !is_int( $value ) && // avoid breaking incr()/decr() + is_finite( $this->segmentationSize ) + ) { + $segmentSize = $this->segmentationSize; + $maxTotalSize = $this->segmentedValueMaxSize; + + $serialized = $this->serialize( $value ); + $size = strlen( $serialized ); + if ( $size > $maxTotalSize ) { + $this->logger->warning( + "Value for {key} exceeds $maxTotalSize bytes; cannot segment.", + [ 'key' => $key ] + ); + } elseif ( $size <= $segmentSize ) { + // The serialized value was already computed, so just use it inline + $entry = SerializedValueContainer::newUnified( $serialized ); + } else { + // Split the serialized value into chunks and store them at different keys + $chunksByKey = []; + $segmentHashes = []; + $count = intdiv( $size, $segmentSize ) + ( ( $size % $segmentSize ) ? 1 : 0 ); + for ( $i = 0; $i < $count; ++$i ) { + $segment = substr( $serialized, $i * $segmentSize, $segmentSize ); + $hash = sha1( $segment ); + $chunkKey = $this->makeGlobalKey( self::SEGMENT_COMPONENT, $key, $hash ); + $chunksByKey[$chunkKey] = $segment; + $segmentHashes[] = $hash; + } + $flags &= ~self::WRITE_ALLOW_SEGMENTS; // sanity + $usable = $this->setMulti( $chunksByKey, $exptime, $flags ); + $entry = SerializedValueContainer::newSegmented( $segmentHashes ); + } + } + + return [ $entry, $usable ]; } /** @@ -856,14 +884,6 @@ abstract class MediumSpecificBagOStuff extends BagOStuff { return ( $value === (string)$integer ); } - /** - * Construct a cache key. - * - * @param string $keyspace - * @param array $args - * @return string Colon-delimited list of $keyspace followed by escaped components of $args - * @since 1.27 - */ public function makeKeyInternal( $keyspace, $args ) { $key = $keyspace; foreach ( $args as $arg ) { @@ -905,18 +925,10 @@ abstract class MediumSpecificBagOStuff extends BagOStuff { return $this->attrMap[$flag] ?? self::QOS_UNKNOWN; } - /** - * @return int|float The chunk size, in bytes, of segmented objects (INF for no limit) - * @since 1.34 - */ public function getSegmentationSize() { return $this->segmentationSize; } - /** - * @return int|float Maximum total segmented object size in bytes (INF for no limit) - * @since 1.34 - */ public function getSegmentedValueMaxSize() { return $this->segmentedValueMaxSize; } diff --git a/includes/libs/objectcache/MemcachedBagOStuff.php b/includes/libs/objectcache/MemcachedBagOStuff.php index dc409315f8..40f283621d 100644 --- a/includes/libs/objectcache/MemcachedBagOStuff.php +++ b/includes/libs/objectcache/MemcachedBagOStuff.php @@ -49,29 +49,25 @@ abstract class MemcachedBagOStuff extends MediumSpecificBagOStuff { // custom prefixes used by thing like WANObjectCache, limit to 205. $charsLeft = 205 - strlen( $keyspace ) - count( $args ); - $args = array_map( - function ( $arg ) use ( &$charsLeft ) { - $arg = strtr( $arg, ' ', '_' ); + foreach ( $args as &$arg ) { + $arg = strtr( $arg, ' ', '_' ); - // Make sure %, #, and non-ASCII chars are escaped - $arg = preg_replace_callback( - '/[^\x21-\x22\x24\x26-\x39\x3b-\x7e]+/', - function ( $m ) { - return rawurlencode( $m[0] ); - }, - $arg - ); + // Make sure %, #, and non-ASCII chars are escaped + $arg = preg_replace_callback( + '/[^\x21-\x22\x24\x26-\x39\x3b-\x7e]+/', + function ( $m ) { + return rawurlencode( $m[0] ); + }, + $arg + ); - // 33 = 32 characters for the MD5 + 1 for the '#' prefix. - if ( $charsLeft > 33 && strlen( $arg ) > $charsLeft ) { - $arg = '#' . md5( $arg ); - } + // 33 = 32 characters for the MD5 + 1 for the '#' prefix. + if ( $charsLeft > 33 && strlen( $arg ) > $charsLeft ) { + $arg = '#' . md5( $arg ); + } - $charsLeft -= strlen( $arg ); - return $arg; - }, - $args - ); + $charsLeft -= strlen( $arg ); + } if ( $charsLeft < 0 ) { return $keyspace . ':BagOStuff-long-key:##' . md5( implode( ':', $args ) ); diff --git a/includes/libs/objectcache/MemcachedClient.php b/includes/libs/objectcache/MemcachedClient.php deleted file mode 100644 index eecf7ec799..0000000000 --- a/includes/libs/objectcache/MemcachedClient.php +++ /dev/null @@ -1,1330 +0,0 @@ - | - * | All rights reserved. | - * | | - * | Redistribution and use in source and binary forms, with or without | - * | modification, are permitted provided that the following conditions | - * | are met: | - * | | - * | 1. Redistributions of source code must retain the above copyright | - * | notice, this list of conditions and the following disclaimer. | - * | 2. Redistributions in binary form must reproduce the above copyright | - * | notice, this list of conditions and the following disclaimer in the | - * | documentation and/or other materials provided with the distribution. | - * | | - * | THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR | - * | IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES | - * | OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. | - * | IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, | - * | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT | - * | NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | - * | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | - * | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | - * | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF | - * | THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | - * +---------------------------------------------------------------------------+ - * | Author: Ryan T. Dean | - * | Heavily influenced by the Perl memcached client by Brad Fitzpatrick. | - * | Permission granted by Brad Fitzpatrick for relicense of ported Perl | - * | client logic under 2-clause BSD license. | - * +---------------------------------------------------------------------------+ - * - * @file - * $TCAnet$ - */ - -/** - * This is a PHP client for memcached - a distributed memory cache daemon. - * - * More information is available at http://www.danga.com/memcached/ - * - * Usage example: - * - * $mc = new MemcachedClient(array( - * 'servers' => array( - * '127.0.0.1:10000', - * array( '192.0.0.1:10010', 2 ), - * '127.0.0.1:10020' - * ), - * 'debug' => false, - * 'compress_threshold' => 10240, - * 'persistent' => true - * )); - * - * $mc->add( 'key', array( 'some', 'array' ) ); - * $mc->replace( 'key', 'some random string' ); - * $val = $mc->get( 'key' ); - * - * @author Ryan T. Dean - * @version 0.1.2 - */ - -use Psr\Log\LoggerInterface; -use Psr\Log\NullLogger; - -// {{{ class MemcachedClient -/** - * memcached client class implemented using (p)fsockopen() - * - * @author Ryan T. Dean - * @ingroup Cache - */ -class MemcachedClient { - // {{{ properties - // {{{ public - - // {{{ constants - // {{{ flags - - /** - * Flag: indicates data is serialized - */ - const SERIALIZED = 1; - - /** - * Flag: indicates data is compressed - */ - const COMPRESSED = 2; - - /** - * Flag: indicates data is an integer - */ - const INTVAL = 4; - - // }}} - - /** - * Minimum savings to store data compressed - */ - const COMPRESSION_SAVINGS = 0.20; - - // }}} - - /** - * Command statistics - * - * @var array - * @access public - */ - public $stats; - - // }}} - // {{{ private - - /** - * Cached Sockets that are connected - * - * @var array - * @access private - */ - public $_cache_sock; - - /** - * Current debug status; 0 - none to 9 - profiling - * - * @var bool - * @access private - */ - public $_debug; - - /** - * Dead hosts, assoc array, 'host'=>'unixtime when ok to check again' - * - * @var array - * @access private - */ - public $_host_dead; - - /** - * Is compression available? - * - * @var bool - * @access private - */ - public $_have_zlib; - - /** - * Do we want to use compression? - * - * @var bool - * @access private - */ - public $_compress_enable; - - /** - * At how many bytes should we compress? - * - * @var int - * @access private - */ - public $_compress_threshold; - - /** - * Are we using persistent links? - * - * @var bool - * @access private - */ - public $_persistent; - - /** - * If only using one server; contains ip:port to connect to - * - * @var string - * @access private - */ - public $_single_sock; - - /** - * Array containing ip:port or array(ip:port, weight) - * - * @var array - * @access private - */ - public $_servers; - - /** - * Our bit buckets - * - * @var array - * @access private - */ - public $_buckets; - - /** - * Total # of bit buckets we have - * - * @var int - * @access private - */ - public $_bucketcount; - - /** - * # of total servers we have - * - * @var int - * @access private - */ - public $_active; - - /** - * Stream timeout in seconds. Applies for example to fread() - * - * @var int - * @access private - */ - public $_timeout_seconds; - - /** - * Stream timeout in microseconds - * - * @var int - * @access private - */ - public $_timeout_microseconds; - - /** - * Connect timeout in seconds - */ - public $_connect_timeout; - - /** - * Number of connection attempts for each server - */ - public $_connect_attempts; - - /** - * @var LoggerInterface - */ - private $_logger; - - // }}} - // }}} - // {{{ methods - // {{{ public functions - // {{{ memcached() - - /** - * Memcache initializer - * - * @param array $args Associative array of settings - */ - public function __construct( $args ) { - $this->set_servers( $args['servers'] ?? array() ); - $this->_debug = $args['debug'] ?? false; - $this->stats = array(); - $this->_compress_threshold = $args['compress_threshold'] ?? 0; - $this->_persistent = $args['persistent'] ?? false; - $this->_compress_enable = true; - $this->_have_zlib = function_exists( 'gzcompress' ); - - $this->_cache_sock = array(); - $this->_host_dead = array(); - - $this->_timeout_seconds = 0; - $this->_timeout_microseconds = $args['timeout'] ?? 500000; - - $this->_connect_timeout = $args['connect_timeout'] ?? 0.1; - $this->_connect_attempts = 2; - - $this->_logger = $args['logger'] ?? new NullLogger(); - } - - // }}} - - /** - * @param mixed $value - * @return string|integer - */ - public function serialize( $value ) { - return serialize( $value ); - } - - /** - * @param string $value - * @return mixed - */ - public function unserialize( $value ) { - return unserialize( $value ); - } - - // {{{ add() - - /** - * Adds a key/value to the memcache server if one isn't already set with - * that key - * - * @param string $key Key to set with data - * @param mixed $val Value to store - * @param int $exp (optional) Expiration time. This can be a number of seconds - * to cache for (up to 30 days inclusive). Any timespans of 30 days + 1 second or - * longer must be the timestamp of the time at which the mapping should expire. It - * is safe to use timestamps in all cases, regardless of expiration - * eg: strtotime("+3 hour") - * - * @return bool - */ - public function add( $key, $val, $exp = 0 ) { - return $this->_set( 'add', $key, $val, $exp ); - } - - // }}} - // {{{ decr() - - /** - * Decrease a value stored on the memcache server - * - * @param string $key Key to decrease - * @param int $amt (optional) amount to decrease - * - * @return mixed False on failure, value on success - */ - public function decr( $key, $amt = 1 ) { - return $this->_incrdecr( 'decr', $key, $amt ); - } - - // }}} - // {{{ delete() - - /** - * Deletes a key from the server, optionally after $time - * - * @param string $key Key to delete - * @param int $time (optional) how long to wait before deleting - * - * @return bool True on success, false on failure - */ - public function delete( $key, $time = 0 ) { - if ( !$this->_active ) { - return false; - } - - $sock = $this->get_sock( $key ); - if ( !is_resource( $sock ) ) { - return false; - } - - $key = is_array( $key ) ? $key[1] : $key; - - if ( isset( $this->stats['delete'] ) ) { - $this->stats['delete']++; - } else { - $this->stats['delete'] = 1; - } - $cmd = "delete $key $time\r\n"; - if ( !$this->_fwrite( $sock, $cmd ) ) { - return false; - } - $res = $this->_fgets( $sock ); - - if ( $this->_debug ) { - $this->_debugprint( sprintf( "MemCache: delete %s (%s)", $key, $res ) ); - } - - if ( $res == "DELETED" || $res == "NOT_FOUND" ) { - return true; - } - - return false; - } - - /** - * Changes the TTL on a key from the server to $time - * - * @param string $key - * @param int $time TTL in seconds - * - * @return bool True on success, false on failure - */ - public function touch( $key, $time = 0 ) { - if ( !$this->_active ) { - return false; - } - - $sock = $this->get_sock( $key ); - if ( !is_resource( $sock ) ) { - return false; - } - - $key = is_array( $key ) ? $key[1] : $key; - - if ( isset( $this->stats['touch'] ) ) { - $this->stats['touch']++; - } else { - $this->stats['touch'] = 1; - } - $cmd = "touch $key $time\r\n"; - if ( !$this->_fwrite( $sock, $cmd ) ) { - return false; - } - $res = $this->_fgets( $sock ); - - if ( $this->_debug ) { - $this->_debugprint( sprintf( "MemCache: touch %s (%s)", $key, $res ) ); - } - - if ( $res == "TOUCHED" ) { - return true; - } - - return false; - } - - /** - * @param string $key - * @param int $timeout - * @return bool - */ - public function lock( $key, $timeout = 0 ) { - /* stub */ - return true; - } - - /** - * @param string $key - * @return bool - */ - public function unlock( $key ) { - /* stub */ - return true; - } - - // }}} - // {{{ disconnect_all() - - /** - * Disconnects all connected sockets - */ - public function disconnect_all() { - foreach ( $this->_cache_sock as $sock ) { - fclose( $sock ); - } - - $this->_cache_sock = array(); - } - - // }}} - // {{{ enable_compress() - - /** - * Enable / Disable compression - * - * @param bool $enable True to enable, false to disable - */ - public function enable_compress( $enable ) { - $this->_compress_enable = $enable; - } - - // }}} - // {{{ forget_dead_hosts() - - /** - * Forget about all of the dead hosts - */ - public function forget_dead_hosts() { - $this->_host_dead = array(); - } - - // }}} - // {{{ get() - - /** - * Retrieves the value associated with the key from the memcache server - * - * @param array|string $key key to retrieve - * @param float $casToken [optional] - * - * @return mixed - */ - public function get( $key, &$casToken = null ) { - if ( $this->_debug ) { - $this->_debugprint( "get($key)" ); - } - - if ( !is_array( $key ) && strval( $key ) === '' ) { - $this->_debugprint( "Skipping key which equals to an empty string" ); - return false; - } - - if ( !$this->_active ) { - return false; - } - - $sock = $this->get_sock( $key ); - - if ( !is_resource( $sock ) ) { - return false; - } - - $key = is_array( $key ) ? $key[1] : $key; - if ( isset( $this->stats['get'] ) ) { - $this->stats['get']++; - } else { - $this->stats['get'] = 1; - } - - $cmd = "gets $key\r\n"; - if ( !$this->_fwrite( $sock, $cmd ) ) { - return false; - } - - $val = array(); - $this->_load_items( $sock, $val, $casToken ); - - if ( $this->_debug ) { - foreach ( $val as $k => $v ) { - $this->_debugprint( - sprintf( "MemCache: sock %s got %s", $this->serialize( $sock ), $k ) ); - } - } - - $value = false; - if ( isset( $val[$key] ) ) { - $value = $val[$key]; - } - return $value; - } - - // }}} - // {{{ get_multi() - - /** - * Get multiple keys from the server(s) - * - * @param array $keys Keys to retrieve - * - * @return array - */ - public function get_multi( $keys ) { - if ( !$this->_active ) { - return array(); - } - - if ( isset( $this->stats['get_multi'] ) ) { - $this->stats['get_multi']++; - } else { - $this->stats['get_multi'] = 1; - } - $sock_keys = array(); - $socks = array(); - foreach ( $keys as $key ) { - $sock = $this->get_sock( $key ); - if ( !is_resource( $sock ) ) { - continue; - } - $key = is_array( $key ) ? $key[1] : $key; - if ( !isset( $sock_keys[$sock] ) ) { - $sock_keys[intval( $sock )] = array(); - $socks[] = $sock; - } - $sock_keys[intval( $sock )][] = $key; - } - - $gather = array(); - // Send out the requests - foreach ( $socks as $sock ) { - $cmd = 'gets'; - foreach ( $sock_keys[intval( $sock )] as $key ) { - $cmd .= ' ' . $key; - } - $cmd .= "\r\n"; - - if ( $this->_fwrite( $sock, $cmd ) ) { - $gather[] = $sock; - } - } - - // Parse responses - $val = array(); - foreach ( $gather as $sock ) { - $this->_load_items( $sock, $val, $casToken ); - } - - if ( $this->_debug ) { - foreach ( $val as $k => $v ) { - $this->_debugprint( sprintf( "MemCache: got %s", $k ) ); - } - } - - return $val; - } - - // }}} - // {{{ incr() - - /** - * Increments $key (optionally) by $amt - * - * @param string $key Key to increment - * @param int $amt (optional) amount to increment - * - * @return int|null Null if the key does not exist yet (this does NOT - * create new mappings if the key does not exist). If the key does - * exist, this returns the new value for that key. - */ - public function incr( $key, $amt = 1 ) { - return $this->_incrdecr( 'incr', $key, $amt ); - } - - // }}} - // {{{ replace() - - /** - * Overwrites an existing value for key; only works if key is already set - * - * @param string $key Key to set value as - * @param mixed $value Value to store - * @param int $exp (optional) Expiration time. This can be a number of seconds - * to cache for (up to 30 days inclusive). Any timespans of 30 days + 1 second or - * longer must be the timestamp of the time at which the mapping should expire. It - * is safe to use timestamps in all cases, regardless of exipration - * eg: strtotime("+3 hour") - * - * @return bool - */ - public function replace( $key, $value, $exp = 0 ) { - return $this->_set( 'replace', $key, $value, $exp ); - } - - // }}} - // {{{ run_command() - - /** - * Passes through $cmd to the memcache server connected by $sock; returns - * output as an array (null array if no output) - * - * @param Resource $sock Socket to send command on - * @param string $cmd Command to run - * - * @return array Output array - */ - public function run_command( $sock, $cmd ) { - if ( !is_resource( $sock ) ) { - return array(); - } - - if ( !$this->_fwrite( $sock, $cmd ) ) { - return array(); - } - - $ret = array(); - while ( true ) { - $res = $this->_fgets( $sock ); - $ret[] = $res; - if ( preg_match( '/^END/', $res ) ) { - break; - } - if ( strlen( $res ) == 0 ) { - break; - } - } - return $ret; - } - - // }}} - // {{{ set() - - /** - * Unconditionally sets a key to a given value in the memcache. Returns true - * if set successfully. - * - * @param string $key Key to set value as - * @param mixed $value Value to set - * @param int $exp (optional) Expiration time. This can be a number of seconds - * to cache for (up to 30 days inclusive). Any timespans of 30 days + 1 second or - * longer must be the timestamp of the time at which the mapping should expire. It - * is safe to use timestamps in all cases, regardless of exipration - * eg: strtotime("+3 hour") - * - * @return bool True on success - */ - public function set( $key, $value, $exp = 0 ) { - return $this->_set( 'set', $key, $value, $exp ); - } - - // }}} - // {{{ cas() - - /** - * Sets a key to a given value in the memcache if the current value still corresponds - * to a known, given value. Returns true if set successfully. - * - * @param float $casToken Current known value - * @param string $key Key to set value as - * @param mixed $value Value to set - * @param int $exp (optional) Expiration time. This can be a number of seconds - * to cache for (up to 30 days inclusive). Any timespans of 30 days + 1 second or - * longer must be the timestamp of the time at which the mapping should expire. It - * is safe to use timestamps in all cases, regardless of exipration - * eg: strtotime("+3 hour") - * - * @return bool True on success - */ - public function cas( $casToken, $key, $value, $exp = 0 ) { - return $this->_set( 'cas', $key, $value, $exp, $casToken ); - } - - // }}} - // {{{ set_compress_threshold() - - /** - * Set the compression threshold - * - * @param int $thresh Threshold to compress if larger than - */ - public function set_compress_threshold( $thresh ) { - $this->_compress_threshold = $thresh; - } - - // }}} - // {{{ set_debug() - - /** - * Set the debug flag - * - * @see __construct() - * @param bool $dbg True for debugging, false otherwise - */ - public function set_debug( $dbg ) { - $this->_debug = $dbg; - } - - // }}} - // {{{ set_servers() - - /** - * Set the server list to distribute key gets and puts between - * - * @see __construct() - * @param array $list Array of servers to connect to - */ - public function set_servers( $list ) { - $this->_servers = $list; - $this->_active = count( $list ); - $this->_buckets = null; - $this->_bucketcount = 0; - - $this->_single_sock = null; - if ( $this->_active == 1 ) { - $this->_single_sock = $this->_servers[0]; - } - } - - /** - * Sets the timeout for new connections - * - * @param int $seconds Number of seconds - * @param int $microseconds Number of microseconds - */ - public function set_timeout( $seconds, $microseconds ) { - $this->_timeout_seconds = $seconds; - $this->_timeout_microseconds = $microseconds; - } - - // }}} - // }}} - // {{{ private methods - // {{{ _close_sock() - - /** - * Close the specified socket - * - * @param string $sock Socket to close - * - * @access private - */ - function _close_sock( $sock ) { - $host = array_search( $sock, $this->_cache_sock ); - fclose( $this->_cache_sock[$host] ); - unset( $this->_cache_sock[$host] ); - } - - // }}} - // {{{ _connect_sock() - - /** - * Connects $sock to $host, timing out after $timeout - * - * @param int $sock Socket to connect - * @param string $host Host:IP to connect to - * - * @return bool - * @access private - */ - function _connect_sock( &$sock, $host ) { - list( $ip, $port ) = preg_split( '/:(?=\d)/', $host ); - $sock = false; - $timeout = $this->_connect_timeout; - $errno = $errstr = null; - for ( $i = 0; !$sock && $i < $this->_connect_attempts; $i++ ) { - Wikimedia\suppressWarnings(); - if ( $this->_persistent == 1 ) { - $sock = pfsockopen( $ip, $port, $errno, $errstr, $timeout ); - } else { - $sock = fsockopen( $ip, $port, $errno, $errstr, $timeout ); - } - Wikimedia\restoreWarnings(); - } - if ( !$sock ) { - $this->_error_log( "Error connecting to $host: $errstr" ); - $this->_dead_host( $host ); - return false; - } - - // Initialise timeout - stream_set_timeout( $sock, $this->_timeout_seconds, $this->_timeout_microseconds ); - - // If the connection was persistent, flush the read buffer in case there - // was a previous incomplete request on this connection - if ( $this->_persistent ) { - $this->_flush_read_buffer( $sock ); - } - return true; - } - - // }}} - // {{{ _dead_sock() - - /** - * Marks a host as dead until 30-40 seconds in the future - * - * @param string $sock Socket to mark as dead - * - * @access private - */ - function _dead_sock( $sock ) { - $host = array_search( $sock, $this->_cache_sock ); - $this->_dead_host( $host ); - } - - /** - * @param string $host - */ - function _dead_host( $host ) { - $ip = explode( ':', $host )[0]; - $this->_host_dead[$ip] = time() + 30 + intval( rand( 0, 10 ) ); - $this->_host_dead[$host] = $this->_host_dead[$ip]; - unset( $this->_cache_sock[$host] ); - } - - // }}} - // {{{ get_sock() - - /** - * get_sock - * - * @param string $key Key to retrieve value for; - * - * @return Resource|bool Resource on success, false on failure - * @access private - */ - function get_sock( $key ) { - if ( !$this->_active ) { - return false; - } - - if ( $this->_single_sock !== null ) { - return $this->sock_to_host( $this->_single_sock ); - } - - $hv = is_array( $key ) ? intval( $key[0] ) : $this->_hashfunc( $key ); - if ( $this->_buckets === null ) { - $bu = array(); - foreach ( $this->_servers as $v ) { - if ( is_array( $v ) ) { - for ( $i = 0; $i < $v[1]; $i++ ) { - $bu[] = $v[0]; - } - } else { - $bu[] = $v; - } - } - $this->_buckets = $bu; - $this->_bucketcount = count( $bu ); - } - - $realkey = is_array( $key ) ? $key[1] : $key; - for ( $tries = 0; $tries < 20; $tries++ ) { - $host = $this->_buckets[$hv % $this->_bucketcount]; - $sock = $this->sock_to_host( $host ); - if ( is_resource( $sock ) ) { - return $sock; - } - $hv = $this->_hashfunc( $hv . $realkey ); - } - - return false; - } - - // }}} - // {{{ _hashfunc() - - /** - * Creates a hash integer based on the $key - * - * @param string $key Key to hash - * - * @return int Hash value - * @access private - */ - function _hashfunc( $key ) { - # Hash function must be in [0,0x7ffffff] - # We take the first 31 bits of the MD5 hash, which unlike the hash - # function used in a previous version of this client, works - return hexdec( substr( md5( $key ), 0, 8 ) ) & 0x7fffffff; - } - - // }}} - // {{{ _incrdecr() - - /** - * Perform increment/decriment on $key - * - * @param string $cmd Command to perform - * @param string|array $key Key to perform it on - * @param int $amt Amount to adjust - * - * @return int New value of $key - * @access private - */ - function _incrdecr( $cmd, $key, $amt = 1 ) { - if ( !$this->_active ) { - return null; - } - - $sock = $this->get_sock( $key ); - if ( !is_resource( $sock ) ) { - return null; - } - - $key = is_array( $key ) ? $key[1] : $key; - if ( isset( $this->stats[$cmd] ) ) { - $this->stats[$cmd]++; - } else { - $this->stats[$cmd] = 1; - } - if ( !$this->_fwrite( $sock, "$cmd $key $amt\r\n" ) ) { - return null; - } - - $line = $this->_fgets( $sock ); - $match = array(); - if ( !preg_match( '/^(\d+)/', $line, $match ) ) { - return null; - } - return $match[1]; - } - - // }}} - // {{{ _load_items() - - /** - * Load items into $ret from $sock - * - * @param Resource $sock Socket to read from - * @param array $ret returned values - * @param float $casToken [optional] - * @return bool True for success, false for failure - * - * @access private - */ - function _load_items( $sock, &$ret, &$casToken = null ) { - $results = array(); - - while ( 1 ) { - $decl = $this->_fgets( $sock ); - - if ( $decl === false ) { - /* - * If nothing can be read, something is wrong because we know exactly when - * to stop reading (right after "END") and we return right after that. - */ - return false; - } elseif ( preg_match( '/^VALUE (\S+) (\d+) (\d+) (\d+)$/', $decl, $match ) ) { - /* - * Read all data returned. This can be either one or multiple values. - * Save all that data (in an array) to be processed later: we'll first - * want to continue reading until "END" before doing anything else, - * to make sure that we don't leave our client in a state where it's - * output is not yet fully read. - */ - $results[] = array( - $match[1], // rkey - $match[2], // flags - $match[3], // len - $match[4], // casToken - $this->_fread( $sock, $match[3] + 2 ), // data - ); - } elseif ( $decl == "END" ) { - if ( count( $results ) == 0 ) { - return false; - } - - /** - * All data has been read, time to process the data and build - * meaningful return values. - */ - foreach ( $results as $vars ) { - list( $rkey, $flags, $len, $casToken, $data ) = $vars; - - if ( $data === false || substr( $data, -2 ) !== "\r\n" ) { - $this->_handle_error( $sock, - 'line ending missing from data block from $1' ); - return false; - } - $data = substr( $data, 0, -2 ); - $ret[$rkey] = $data; - - if ( $this->_have_zlib && $flags & self::COMPRESSED ) { - $ret[$rkey] = gzuncompress( $ret[$rkey] ); - } - - /* - * This unserialize is the exact reason that we only want to - * process data after having read until "END" (instead of doing - * this right away): "unserialize" can trigger outside code: - * in the event that $ret[$rkey] is a serialized object, - * unserializing it will trigger __wakeup() if present. If that - * function attempted to read from memcached (while we did not - * yet read "END"), these 2 calls would collide. - */ - if ( $flags & self::SERIALIZED ) { - $ret[$rkey] = $this->unserialize( $ret[$rkey] ); - } elseif ( $flags & self::INTVAL ) { - $ret[$rkey] = intval( $ret[$rkey] ); - } - } - - return true; - } else { - $this->_handle_error( $sock, 'Error parsing response from $1' ); - return false; - } - } - } - - // }}} - // {{{ _set() - - /** - * Performs the requested storage operation to the memcache server - * - * @param string $cmd Command to perform - * @param string $key Key to act on - * @param mixed $val What we need to store - * @param int $exp (optional) Expiration time. This can be a number of seconds - * to cache for (up to 30 days inclusive). Any timespans of 30 days + 1 second or - * longer must be the timestamp of the time at which the mapping should expire. It - * is safe to use timestamps in all cases, regardless of exipration - * eg: strtotime("+3 hour") - * @param float $casToken [optional] - * - * @return bool - * @access private - */ - function _set( $cmd, $key, $val, $exp, $casToken = null ) { - if ( !$this->_active ) { - return false; - } - - $sock = $this->get_sock( $key ); - if ( !is_resource( $sock ) ) { - return false; - } - - if ( isset( $this->stats[$cmd] ) ) { - $this->stats[$cmd]++; - } else { - $this->stats[$cmd] = 1; - } - - $flags = 0; - - if ( is_int( $val ) ) { - $flags |= self::INTVAL; - } elseif ( !is_scalar( $val ) ) { - $val = $this->serialize( $val ); - $flags |= self::SERIALIZED; - if ( $this->_debug ) { - $this->_debugprint( sprintf( "client: serializing data as it is not scalar" ) ); - } - } - - $len = strlen( $val ); - - if ( $this->_have_zlib && $this->_compress_enable - && $this->_compress_threshold && $len >= $this->_compress_threshold - ) { - $c_val = gzcompress( $val, 9 ); - $c_len = strlen( $c_val ); - - if ( $c_len < $len * ( 1 - self::COMPRESSION_SAVINGS ) ) { - if ( $this->_debug ) { - $this->_debugprint( sprintf( "client: compressing data; was %d bytes is now %d bytes", $len, $c_len ) ); - } - $val = $c_val; - $len = $c_len; - $flags |= self::COMPRESSED; - } - } - - $command = "$cmd $key $flags $exp $len"; - if ( $casToken ) { - $command .= " $casToken"; - } - - if ( !$this->_fwrite( $sock, "$command\r\n$val\r\n" ) ) { - return false; - } - - $line = $this->_fgets( $sock ); - - if ( $this->_debug ) { - $this->_debugprint( sprintf( "%s %s (%s)", $cmd, $key, $line ) ); - } - if ( $line === "STORED" ) { - return true; - } elseif ( $line === "NOT_STORED" && $cmd === "set" ) { - // "Not stored" is always used as the mcrouter response with AllAsyncRoute - return true; - } - - return false; - } - - // }}} - // {{{ sock_to_host() - - /** - * Returns the socket for the host - * - * @param string $host Host:IP to get socket for - * - * @return Resource|bool IO Stream or false - * @access private - */ - function sock_to_host( $host ) { - if ( isset( $this->_cache_sock[$host] ) ) { - return $this->_cache_sock[$host]; - } - - $sock = null; - $now = time(); - list( $ip, /* $port */) = explode( ':', $host ); - if ( isset( $this->_host_dead[$host] ) && $this->_host_dead[$host] > $now || - isset( $this->_host_dead[$ip] ) && $this->_host_dead[$ip] > $now - ) { - return null; - } - - if ( !$this->_connect_sock( $sock, $host ) ) { - return null; - } - - // Do not buffer writes - stream_set_write_buffer( $sock, 0 ); - - $this->_cache_sock[$host] = $sock; - - return $this->_cache_sock[$host]; - } - - /** - * @param string $text - */ - function _debugprint( $text ) { - $this->_logger->debug( $text ); - } - - /** - * @param string $text - */ - function _error_log( $text ) { - $this->_logger->error( "Memcached error: $text" ); - } - - /** - * Write to a stream. If there is an error, mark the socket dead. - * - * @param Resource $sock The socket - * @param string $buf The string to write - * @return bool True on success, false on failure - */ - function _fwrite( $sock, $buf ) { - $bytesWritten = 0; - $bufSize = strlen( $buf ); - while ( $bytesWritten < $bufSize ) { - $result = fwrite( $sock, $buf ); - $data = stream_get_meta_data( $sock ); - if ( $data['timed_out'] ) { - $this->_handle_error( $sock, 'timeout writing to $1' ); - return false; - } - // Contrary to the documentation, fwrite() returns zero on error in PHP 5.3. - if ( $result === false || $result === 0 ) { - $this->_handle_error( $sock, 'error writing to $1' ); - return false; - } - $bytesWritten += $result; - } - - return true; - } - - /** - * Handle an I/O error. Mark the socket dead and log an error. - * - * @param Resource $sock - * @param string $msg - */ - function _handle_error( $sock, $msg ) { - $peer = stream_socket_get_name( $sock, true /** remote **/ ); - if ( strval( $peer ) === '' ) { - $peer = array_search( $sock, $this->_cache_sock ); - if ( $peer === false ) { - $peer = '[unknown host]'; - } - } - $msg = str_replace( '$1', $peer, $msg ); - $this->_error_log( "$msg" ); - $this->_dead_sock( $sock ); - } - - /** - * Read the specified number of bytes from a stream. If there is an error, - * mark the socket dead. - * - * @param Resource $sock The socket - * @param int $len The number of bytes to read - * @return string|bool The string on success, false on failure. - */ - function _fread( $sock, $len ) { - $buf = ''; - while ( $len > 0 ) { - $result = fread( $sock, $len ); - $data = stream_get_meta_data( $sock ); - if ( $data['timed_out'] ) { - $this->_handle_error( $sock, 'timeout reading from $1' ); - return false; - } - if ( $result === false ) { - $this->_handle_error( $sock, 'error reading buffer from $1' ); - return false; - } - if ( $result === '' ) { - // This will happen if the remote end of the socket is shut down - $this->_handle_error( $sock, 'unexpected end of file reading from $1' ); - return false; - } - $len -= strlen( $result ); - $buf .= $result; - } - return $buf; - } - - /** - * Read a line from a stream. If there is an error, mark the socket dead. - * The \r\n line ending is stripped from the response. - * - * @param Resource $sock The socket - * @return string|bool The string on success, false on failure - */ - function _fgets( $sock ) { - $result = fgets( $sock ); - // fgets() may return a partial line if there is a select timeout after - // a successful recv(), so we have to check for a timeout even if we - // got a string response. - $data = stream_get_meta_data( $sock ); - if ( $data['timed_out'] ) { - $this->_handle_error( $sock, 'timeout reading line from $1' ); - return false; - } - if ( $result === false ) { - $this->_handle_error( $sock, 'error reading line from $1' ); - return false; - } - if ( substr( $result, -2 ) === "\r\n" ) { - $result = substr( $result, 0, -2 ); - } elseif ( substr( $result, -1 ) === "\n" ) { - $result = substr( $result, 0, -1 ); - } else { - $this->_handle_error( $sock, 'line ending missing in response from $1' ); - return false; - } - return $result; - } - - /** - * Flush the read buffer of a stream - * @param Resource $f - */ - function _flush_read_buffer( $f ) { - if ( !is_resource( $f ) ) { - return; - } - $r = array( $f ); - $w = null; - $e = null; - $n = stream_select( $r, $w, $e, 0, 0 ); - while ( $n == 1 && !feof( $f ) ) { - fread( $f, 1024 ); - $r = array( $f ); - $w = null; - $e = null; - $n = stream_select( $r, $w, $e, 0, 0 ); - } - } - - // }}} - // }}} - // }}} -} - -// }}} diff --git a/includes/libs/objectcache/MemcachedPeclBagOStuff.php b/includes/libs/objectcache/MemcachedPeclBagOStuff.php index cc7ee2a5f5..9bf3f42126 100644 --- a/includes/libs/objectcache/MemcachedPeclBagOStuff.php +++ b/includes/libs/objectcache/MemcachedPeclBagOStuff.php @@ -214,7 +214,7 @@ class MemcachedPeclBagOStuff extends MemcachedBagOStuff { : $this->checkResult( $key, $result ); } - protected function cas( $casToken, $key, $value, $exptime = 0, $flags = 0 ) { + protected function doCas( $casToken, $key, $value, $exptime = 0, $flags = 0 ) { $this->debug( "cas($key)" ); $result = $this->acquireSyncClient()->cas( @@ -238,7 +238,7 @@ class MemcachedPeclBagOStuff extends MemcachedBagOStuff { : $this->checkResult( $key, $result ); } - public function add( $key, $value, $exptime = 0, $flags = 0 ) { + protected function doAdd( $key, $value, $exptime = 0, $flags = 0 ) { $this->debug( "add($key)" ); $result = $this->acquireSyncClient()->add( @@ -250,7 +250,7 @@ class MemcachedPeclBagOStuff extends MemcachedBagOStuff { return $this->checkResult( $key, $result ); } - public function incr( $key, $value = 1 ) { + public function incr( $key, $value = 1, $flags = 0 ) { $this->debug( "incr($key)" ); $result = $this->acquireSyncClient()->increment( $key, $value ); @@ -258,7 +258,7 @@ class MemcachedPeclBagOStuff extends MemcachedBagOStuff { return $this->checkResult( $key, $result ); } - public function decr( $key, $value = 1 ) { + public function decr( $key, $value = 1, $flags = 0 ) { $this->debug( "decr($key)" ); $result = $this->acquireSyncClient()->decrement( $key, $value ); @@ -332,7 +332,7 @@ class MemcachedPeclBagOStuff extends MemcachedBagOStuff { // The PECL implementation is a naïve for-loop so use async I/O to pipeline; // https://github.com/php-memcached-dev/php-memcached/blob/master/php_memcached.c#L1852 - if ( ( $flags & self::WRITE_BACKGROUND ) == self::WRITE_BACKGROUND ) { + if ( $this->fieldHasFlags( $flags, self::WRITE_BACKGROUND ) ) { $client = $this->acquireAsyncClient(); $result = $client->setMulti( $data, $exptime ); $this->releaseAsyncClient( $client ); @@ -352,7 +352,7 @@ class MemcachedPeclBagOStuff extends MemcachedBagOStuff { // The PECL implementation is a naïve for-loop so use async I/O to pipeline; // https://github.com/php-memcached-dev/php-memcached/blob/7443d16d02fb73cdba2e90ae282446f80969229c/php_memcached.c#L1852 - if ( ( $flags & self::WRITE_BACKGROUND ) == self::WRITE_BACKGROUND ) { + if ( $this->fieldHasFlags( $flags, self::WRITE_BACKGROUND ) ) { $client = $this->acquireAsyncClient(); $resultArray = $client->deleteMulti( $keys ) ?: []; $this->releaseAsyncClient( $client ); diff --git a/includes/libs/objectcache/MemcachedPhpBagOStuff.php b/includes/libs/objectcache/MemcachedPhpBagOStuff.php index b1d5d29f16..fc6deefd93 100644 --- a/includes/libs/objectcache/MemcachedPhpBagOStuff.php +++ b/includes/libs/objectcache/MemcachedPhpBagOStuff.php @@ -76,7 +76,7 @@ class MemcachedPhpBagOStuff extends MemcachedBagOStuff { return $this->client->delete( $this->validateKeyEncoding( $key ) ); } - public function add( $key, $value, $exptime = 0, $flags = 0 ) { + protected function doAdd( $key, $value, $exptime = 0, $flags = 0 ) { return $this->client->add( $this->validateKeyEncoding( $key ), $value, @@ -84,7 +84,7 @@ class MemcachedPhpBagOStuff extends MemcachedBagOStuff { ); } - protected function cas( $casToken, $key, $value, $exptime = 0, $flags = 0 ) { + protected function doCas( $casToken, $key, $value, $exptime = 0, $flags = 0 ) { return $this->client->cas( $casToken, $this->validateKeyEncoding( $key ), @@ -93,13 +93,13 @@ class MemcachedPhpBagOStuff extends MemcachedBagOStuff { ); } - public function incr( $key, $value = 1 ) { + public function incr( $key, $value = 1, $flags = 0 ) { $n = $this->client->incr( $this->validateKeyEncoding( $key ), $value ); return ( $n !== false && $n !== null ) ? $n : false; } - public function decr( $key, $value = 1 ) { + public function decr( $key, $value = 1, $flags = 0 ) { $n = $this->client->decr( $this->validateKeyEncoding( $key ), $value ); return ( $n !== false && $n !== null ) ? $n : false; diff --git a/includes/libs/objectcache/MultiWriteBagOStuff.php b/includes/libs/objectcache/MultiWriteBagOStuff.php index d150880750..77a78830e2 100644 --- a/includes/libs/objectcache/MultiWriteBagOStuff.php +++ b/includes/libs/objectcache/MultiWriteBagOStuff.php @@ -46,7 +46,7 @@ class MultiWriteBagOStuff extends BagOStuff { /** * $params include: * - caches: A numbered array of either ObjectFactory::getObjectFromSpec - * arrays yeilding BagOStuff objects or direct BagOStuff objects. + * arrays yielding BagOStuff objects or direct BagOStuff objects. * If using the former, the 'args' field *must* be set. * The first cache is the primary one, being the first to * be read in the fallback chain. Writes happen to all stores @@ -61,6 +61,7 @@ class MultiWriteBagOStuff extends BagOStuff { * invalidation uses logical TTLs, invalidation uses etag/timestamp * validation against the DB, or merge() is used to handle races. * @param array $params + * @phan-param array{caches:array,replication:string} $params * @throws InvalidArgumentException */ public function __construct( $params ) { @@ -106,7 +107,7 @@ class MultiWriteBagOStuff extends BagOStuff { } public function get( $key, $flags = 0 ) { - if ( ( $flags & self::READ_LATEST ) == self::READ_LATEST ) { + if ( $this->fieldHasFlags( $flags, self::READ_LATEST ) ) { // If the latest write was a delete(), we do NOT want to fallback // to the other tiers and possibly see the old value. Also, this // is used by merge(), which only needs to hit the primary. @@ -123,9 +124,10 @@ class MultiWriteBagOStuff extends BagOStuff { $missIndexes[] = $i; } - if ( $value !== false - && $missIndexes - && ( $flags & self::READ_VERIFIED ) == self::READ_VERIFIED + if ( + $value !== false && + $this->fieldHasFlags( $flags, self::READ_VERIFIED ) && + $missIndexes ) { // Backfill the value to the higher (and often faster/smaller) cache tiers $this->doWrite( @@ -265,7 +267,7 @@ class MultiWriteBagOStuff extends BagOStuff { ); } - public function incr( $key, $value = 1 ) { + public function incr( $key, $value = 1, $flags = 0 ) { return $this->doWrite( $this->cacheIndexes, $this->asyncWrites, @@ -274,7 +276,7 @@ class MultiWriteBagOStuff extends BagOStuff { ); } - public function decr( $key, $value = 1 ) { + public function decr( $key, $value = 1, $flags = 0 ) { return $this->doWrite( $this->cacheIndexes, $this->asyncWrites, @@ -283,7 +285,7 @@ class MultiWriteBagOStuff extends BagOStuff { ); } - public function incrWithInit( $key, $ttl, $value = 1, $init = 1 ) { + public function incrWithInit( $key, $exptime, $value = 1, $init = null, $flags = 0 ) { return $this->doWrite( $this->cacheIndexes, $this->asyncWrites, @@ -346,7 +348,7 @@ class MultiWriteBagOStuff extends BagOStuff { * @return bool */ protected function usesAsyncWritesGivenFlags( $flags ) { - return ( ( $flags & self::WRITE_SYNC ) == self::WRITE_SYNC ) ? false : $this->asyncWrites; + return $this->fieldHasFlags( $flags, self::WRITE_SYNC ) ? false : $this->asyncWrites; } public function makeKeyInternal( $keyspace, $args ) { diff --git a/includes/libs/objectcache/RESTBagOStuff.php b/includes/libs/objectcache/RESTBagOStuff.php index aa4a9b31fc..82b5ac05bf 100644 --- a/includes/libs/objectcache/RESTBagOStuff.php +++ b/includes/libs/objectcache/RESTBagOStuff.php @@ -164,7 +164,7 @@ class RESTBagOStuff extends MediumSpecificBagOStuff { return $this->handleError( "Failed to store $key", $rcode, $rerr, $rhdrs, $rbody ); } - public function add( $key, $value, $exptime = 0, $flags = 0 ) { + protected function doAdd( $key, $value, $exptime = 0, $flags = 0 ) { // @TODO: make this atomic if ( $this->get( $key ) === false ) { return $this->set( $key, $value, $exptime, $flags ); @@ -188,11 +188,11 @@ class RESTBagOStuff extends MediumSpecificBagOStuff { return $this->handleError( "Failed to delete $key", $rcode, $rerr, $rhdrs, $rbody ); } - public function incr( $key, $value = 1 ) { + public function incr( $key, $value = 1, $flags = 0 ) { // @TODO: make this atomic $n = $this->get( $key, self::READ_LATEST ); if ( $this->isInteger( $n ) ) { // key exists? - $n = max( $n + intval( $value ), 0 ); + $n = max( $n + (int)$value, 0 ); // @TODO: respect $exptime return $this->set( $key, $n ) ? $n : false; } @@ -200,6 +200,10 @@ class RESTBagOStuff extends MediumSpecificBagOStuff { return false; } + public function decr( $key, $value = 1, $flags = 0 ) { + return $this->incr( $key, -$value, $flags ); + } + /** * Processes the response body. * diff --git a/includes/libs/objectcache/RedisBagOStuff.php b/includes/libs/objectcache/RedisBagOStuff.php index f75d3a1015..aaed69f441 100644 --- a/includes/libs/objectcache/RedisBagOStuff.php +++ b/includes/libs/objectcache/RedisBagOStuff.php @@ -28,6 +28,7 @@ * * @ingroup Cache * @ingroup Redis + * @phan-file-suppress PhanTypeComparisonFromArray It's unclear whether exec() can return false */ class RedisBagOStuff extends MediumSpecificBagOStuff { /** @var RedisConnectionPool */ @@ -341,7 +342,7 @@ class RedisBagOStuff extends MediumSpecificBagOStuff { return $result; } - public function add( $key, $value, $expiry = 0, $flags = 0 ) { + protected function doAdd( $key, $value, $expiry = 0, $flags = 0 ) { $conn = $this->getConnection( $key ); if ( !$conn ) { return false; @@ -364,7 +365,7 @@ class RedisBagOStuff extends MediumSpecificBagOStuff { return $result; } - public function incr( $key, $value = 1 ) { + public function incr( $key, $value = 1, $flags = 0 ) { $conn = $this->getConnection( $key ); if ( !$conn ) { return false; @@ -386,6 +387,28 @@ class RedisBagOStuff extends MediumSpecificBagOStuff { return $result; } + public function decr( $key, $value = 1, $flags = 0 ) { + $conn = $this->getConnection( $key ); + if ( !$conn ) { + return false; + } + + try { + if ( !$conn->exists( $key ) ) { + return false; + } + // @FIXME: on races, the key may have a 0 TTL + $result = $conn->decrBy( $key, $value ); + } catch ( RedisException $e ) { + $result = false; + $this->handleException( $conn, $e ); + } + + $this->logRequest( 'decr', $key, $conn->getServer(), $result ); + + return $result; + } + protected function doChangeTTL( $key, $exptime, $flags ) { $conn = $this->getConnection( $key ); if ( !$conn ) { diff --git a/includes/libs/objectcache/ReplicatedBagOStuff.php b/includes/libs/objectcache/ReplicatedBagOStuff.php index 504d51534e..9f953e1721 100644 --- a/includes/libs/objectcache/ReplicatedBagOStuff.php +++ b/includes/libs/objectcache/ReplicatedBagOStuff.php @@ -33,16 +33,26 @@ use Wikimedia\ObjectFactory; */ class ReplicatedBagOStuff extends BagOStuff { /** @var BagOStuff */ - protected $writeStore; + private $writeStore; /** @var BagOStuff */ - protected $readStore; + private $readStore; + + /** @var int Seconds to read from the master source for a key after writing to it */ + private $consistencyWindow; + /** @var float[] Map of (key => UNIX timestamp) */ + private $lastKeyWrites = []; + + /** @var int Max expected delay (in seconds) for writes to reach replicas */ + const MAX_WRITE_DELAY = 5; /** * Constructor. Parameters are: - * - writeFactory : ObjectFactory::getObjectFromSpec array yeilding BagOStuff. - * This object will be used for writes (e.g. the master DB). - * - readFactory : ObjectFactory::getObjectFromSpec array yeilding BagOStuff. - * This object will be used for reads (e.g. a replica DB). + * - writeFactory: ObjectFactory::getObjectFromSpec array yielding BagOStuff. + * This object will be used for writes (e.g. the master DB). + * - readFactory: ObjectFactory::getObjectFromSpec array yielding BagOStuff. + * This object will be used for reads (e.g. a replica DB). + * - sessionConsistencyWindow: Seconds to read from the master source for a key + * after writing to it. [Default: ReplicatedBagOStuff::MAX_WRITE_DELAY] * * @param array $params * @throws InvalidArgumentException @@ -53,19 +63,18 @@ class ReplicatedBagOStuff extends BagOStuff { if ( !isset( $params['writeFactory'] ) ) { throw new InvalidArgumentException( __METHOD__ . ': the "writeFactory" parameter is required' ); - } - if ( !isset( $params['readFactory'] ) ) { + } elseif ( !isset( $params['readFactory'] ) ) { throw new InvalidArgumentException( __METHOD__ . ': the "readFactory" parameter is required' ); } - $opts = [ 'reportDupes' => false ]; // redundant + $this->consistencyWindow = $params['sessionConsistencyWindow'] ?? self::MAX_WRITE_DELAY; $this->writeStore = ( $params['writeFactory'] instanceof BagOStuff ) ? $params['writeFactory'] - : ObjectFactory::getObjectFromSpec( $opts + $params['writeFactory'] ); + : ObjectFactory::getObjectFromSpec( $params['writeFactory'] ); $this->readStore = ( $params['readFactory'] instanceof BagOStuff ) ? $params['readFactory'] - : ObjectFactory::getObjectFromSpec( $opts + $params['readFactory'] ); + : ObjectFactory::getObjectFromSpec( $params['readFactory'] ); $this->attrMap = $this->mergeFlagMaps( [ $this->readStore, $this->writeStore ] ); } @@ -76,28 +85,41 @@ class ReplicatedBagOStuff extends BagOStuff { } public function get( $key, $flags = 0 ) { - return ( ( $flags & self::READ_LATEST ) == self::READ_LATEST ) + return ( + $this->hadRecentSessionWrite( [ $key ] ) || + $this->fieldHasFlags( $flags, self::READ_LATEST ) + ) ? $this->writeStore->get( $key, $flags ) : $this->readStore->get( $key, $flags ); } public function set( $key, $value, $exptime = 0, $flags = 0 ) { + $this->remarkRecentSessionWrite( [ $key ] ); + return $this->writeStore->set( $key, $value, $exptime, $flags ); } public function delete( $key, $flags = 0 ) { + $this->remarkRecentSessionWrite( [ $key ] ); + return $this->writeStore->delete( $key, $flags ); } public function add( $key, $value, $exptime = 0, $flags = 0 ) { + $this->remarkRecentSessionWrite( [ $key ] ); + return $this->writeStore->add( $key, $value, $exptime, $flags ); } public function merge( $key, callable $callback, $exptime = 0, $attempts = 10, $flags = 0 ) { + $this->remarkRecentSessionWrite( [ $key ] ); + return $this->writeStore->merge( $key, $callback, $exptime, $attempts, $flags ); } public function changeTTL( $key, $exptime = 0, $flags = 0 ) { + $this->remarkRecentSessionWrite( [ $key ] ); + return $this->writeStore->changeTTL( $key, $exptime, $flags ); } @@ -118,37 +140,52 @@ class ReplicatedBagOStuff extends BagOStuff { } public function getMulti( array $keys, $flags = 0 ) { - return ( ( $flags & self::READ_LATEST ) == self::READ_LATEST ) + return ( + $this->hadRecentSessionWrite( $keys ) || + $this->fieldHasFlags( $flags, self::READ_LATEST ) + ) ? $this->writeStore->getMulti( $keys, $flags ) : $this->readStore->getMulti( $keys, $flags ); } public function setMulti( array $data, $exptime = 0, $flags = 0 ) { + $this->remarkRecentSessionWrite( array_keys( $data ) ); + return $this->writeStore->setMulti( $data, $exptime, $flags ); } public function deleteMulti( array $keys, $flags = 0 ) { + $this->remarkRecentSessionWrite( $keys ); + return $this->writeStore->deleteMulti( $keys, $flags ); } public function changeTTLMulti( array $keys, $exptime, $flags = 0 ) { + $this->remarkRecentSessionWrite( $keys ); + return $this->writeStore->changeTTLMulti( $keys, $exptime, $flags ); } - public function incr( $key, $value = 1 ) { - return $this->writeStore->incr( $key, $value ); + public function incr( $key, $value = 1, $flags = 0 ) { + $this->remarkRecentSessionWrite( [ $key ] ); + + return $this->writeStore->incr( $key, $value, $flags ); } - public function decr( $key, $value = 1 ) { - return $this->writeStore->decr( $key, $value ); + public function decr( $key, $value = 1, $flags = 0 ) { + $this->remarkRecentSessionWrite( [ $key ] ); + + return $this->writeStore->decr( $key, $value, $flags ); } - public function incrWithInit( $key, $ttl, $value = 1, $init = 1 ) { - return $this->writeStore->incrWithInit( $key, $ttl, $value, $init ); + public function incrWithInit( $key, $exptime, $value = 1, $init = null, $flags = 0 ) { + $this->remarkRecentSessionWrite( [ $key ] ); + + return $this->writeStore->incrWithInit( $key, $exptime, $value, $init, $flags ); } public function getLastError() { - return ( $this->writeStore->getLastError() != self::ERR_NONE ) + return ( $this->writeStore->getLastError() !== self::ERR_NONE ) ? $this->writeStore->getLastError() : $this->readStore->getLastError(); } @@ -179,4 +216,40 @@ class ReplicatedBagOStuff extends BagOStuff { $this->writeStore->setMockTime( $time ); $this->readStore->setMockTime( $time ); } + + /** + * @param string[] $keys + * @return bool + */ + private function hadRecentSessionWrite( array $keys ) { + $now = $this->getCurrentTime(); + foreach ( $keys as $key ) { + $ts = $this->lastKeyWrites[$key] ?? 0; + if ( $ts && ( $now - $ts ) <= $this->consistencyWindow ) { + return true; + } + } + + return false; + } + + /** + * @param string[] $keys + */ + private function remarkRecentSessionWrite( array $keys ) { + $now = $this->getCurrentTime(); + foreach ( $keys as $key ) { + unset( $this->lastKeyWrites[$key] ); // move to the end + $this->lastKeyWrites[$key] = $now; + } + // Prune out the map if the first key is obsolete + if ( ( $now - reset( $this->lastKeyWrites ) ) > $this->consistencyWindow ) { + $this->lastKeyWrites = array_filter( + $this->lastKeyWrites, + function ( $timestamp ) use ( $now ) { + return ( ( $now - $timestamp ) <= $this->consistencyWindow ); + } + ); + } + } } diff --git a/includes/libs/objectcache/WinCacheBagOStuff.php b/includes/libs/objectcache/WinCacheBagOStuff.php index 0e4e3fb63d..5b38628403 100644 --- a/includes/libs/objectcache/WinCacheBagOStuff.php +++ b/includes/libs/objectcache/WinCacheBagOStuff.php @@ -28,6 +28,11 @@ * @ingroup Cache */ class WinCacheBagOStuff extends MediumSpecificBagOStuff { + public function __construct( array $params = [] ) { + $params['segmentationSize'] = $params['segmentationSize'] ?? INF; + parent::__construct( $params ); + } + protected function doGet( $key, $flags = 0, &$casToken = null ) { $casToken = null; @@ -44,7 +49,7 @@ class WinCacheBagOStuff extends MediumSpecificBagOStuff { return $value; } - protected function cas( $casToken, $key, $value, $exptime = 0, $flags = 0 ) { + protected function doCas( $casToken, $key, $value, $exptime = 0, $flags = 0 ) { if ( !wincache_lock( $key ) ) { // optimize with FIFO lock return false; } @@ -76,7 +81,7 @@ class WinCacheBagOStuff extends MediumSpecificBagOStuff { return ( $result === [] || $result === true ); } - public function add( $key, $value, $exptime = 0, $flags = 0 ) { + protected function doAdd( $key, $value, $exptime = 0, $flags = 0 ) { if ( wincache_ucache_exists( $key ) ) { return false; // avoid warnings } @@ -95,14 +100,6 @@ class WinCacheBagOStuff extends MediumSpecificBagOStuff { return true; } - /** - * Construct a cache key. - * - * @since 1.27 - * @param string $keyspace - * @param array $args - * @return string - */ public function makeKeyInternal( $keyspace, $args ) { // WinCache keys have a maximum length of 150 characters. From that, // subtract the number of characters we need for the keyspace and for @@ -131,13 +128,7 @@ class WinCacheBagOStuff extends MediumSpecificBagOStuff { return $keyspace . ':' . implode( ':', $args ); } - /** - * Increase stored value of $key by $value while preserving its original TTL - * @param string $key Key to increase - * @param int $value Value to add to $key (Default 1) - * @return int|bool New value or false on failure - */ - public function incr( $key, $value = 1 ) { + public function incr( $key, $value = 1, $flags = 0 ) { if ( !wincache_lock( $key ) ) { // optimize with FIFO lock return false; } @@ -155,4 +146,8 @@ class WinCacheBagOStuff extends MediumSpecificBagOStuff { return $n; } + + public function decr( $key, $value = 1, $flags = 0 ) { + return $this->incr( $key, -$value, $flags ); + } } diff --git a/includes/libs/objectcache/utils/MemcachedClient.php b/includes/libs/objectcache/utils/MemcachedClient.php new file mode 100644 index 0000000000..2c4085433e --- /dev/null +++ b/includes/libs/objectcache/utils/MemcachedClient.php @@ -0,0 +1,1311 @@ + | + * | All rights reserved. | + * | | + * | Redistribution and use in source and binary forms, with or without | + * | modification, are permitted provided that the following conditions | + * | are met: | + * | | + * | 1. Redistributions of source code must retain the above copyright | + * | notice, this list of conditions and the following disclaimer. | + * | 2. Redistributions in binary form must reproduce the above copyright | + * | notice, this list of conditions and the following disclaimer in the | + * | documentation and/or other materials provided with the distribution. | + * | | + * | THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR | + * | IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES | + * | OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. | + * | IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, | + * | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT | + * | NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | + * | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | + * | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | + * | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF | + * | THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | + * +---------------------------------------------------------------------------+ + * | Author: Ryan T. Dean | + * | Heavily influenced by the Perl memcached client by Brad Fitzpatrick. | + * | Permission granted by Brad Fitzpatrick for relicense of ported Perl | + * | client logic under 2-clause BSD license. | + * +---------------------------------------------------------------------------+ + * + * @file + * $TCAnet$ + */ + +/** + * This is a PHP client for memcached - a distributed memory cache daemon. + * + * More information is available at http://www.danga.com/memcached/ + * + * Usage example: + * + * $mc = new MemcachedClient(array( + * 'servers' => array( + * '127.0.0.1:10000', + * array( '192.0.0.1:10010', 2 ), + * '127.0.0.1:10020' + * ), + * 'debug' => false, + * 'compress_threshold' => 10240, + * 'persistent' => true + * )); + * + * $mc->add( 'key', array( 'some', 'array' ) ); + * $mc->replace( 'key', 'some random string' ); + * $val = $mc->get( 'key' ); + * + * @author Ryan T. Dean + * @version 0.1.2 + */ + +use Psr\Log\LoggerInterface; +use Psr\Log\NullLogger; + +// {{{ class MemcachedClient +/** + * memcached client class implemented using (p)fsockopen() + * + * @author Ryan T. Dean + * @ingroup Cache + */ +class MemcachedClient { + // {{{ properties + // {{{ public + + // {{{ constants + // {{{ flags + + /** + * Flag: indicates data is serialized + */ + const SERIALIZED = 1; + + /** + * Flag: indicates data is compressed + */ + const COMPRESSED = 2; + + /** + * Flag: indicates data is an integer + */ + const INTVAL = 4; + + // }}} + + /** + * Minimum savings to store data compressed + */ + const COMPRESSION_SAVINGS = 0.20; + + // }}} + + /** + * Command statistics + * + * @var array + * @access public + */ + public $stats; + + // }}} + // {{{ private + + /** + * Cached Sockets that are connected + * + * @var array + * @access private + */ + public $_cache_sock; + + /** + * Current debug status; 0 - none to 9 - profiling + * + * @var bool + * @access private + */ + public $_debug; + + /** + * Dead hosts, assoc array, 'host'=>'unixtime when ok to check again' + * + * @var array + * @access private + */ + public $_host_dead; + + /** + * Is compression available? + * + * @var bool + * @access private + */ + public $_have_zlib; + + /** + * Do we want to use compression? + * + * @var bool + * @access private + */ + public $_compress_enable; + + /** + * At how many bytes should we compress? + * + * @var int + * @access private + */ + public $_compress_threshold; + + /** + * Are we using persistent links? + * + * @var bool + * @access private + */ + public $_persistent; + + /** + * If only using one server; contains ip:port to connect to + * + * @var string + * @access private + */ + public $_single_sock; + + /** + * Array containing ip:port or array(ip:port, weight) + * + * @var array + * @access private + */ + public $_servers; + + /** + * Our bit buckets + * + * @var array + * @access private + */ + public $_buckets; + + /** + * Total # of bit buckets we have + * + * @var int + * @access private + */ + public $_bucketcount; + + /** + * # of total servers we have + * + * @var int + * @access private + */ + public $_active; + + /** + * Stream timeout in seconds. Applies for example to fread() + * + * @var int + * @access private + */ + public $_timeout_seconds; + + /** + * Stream timeout in microseconds + * + * @var int + * @access private + */ + public $_timeout_microseconds; + + /** + * Connect timeout in seconds + */ + public $_connect_timeout; + + /** + * Number of connection attempts for each server + */ + public $_connect_attempts; + + /** + * @var LoggerInterface + */ + private $_logger; + + // }}} + // }}} + // {{{ methods + // {{{ public functions + // {{{ memcached() + + /** + * Memcache initializer + * + * @param array $args Associative array of settings + */ + public function __construct( $args ) { + $this->set_servers( $args['servers'] ?? array() ); + $this->_debug = $args['debug'] ?? false; + $this->stats = array(); + $this->_compress_threshold = $args['compress_threshold'] ?? 0; + $this->_persistent = $args['persistent'] ?? false; + $this->_compress_enable = true; + $this->_have_zlib = function_exists( 'gzcompress' ); + + $this->_cache_sock = array(); + $this->_host_dead = array(); + + $this->_timeout_seconds = 0; + $this->_timeout_microseconds = $args['timeout'] ?? 500000; + + $this->_connect_timeout = $args['connect_timeout'] ?? 0.1; + $this->_connect_attempts = 2; + + $this->_logger = $args['logger'] ?? new NullLogger(); + } + + // }}} + + /** + * @param mixed $value + * @return string|integer + */ + public function serialize( $value ) { + return serialize( $value ); + } + + /** + * @param string $value + * @return mixed + */ + public function unserialize( $value ) { + return unserialize( $value ); + } + + // {{{ add() + + /** + * Adds a key/value to the memcache server if one isn't already set with + * that key + * + * @param string $key Key to set with data + * @param mixed $val Value to store + * @param int $exp (optional) Expiration time. This can be a number of seconds + * to cache for (up to 30 days inclusive). Any timespans of 30 days + 1 second or + * longer must be the timestamp of the time at which the mapping should expire. It + * is safe to use timestamps in all cases, regardless of expiration + * eg: strtotime("+3 hour") + * + * @return bool + */ + public function add( $key, $val, $exp = 0 ) { + return $this->_set( 'add', $key, $val, $exp ); + } + + // }}} + // {{{ decr() + + /** + * Decrease a value stored on the memcache server + * + * @param string $key Key to decrease + * @param int $amt (optional) amount to decrease + * + * @return mixed False on failure, value on success + */ + public function decr( $key, $amt = 1 ) { + return $this->_incrdecr( 'decr', $key, $amt ); + } + + // }}} + // {{{ delete() + + /** + * Deletes a key from the server, optionally after $time + * + * @param string $key Key to delete + * @param int $time (optional) how long to wait before deleting + * + * @return bool True on success, false on failure + */ + public function delete( $key, $time = 0 ) { + if ( !$this->_active ) { + return false; + } + + $sock = $this->get_sock( $key ); + if ( !is_resource( $sock ) ) { + return false; + } + + $key = is_array( $key ) ? $key[1] : $key; + + if ( isset( $this->stats['delete'] ) ) { + $this->stats['delete']++; + } else { + $this->stats['delete'] = 1; + } + $cmd = "delete $key $time\r\n"; + if ( !$this->_fwrite( $sock, $cmd ) ) { + return false; + } + $res = $this->_fgets( $sock ); + + if ( $this->_debug ) { + $this->_debugprint( sprintf( "MemCache: delete %s (%s)", $key, $res ) ); + } + + if ( $res == "DELETED" || $res == "NOT_FOUND" ) { + return true; + } + + return false; + } + + /** + * Changes the TTL on a key from the server to $time + * + * @param string $key + * @param int $time TTL in seconds + * + * @return bool True on success, false on failure + */ + public function touch( $key, $time = 0 ) { + if ( !$this->_active ) { + return false; + } + + $sock = $this->get_sock( $key ); + if ( !is_resource( $sock ) ) { + return false; + } + + $key = is_array( $key ) ? $key[1] : $key; + + if ( isset( $this->stats['touch'] ) ) { + $this->stats['touch']++; + } else { + $this->stats['touch'] = 1; + } + $cmd = "touch $key $time\r\n"; + if ( !$this->_fwrite( $sock, $cmd ) ) { + return false; + } + $res = $this->_fgets( $sock ); + + if ( $this->_debug ) { + $this->_debugprint( sprintf( "MemCache: touch %s (%s)", $key, $res ) ); + } + + if ( $res == "TOUCHED" ) { + return true; + } + + return false; + } + + // }}} + // {{{ disconnect_all() + + /** + * Disconnects all connected sockets + */ + public function disconnect_all() { + foreach ( $this->_cache_sock as $sock ) { + fclose( $sock ); + } + + $this->_cache_sock = array(); + } + + // }}} + // {{{ enable_compress() + + /** + * Enable / Disable compression + * + * @param bool $enable True to enable, false to disable + */ + public function enable_compress( $enable ) { + $this->_compress_enable = $enable; + } + + // }}} + // {{{ forget_dead_hosts() + + /** + * Forget about all of the dead hosts + */ + public function forget_dead_hosts() { + $this->_host_dead = array(); + } + + // }}} + // {{{ get() + + /** + * Retrieves the value associated with the key from the memcache server + * + * @param array|string $key key to retrieve + * @param float $casToken [optional] + * + * @return mixed + */ + public function get( $key, &$casToken = null ) { + if ( $this->_debug ) { + $this->_debugprint( "get($key)" ); + } + + if ( !is_array( $key ) && strval( $key ) === '' ) { + $this->_debugprint( "Skipping key which equals to an empty string" ); + return false; + } + + if ( !$this->_active ) { + return false; + } + + $sock = $this->get_sock( $key ); + + if ( !is_resource( $sock ) ) { + return false; + } + + $key = is_array( $key ) ? $key[1] : $key; + if ( isset( $this->stats['get'] ) ) { + $this->stats['get']++; + } else { + $this->stats['get'] = 1; + } + + $cmd = "gets $key\r\n"; + if ( !$this->_fwrite( $sock, $cmd ) ) { + return false; + } + + $val = array(); + $this->_load_items( $sock, $val, $casToken ); + + if ( $this->_debug ) { + foreach ( $val as $k => $v ) { + $this->_debugprint( + sprintf( "MemCache: sock %s got %s", $this->serialize( $sock ), $k ) ); + } + } + + $value = false; + if ( isset( $val[$key] ) ) { + $value = $val[$key]; + } + return $value; + } + + // }}} + // {{{ get_multi() + + /** + * Get multiple keys from the server(s) + * + * @param array $keys Keys to retrieve + * + * @return array + */ + public function get_multi( $keys ) { + if ( !$this->_active ) { + return array(); + } + + if ( isset( $this->stats['get_multi'] ) ) { + $this->stats['get_multi']++; + } else { + $this->stats['get_multi'] = 1; + } + $sock_keys = array(); + $socks = array(); + foreach ( $keys as $key ) { + $sock = $this->get_sock( $key ); + if ( !is_resource( $sock ) ) { + continue; + } + $key = is_array( $key ) ? $key[1] : $key; + if ( !isset( $sock_keys[$sock] ) ) { + $sock_keys[intval( $sock )] = array(); + $socks[] = $sock; + } + $sock_keys[intval( $sock )][] = $key; + } + + $gather = array(); + // Send out the requests + foreach ( $socks as $sock ) { + $cmd = 'gets'; + foreach ( $sock_keys[intval( $sock )] as $key ) { + $cmd .= ' ' . $key; + } + $cmd .= "\r\n"; + + if ( $this->_fwrite( $sock, $cmd ) ) { + $gather[] = $sock; + } + } + + // Parse responses + $val = array(); + foreach ( $gather as $sock ) { + $this->_load_items( $sock, $val, $casToken ); + } + + if ( $this->_debug ) { + foreach ( $val as $k => $v ) { + $this->_debugprint( sprintf( "MemCache: got %s", $k ) ); + } + } + + return $val; + } + + // }}} + // {{{ incr() + + /** + * Increments $key (optionally) by $amt + * + * @param string $key Key to increment + * @param int $amt (optional) amount to increment + * + * @return int|null Null if the key does not exist yet (this does NOT + * create new mappings if the key does not exist). If the key does + * exist, this returns the new value for that key. + */ + public function incr( $key, $amt = 1 ) { + return $this->_incrdecr( 'incr', $key, $amt ); + } + + // }}} + // {{{ replace() + + /** + * Overwrites an existing value for key; only works if key is already set + * + * @param string $key Key to set value as + * @param mixed $value Value to store + * @param int $exp (optional) Expiration time. This can be a number of seconds + * to cache for (up to 30 days inclusive). Any timespans of 30 days + 1 second or + * longer must be the timestamp of the time at which the mapping should expire. It + * is safe to use timestamps in all cases, regardless of exipration + * eg: strtotime("+3 hour") + * + * @return bool + */ + public function replace( $key, $value, $exp = 0 ) { + return $this->_set( 'replace', $key, $value, $exp ); + } + + // }}} + // {{{ run_command() + + /** + * Passes through $cmd to the memcache server connected by $sock; returns + * output as an array (null array if no output) + * + * @param Resource $sock Socket to send command on + * @param string $cmd Command to run + * + * @return array Output array + */ + public function run_command( $sock, $cmd ) { + if ( !is_resource( $sock ) ) { + return array(); + } + + if ( !$this->_fwrite( $sock, $cmd ) ) { + return array(); + } + + $ret = array(); + while ( true ) { + $res = $this->_fgets( $sock ); + $ret[] = $res; + if ( preg_match( '/^END/', $res ) ) { + break; + } + if ( strlen( $res ) == 0 ) { + break; + } + } + return $ret; + } + + // }}} + // {{{ set() + + /** + * Unconditionally sets a key to a given value in the memcache. Returns true + * if set successfully. + * + * @param string $key Key to set value as + * @param mixed $value Value to set + * @param int $exp (optional) Expiration time. This can be a number of seconds + * to cache for (up to 30 days inclusive). Any timespans of 30 days + 1 second or + * longer must be the timestamp of the time at which the mapping should expire. It + * is safe to use timestamps in all cases, regardless of exipration + * eg: strtotime("+3 hour") + * + * @return bool True on success + */ + public function set( $key, $value, $exp = 0 ) { + return $this->_set( 'set', $key, $value, $exp ); + } + + // }}} + // {{{ cas() + + /** + * Sets a key to a given value in the memcache if the current value still corresponds + * to a known, given value. Returns true if set successfully. + * + * @param float $casToken Current known value + * @param string $key Key to set value as + * @param mixed $value Value to set + * @param int $exp (optional) Expiration time. This can be a number of seconds + * to cache for (up to 30 days inclusive). Any timespans of 30 days + 1 second or + * longer must be the timestamp of the time at which the mapping should expire. It + * is safe to use timestamps in all cases, regardless of exipration + * eg: strtotime("+3 hour") + * + * @return bool True on success + */ + public function cas( $casToken, $key, $value, $exp = 0 ) { + return $this->_set( 'cas', $key, $value, $exp, $casToken ); + } + + // }}} + // {{{ set_compress_threshold() + + /** + * Set the compression threshold + * + * @param int $thresh Threshold to compress if larger than + */ + public function set_compress_threshold( $thresh ) { + $this->_compress_threshold = $thresh; + } + + // }}} + // {{{ set_debug() + + /** + * Set the debug flag + * + * @see __construct() + * @param bool $dbg True for debugging, false otherwise + */ + public function set_debug( $dbg ) { + $this->_debug = $dbg; + } + + // }}} + // {{{ set_servers() + + /** + * Set the server list to distribute key gets and puts between + * + * @see __construct() + * @param array $list Array of servers to connect to + */ + public function set_servers( $list ) { + $this->_servers = $list; + $this->_active = count( $list ); + $this->_buckets = null; + $this->_bucketcount = 0; + + $this->_single_sock = null; + if ( $this->_active == 1 ) { + $this->_single_sock = $this->_servers[0]; + } + } + + /** + * Sets the timeout for new connections + * + * @param int $seconds Number of seconds + * @param int $microseconds Number of microseconds + */ + public function set_timeout( $seconds, $microseconds ) { + $this->_timeout_seconds = $seconds; + $this->_timeout_microseconds = $microseconds; + } + + // }}} + // }}} + // {{{ private methods + // {{{ _close_sock() + + /** + * Close the specified socket + * + * @param string $sock Socket to close + * + * @access private + */ + function _close_sock( $sock ) { + $host = array_search( $sock, $this->_cache_sock ); + fclose( $this->_cache_sock[$host] ); + unset( $this->_cache_sock[$host] ); + } + + // }}} + // {{{ _connect_sock() + + /** + * Connects $sock to $host, timing out after $timeout + * + * @param int $sock Socket to connect + * @param string $host Host:IP to connect to + * + * @return bool + * @access private + */ + function _connect_sock( &$sock, $host ) { + list( $ip, $port ) = preg_split( '/:(?=\d)/', $host ); + $sock = false; + $timeout = $this->_connect_timeout; + $errno = $errstr = null; + for ( $i = 0; !$sock && $i < $this->_connect_attempts; $i++ ) { + Wikimedia\suppressWarnings(); + if ( $this->_persistent == 1 ) { + $sock = pfsockopen( $ip, $port, $errno, $errstr, $timeout ); + } else { + $sock = fsockopen( $ip, $port, $errno, $errstr, $timeout ); + } + Wikimedia\restoreWarnings(); + } + if ( !$sock ) { + $this->_error_log( "Error connecting to $host: $errstr" ); + $this->_dead_host( $host ); + return false; + } + + // Initialise timeout + stream_set_timeout( $sock, $this->_timeout_seconds, $this->_timeout_microseconds ); + + // If the connection was persistent, flush the read buffer in case there + // was a previous incomplete request on this connection + if ( $this->_persistent ) { + $this->_flush_read_buffer( $sock ); + } + return true; + } + + // }}} + // {{{ _dead_sock() + + /** + * Marks a host as dead until 30-40 seconds in the future + * + * @param string $sock Socket to mark as dead + * + * @access private + */ + function _dead_sock( $sock ) { + $host = array_search( $sock, $this->_cache_sock ); + $this->_dead_host( $host ); + } + + /** + * @param string $host + */ + function _dead_host( $host ) { + $ip = explode( ':', $host )[0]; + $this->_host_dead[$ip] = time() + 30 + intval( rand( 0, 10 ) ); + $this->_host_dead[$host] = $this->_host_dead[$ip]; + unset( $this->_cache_sock[$host] ); + } + + // }}} + // {{{ get_sock() + + /** + * get_sock + * + * @param string $key Key to retrieve value for; + * + * @return Resource|bool Resource on success, false on failure + * @access private + */ + function get_sock( $key ) { + if ( !$this->_active ) { + return false; + } + + if ( $this->_single_sock !== null ) { + return $this->sock_to_host( $this->_single_sock ); + } + + $hv = is_array( $key ) ? intval( $key[0] ) : $this->_hashfunc( $key ); + if ( $this->_buckets === null ) { + $bu = array(); + foreach ( $this->_servers as $v ) { + if ( is_array( $v ) ) { + for ( $i = 0; $i < $v[1]; $i++ ) { + $bu[] = $v[0]; + } + } else { + $bu[] = $v; + } + } + $this->_buckets = $bu; + $this->_bucketcount = count( $bu ); + } + + $realkey = is_array( $key ) ? $key[1] : $key; + for ( $tries = 0; $tries < 20; $tries++ ) { + $host = $this->_buckets[$hv % $this->_bucketcount]; + $sock = $this->sock_to_host( $host ); + if ( is_resource( $sock ) ) { + return $sock; + } + $hv = $this->_hashfunc( $hv . $realkey ); + } + + return false; + } + + // }}} + // {{{ _hashfunc() + + /** + * Creates a hash integer based on the $key + * + * @param string $key Key to hash + * + * @return int Hash value + * @access private + */ + function _hashfunc( $key ) { + # Hash function must be in [0,0x7ffffff] + # We take the first 31 bits of the MD5 hash, which unlike the hash + # function used in a previous version of this client, works + return hexdec( substr( md5( $key ), 0, 8 ) ) & 0x7fffffff; + } + + // }}} + // {{{ _incrdecr() + + /** + * Perform increment/decriment on $key + * + * @param string $cmd Command to perform + * @param string|array $key Key to perform it on + * @param int $amt Amount to adjust + * + * @return int New value of $key + * @access private + */ + function _incrdecr( $cmd, $key, $amt = 1 ) { + if ( !$this->_active ) { + return null; + } + + $sock = $this->get_sock( $key ); + if ( !is_resource( $sock ) ) { + return null; + } + + $key = is_array( $key ) ? $key[1] : $key; + if ( isset( $this->stats[$cmd] ) ) { + $this->stats[$cmd]++; + } else { + $this->stats[$cmd] = 1; + } + if ( !$this->_fwrite( $sock, "$cmd $key $amt\r\n" ) ) { + return null; + } + + $line = $this->_fgets( $sock ); + $match = array(); + if ( !preg_match( '/^(\d+)/', $line, $match ) ) { + return null; + } + return $match[1]; + } + + // }}} + // {{{ _load_items() + + /** + * Load items into $ret from $sock + * + * @param Resource $sock Socket to read from + * @param array $ret returned values + * @param float $casToken [optional] + * @return bool True for success, false for failure + * + * @access private + */ + function _load_items( $sock, &$ret, &$casToken = null ) { + $results = array(); + + while ( 1 ) { + $decl = $this->_fgets( $sock ); + + if ( $decl === false ) { + /* + * If nothing can be read, something is wrong because we know exactly when + * to stop reading (right after "END") and we return right after that. + */ + return false; + } elseif ( preg_match( '/^VALUE (\S+) (\d+) (\d+) (\d+)$/', $decl, $match ) ) { + /* + * Read all data returned. This can be either one or multiple values. + * Save all that data (in an array) to be processed later: we'll first + * want to continue reading until "END" before doing anything else, + * to make sure that we don't leave our client in a state where it's + * output is not yet fully read. + */ + $results[] = array( + $match[1], // rkey + $match[2], // flags + $match[3], // len + $match[4], // casToken + $this->_fread( $sock, $match[3] + 2 ), // data + ); + } elseif ( $decl == "END" ) { + if ( count( $results ) == 0 ) { + return false; + } + + /** + * All data has been read, time to process the data and build + * meaningful return values. + */ + foreach ( $results as $vars ) { + list( $rkey, $flags, $len, $casToken, $data ) = $vars; + + if ( $data === false || substr( $data, -2 ) !== "\r\n" ) { + $this->_handle_error( $sock, + 'line ending missing from data block from $1' ); + return false; + } + $data = substr( $data, 0, -2 ); + $ret[$rkey] = $data; + + if ( $this->_have_zlib && $flags & self::COMPRESSED ) { + $ret[$rkey] = gzuncompress( $ret[$rkey] ); + } + + /* + * This unserialize is the exact reason that we only want to + * process data after having read until "END" (instead of doing + * this right away): "unserialize" can trigger outside code: + * in the event that $ret[$rkey] is a serialized object, + * unserializing it will trigger __wakeup() if present. If that + * function attempted to read from memcached (while we did not + * yet read "END"), these 2 calls would collide. + */ + if ( $flags & self::SERIALIZED ) { + $ret[$rkey] = $this->unserialize( $ret[$rkey] ); + } elseif ( $flags & self::INTVAL ) { + $ret[$rkey] = intval( $ret[$rkey] ); + } + } + + return true; + } else { + $this->_handle_error( $sock, 'Error parsing response from $1' ); + return false; + } + } + } + + // }}} + // {{{ _set() + + /** + * Performs the requested storage operation to the memcache server + * + * @param string $cmd Command to perform + * @param string $key Key to act on + * @param mixed $val What we need to store + * @param int $exp (optional) Expiration time. This can be a number of seconds + * to cache for (up to 30 days inclusive). Any timespans of 30 days + 1 second or + * longer must be the timestamp of the time at which the mapping should expire. It + * is safe to use timestamps in all cases, regardless of exipration + * eg: strtotime("+3 hour") + * @param float $casToken [optional] + * + * @return bool + * @access private + */ + function _set( $cmd, $key, $val, $exp, $casToken = null ) { + if ( !$this->_active ) { + return false; + } + + $sock = $this->get_sock( $key ); + if ( !is_resource( $sock ) ) { + return false; + } + + if ( isset( $this->stats[$cmd] ) ) { + $this->stats[$cmd]++; + } else { + $this->stats[$cmd] = 1; + } + + $flags = 0; + + if ( is_int( $val ) ) { + $flags |= self::INTVAL; + } elseif ( !is_scalar( $val ) ) { + $val = $this->serialize( $val ); + $flags |= self::SERIALIZED; + if ( $this->_debug ) { + $this->_debugprint( sprintf( "client: serializing data as it is not scalar" ) ); + } + } + + $len = strlen( $val ); + + if ( $this->_have_zlib && $this->_compress_enable + && $this->_compress_threshold && $len >= $this->_compress_threshold + ) { + $c_val = gzcompress( $val, 9 ); + $c_len = strlen( $c_val ); + + if ( $c_len < $len * ( 1 - self::COMPRESSION_SAVINGS ) ) { + if ( $this->_debug ) { + $this->_debugprint( sprintf( "client: compressing data; was %d bytes is now %d bytes", $len, $c_len ) ); + } + $val = $c_val; + $len = $c_len; + $flags |= self::COMPRESSED; + } + } + + $command = "$cmd $key $flags $exp $len"; + if ( $casToken ) { + $command .= " $casToken"; + } + + if ( !$this->_fwrite( $sock, "$command\r\n$val\r\n" ) ) { + return false; + } + + $line = $this->_fgets( $sock ); + + if ( $this->_debug ) { + $this->_debugprint( sprintf( "%s %s (%s)", $cmd, $key, $line ) ); + } + if ( $line === "STORED" ) { + return true; + } elseif ( $line === "NOT_STORED" && $cmd === "set" ) { + // "Not stored" is always used as the mcrouter response with AllAsyncRoute + return true; + } + + return false; + } + + // }}} + // {{{ sock_to_host() + + /** + * Returns the socket for the host + * + * @param string $host Host:IP to get socket for + * + * @return Resource|bool IO Stream or false + * @access private + */ + function sock_to_host( $host ) { + if ( isset( $this->_cache_sock[$host] ) ) { + return $this->_cache_sock[$host]; + } + + $sock = null; + $now = time(); + list( $ip, /* $port */) = explode( ':', $host ); + if ( isset( $this->_host_dead[$host] ) && $this->_host_dead[$host] > $now || + isset( $this->_host_dead[$ip] ) && $this->_host_dead[$ip] > $now + ) { + return null; + } + + if ( !$this->_connect_sock( $sock, $host ) ) { + return null; + } + + // Do not buffer writes + stream_set_write_buffer( $sock, 0 ); + + $this->_cache_sock[$host] = $sock; + + return $this->_cache_sock[$host]; + } + + /** + * @param string $text + */ + function _debugprint( $text ) { + $this->_logger->debug( $text ); + } + + /** + * @param string $text + */ + function _error_log( $text ) { + $this->_logger->error( "Memcached error: $text" ); + } + + /** + * Write to a stream. If there is an error, mark the socket dead. + * + * @param Resource $sock The socket + * @param string $buf The string to write + * @return bool True on success, false on failure + */ + function _fwrite( $sock, $buf ) { + $bytesWritten = 0; + $bufSize = strlen( $buf ); + while ( $bytesWritten < $bufSize ) { + $result = fwrite( $sock, $buf ); + $data = stream_get_meta_data( $sock ); + if ( $data['timed_out'] ) { + $this->_handle_error( $sock, 'timeout writing to $1' ); + return false; + } + // Contrary to the documentation, fwrite() returns zero on error in PHP 5.3. + if ( $result === false || $result === 0 ) { + $this->_handle_error( $sock, 'error writing to $1' ); + return false; + } + $bytesWritten += $result; + } + + return true; + } + + /** + * Handle an I/O error. Mark the socket dead and log an error. + * + * @param Resource $sock + * @param string $msg + */ + function _handle_error( $sock, $msg ) { + $peer = stream_socket_get_name( $sock, true /** remote **/ ); + if ( strval( $peer ) === '' ) { + $peer = array_search( $sock, $this->_cache_sock ); + if ( $peer === false ) { + $peer = '[unknown host]'; + } + } + $msg = str_replace( '$1', $peer, $msg ); + $this->_error_log( "$msg" ); + $this->_dead_sock( $sock ); + } + + /** + * Read the specified number of bytes from a stream. If there is an error, + * mark the socket dead. + * + * @param Resource $sock The socket + * @param int $len The number of bytes to read + * @return string|bool The string on success, false on failure. + */ + function _fread( $sock, $len ) { + $buf = ''; + while ( $len > 0 ) { + $result = fread( $sock, $len ); + $data = stream_get_meta_data( $sock ); + if ( $data['timed_out'] ) { + $this->_handle_error( $sock, 'timeout reading from $1' ); + return false; + } + if ( $result === false ) { + $this->_handle_error( $sock, 'error reading buffer from $1' ); + return false; + } + if ( $result === '' ) { + // This will happen if the remote end of the socket is shut down + $this->_handle_error( $sock, 'unexpected end of file reading from $1' ); + return false; + } + $len -= strlen( $result ); + $buf .= $result; + } + return $buf; + } + + /** + * Read a line from a stream. If there is an error, mark the socket dead. + * The \r\n line ending is stripped from the response. + * + * @param Resource $sock The socket + * @return string|bool The string on success, false on failure + */ + function _fgets( $sock ) { + $result = fgets( $sock ); + // fgets() may return a partial line if there is a select timeout after + // a successful recv(), so we have to check for a timeout even if we + // got a string response. + $data = stream_get_meta_data( $sock ); + if ( $data['timed_out'] ) { + $this->_handle_error( $sock, 'timeout reading line from $1' ); + return false; + } + if ( $result === false ) { + $this->_handle_error( $sock, 'error reading line from $1' ); + return false; + } + if ( substr( $result, -2 ) === "\r\n" ) { + $result = substr( $result, 0, -2 ); + } elseif ( substr( $result, -1 ) === "\n" ) { + $result = substr( $result, 0, -1 ); + } else { + $this->_handle_error( $sock, 'line ending missing in response from $1' ); + return false; + } + return $result; + } + + /** + * Flush the read buffer of a stream + * @param Resource $f + */ + function _flush_read_buffer( $f ) { + if ( !is_resource( $f ) ) { + return; + } + $r = array( $f ); + $w = null; + $e = null; + $n = stream_select( $r, $w, $e, 0, 0 ); + while ( $n == 1 && !feof( $f ) ) { + fread( $f, 1024 ); + $r = array( $f ); + $w = null; + $e = null; + $n = stream_select( $r, $w, $e, 0, 0 ); + } + } + + // }}} + // }}} + // }}} +} + +// }}} diff --git a/includes/libs/objectcache/wancache/WANObjectCache.php b/includes/libs/objectcache/wancache/WANObjectCache.php index 1852685d6c..70f35532d2 100644 --- a/includes/libs/objectcache/wancache/WANObjectCache.php +++ b/includes/libs/objectcache/wancache/WANObjectCache.php @@ -125,7 +125,7 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt /** @var callable|null Function that takes a WAN cache callback and runs it later */ protected $asyncHandler; - /** @bar bool Whether to use mcrouter key prefixing for routing */ + /** @var bool Whether to use mcrouter key prefixing for routing */ protected $mcrouterAware; /** @var string Physical region for mcrouter use */ protected $region; @@ -506,6 +506,7 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt } $purgeValues[] = $purge; } + return $purgeValues; } @@ -577,6 +578,9 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt * - version: Integer version number signifiying the format of the value. * Default: null * - walltime: How long the value took to generate in seconds. Default: 0.0 + * @codingStandardsIgnoreStart + * @phan-param array{lag?:int,since?:int,pending?:bool,lockTSE?:int,staleTTL?:int,creating?:bool,version?:?string,walltime?:int|float} $opts + * @codingStandardsIgnoreEnd * @note Options added in 1.28: staleTTL * @note Options added in 1.33: creating * @note Options added in 1.34: version, walltime @@ -1245,6 +1249,9 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt * most sense for values that are moderately to highly expensive to regenerate and easy * to query for dependency timestamps. The use of "pcTTL" reduces timestamp queries. * Default: null. + * @codingStandardsIgnoreStart + * @phan-param array{checkKeys?:string[],graceTTL?:int,lockTSE?:int,busyValue?:mixed,pcTTL?:int,pcGroup?:string,version?:int,minAsOf?:int,hotTTR?:int,lowTTL?:int,ageNew?:int,staleTTL?:int,touchedCallback?:callable} $opts + * @codingStandardsIgnoreEnd * @return mixed Value found or written to the key * @note Options added in 1.28: version, busyValue, hotTTR, ageNew, pcGroup, minAsOf * @note Options added in 1.31: staleTTL, graceTTL @@ -1304,6 +1311,7 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt * - Cached or regenerated value version number or null if not versioned * - Timestamp of the current cached value at the key or null if there is no value * @note Callable type hints are not used to avoid class-autoloading + * @suppress PhanTypeArraySuspicious */ private function fetchOrRegenerate( $key, $ttl, $callback, array $opts ) { $checkKeys = $opts['checkKeys'] ?? []; @@ -2207,14 +2215,14 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt // Wildcards select all matching routes, e.g. the WAN cluster on all DCs $ok = $this->cache->set( "/*/{$this->cluster}/{$key}", - $this->makePurgeValue( $this->getCurrentTime(), self::HOLDOFF_TTL_NONE ), + $this->makePurgeValue( $this->getCurrentTime(), $holdoff ), $ttl ); } else { - // This handles the mcrouter and the single-DC case + // Some other proxy handles broadcasting or there is only one datacenter $ok = $this->cache->set( $key, - $this->makePurgeValue( $this->getCurrentTime(), self::HOLDOFF_TTL_NONE ), + $this->makePurgeValue( $this->getCurrentTime(), $holdoff ), $ttl ); } @@ -2317,6 +2325,7 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt $chance = ( 1 - $curTTL / $lowTTL ); + // @phan-suppress-next-line PhanTypeMismatchArgumentInternal return mt_rand( 1, 1e9 ) <= 1e9 * $chance; } @@ -2359,6 +2368,7 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt // Ramp up $chance from 0 to its nominal value over RAMPUP_TTL seconds to avoid stampedes $chance *= ( $timeOld <= self::$RAMPUP_TTL ) ? $timeOld / self::$RAMPUP_TTL : 1; + // @phan-suppress-next-line PhanTypeMismatchArgumentInternal return mt_rand( 1, 1e9 ) <= 1e9 * $chance; } @@ -2420,6 +2430,7 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt * - curTTL: remaining time-to-live (negative if tombstoned) or null if there is no value * - version: value version number or null if the if there is no value * - tombAsOf: UNIX timestamp of the tombstone or null if there is no tombstone + * @phan-return array{0:mixed,1:array{asOf:?mixed,curTTL:?int|float,version:?mixed,tombAsOf:?mixed}} */ private function unwrap( $wrapped, $now ) { $value = false; @@ -2477,8 +2488,9 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt */ private function determineKeyClassForStats( $key ) { $parts = explode( ':', $key, 3 ); - - return $parts[1] ?? $parts[0]; // sanity + // Sanity fallback in case the key was not made by makeKey. + // Replace dots because they are special in StatsD (T232907) + return strtr( $parts[1] ?? $parts[0], '.', '_' ); } /** diff --git a/includes/libs/rdbms/ChronologyProtector.php b/includes/libs/rdbms/ChronologyProtector.php index 8615cfc630..e1398b815b 100644 --- a/includes/libs/rdbms/ChronologyProtector.php +++ b/includes/libs/rdbms/ChronologyProtector.php @@ -224,10 +224,9 @@ class ChronologyProtector implements LoggerAwareInterface { implode( ', ', array_keys( $this->shutdownPositions ) ) . "\n" ); - // CP-protected writes should overwhelmingly go to the master datacenter, so use a - // DC-local lock to merge the values. Use a DC-local get() and a synchronous all-DC - // set(). This makes it possible for the BagOStuff class to write in parallel to all - // DCs with one RTT. The use of WRITE_SYNC avoids needing READ_LATEST for the get(). + // CP-protected writes should overwhelmingly go to the master datacenter, so merge the + // positions with a DC-local lock, a DC-local get(), and an all-DC set() with WRITE_SYNC. + // If set() returns success, then any get() should be able to see the new positions. if ( $store->lock( $this->key, 3 ) ) { if ( $workCallback ) { // Let the store run the work before blocking on a replication sync barrier. diff --git a/includes/libs/rdbms/database/DBConnRef.php b/includes/libs/rdbms/database/DBConnRef.php index f27d042ca9..7870f69c2f 100644 --- a/includes/libs/rdbms/database/DBConnRef.php +++ b/includes/libs/rdbms/database/DBConnRef.php @@ -80,6 +80,11 @@ class DBConnRef implements IDatabase { return $this->__call( __FUNCTION__, func_get_args() ); } + /** + * @param bool|null $buffer + * @return bool + * @deprecated Since 1.34 Use query batching + */ public function bufferResults( $buffer = null ) { return $this->__call( __FUNCTION__, func_get_args() ); } @@ -275,7 +280,7 @@ class DBConnRef implements IDatabase { return $this->__call( __FUNCTION__, func_get_args() ); } - public function close() { + public function close( $fname = __METHOD__, $owner = null ) { throw new DBUnexpectedError( $this->conn, 'Cannot close shared connection.' ); } diff --git a/includes/libs/rdbms/database/Database.php b/includes/libs/rdbms/database/Database.php index aa8a899b85..51596da7c3 100644 --- a/includes/libs/rdbms/database/Database.php +++ b/includes/libs/rdbms/database/Database.php @@ -47,37 +47,6 @@ use Throwable; * @since 1.28 */ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAwareInterface { - /** @var string Server that this instance is currently connected to */ - protected $server; - /** @var string User that this instance is currently connected under the name of */ - protected $user; - /** @var string Password used to establish the current connection */ - protected $password; - /** @var array[] Map of (table => (dbname, schema, prefix) map) */ - protected $tableAliases = []; - /** @var string[] Map of (index alias => index) */ - protected $indexAliases = []; - /** @var bool Whether this PHP instance is for a CLI script */ - protected $cliMode; - /** @var string Agent name for query profiling */ - protected $agent; - /** @var int Bit field of class DBO_* constants */ - protected $flags; - /** @var array LoadBalancer tracking information */ - protected $lbInfo = []; - /** @var array|bool Variables use for schema element placeholders */ - protected $schemaVars = false; - /** @var array Parameters used by initConnection() to establish a connection */ - protected $connectionParams = []; - /** @var array SQL variables values to use for all new connections */ - protected $connectionVariables = []; - /** @var string Current SQL query delimiter */ - protected $delimiter = ';'; - /** @var string|bool|null Stashed value of html_errors INI setting */ - protected $htmlErrors; - /** @var int Row batch size to use for emulated INSERT SELECT queries */ - protected $nonNativeInsertSelectBatchSize = 10000; - /** @var BagOStuff APC cache */ protected $srvCache; /** @var LoggerInterface */ @@ -92,25 +61,62 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware protected $profiler; /** @var TransactionProfiler */ protected $trxProfiler; + /** @var DatabaseDomain */ protected $currentDomain; + /** @var object|resource|null Database connection */ protected $conn; /** @var IDatabase|null Lazy handle to the master DB this server replicates from */ private $lazyMasterHandle; + /** @var string Server that this instance is currently connected to */ + protected $server; + /** @var string User that this instance is currently connected under the name of */ + protected $user; + /** @var string Password used to establish the current connection */ + protected $password; + /** @var bool Whether this PHP instance is for a CLI script */ + protected $cliMode; + /** @var string Agent name for query profiling */ + protected $agent; + /** @var array Parameters used by initConnection() to establish a connection */ + protected $connectionParams; + /** @var string[]|int[]|float[] SQL variables values to use for all new connections */ + protected $connectionVariables; + /** @var int Row batch size to use for emulated INSERT SELECT queries */ + protected $nonNativeInsertSelectBatchSize; + + /** @var int Current bit field of class DBO_* constants */ + protected $flags; + /** @var array Current LoadBalancer tracking information */ + protected $lbInfo = []; + /** @var string Current SQL query delimiter */ + protected $delimiter = ';'; + /** @var array[] Current map of (table => (dbname, schema, prefix) map) */ + protected $tableAliases = []; + /** @var string[] Current map of (index alias => index) */ + protected $indexAliases = []; + /** @var array|null Current variables use for schema element placeholders */ + protected $schemaVars; + + /** @var string|bool|null Stashed value of html_errors INI setting */ + private $htmlErrors; + /** @var int[] Prior flags member variable values */ + private $priorFlags = []; + /** @var array Map of (name => 1) for locks obtained via lock() */ protected $sessionNamedLocks = []; /** @var array Map of (table name => 1) for TEMPORARY tables */ protected $sessionTempTables = []; /** @var string ID of the active transaction or the empty string otherwise */ - protected $trxShortId = ''; + private $trxShortId = ''; /** @var int Transaction status */ - protected $trxStatus = self::STATUS_TRX_NONE; + private $trxStatus = self::STATUS_TRX_NONE; /** @var Exception|null The last error that caused the status to become STATUS_TRX_ERROR */ - protected $trxStatusCause; + private $trxStatusCause; /** @var array|null Error details of the last statement-only rollback */ private $trxStatusIgnoredCause; /** @var float|null UNIX timestamp at the time of BEGIN for the last transaction */ @@ -154,9 +160,6 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware /** @var bool Whether to suppress triggering of transaction end callbacks */ private $trxEndCallbacksSuppressed = false; - /** @var int[] Prior flags member variable values */ - private $priorFlags = []; - /** @var integer|null Rows affected by the last query to query() or its CRUD wrappers */ protected $affectedRowCount; @@ -171,6 +174,9 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware /** @var float Query rount trip time estimate */ private $lastRoundTripEstimate = 0.0; + /** @var int|null Integer ID of the managing LBFactory instance or null if none */ + private $ownerId; + /** @var string Lock granularity is on the level of the entire database */ const ATTR_DB_LEVEL_LOCKING = 'db-level-locking'; /** @var string The SCHEMA keyword refers to a grouping of tables in a database */ @@ -233,15 +239,14 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware * @note exceptions for missing libraries/drivers should be thrown in initConnection() * @param array $params Parameters passed from Database::factory() */ - protected function __construct( array $params ) { + public function __construct( array $params ) { + $this->connectionParams = []; foreach ( [ 'host', 'user', 'password', 'dbname', 'schema', 'tablePrefix' ] as $name ) { $this->connectionParams[$name] = $params[$name]; } - + $this->connectionVariables = $params['variables'] ?? []; $this->cliMode = $params['cliMode']; - // Agent name is added to SQL queries in a comment, so make sure it can't break out - $this->agent = str_replace( '/', '-', $params['agent'] ); - + $this->agent = $params['agent']; $this->flags = $params['flags']; if ( $this->flags & self::DBO_DEFAULT ) { if ( $this->cliMode ) { @@ -250,13 +255,9 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware $this->flags |= self::DBO_TRX; } } - // Disregard deprecated DBO_IGNORE flag (T189999) - $this->flags &= ~self::DBO_IGNORE; - - $this->connectionVariables = $params['variables']; + $this->nonNativeInsertSelectBatchSize = $params['nonNativeInsertSelectBatchSize'] ?? 10000; $this->srvCache = $params['srvCache'] ?? new HashBagOStuff(); - $this->profiler = is_callable( $params['profiler'] ) ? $params['profiler'] : null; $this->trxProfiler = $params['trxProfiler']; $this->connLogger = $params['connLogger']; @@ -264,16 +265,14 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware $this->errorLogger = $params['errorLogger']; $this->deprecationLogger = $params['deprecationLogger']; - if ( isset( $params['nonNativeInsertSelectBatchSize'] ) ) { - $this->nonNativeInsertSelectBatchSize = $params['nonNativeInsertSelectBatchSize']; - } - // Set initial dummy domain until open() sets the final DB/prefix $this->currentDomain = new DatabaseDomain( $params['dbname'] != '' ? $params['dbname'] : null, $params['schema'] != '' ? $params['schema'] : null, $params['tablePrefix'] ); + + $this->ownerId = $params['ownerId'] ?? null; } /** @@ -361,7 +360,8 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware * - cliMode: Whether to consider the execution context that of a CLI script. * - agent: Optional name used to identify the end-user in query profiling/logging. * - srvCache: Optional BagOStuff instance to an APC-style cache. - * - nonNativeInsertSelectBatchSize: Optional batch size for non-native INSERT SELECT emulation. + * - nonNativeInsertSelectBatchSize: Optional batch size for non-native INSERT SELECT. + * - ownerId: Optional integer ID of a LoadBalancer instance that manages this instance. * @param int $connect One of the class constants (NEW_CONNECTED, NEW_UNCONNECTED) [optional] * @return Database|null If the database driver or extension cannot be found * @throws InvalidArgumentException If the database driver or extension cannot be found @@ -381,7 +381,8 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware 'flags' => 0, 'variables' => [], 'cliMode' => ( PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg' ), - 'agent' => basename( $_SERVER['SCRIPT_NAME'] ) . '@' . gethostname() + 'agent' => basename( $_SERVER['SCRIPT_NAME'] ) . '@' . gethostname(), + 'ownerId' => null ]; $normalizedParams = [ @@ -397,6 +398,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware 'cliMode' => (bool)$params['cliMode'], 'agent' => (string)$params['agent'], // Objects and callbacks + 'srvCache' => $params['srvCache'] ?? new HashBagOStuff(), 'profiler' => $params['profiler'] ?? null, 'trxProfiler' => $params['trxProfiler'] ?? new TransactionProfiler(), 'connLogger' => $params['connLogger'] ?? new NullLogger(), @@ -493,7 +495,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware } /** - * @return array Map of (Database::ATTR_* constant => value + * @return array Map of (Database::ATTR_* constant => value) * @since 1.31 */ protected static function getAttributes() { @@ -515,15 +517,15 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware return $this->getServerVersion(); } + /** + * Backwards-compatibility no-op method for disabling query buffering + * + * @param null|bool $buffer Whether to buffer queries (ignored) + * @return bool Whether buffering was already enabled (always true) + * @deprecated Since 1.34 Use query batching; this no longer does anything + */ public function bufferResults( $buffer = null ) { - $res = !$this->getFlag( self::DBO_NOBUFFER ); - if ( $buffer !== null ) { - $buffer - ? $this->clearFlag( self::DBO_NOBUFFER ) - : $this->setFlag( self::DBO_NOBUFFER ); - } - - return $res; + return true; } final public function trxLevel() { @@ -871,8 +873,8 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware ); } - final public function close() { - $exception = null; // error to throw after disconnecting + final public function close( $fname = __METHOD__, $owner = null ) { + $error = null; // error to throw after disconnecting $wasOpen = (bool)$this->conn; // This should mostly do nothing if the connection is already closed @@ -882,34 +884,22 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware if ( $this->trxAtomicLevels ) { // Cannot let incomplete atomic sections be committed $levels = $this->flatAtomicSectionList(); - $exception = new DBUnexpectedError( - $this, - __METHOD__ . ": atomic sections $levels are still open" - ); + $error = "$fname: atomic sections $levels are still open"; } elseif ( $this->trxAutomatic ) { // Only the connection manager can commit non-empty DBO_TRX transactions // (empty ones we can silently roll back) if ( $this->writesOrCallbacksPending() ) { - $exception = new DBUnexpectedError( - $this, - __METHOD__ . - ": mass commit/rollback of peer transaction required (DBO_TRX set)" - ); + $error = "$fname: " . + "expected mass rollback of all peer transactions (DBO_TRX set)"; } } else { // Manual transactions should have been committed or rolled // back, even if empty. - $exception = new DBUnexpectedError( - $this, - __METHOD__ . ": transaction is still open (from {$this->trxFname})" - ); + $error = "$fname: transaction is still open (from {$this->trxFname})"; } - if ( $this->trxEndCallbacksSuppressed ) { - $exception = $exception ?: new DBUnexpectedError( - $this, - __METHOD__ . ': callbacks are suppressed; cannot properly commit' - ); + if ( $this->trxEndCallbacksSuppressed && $error === null ) { + $error = "$fname: callbacks are suppressed; cannot properly commit"; } // Rollback the changes and run any callbacks as needed @@ -924,9 +914,16 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware $this->conn = null; - // Throw any unexpected errors after having disconnected - if ( $exception instanceof Exception ) { - throw $exception; + // Log or throw any unexpected errors after having disconnected + if ( $error !== null ) { + // T217819, T231443: if this is probably just LoadBalancer trying to recover from + // errors and shutdown, then log any problems and move on since the request has to + // end one way or another. Throwing errors is not very useful at some point. + if ( $this->ownerId !== null && $owner === $this->ownerId ) { + $this->queryLogger->error( $error ); + } else { + throw new DBUnexpectedError( $this, $error ); + } } // Note that various subclasses call close() at the start of open(), which itself is @@ -967,7 +964,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware * @throws DBReadOnlyError */ protected function assertIsWritableMaster() { - if ( $this->getLBInfo( 'replica' ) === true ) { + if ( $this->getLBInfo( 'replica' ) ) { throw new DBReadOnlyRoleError( $this, 'Write operations are not allowed on replica database connections' @@ -1150,7 +1147,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware // Send the query to the server and fetch any corresponding errors list( $ret, $err, $errno, $unignorable ) = $this->executeQuery( $sql, $fname, $flags ); if ( $ret === false ) { - $ignoreErrors = $this->hasFlags( $flags, self::QUERY_SILENCE_ERRORS ); + $ignoreErrors = $this->fieldHasBit( $flags, self::QUERY_SILENCE_ERRORS ); // Throw an error unless both the ignore flag was set and a rollback is not needed $this->reportQueryError( $err, $errno, $sql, $fname, $ignoreErrors && !$unignorable ); } @@ -1190,11 +1187,11 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware // Do not treat temporary table writes as "meaningful writes" since they are only // visible to one session and are not permanent. Profile them as reads. Integration // tests can override this behavior via $flags. - $pseudoPermanent = $this->hasFlags( $flags, self::QUERY_PSEUDO_PERMANENT ); + $pseudoPermanent = $this->fieldHasBit( $flags, self::QUERY_PSEUDO_PERMANENT ); list( $tmpType, $tmpNew, $tmpDel ) = $this->getTempWrites( $sql, $pseudoPermanent ); $isPermWrite = ( $tmpType !== self::$TEMP_NORMAL ); // DBConnRef uses QUERY_REPLICA_ROLE to enforce the replica role for raw SQL queries - if ( $isPermWrite && $this->hasFlags( $flags, self::QUERY_REPLICA_ROLE ) ) { + if ( $isPermWrite && $this->fieldHasBit( $flags, self::QUERY_REPLICA_ROLE ) ) { throw new DBReadOnlyRoleError( $this, "Cannot write; target role is DB_REPLICA" ); } } else { @@ -1205,8 +1202,9 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware } // Add trace comment to the begin of the sql string, right after the operator. - // Or, for one-word queries (like "BEGIN" or COMMIT") add it to the end (T44598) - $commentedSql = preg_replace( '/\s|$/', " /* $fname {$this->agent} */ ", $sql, 1 ); + // Or, for one-word queries (like "BEGIN" or COMMIT") add it to the end (T44598). + $encAgent = str_replace( '/', '-', $this->agent ); + $commentedSql = preg_replace( '/\s|$/', " /* $fname $encAgent */ ", $sql, 1 ); // Send the query to the server and fetch any corresponding errors. // This also doubles as a "ping" to see if the connection was dropped. @@ -1214,7 +1212,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware $this->executeQueryAttempt( $sql, $commentedSql, $isPermWrite, $fname, $flags ); // Check if the query failed due to a recoverable connection loss - $allowRetry = !$this->hasFlags( $flags, self::QUERY_NO_RETRY ); + $allowRetry = !$this->fieldHasBit( $flags, self::QUERY_NO_RETRY ); if ( $ret === false && $recoverableCL && $reconnected && $allowRetry ) { // Silently resend the query to the server since it is safe and possible list( $ret, $err, $errno, $recoverableSR, $recoverableCL ) = @@ -1285,7 +1283,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware } } - $prefix = !is_null( $this->getLBInfo( 'master' ) ) ? 'query-m: ' : 'query: '; + $prefix = $this->getLBInfo( 'master' ) ? 'query-m: ' : 'query: '; $generalizedSql = new GeneralizedSql( $sql, $this->trxShortId, $prefix ); $startTime = microtime( true ); @@ -1491,6 +1489,8 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware $this->trxAtomicCounter = 0; $this->trxIdleCallbacks = []; // T67263; transaction already lost $this->trxPreCommitCallbacks = []; // T67263; transaction already lost + // Clear additional subclass fields + $this->doHandleSessionLossPreconnect(); // @note: leave trxRecurringCallbacks in place if ( $this->trxDoneWrites ) { $this->trxProfiler->transactionWritingOut( @@ -1503,6 +1503,13 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware } } + /** + * Reset any additional subclass trx* and session* fields + */ + protected function doHandleSessionLossPreconnect() { + // no-op + } + /** * Clean things up after session (and thus transaction) loss after reconnect */ @@ -1676,12 +1683,13 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware * Returns an optional USE INDEX clause to go after the table, and a * string to go at the end of the query. * + * @see Database::select() + * * @param array $options Associative array of options to be turned into * an SQL query, valid keys are listed in the function. * @return array - * @see Database::select() */ - protected function makeSelectOptions( $options ) { + protected function makeSelectOptions( array $options ) { $preLimitTail = $postLimitTail = ''; $startOpts = ''; @@ -1822,7 +1830,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware $this->selectOptionsIncludeLocking( $options ) && $this->selectFieldsOrOptionsAggregate( $vars, $options ) ) { - // Some DB types (postgres/oracle) disallow FOR UPDATE with aggregate + // Some DB types (e.g. postgres) disallow FOR UPDATE with aggregate // functions. Discourage use of such queries to encourage compatibility. call_user_func( $this->deprecationLogger, @@ -3526,7 +3534,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware if ( in_array( $entry[2], $sectionIds, true ) ) { $callback = $entry[0]; $this->trxEndCallbacks[$key][0] = function () use ( $callback ) { - // @phan-suppress-next-line PhanInfiniteRecursion No recursion at all here, phan is confused + // @phan-suppress-next-line PhanInfiniteRecursion, PhanUndeclaredInvokeInCallable return $callback( self::TRIGGER_ROLLBACK, $this ); }; // This "on resolution" callback no longer belongs to a section. @@ -3651,6 +3659,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware try { ++$count; list( $phpCallback ) = $callback; + // @phan-suppress-next-line PhanUndeclaredInvokeInCallable $phpCallback( $this ); } catch ( Exception $ex ) { ( $this->errorLogger )( $ex ); @@ -3686,6 +3695,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware foreach ( $callbacks as $entry ) { if ( $sectionIds === null || in_array( $entry[2], $sectionIds, true ) ) { try { + // @phan-suppress-next-line PhanUndeclaredInvokeInCallable $entry[0]( $trigger, $this ); } catch ( Exception $ex ) { ( $this->errorLogger )( $ex ); @@ -3973,17 +3983,17 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware if ( $this->trxLevel() ) { if ( $this->trxAtomicLevels ) { $levels = $this->flatAtomicSectionList(); - $msg = "$fname: Got explicit BEGIN while atomic section(s) $levels are open"; + $msg = "$fname: got explicit BEGIN while atomic section(s) $levels are open"; throw new DBUnexpectedError( $this, $msg ); } elseif ( !$this->trxAutomatic ) { - $msg = "$fname: Explicit transaction already active (from {$this->trxFname})"; + $msg = "$fname: explicit transaction already active (from {$this->trxFname})"; throw new DBUnexpectedError( $this, $msg ); } else { - $msg = "$fname: Implicit transaction already active (from {$this->trxFname})"; + $msg = "$fname: implicit transaction already active (from {$this->trxFname})"; throw new DBUnexpectedError( $this, $msg ); } } elseif ( $this->getFlag( self::DBO_TRX ) && $mode !== self::TRANSACTION_INTERNAL ) { - $msg = "$fname: Implicit transaction expected (DBO_TRX set)"; + $msg = "$fname: implicit transaction expected (DBO_TRX set)"; throw new DBUnexpectedError( $this, $msg ); } @@ -4037,7 +4047,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware $levels = $this->flatAtomicSectionList(); throw new DBUnexpectedError( $this, - "$fname: Got COMMIT while atomic sections $levels are still open" + "$fname: got COMMIT while atomic sections $levels are still open" ); } @@ -4047,17 +4057,17 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware } elseif ( !$this->trxAutomatic ) { throw new DBUnexpectedError( $this, - "$fname: Flushing an explicit transaction, getting out of sync" + "$fname: flushing an explicit transaction, getting out of sync" ); } } elseif ( !$this->trxLevel() ) { $this->queryLogger->error( - "$fname: No transaction to commit, something got out of sync" ); + "$fname: no transaction to commit, something got out of sync" ); return; // nothing to do } elseif ( $this->trxAutomatic ) { throw new DBUnexpectedError( $this, - "$fname: Expected mass commit of all peer transactions (DBO_TRX set)" + "$fname: expected mass commit of all peer transactions (DBO_TRX set)" ); } @@ -4281,10 +4291,8 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware } // This will reconnect if possible or return false if not - $this->clearFlag( self::DBO_TRX, self::REMEMBER_PRIOR ); - $ok = ( $this->query( self::$PING_QUERY, __METHOD__, true ) !== false ); - $this->restoreFlags( self::RESTORE_PRIOR ); - + $flags = self::QUERY_IGNORE_DBO_TRX | self::QUERY_SILENCE_ERRORS; + $ok = ( $this->query( self::$PING_QUERY, __METHOD__, $flags ) !== false ); if ( $ok ) { $rtt = $this->lastRoundTripEstimate; } @@ -4474,7 +4482,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware } public function setSchemaVars( $vars ) { - $this->schemaVars = $vars; + $this->schemaVars = is_array( $vars ) ? $vars : null; } public function sourceStream( @@ -4626,11 +4634,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware * @return array */ protected function getSchemaVars() { - if ( $this->schemaVars ) { - return $this->schemaVars; - } else { - return $this->getDefaultSchemaVars(); - } + return $this->schemaVars ?? $this->getDefaultSchemaVars(); } /** @@ -4820,8 +4824,9 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware * @param int $field * @param int $flags * @return bool + * @since 1.34 */ - protected function hasFlags( $field, $flags ) { + final protected function fieldHasBit( $field, $flags ) { return ( ( $field & $flags ) === $flags ); } diff --git a/includes/libs/rdbms/database/DatabaseMysqlBase.php b/includes/libs/rdbms/database/DatabaseMysqlBase.php index a9223ac3b9..464e68c410 100644 --- a/includes/libs/rdbms/database/DatabaseMysqlBase.php +++ b/includes/libs/rdbms/database/DatabaseMysqlBase.php @@ -64,8 +64,6 @@ abstract class DatabaseMysqlBase extends Database { /** @var bool|null */ protected $defaultBigSelects = null; - /** @var string|null */ - private $serverVersion = null; /** @var bool|null */ private $insertSelectIsSafe = null; /** @var stdClass|null */ @@ -814,22 +812,16 @@ abstract class DatabaseMysqlBase extends Database { protected function getHeartbeatData( array $conds ) { // Query time and trip time are not counted $nowUnix = microtime( true ); - // Do not bother starting implicit transactions here - $this->clearFlag( self::DBO_TRX, self::REMEMBER_PRIOR ); - try { - $whereSQL = $this->makeList( $conds, self::LIST_AND ); - // Use ORDER BY for channel based queries since that field might not be UNIQUE. - // Note: this would use "TIMESTAMPDIFF(MICROSECOND,ts,UTC_TIMESTAMP(6))" but the - // percision field is not supported in MySQL <= 5.5. - $res = $this->query( - "SELECT ts FROM heartbeat.heartbeat WHERE $whereSQL ORDER BY ts DESC LIMIT 1", - __METHOD__, - self::QUERY_SILENCE_ERRORS | self::QUERY_IGNORE_DBO_TRX - ); - $row = $res ? $res->fetchObject() : false; - } finally { - $this->restoreFlags(); - } + $whereSQL = $this->makeList( $conds, self::LIST_AND ); + // Use ORDER BY for channel based queries since that field might not be UNIQUE. + // Note: this would use "TIMESTAMPDIFF(MICROSECOND,ts,UTC_TIMESTAMP(6))" but the + // percision field is not supported in MySQL <= 5.5. + $res = $this->query( + "SELECT ts FROM heartbeat.heartbeat WHERE $whereSQL ORDER BY ts DESC LIMIT 1", + __METHOD__, + self::QUERY_SILENCE_ERRORS | self::QUERY_IGNORE_DBO_TRX + ); + $row = $res ? $res->fetchObject() : false; return [ $row ? $row->ts : null, $nowUnix ]; } @@ -858,27 +850,44 @@ abstract class DatabaseMysqlBase extends Database { } if ( $this->getLBInfo( 'is static' ) === true ) { + $this->queryLogger->debug( + "Bypassed replication wait; database has a static dataset", + $this->getLogContext( [ 'method' => __METHOD__ ] ) + ); + return 0; // this is a copy of a read-only dataset with no master DB } elseif ( $this->lastKnownReplicaPos && $this->lastKnownReplicaPos->hasReached( $pos ) ) { + $this->queryLogger->debug( + "Bypassed replication wait; replication already known to have reached $pos", + $this->getLogContext( [ 'method' => __METHOD__ ] ) + ); + return 0; // already reached this point for sure } // Call doQuery() directly, to avoid opening a transaction if DBO_TRX is set if ( $pos->getGTIDs() ) { - // Ignore GTIDs from domains exclusive to the master DB (presumably inactive) - $rpos = $this->getReplicaPos(); - $gtidsWait = $rpos ? MySQLMasterPos::getCommonDomainGTIDs( $pos, $rpos ) : []; + // Get the GTIDs from this replica server too see the domains (channels) + $refPos = $this->getReplicaPos(); + if ( !$refPos ) { + $this->queryLogger->error( + "Could not get replication position", + $this->getLogContext( [ 'method' => __METHOD__ ] ) + ); + + return -1; // this is the master itself? + } + // GTIDs with domains (channels) that are active and are present on the replica + $gtidsWait = $pos::getRelevantActiveGTIDs( $pos, $refPos ); if ( !$gtidsWait ) { $this->queryLogger->error( - "No GTIDs with the same domain between master ($pos) and replica ($rpos)", - $this->getLogContext( [ - 'method' => __METHOD__, - ] ) + "No active GTIDs in $pos share a domain with those in $refPos", + $this->getLogContext( [ 'method' => __METHOD__, 'activeDomain' => $pos ] ) ); return -1; // $pos is from the wrong cluster? } - // Wait on the GTID set (MariaDB only) + // Wait on the GTID set $gtidArg = $this->addQuotes( implode( ',', $gtidsWait ) ); if ( strpos( $gtidArg, ':' ) !== false ) { // MySQL GTIDs, e.g "source_id:transaction_id" @@ -894,28 +903,28 @@ abstract class DatabaseMysqlBase extends Database { $sql = "SELECT MASTER_POS_WAIT($encFile, $encPos, $timeout)"; } - list( $res, $err ) = $this->executeQuery( $sql, __METHOD__, self::QUERY_IGNORE_DBO_TRX ); - $row = $res ? $this->fetchRow( $res ) : false; - if ( !$row ) { - throw new DBExpectedError( $this, "Replication wait failed: {$err}" ); - } + $res = $this->query( $sql, __METHOD__, self::QUERY_IGNORE_DBO_TRX ); + $row = $this->fetchRow( $res ); // Result can be NULL (error), -1 (timeout), or 0+ per the MySQL manual $status = ( $row[0] !== null ) ? intval( $row[0] ) : null; if ( $status === null ) { - if ( !$pos->getGTIDs() ) { - // T126436: jobs programmed to wait on master positions might be referencing - // binlogs with an old master hostname; this makes MASTER_POS_WAIT() return null. - // Try to detect this case and treat the replica DB as having reached the given - // position (any master switchover already requires that the new master be caught - // up before the switch). - $replicationPos = $this->getReplicaPos(); - if ( $replicationPos && !$replicationPos->channelsMatch( $pos ) ) { - $this->lastKnownReplicaPos = $replicationPos; - $status = 0; - } - } + $this->queryLogger->error( + "An error occurred while waiting for replication to reach $pos", + $this->getLogContext( [ 'method' => __METHOD__, 'sql' => $sql ] ) + ); + } elseif ( $status < 0 ) { + $this->queryLogger->error( + "Timed out waiting for replication to reach $pos", + $this->getLogContext( [ + 'method' => __METHOD__, 'sql' => $sql, 'timeout' => $timeout + ] ) + ); } elseif ( $status >= 0 ) { + $this->queryLogger->debug( + "Replication has reached $pos", + $this->getLogContext( [ 'method' => __METHOD__ ] ) + ); // Remember that this position was reached to save queries next time $this->lastKnownReplicaPos = $pos; } @@ -1062,11 +1071,12 @@ abstract class DatabaseMysqlBase extends Database { } public function serverIsReadOnly() { - $flags = self::QUERY_IGNORE_DBO_TRX; - $res = $this->query( "SHOW GLOBAL VARIABLES LIKE 'read_only'", __METHOD__, $flags ); + // Avoid SHOW to avoid internal temporary tables + $flags = self::QUERY_IGNORE_DBO_TRX | self::QUERY_SILENCE_ERRORS; + $res = $this->query( "SELECT @@GLOBAL.read_only AS Value", __METHOD__, $flags ); $row = $this->fetchObject( $res ); - return $row ? ( strtolower( $row->Value ) === 'on' ) : false; + return $row ? (bool)$row->Value : false; } /** @@ -1107,13 +1117,19 @@ abstract class DatabaseMysqlBase extends Database { * @return string */ public function getServerVersion() { - // Not using mysql_get_server_info() or similar for consistency: in the handshake, - // MariaDB 10 adds the prefix "5.5.5-", and only some newer client libraries strip - // it off (see RPL_VERSION_HACK in include/mysql_com.h). - if ( $this->serverVersion === null ) { - $this->serverVersion = $this->selectField( '', 'VERSION()', '', __METHOD__ ); - } - return $this->serverVersion; + $cache = $this->srvCache; + $fname = __METHOD__; + + return $cache->getWithSetCallback( + $cache->makeGlobalKey( 'mysql-server-version', $this->getServer() ), + $cache::TTL_HOUR, + function () use ( $fname ) { + // Not using mysql_get_server_info() or similar for consistency: in the handshake, + // MariaDB 10 adds the prefix "5.5.5-", and only some newer client libraries strip + // it off (see RPL_VERSION_HACK in include/mysql_com.h). + return $this->selectField( '', 'VERSION()', '', $fname ); + } + ); } /** diff --git a/includes/libs/rdbms/database/DatabaseMysqli.php b/includes/libs/rdbms/database/DatabaseMysqli.php index 4c189115d2..b9a1af625c 100644 --- a/includes/libs/rdbms/database/DatabaseMysqli.php +++ b/includes/libs/rdbms/database/DatabaseMysqli.php @@ -26,6 +26,7 @@ use mysqli; use mysqli_result; use IP; use stdClass; +use Wikimedia\AtEase\AtEase; /** * Database abstraction object for PHP extension mysqli. @@ -33,6 +34,7 @@ use stdClass; * @ingroup Database * @since 1.22 * @see Database + * @phan-file-suppress PhanParamSignatureMismatch resource vs mysqli_result */ class DatabaseMysqli extends DatabaseMysqlBase { /** @@ -40,15 +42,11 @@ class DatabaseMysqli extends DatabaseMysqlBase { * @return mysqli_result|bool */ protected function doQuery( $sql ) { - $conn = $this->getBindingHandle(); - - if ( $this->bufferResults() ) { - $ret = $conn->query( $sql ); - } else { - $ret = $conn->query( $sql, MYSQLI_USE_RESULT ); - } + AtEase::suppressWarnings(); + $res = $this->getBindingHandle()->query( $sql ); + AtEase::restoreWarnings(); - return $ret; + return $res; } /** @@ -64,9 +62,14 @@ class DatabaseMysqli extends DatabaseMysqlBase { ); } - // Other than mysql_connect, mysqli_real_connect expects an explicit port - // and socket parameters. So we need to parse the port and socket out of - // $realServer + // Other than mysql_connect, mysqli_real_connect expects an explicit port number + // e.g. "localhost:1234" or "127.0.0.1:1234" + // or Unix domain socket path + // e.g. "localhost:/socket_path" or "localhost:/foo/bar:bar:bar" + // colons are known to be used by Google AppEngine, + // see + // + // We need to parse the port or socket path out of $realServer $port = null; $socket = null; $hostAndPort = IP::splitHostAndPort( $realServer ); @@ -75,9 +78,9 @@ class DatabaseMysqli extends DatabaseMysqlBase { if ( $hostAndPort[1] ) { $port = $hostAndPort[1]; } - } elseif ( substr_count( $realServer, ':' ) == 1 ) { - // If we have a colon and something that's not a port number - // inside the hostname, assume it's the socket location + } elseif ( substr_count( $realServer, ':/' ) == 1 ) { + // If we have a colon slash instead of a colon and a port number + // after the ip or hostname, assume it's the Unix domain socket path list( $realServer, $socket ) = explode( ':', $realServer, 2 ); } diff --git a/includes/libs/rdbms/database/DatabasePostgres.php b/includes/libs/rdbms/database/DatabasePostgres.php index a7ebc86e50..c075a1b4c2 100644 --- a/includes/libs/rdbms/database/DatabasePostgres.php +++ b/includes/libs/rdbms/database/DatabasePostgres.php @@ -1301,7 +1301,7 @@ SQL; return "'" . pg_escape_string( $conn, (string)$s ) . "'"; } - public function makeSelectOptions( $options ) { + protected function makeSelectOptions( array $options ) { $preLimitTail = $postLimitTail = ''; $startOpts = $useIndex = $ignoreIndex = ''; diff --git a/includes/libs/rdbms/database/DatabaseSqlite.php b/includes/libs/rdbms/database/DatabaseSqlite.php index b1521dca2f..0e2dc8fa9a 100644 --- a/includes/libs/rdbms/database/DatabaseSqlite.php +++ b/includes/libs/rdbms/database/DatabaseSqlite.php @@ -55,10 +55,7 @@ class DatabaseSqlite extends Database { protected $lockMgr; /** @var array List of shared database already attached to this connection */ - private $alreadyAttached = []; - - /** @var bool Whether full text is enabled */ - private static $fulltextEnabled = null; + private $sessionAttachedDbs = []; /** @var string[] See https://www.sqlite.org/lang_transaction.html */ private static $VALID_TRX_MODES = [ '', 'DEFERRED', 'IMMEDIATE', 'EXCLUSIVE' ]; @@ -185,6 +182,7 @@ class DatabaseSqlite extends Database { if ( in_array( $sync, [ 'EXTRA', 'FULL', 'NORMAL', 'OFF' ], true ) ) { $this->query( "PRAGMA synchronous = $sync", __METHOD__, $flags ); } + $this->attachDatabasesFromTableAliases(); } catch ( Exception $e ) { throw $this->newExceptionAfterConnectError( $e->getMessage() ); } @@ -291,8 +289,9 @@ class DatabaseSqlite extends Database { } /** - * Attaches external database to our connection, see https://sqlite.org/lang_attach.html - * for details. + * Attaches external database to the connection handle + * + * @see https://sqlite.org/lang_attach.html * * @param string $name Database name to be used in queries like * SELECT foo FROM dbname.table @@ -604,15 +603,10 @@ class DatabaseSqlite extends Database { return in_array( 'UNIQUE', $options ); } - /** - * Filter the options used in SELECT statements - * - * @param array $options - * @return array - */ - function makeSelectOptions( $options ) { + protected function makeSelectOptions( array $options ) { + // Remove problematic options that the base implementation converts to SQL foreach ( $options as $k => $v ) { - if ( is_numeric( $k ) && ( $v == 'FOR UPDATE' || $v == 'LOCK IN SHARE MODE' ) ) { + if ( is_numeric( $k ) && ( $v === 'FOR UPDATE' || $v === 'LOCK IN SHARE MODE' ) ) { $options[$k] = ''; } } @@ -1126,12 +1120,23 @@ class DatabaseSqlite extends Database { public function setTableAliases( array $aliases ) { parent::setTableAliases( $aliases ); + if ( $this->isOpen() ) { + $this->attachDatabasesFromTableAliases(); + } + } + + /** + * Issue ATTATCH statements for all unattached foreign DBs in table aliases + */ + private function attachDatabasesFromTableAliases() { foreach ( $this->tableAliases as $params ) { - if ( isset( $this->alreadyAttached[$params['dbname']] ) ) { - continue; + if ( + $params['dbname'] !== $this->getDBname() && + !isset( $this->sessionAttachedDbs[$params['dbname']] ) + ) { + $this->attachDatabase( $params['dbname'] ); + $this->sessionAttachedDbs[$params['dbname']] = true; } - $this->attachDatabase( $params['dbname'] ); - $this->alreadyAttached[$params['dbname']] = true; } } @@ -1149,6 +1154,10 @@ class DatabaseSqlite extends Database { return true; } + protected function doHandleSessionLossPreconnect() { + $this->sessionAttachedDbs = []; + } + /** * @return PDO */ diff --git a/includes/libs/rdbms/database/IDatabase.php b/includes/libs/rdbms/database/IDatabase.php index 6b028679ee..befc80ce9c 100644 --- a/includes/libs/rdbms/database/IDatabase.php +++ b/includes/libs/rdbms/database/IDatabase.php @@ -21,7 +21,7 @@ namespace Wikimedia\Rdbms; use InvalidArgumentException; use Wikimedia\ScopedCallback; -use RuntimeException; +use Exception; use stdClass; /** @@ -89,9 +89,9 @@ interface IDatabase { /** @var int Enable debug logging of all SQL queries */ const DBO_DEBUG = 1; - /** @var int Disable query buffering (only one result set can be iterated at a time) */ + /** @var int Unused since 1.34 */ const DBO_NOBUFFER = 2; - /** @var int Ignore query errors (internal use only!) */ + /** @var int Unused since 1.31 */ const DBO_IGNORE = 4; /** @var int Automatically start a transaction before running a query if none is active */ const DBO_TRX = 8; @@ -99,9 +99,9 @@ interface IDatabase { const DBO_DEFAULT = 16; /** @var int Use DB persistent connections if possible */ const DBO_PERSISTENT = 32; - /** @var int DBA session mode; mostly for Oracle */ + /** @var int DBA session mode; was used by Oracle */ const DBO_SYSDBA = 64; - /** @var int Schema file mode; mostly for Oracle */ + /** @var int Schema file mode; was used by Oracle */ const DBO_DDLMODE = 128; /** @var int Enable SSL/TLS in connection protocol */ const DBO_SSL = 256; @@ -130,36 +130,14 @@ interface IDatabase { const UNION_DISTINCT = false; /** - * A string describing the current software version, and possibly - * other details in a user-friendly way. Will be listed on Special:Version, etc. + * Get a human-readable string describing the current software version + * * Use getServerVersion() to get machine-friendly information. * * @return string Version information from the database server */ public function getServerInfo(); - /** - * Turns buffering of SQL result sets on (true) or off (false). Default is "on". - * - * Unbuffered queries are very troublesome in MySQL: - * - * - If another query is executed while the first query is being read - * out, the first query is killed. This means you can't call normal - * Database functions while you are reading an unbuffered query result - * from a normal Database connection. - * - * - Unbuffered queries cause the MySQL server to use large amounts of - * memory and to hold broad locks which block other queries. - * - * If you want to limit client-side memory, it's almost always better to - * split up queries into batches using a LIMIT clause than to switch off - * buffering. - * - * @param null|bool $buffer - * @return null|bool The previous value of the flag - */ - public function bufferResults( $buffer = null ); - /** * Gets the current transaction level. * @@ -190,34 +168,33 @@ interface IDatabase { public function explicitTrxActive(); /** - * Assert that all explicit transactions or atomic sections have been closed. + * Assert that all explicit transactions or atomic sections have been closed + * * @throws DBTransactionError * @since 1.32 */ public function assertNoOpenTransactions(); /** - * Get/set the table prefix. - * @param string|null $prefix The table prefix to set, or omitted to leave it unchanged. + * Get/set the table prefix + * + * @param string|null $prefix The table prefix to set, or omitted to leave it unchanged * @return string The previous table prefix - * @throws DBUnexpectedError */ public function tablePrefix( $prefix = null ); /** - * Get/set the db schema. - * @param string|null $schema The database schema to set, or omitted to leave it unchanged. + * Get/set the db schema + * + * @param string|null $schema The database schema to set, or omitted to leave it unchanged * @return string The previous db schema */ public function dbSchema( $schema = null ); /** - * Get properties passed down from the server info array of the load - * balancer. - * - * @param string|null $name The entry of the info array to get, or null to get the - * whole array + * Get properties passed down from the server info array of the load balancer * + * @param string|null $name The entry of the info array to get, or null to get the whole array * @return array|mixed|null */ public function getLBInfo( $name = null ); @@ -247,14 +224,14 @@ interface IDatabase { public function implicitOrderby(); /** - * Return the last query that sent on account of IDatabase::query() + * Get the last query that sent on account of IDatabase::query() + * * @return string SQL text or empty string if there was no such query */ public function lastQuery(); /** - * Returns the last time the connection may have been used for write queries. - * Should return a timestamp if unsure. + * Get the last time the connection may have been used for a write query * * @return int|float UNIX timestamp or false * @since 1.24 @@ -286,7 +263,7 @@ interface IDatabase { /** * Get the time spend running write queries for this transaction * - * High times could be due to scanning, updates, locking, and such + * High values could be due to scanning, updates, locking, and such. * * @param string $type IDatabase::ESTIMATE_* constant [default: ESTIMATE_ALL] * @return float|bool Returns false if not transaction is active @@ -311,15 +288,14 @@ interface IDatabase { public function pendingWriteRowsAffected(); /** - * Is a connection to the database open? - * @return bool + * @return bool Whether a connection to the database open */ public function isOpen(); /** * Set a flag for this connection * - * @param int $flag IDatabase::DBO_DEBUG, IDatabase::DBO_NOBUFFER, or IDatabase::DBO_TRX + * @param int $flag One of (IDatabase::DBO_DEBUG, IDatabase::DBO_TRX) * @param string $remember IDatabase::REMEMBER_* constant [default: REMEMBER_NOTHING] */ public function setFlag( $flag, $remember = self::REMEMBER_NOTHING ); @@ -327,7 +303,7 @@ interface IDatabase { /** * Clear a flag for this connection * - * @param int $flag IDatabase::DBO_DEBUG, IDatabase::DBO_NOBUFFER, or IDatabase::DBO_TRX + * @param int $flag One of (IDatabase::DBO_DEBUG, IDatabase::DBO_TRX) * @param string $remember IDatabase::REMEMBER_* constant [default: REMEMBER_NOTHING] */ public function clearFlag( $flag, $remember = self::REMEMBER_NOTHING ); @@ -358,38 +334,38 @@ interface IDatabase { public function getDomainID(); /** - * Get the type of the DBMS, as it appears in $wgDBtype. + * Get the type of the DBMS (e.g. "mysql", "sqlite") * * @return string */ public function getType(); /** - * Fetch the next row from the given result object, in object form. + * Fetch the next row from the given result object, in object form + * * Fields can be retrieved with $row->fieldname, with fields acting like - * member variables. - * If no more rows are available, false is returned. + * member variables. If no more rows are available, false is returned. * * @param IResultWrapper|stdClass $res Object as returned from IDatabase::query(), etc. * @return stdClass|bool - * @throws DBUnexpectedError Thrown if the database returns an error */ public function fetchObject( $res ); /** - * Fetch the next row from the given result object, in associative array - * form. Fields are retrieved with $row['fieldname']. + * Fetch the next row from the given result object, in associative array form + * + * Fields are retrieved with $row['fieldname']. * If no more rows are available, false is returned. * * @param IResultWrapper $res Result object as returned from IDatabase::query(), etc. * @return array|bool - * @throws DBUnexpectedError Thrown if the database returns an error */ public function fetchRow( $res ); /** - * Get the number of rows in a query result. If the query did not return - * any rows (for example, if it was a write query), this returns zero. + * Get the number of rows in a query result + * + * Returns zero if the query did not return any rows or was a write query. * * @param mixed $res A SQL result * @return int @@ -452,26 +428,26 @@ interface IDatabase { public function lastError(); /** - * Get the number of rows affected by the last write query - * @see https://www.php.net/mysql_affected_rows + * Get the number of rows affected by the last write query. + * Similar to https://www.php.net/mysql_affected_rows but includes rows matched + * but not changed (ie. an UPDATE which sets all fields to the same value they already have). + * To get the old mysql_affected_rows behavior, include non-equality of the fields in WHERE. * * @return int */ public function affectedRows(); /** - * Returns a wikitext link to the DB's website, e.g., - * return "[https://www.mysql.com/ MySQL]"; - * Should at least contain plain text, if for some reason - * your database has no website. + * Returns a wikitext style link to the DB's website (e.g. "[https://www.mysql.com/ MySQL]") + * + * Should at least contain plain text, if for some reason your database has no website. * * @return string Wikitext of a link to the server software's web site */ public function getSoftwareLink(); /** - * A string describing the current software version, like from - * mysql_get_server_info(). + * A string describing the current software version, like from mysql_get_server_info() * * @return string Version information from the database server. */ @@ -484,14 +460,15 @@ interface IDatabase { * aside from read-only automatic transactions (assuming no callbacks are registered). * If a transaction is still open anyway, it will be rolled back. * + * @param string $fname Caller name + * @param int|null $owner ID of the calling instance (e.g. the LBFactory ID) + * @return bool Success * @throws DBError - * @return bool Operation success. true if already closed. */ - public function close(); + public function close( $fname = __METHOD__, $owner = null ); /** - * Run an SQL query and return the result. Normally throws a DBQueryError - * on failure. If errors are ignored, returns false instead. + * Run an SQL query and return the result * * If a connection loss is detected, then an attempt to reconnect will be made. * For queries that involve no larger transactions or locks, they will be re-issued @@ -513,24 +490,24 @@ interface IDatabase { * of errors is best handled by try/catch rather than using one of these flags. * @return bool|IResultWrapper True for a successful write query, IResultWrapper object * for a successful read query, or false on failure if QUERY_SILENCE_ERRORS is set. - * @throws DBError + * @throws DBQueryError If the query is issued, fails, and QUERY_SILENCE_ERRORS is not set. + * @throws DBExpectedError If the query is not, and cannot, be issued yet (non-DBQueryError) + * @throws DBError If the query is inherently not allowed (non-DBExpectedError) */ public function query( $sql, $fname = __METHOD__, $flags = 0 ); /** - * Free a result object returned by query() or select(). It's usually not - * necessary to call this, just use unset() or let the variable holding - * the result object go out of scope. + * Free a result object returned by query() or select() + * + * It's usually not necessary to call this, just use unset() or let the variable + * holding the result object go out of scope. * * @param mixed $res A SQL result */ public function freeResult( $res ); /** - * A SELECT wrapper which returns a single field from a single result row. - * - * Usually throws a DBQueryError on failure. If errors are explicitly - * ignored, returns false on failure. + * A SELECT wrapper which returns a single field from a single result row * * If no result rows are returned from the query, false is returned. * @@ -541,19 +518,15 @@ interface IDatabase { * @param string $fname The function name of the caller. * @param string|array $options The query options. See IDatabase::select() for details. * @param string|array $join_conds The query join conditions. See IDatabase::select() for details. - * * @return mixed The value from the field - * @throws DBError + * @throws DBError If an error occurs, see IDatabase::query() */ public function selectField( $table, $var, $cond = '', $fname = __METHOD__, $options = [], $join_conds = [] ); /** - * A SELECT wrapper which returns a list of single field values from result rows. - * - * Usually throws a DBQueryError on failure. If errors are explicitly - * ignored, returns false on failure. + * A SELECT wrapper which returns a list of single field values from result rows * * If no result rows are returned from the query, false is returned. * @@ -566,7 +539,7 @@ interface IDatabase { * @param string|array $join_conds The query join conditions. See IDatabase::select() for details. * * @return array The values from the field in the order they were returned from the DB - * @throws DBError + * @throws DBError If an error occurs, see IDatabase::query() * @since 1.25 */ public function selectFieldValues( @@ -574,8 +547,7 @@ interface IDatabase { ); /** - * Execute a SELECT query constructed using the various parameters provided. - * See below for full details of the parameters. + * Execute a SELECT query constructed using the various parameters provided * * @param string|array $table Table name(s) * @@ -732,18 +704,23 @@ interface IDatabase { * [ 'page' => [ 'LEFT JOIN', 'page_latest=rev_id' ] ] * * @return IResultWrapper Resulting rows - * @throws DBError + * @throws DBError If an error occurs, see IDatabase::query() */ public function select( - $table, $vars, $conds = '', $fname = __METHOD__, - $options = [], $join_conds = [] + $table, + $vars, + $conds = '', + $fname = __METHOD__, + $options = [], + $join_conds = [] ); /** - * The equivalent of IDatabase::select() except that the constructed SQL - * is returned, instead of being immediately executed. This can be useful for - * doing UNION queries, where the SQL text of each query is needed. In general, - * however, callers outside of Database classes should just use select(). + * Take the same arguments as IDatabase::select() and return the SQL it would use + * + * This can be useful for making UNION queries, where the SQL text of each query + * is needed. In general, however, callers outside of Database classes should just + * use select(). * * @see IDatabase::select() * @@ -756,14 +733,20 @@ interface IDatabase { * @return string SQL query string */ public function selectSQLText( - $table, $vars, $conds = '', $fname = __METHOD__, - $options = [], $join_conds = [] + $table, + $vars, + $conds = '', + $fname = __METHOD__, + $options = [], + $join_conds = [] ); /** - * Single row SELECT wrapper. Equivalent to IDatabase::select(), except - * that a single row object is returned. If the query returns no rows, - * false is returned. + * Wrapper to IDatabase::select() that only fetches one row (via LIMIT) + * + * If the query returns no rows, false is returned. + * + * This method is convenient for fetching a row based on a unique key condition. * * @param string|array $table Table name * @param string|array $vars Field names @@ -771,12 +754,16 @@ interface IDatabase { * @param string $fname Caller function name * @param string|array $options Query options * @param array|string $join_conds Join conditions - * * @return stdClass|bool - * @throws DBError + * @throws DBError If an error occurs, see IDatabase::query() */ - public function selectRow( $table, $vars, $conds, $fname = __METHOD__, - $options = [], $join_conds = [] + public function selectRow( + $table, + $vars, + $conds, + $fname = __METHOD__, + $options = [], + $join_conds = [] ); /** @@ -799,7 +786,7 @@ interface IDatabase { * @param array $options Options for select * @param array|string $join_conds Join conditions * @return int Row count - * @throws DBError + * @throws DBError If an error occurs, see IDatabase::query() */ public function estimateRowCount( $table, $var = '*', $conds = '', $fname = __METHOD__, $options = [], $join_conds = [] @@ -821,7 +808,7 @@ interface IDatabase { * @param array $options Options for select * @param array $join_conds Join conditions (since 1.27) * @return int Row count - * @throws DBError + * @throws DBError If an error occurs, see IDatabase::query() */ public function selectRowCount( $tables, $var = '*', $conds = '', $fname = __METHOD__, $options = [], $join_conds = [] @@ -836,6 +823,7 @@ interface IDatabase { * @param array $options Options for select ("FOR UPDATE" is added automatically) * @param array $join_conds Join conditions * @return int Number of matching rows found (and locked) + * @throws DBError If an error occurs, see IDatabase::query() * @since 1.32 */ public function lockForUpdate( @@ -849,20 +837,18 @@ interface IDatabase { * @param string $field Filed to check on that table * @param string $fname Calling function name (optional) * @return bool Whether $table has filed $field - * @throws DBError + * @throws DBError If an error occurs, see IDatabase::query() */ public function fieldExists( $table, $field, $fname = __METHOD__ ); /** * Determines whether an index exists - * Usually throws a DBQueryError on failure - * If errors are explicitly ignored, returns NULL on failure * * @param string $table * @param string $index * @param string $fname * @return bool|null - * @throws DBError + * @throws DBError If an error occurs, see IDatabase::query() */ public function indexExists( $table, $index, $fname = __METHOD__ ); @@ -872,12 +858,12 @@ interface IDatabase { * @param string $table * @param string $fname * @return bool - * @throws DBError + * @throws DBError If an error occurs, see IDatabase::query() */ public function tableExists( $table, $fname = __METHOD__ ); /** - * INSERT wrapper, inserts an array into a table. + * INSERT wrapper, inserts an array into a table * * $a may be either: * @@ -889,9 +875,6 @@ interface IDatabase { * This causes a multi-row INSERT on DBMSs that support it. The keys in * each subarray must be identical to each other, and in the same order. * - * Usually throws a DBQueryError on failure. If errors are explicitly ignored, - * returns success. - * * $options is an array of options, with boolean options encoded as values * with numeric keys, in the same style as $options in * IDatabase::select(). Supported options are: @@ -907,7 +890,7 @@ interface IDatabase { * @param string $fname Calling function name (use __METHOD__) for logs/profiling * @param array $options Array of options * @return bool Return true if no exception was thrown (deprecated since 1.33) - * @throws DBError + * @throws DBError If an error occurs, see IDatabase::query() */ public function insert( $table, $a, $fname = __METHOD__, $options = [] ); @@ -921,7 +904,7 @@ interface IDatabase { * that field to. The data will be quoted by IDatabase::addQuotes(). * Values with integer keys form unquoted SET statements, which can be used for * things like "field = field + 1" or similar computed values. - * @param array $conds An array of conditions (WHERE). See + * @param array|string $conds An array of conditions (WHERE). See * IDatabase::select() for the details of the format of condition * arrays. Use '*' to update all rows. * @param string $fname The function name of the caller (from __METHOD__), @@ -929,7 +912,7 @@ interface IDatabase { * @param array $options An array of UPDATE options, can be: * - IGNORE: Ignore unique key conflicts * @return bool Return true if no exception was thrown (deprecated since 1.33) - * @throws DBError + * @throws DBError If an error occurs, see IDatabase::query() */ public function update( $table, $values, $conds, $fname = __METHOD__, $options = [] ); @@ -955,7 +938,7 @@ interface IDatabase { * - IDatabase::LIST_OR: ORed WHERE clause (without the WHERE) * - IDatabase::LIST_SET: Comma separated with field names, like a SET clause * - IDatabase::LIST_NAMES: Comma separated field names - * @throws DBError + * @throws DBError If an error occurs, see IDatabase::query() * @return string */ public function makeList( $a, $mode = self::LIST_COMMA ); @@ -978,7 +961,7 @@ interface IDatabase { * @param array $valuedata * @param string $valuename * - * @return string + * @return array|string * @deprecated Since 1.33 */ public function aggregateValue( $valuedata, $valuename = 'value' ); @@ -1005,8 +988,7 @@ interface IDatabase { /** * Build a concatenation list to feed into a SQL query - * @param array $stringList List of raw SQL expressions; caller is - * responsible for any quoting + * @param string[] $stringList Raw SQL expression list; caller is responsible for escaping * @return string */ public function buildConcat( $stringList ); @@ -1032,7 +1014,7 @@ interface IDatabase { ); /** - * Build a SUBSTRING function. + * Build a SUBSTRING function * * Behavior for non-ASCII values is undefined. * @@ -1074,13 +1056,18 @@ interface IDatabase { * @since 1.31 */ public function buildSelectSubquery( - $table, $vars, $conds = '', $fname = __METHOD__, - $options = [], $join_conds = [] + $table, + $vars, + $conds = '', + $fname = __METHOD__, + $options = [], + $join_conds = [] ); /** - * Construct a LIMIT query with optional offset. This is used for query - * pages. The SQL should be adjusted so that only the first $limit rows + * Construct a LIMIT query with optional offset + * + * The SQL should be adjusted so that only the first $limit rows * are returned. If $offset is provided as well, then the first $offset * rows should be discarded, and the next $limit rows should be returned. * If the result of the query is not ordered, then the rows to be returned @@ -1091,7 +1078,6 @@ interface IDatabase { * @param string $sql SQL query we will append the limit too * @param int $limit The SQL limit * @param int|bool $offset The SQL offset (default false) - * @throws DBUnexpectedError * @return string * @since 1.34 */ @@ -1150,7 +1136,7 @@ interface IDatabase { public function getServer(); /** - * Adds quotes and backslashes. + * Escape and quote a raw value string for use in a SQL query * * @param string|int|null|bool|Blob $s * @return string|int @@ -1158,7 +1144,7 @@ interface IDatabase { public function addQuotes( $s ); /** - * Quotes an identifier, in order to make user controlled input safe + * Escape a SQL identifier (e.g. table, column, database) for use in a SQL query * * Depending on the database this will either be `backticks` or "double quotes" * @@ -1169,11 +1155,12 @@ interface IDatabase { public function addIdentifierQuotes( $s ); /** - * LIKE statement wrapper, receives a variable-length argument list with - * parts of pattern to match containing either string literals that will be - * escaped or tokens returned by anyChar() or anyString(). Alternatively, - * the function could be provided with an array of aforementioned - * parameters. + * LIKE statement wrapper + * + * This takes a variable-length argument list with parts of pattern to match + * containing either string literals that will be escaped or tokens returned by + * anyChar() or anyString(). Alternatively, the function could be provided with + * an array of aforementioned parameters. * * Example: $dbr->buildLike( 'My_page_title/', $dbr->anyString() ) returns * a LIKE clause that searches for subpages of 'My page title'. @@ -1204,12 +1191,12 @@ interface IDatabase { public function anyString(); /** - * Deprecated method, calls should be removed. + * Deprecated method, calls should be removed * - * This was formerly used for PostgreSQL and Oracle to handle + * This was formerly used for PostgreSQL to handle * self::insertId() auto-incrementing fields. It is no longer necessary * since DatabasePostgres::insertId() has been reimplemented using - * `lastval()` and Oracle has been reimplemented using triggers. + * `lastval()` * * Implementations should return null if inserting `NULL` into an * auto-incrementing field works, otherwise it should return an instance of @@ -1222,7 +1209,7 @@ interface IDatabase { public function nextSequenceValue( $seqName ); /** - * REPLACE query wrapper. + * REPLACE query wrapper * * REPLACE is a very handy MySQL extension, which functions like an INSERT * except that when there is a duplicate key error, the old row is deleted @@ -1244,7 +1231,7 @@ interface IDatabase { * @param array $rows Can be either a single row to insert, or multiple rows, * in the same format as for IDatabase::insert() * @param string $fname Calling function name (use __METHOD__) for logs/profiling - * @throws DBError + * @throws DBError If an error occurs, see IDatabase::query() */ public function replace( $table, $uniqueIndexes, $rows, $fname = __METHOD__ ); @@ -1267,11 +1254,6 @@ interface IDatabase { * to collide. However if you do this, you run the risk of encountering * errors which wouldn't have occurred in MySQL. * - * Usually throws a DBQueryError on failure. If errors are explicitly ignored, - * returns success. - * - * @since 1.22 - * * @param string $table Table name. This will be passed through Database::tableName(). * @param array $rows A single row or list of rows to insert * @param array[]|string[]|string $uniqueIndexes All unique indexes. One of the following: @@ -1284,8 +1266,9 @@ interface IDatabase { * Values with integer keys form unquoted SET statements, which can be used for * things like "field = field + 1" or similar computed values. * @param string $fname Calling function name (use __METHOD__) for logs/profiling - * @throws DBError * @return bool Return true if no exception was thrown (deprecated since 1.33) + * @throws DBError If an error occurs, see IDatabase::query() + * @since 1.22 */ public function upsert( $table, array $rows, $uniqueIndexes, array $set, $fname = __METHOD__ @@ -1306,31 +1289,34 @@ interface IDatabase { * @param string $joinTable The other table. * @param string $delVar The variable to join on, in the first table. * @param string $joinVar The variable to join on, in the second table. - * @param array $conds Condition array of field names mapped to variables, + * @param array|string $conds Condition array of field names mapped to variables, * ANDed together in the WHERE clause * @param string $fname Calling function name (use __METHOD__) for logs/profiling - * @throws DBError - */ - public function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, + * @throws DBError If an error occurs, see IDatabase::query() + */ + public function deleteJoin( + $delTable, + $joinTable, + $delVar, + $joinVar, + $conds, $fname = __METHOD__ ); /** - * DELETE query wrapper. + * DELETE query wrapper * * @param string $table Table name * @param string|array $conds Array of conditions. See $conds in IDatabase::select() * for the format. Use $conds == "*" to delete all rows * @param string $fname Name of the calling function - * @throws DBUnexpectedError * @return bool Return true if no exception was thrown (deprecated since 1.33) - * @throws DBError + * @throws DBError If an error occurs, see IDatabase::query() */ public function delete( $table, $conds, $fname = __METHOD__ ); /** - * INSERT SELECT wrapper. Takes data from a SELECT query and inserts it - * into another table. + * INSERT SELECT wrapper * * @warning If the insert will use an auto-increment or sequence to * determine the value of a column, this may break replication on @@ -1340,18 +1326,14 @@ interface IDatabase { * @param string $destTable The table name to insert into * @param string|array $srcTable May be either a table name, or an array of table names * to include in a join. - * * @param array $varMap Must be an associative array of the form * [ 'dest1' => 'source1', ... ]. Source items may be literals * rather than field names, but strings should be quoted with * IDatabase::addQuotes() - * * @param array $conds Condition array. See $conds in IDatabase::select() for * the details of the format of condition arrays. May be "*" to copy the * whole table. - * * @param string $fname The function name of the caller, from __METHOD__ - * * @param array $insertOptions Options for the INSERT part of the query, see * IDatabase::insert() for details. Also, one additional option is * available: pass 'NO_AUTO_COLUMNS' to hint that the query does not use @@ -1360,24 +1342,30 @@ interface IDatabase { * IDatabase::select() for details. * @param array $selectJoinConds Join conditions for the SELECT part of the query, see * IDatabase::select() for details. - * * @return bool Return true if no exception was thrown (deprecated since 1.33) - * @throws DBError + * @throws DBError If an error occurs, see IDatabase::query() */ - public function insertSelect( $destTable, $srcTable, $varMap, $conds, + public function insertSelect( + $destTable, + $srcTable, + $varMap, + $conds, $fname = __METHOD__, - $insertOptions = [], $selectOptions = [], $selectJoinConds = [] + $insertOptions = [], + $selectOptions = [], + $selectJoinConds = [] ); /** - * Returns true if current database backend supports ORDER BY or LIMIT for separate subqueries - * within the UNION construct. + * Determine if the RDBMS supports ORDER BY and LIMIT for separate subqueries within UNION + * * @return bool */ public function unionSupportsOrderAndLimit(); /** * Construct a UNION query + * * This is used for providing overload point for other DB abstractions * not compatible with the MySQL syntax. * @param array $sqls SQL statements to combine @@ -1395,7 +1383,6 @@ interface IDatabase { * conditions and unions them all together. * * @see IDatabase::select() - * @since 1.30 * @param string|array $table Table name * @param string|array $vars Field names * @param array $permute_conds Conditions for the Cartesian product. Keys @@ -1411,15 +1398,22 @@ interface IDatabase { * instead of ORDER BY. * @param string|array $join_conds Join conditions * @return string SQL query string. + * @since 1.30 */ public function unionConditionPermutations( - $table, $vars, array $permute_conds, $extra_conds = '', $fname = __METHOD__, - $options = [], $join_conds = [] + $table, + $vars, + array $permute_conds, + $extra_conds = '', + $fname = __METHOD__, + $options = [], + $join_conds = [] ); /** - * Returns an SQL expression for a simple conditional. This doesn't need - * to be overridden unless CASE isn't supported in your DBMS. + * Returns an SQL expression for a simple conditional + * + * This doesn't need to be overridden unless CASE isn't supported in the RDBMS. * * @param string|array $cond SQL expression which will result in a boolean value * @param string $trueVal SQL expression to return if true @@ -1429,13 +1423,11 @@ interface IDatabase { public function conditional( $cond, $trueVal, $falseVal ); /** - * Returns a command for str_replace function in SQL query. - * Uses REPLACE() in MySQL + * Returns a SQL expression for simple string replacement (e.g. REPLACE() in mysql) * * @param string $orig Column to modify * @param string $old Column to seek * @param string $new Column to replace with - * * @return string */ public function strreplace( $orig, $old, $new ); @@ -1477,7 +1469,7 @@ interface IDatabase { public function wasConnectionLoss(); /** - * Determines if the last failure was due to the database being read-only. + * Determines if the last failure was due to the database being read-only * * @return bool */ @@ -1504,7 +1496,7 @@ interface IDatabase { * @return int|null Zero if the replica DB was past that position already, * greater than zero if we waited for some period of time, less than * zero if it timed out, and null on error - * @throws DBError + * @throws DBError If an error occurs, see IDatabase::query() */ public function masterPosWait( DBMasterPos $pos, $timeout ); @@ -1512,7 +1504,7 @@ interface IDatabase { * Get the replication position of this replica DB * * @return DBMasterPos|bool False if this is not a replica DB - * @throws DBError + * @throws DBError If an error occurs, see IDatabase::query() */ public function getReplicaPos(); @@ -1520,7 +1512,7 @@ interface IDatabase { * Get the position of this master * * @return DBMasterPos|bool False if this is not a master - * @throws DBError + * @throws DBError If an error occurs, see IDatabase::query() */ public function getMasterPos(); @@ -1531,7 +1523,8 @@ interface IDatabase { public function serverIsReadOnly(); /** - * Run a callback as soon as the current transaction commits or rolls back. + * Run a callback as soon as the current transaction commits or rolls back + * * An error is thrown if no transaction is pending. Queries in the function will run in * AUTOCOMMIT mode unless there are begin() calls. Callbacks must commit any transactions * that they begin. @@ -1549,12 +1542,15 @@ interface IDatabase { * * @param callable $callback * @param string $fname Caller name + * @throws DBError If an error occurs, see IDatabase::query() + * @throws Exception If the callback runs immediately and an error occurs in it * @since 1.28 */ public function onTransactionResolution( callable $callback, $fname = __METHOD__ ); /** - * Run a callback as soon as there is no transaction pending. + * Run a callback as soon as there is no transaction pending + * * If there is a transaction and it is rolled back, then the callback is cancelled. * * When transaction round mode (DBO_TRX) is set, the callback will run at the end @@ -1583,6 +1579,8 @@ interface IDatabase { * * @param callable $callback * @param string $fname Caller name + * @throws DBError If an error occurs, see IDatabase::query() + * @throws Exception If the callback runs immediately and an error occurs in it * @since 1.32 */ public function onTransactionCommitOrIdle( callable $callback, $fname = __METHOD__ ); @@ -1598,7 +1596,8 @@ interface IDatabase { public function onTransactionIdle( callable $callback, $fname = __METHOD__ ); /** - * Run a callback before the current transaction commits or now if there is none. + * Run a callback before the current transaction commits or now if there is none + * * If there is a transaction and it is rolled back, then the callback is cancelled. * * When transaction round mode (DBO_TRX) is set, the callback will run at the end @@ -1618,12 +1617,14 @@ interface IDatabase { * * @param callable $callback * @param string $fname Caller name + * @throws DBError If an error occurs, see IDatabase::query() + * @throws Exception If the callback runs immediately and an error occurs in it * @since 1.22 */ public function onTransactionPreCommitOrIdle( callable $callback, $fname = __METHOD__ ); /** - * Run a callback when the atomic section is cancelled. + * Run a callback when the atomic section is cancelled * * The callback is run just after the current atomic section, any outer * atomic section, or the whole transaction is rolled back. @@ -1738,7 +1739,7 @@ interface IDatabase { * @param string $cancelable Pass self::ATOMIC_CANCELABLE to use a * savepoint and enable self::cancelAtomic() for this section. * @return AtomicSectionIdentifier section ID token - * @throws DBError + * @throws DBError If an error occurs, see IDatabase::query() */ public function startAtomic( $fname = __METHOD__, $cancelable = self::ATOMIC_NOT_CANCELABLE ); @@ -1751,7 +1752,7 @@ interface IDatabase { * @since 1.23 * @see IDatabase::startAtomic * @param string $fname - * @throws DBError + * @throws DBError If an error occurs, see IDatabase::query() */ public function endAtomic( $fname = __METHOD__ ); @@ -1778,7 +1779,7 @@ interface IDatabase { * @param string $fname * @param AtomicSectionIdentifier|null $sectionId Section ID from startAtomic(); * passing this enables cancellation of unclosed nested sections [optional] - * @throws DBError + * @throws DBError If an error occurs, see IDatabase::query() */ public function cancelAtomic( $fname = __METHOD__, AtomicSectionIdentifier $sectionId = null ); @@ -1848,8 +1849,8 @@ interface IDatabase { * @param string $cancelable Pass self::ATOMIC_CANCELABLE to use a * savepoint and enable self::cancelAtomic() for this section. * @return mixed $res Result of the callback (since 1.28) - * @throws DBError - * @throws RuntimeException + * @throws DBError If an error occurs, see IDatabase::query() + * @throws Exception If an error occurs in the callback * @since 1.27; prior to 1.31 this did a rollback() instead of * cancelAtomic(), and assumed no callers up the stack would ever try to * catch the exception. @@ -1859,8 +1860,7 @@ interface IDatabase { ); /** - * Begin a transaction. If a transaction is already in progress, - * that transaction will be committed before the new transaction is started. + * Begin a transaction * * Only call this from code with outer transcation scope. * See https://www.mediawiki.org/wiki/Database_transactions for details. @@ -1876,12 +1876,13 @@ interface IDatabase { * * @param string $fname Calling function name * @param string $mode A situationally valid IDatabase::TRANSACTION_* constant [optional] - * @throws DBError + * @throws DBError If an error occurs, see IDatabase::query() */ public function begin( $fname = __METHOD__, $mode = self::TRANSACTION_EXPLICIT ); /** - * Commits a transaction previously started using begin(). + * Commits a transaction previously started using begin() + * * If no transaction is in progress, a warning is issued. * * Only call this from code with outer transcation scope. @@ -1892,19 +1893,15 @@ interface IDatabase { * @param string $flush Flush flag, set to situationally valid IDatabase::FLUSHING_* * constant to disable warnings about explicitly committing implicit transactions, * or calling commit when no transaction is in progress. - * * This will trigger an exception if there is an ongoing explicit transaction. - * * Only set the flush flag if you are sure that these warnings are not applicable, * and no explicit transactions are open. - * - * @throws DBError + * @throws DBError If an error occurs, see IDatabase::query() */ public function commit( $fname = __METHOD__, $flush = self::FLUSHING_ONE ); /** - * Rollback a transaction previously started using begin(). - * If no transaction is in progress, a warning is issued. + * Rollback a transaction previously started using begin() * * Only call this from code with outer transcation scope. * See https://www.mediawiki.org/wiki/Database_transactions for details. @@ -1919,7 +1916,7 @@ interface IDatabase { * constant to disable warnings about calling rollback when no transaction is in * progress. This will silently break any ongoing explicit transaction. Only set the * flush flag if you are sure that it is safe to ignore these warnings in your context. - * @throws DBError + * @throws DBError If an error occurs, see IDatabase::query() * @since 1.23 Added $flush parameter */ public function rollback( $fname = __METHOD__, $flush = self::FLUSHING_ONE ); @@ -1936,21 +1933,18 @@ interface IDatabase { * @param string $flush Flush flag, set to situationally valid IDatabase::FLUSHING_* * constant to disable warnings about explicitly committing implicit transactions, * or calling commit when no transaction is in progress. - * * This will trigger an exception if there is an ongoing explicit transaction. - * * Only set the flush flag if you are sure that these warnings are not applicable, * and no explicit transactions are open. - * - * @throws DBError + * @throws DBError If an error occurs, see IDatabase::query() * @since 1.28 * @since 1.34 Added $flush parameter */ public function flushSnapshot( $fname = __METHOD__, $flush = self::FLUSHING_ONE ); /** - * Convert a timestamp in one of the formats accepted by wfTimestamp() - * to the format used for inserting into timestamp fields in this DBMS. + * Convert a timestamp in one of the formats accepted by ConvertibleTimestamp + * to the format used for inserting into timestamp fields in this DBMS * * The result is unquoted, and needs to be passed through addQuotes() * before it can be included in raw SQL. @@ -1962,9 +1956,10 @@ interface IDatabase { public function timestamp( $ts = 0 ); /** - * Convert a timestamp in one of the formats accepted by wfTimestamp() - * to the format used for inserting into timestamp fields in this DBMS. If - * NULL is input, it is passed through, allowing NULL values to be inserted + * Convert a timestamp in one of the formats accepted by ConvertibleTimestamp + * to the format used for inserting into timestamp fields in this DBMS + * + * If NULL is input, it is passed through, allowing NULL values to be inserted * into timestamp fields. * * The result is unquoted, and needs to be passed through addQuotes() @@ -1990,7 +1985,7 @@ interface IDatabase { * Callers should avoid using this method while a transaction is active * * @return int|bool Database replication lag in seconds or false on error - * @throws DBError + * @throws DBError If an error occurs, see IDatabase::query() */ public function getLag(); @@ -2005,13 +2000,13 @@ interface IDatabase { * indication of the staleness of subsequent reads. * * @return array ('lag': seconds or false on error, 'since': UNIX timestamp of BEGIN) - * @throws DBError + * @throws DBError If an error occurs, see IDatabase::query() * @since 1.27 */ public function getSessionLagStatus(); /** - * Return the maximum number of items allowed in a list, or 0 for unlimited. + * Return the maximum number of items allowed in a list, or 0 for unlimited * * @return int */ @@ -2025,6 +2020,7 @@ interface IDatabase { * * @param string $b * @return string|Blob + * @throws DBError */ public function encodeBlob( $b ); @@ -2035,6 +2031,7 @@ interface IDatabase { * * @param string|Blob $b * @return string + * @throws DBError */ public function decodeBlob( $b ); @@ -2047,7 +2044,7 @@ interface IDatabase { * * @param array $options * @return void - * @throws DBError + * @throws DBError If an error occurs, see IDatabase::query() */ public function setSessionOptions( array $options ); @@ -2066,7 +2063,7 @@ interface IDatabase { * @param string $lockName Name of lock to poll * @param string $method Name of method calling us * @return bool - * @throws DBError + * @throws DBError If an error occurs, see IDatabase::query() * @since 1.20 */ public function lockIsFree( $lockName, $method ); @@ -2079,8 +2076,8 @@ interface IDatabase { * @param string $lockName Name of lock to aquire * @param string $method Name of the calling method * @param int $timeout Acquisition timeout in seconds (0 means non-blocking) - * @return bool - * @throws DBError + * @return bool Success + * @throws DBError If an error occurs, see IDatabase::query() */ public function lock( $lockName, $method, $timeout = 5 ); @@ -2091,12 +2088,8 @@ interface IDatabase { * * @param string $lockName Name of lock to release * @param string $method Name of the calling method - * - * @return int Returns 1 if the lock was released, 0 if the lock was not established - * by this thread (in which case the lock is not released), and NULL if the named lock - * did not exist - * - * @throws DBError + * @return bool Success + * @throws DBError If an error occurs, see IDatabase::query() */ public function unlock( $lockName, $method ); @@ -2118,7 +2111,7 @@ interface IDatabase { * @param string $fname Name of the calling method * @param int $timeout Acquisition timeout in seconds * @return ScopedCallback|null - * @throws DBError + * @throws DBError If an error occurs, see IDatabase::query() * @since 1.27 */ public function getScopedLockAndFlush( $lockKey, $fname, $timeout ); diff --git a/includes/libs/rdbms/database/domain/DatabaseDomain.php b/includes/libs/rdbms/database/domain/DatabaseDomain.php index 5698cf824d..3ceb339b76 100644 --- a/includes/libs/rdbms/database/domain/DatabaseDomain.php +++ b/includes/libs/rdbms/database/domain/DatabaseDomain.php @@ -23,7 +23,19 @@ namespace Wikimedia\Rdbms; use InvalidArgumentException; /** - * Class to handle database/prefix specification for IDatabase domains + * Class to handle database/schema/prefix specifications for IDatabase + * + * The components of a database domain are defined as follows: + * - database: name of a server-side collection of schemas that is client-selectable + * - schema: name of a server-side collection of tables within the given database + * - prefix: table name prefix of an application-defined table collection + * + * If an RDBMS does not support server-side collections of table collections (schemas) then + * the schema component should be null and the "database" component treated as a collection + * of exactly one table collection (the implied schema for that "database"). + * + * The above criteria should determine how components should map to RDBMS specific keywords + * rather than "database"/"schema" always mapping to "DATABASE"/"SCHEMA" as used by the RDBMS. */ class DatabaseDomain { /** @var string|null */ diff --git a/includes/libs/rdbms/database/position/MySQLMasterPos.php b/includes/libs/rdbms/database/position/MySQLMasterPos.php index 54eca79a44..e1b8f9a252 100644 --- a/includes/libs/rdbms/database/position/MySQLMasterPos.php +++ b/includes/libs/rdbms/database/position/MySQLMasterPos.php @@ -17,7 +17,7 @@ use UnexpectedValueException; * @see https://dev.mysql.com/doc/refman/5.6/en/replication-gtids-concepts.html */ class MySQLMasterPos implements DBMasterPos { - /** @var int One of (BINARY_LOG, GTID_MYSQL, GTID_MARIA) */ + /** @var string One of (BINARY_LOG, GTID_MYSQL, GTID_MARIA) */ private $style; /** @var string|null Base name of all Binary Log files */ private $binLog; @@ -190,39 +190,69 @@ class MySQLMasterPos implements DBMasterPos { } /** + * Set the GTID domain known to be used in new commits on a replication stream of interest + * + * This makes getRelevantActiveGTIDs() filter out GTIDs from other domains + * + * @see MySQLMasterPos::getRelevantActiveGTIDs() + * @see https://mariadb.com/kb/en/library/gtid/#gtid_domain_id + * * @param int|null $id @@gtid_domain_id of the active replication stream + * @return MySQLMasterPos This instance (since 1.34) * @since 1.31 */ public function setActiveDomain( $id ) { $this->activeDomain = (int)$id; + + return $this; } /** + * Set the server ID known to be used in new commits on a replication stream of interest + * + * This makes getRelevantActiveGTIDs() filter out GTIDs from other origin servers + * + * @see MySQLMasterPos::getRelevantActiveGTIDs() + * * @param int|null $id @@server_id of the server were writes originate + * @return MySQLMasterPos This instance (since 1.34) * @since 1.31 */ public function setActiveOriginServerId( $id ) { $this->activeServerId = (int)$id; + + return $this; } /** + * Set the server UUID known to be used in new commits on a replication stream of interest + * + * This makes getRelevantActiveGTIDs() filter out GTIDs from other origin servers + * + * @see MySQLMasterPos::getRelevantActiveGTIDs() + * * @param string|null $id @@server_uuid of the server were writes originate + * @return MySQLMasterPos This instance (since 1.34) * @since 1.31 */ public function setActiveOriginServerUUID( $id ) { $this->activeServerUUID = $id; + + return $this; } /** * @param MySQLMasterPos $pos * @param MySQLMasterPos $refPos - * @return string[] List of GTIDs from $pos that have domains in $refPos - * @since 1.31 + * @return string[] List of active GTIDs from $pos that have domains in $refPos + * @since 1.34 */ - public static function getCommonDomainGTIDs( MySQLMasterPos $pos, MySQLMasterPos $refPos ) { - return array_values( - array_intersect_key( $pos->gtids, $refPos->getActiveGtidCoordinates() ) - ); + public static function getRelevantActiveGTIDs( MySQLMasterPos $pos, MySQLMasterPos $refPos ) { + return array_values( array_intersect_key( + $pos->gtids, + $pos->getActiveGtidCoordinates(), + $refPos->gtids + ) ); } /** diff --git a/includes/libs/rdbms/database/resultwrapper/IResultWrapper.php b/includes/libs/rdbms/database/resultwrapper/IResultWrapper.php index 616fed9d13..955725165b 100644 --- a/includes/libs/rdbms/database/resultwrapper/IResultWrapper.php +++ b/includes/libs/rdbms/database/resultwrapper/IResultWrapper.php @@ -76,6 +76,7 @@ interface IResultWrapper extends Iterator { /** * @return stdClass + * @suppress PhanParamSignatureMismatchInternal */ function next(); } diff --git a/includes/libs/rdbms/database/resultwrapper/MssqlResultWrapper.php b/includes/libs/rdbms/database/resultwrapper/MssqlResultWrapper.php deleted file mode 100644 index ba79be14f0..0000000000 --- a/includes/libs/rdbms/database/resultwrapper/MssqlResultWrapper.php +++ /dev/null @@ -1,76 +0,0 @@ -result; - - if ( $this->seekTo !== null ) { - $result = sqlsrv_fetch_object( $res, stdClass::class, [], - SQLSRV_SCROLL_ABSOLUTE, $this->seekTo ); - $this->seekTo = null; - } else { - $result = sqlsrv_fetch_object( $res ); - } - - // Return boolean false when there are no more rows instead of null - if ( $result === null ) { - return false; - } - - return $result; - } - - /** - * @return array|bool - */ - public function fetchRow() { - $res = $this->result; - - if ( $this->seekTo !== null ) { - $result = sqlsrv_fetch_array( $res, SQLSRV_FETCH_BOTH, - SQLSRV_SCROLL_ABSOLUTE, $this->seekTo ); - $this->seekTo = null; - } else { - $result = sqlsrv_fetch_array( $res ); - } - - // Return boolean false when there are no more rows instead of null - if ( $result === null ) { - return false; - } - - return $result; - } - - /** - * @param int $row - * @return bool - */ - public function seek( $row ) { - $res = $this->result; - - // check bounds - $numRows = $this->db->numRows( $res ); - $row = intval( $row ); - - if ( $numRows === 0 ) { - return false; - } elseif ( $row < 0 || $row > $numRows - 1 ) { - return false; - } - - // Unlike MySQL, the seek actually happens on the next access - $this->seekTo = $row; - return true; - } -} diff --git a/includes/libs/rdbms/encasing/MssqlBlob.php b/includes/libs/rdbms/encasing/MssqlBlob.php deleted file mode 100644 index 1819a9ac40..0000000000 --- a/includes/libs/rdbms/encasing/MssqlBlob.php +++ /dev/null @@ -1,39 +0,0 @@ -data = $data->data; - } elseif ( $data instanceof Blob ) { - $this->data = $data->fetch(); - } else { - $this->data = $data; - } - } - - /** - * Returns an unquoted hex representation of a binary string - * for insertion into varbinary-type fields - * @return string - */ - public function fetch() { - if ( $this->data === null ) { - return 'null'; - } - - $ret = '0x'; - $dataLength = strlen( $this->data ); - for ( $i = 0; $i < $dataLength; $i++ ) { - $ret .= bin2hex( pack( 'C', ord( $this->data[$i] ) ) ); - } - - return $ret; - } -} diff --git a/includes/libs/rdbms/field/MssqlField.php b/includes/libs/rdbms/field/MssqlField.php deleted file mode 100644 index 98cc2b1893..0000000000 --- a/includes/libs/rdbms/field/MssqlField.php +++ /dev/null @@ -1,40 +0,0 @@ -name = $info['COLUMN_NAME']; - $this->tableName = $info['TABLE_NAME']; - $this->default = $info['COLUMN_DEFAULT']; - $this->max_length = $info['CHARACTER_MAXIMUM_LENGTH']; - $this->nullable = !( strtolower( $info['IS_NULLABLE'] ) == 'no' ); - $this->type = $info['DATA_TYPE']; - } - - function name() { - return $this->name; - } - - function tableName() { - return $this->tableName; - } - - function defaultValue() { - return $this->default; - } - - function maxLength() { - return $this->max_length; - } - - function isNullable() { - return $this->nullable; - } - - function type() { - return $this->type; - } -} diff --git a/includes/libs/rdbms/lbfactory/ILBFactory.php b/includes/libs/rdbms/lbfactory/ILBFactory.php index 4b6afe7089..23232f40ac 100644 --- a/includes/libs/rdbms/lbfactory/ILBFactory.php +++ b/includes/libs/rdbms/lbfactory/ILBFactory.php @@ -38,6 +38,9 @@ interface ILBFactory { /** @var int Save DB positions, waiting on all DCs */ const SHUTDOWN_CHRONPROT_SYNC = 2; + /** @var string Default main LB cluster name (do not change this) */ + const CLUSTER_MAIN_DEFAULT = 'DEFAULT'; + /** * Construct a manager of ILoadBalancer objects * @@ -106,9 +109,10 @@ interface ILBFactory { * but still use DBO_TRX transaction rounds on other tables. * * @param bool|string $domain Domain ID, or false for the current domain + * @param int|null $owner Owner ID of the new instance (e.g. this LBFactory ID) * @return ILoadBalancer */ - public function newMainLB( $domain = false ); + public function newMainLB( $domain = false, $owner = null ); /** * Get a cached (tracked) load balancer object. @@ -128,9 +132,10 @@ interface ILBFactory { * (DBO_TRX off) but still use DBO_TRX transaction rounds on other tables. * * @param string $cluster External storage cluster name + * @param int|null $owner Owner ID of the new instance (e.g. this LBFactory ID) * @return ILoadBalancer */ - public function newExternalLB( $cluster ); + public function newExternalLB( $cluster, $owner = null ); /** * Get a cached (tracked) load balancer for external storage diff --git a/includes/libs/rdbms/lbfactory/LBFactory.php b/includes/libs/rdbms/lbfactory/LBFactory.php index 4426654ca2..07a5fe6d6c 100644 --- a/includes/libs/rdbms/lbfactory/LBFactory.php +++ b/includes/libs/rdbms/lbfactory/LBFactory.php @@ -155,13 +155,16 @@ abstract class LBFactory implements ILBFactory { $this->defaultGroup = $conf['defaultGroup'] ?? null; $this->secret = $conf['secret'] ?? ''; - $this->id = mt_rand(); - $this->ticket = mt_rand(); + static $nextId, $nextTicket; + $this->id = $nextId = ( is_int( $nextId ) ? $nextId++ : mt_rand() ); + $this->ticket = $nextTicket = ( is_int( $nextTicket ) ? $nextTicket++ : mt_rand() ); } public function destroy() { - $this->shutdown( self::SHUTDOWN_NO_CHRONPROT ); - $this->forEachLBCallMethod( 'disable' ); + /** @noinspection PhpUnusedLocalVariableInspection */ + $scope = ScopedCallback::newScopedIgnoreUserAbort(); + + $this->forEachLBCallMethod( 'disable', [ __METHOD__, $this->id ] ); } public function getLocalDomainID() { @@ -178,6 +181,9 @@ abstract class LBFactory implements ILBFactory { &$cpIndex = null, &$cpClientId = null ) { + /** @noinspection PhpUnusedLocalVariableInspection */ + $scope = ScopedCallback::newScopedIgnoreUserAbort(); + $chronProt = $this->getChronologyProtector(); if ( $mode === self::SHUTDOWN_CHRONPROT_SYNC ) { $this->shutdownChronologyProtector( $chronProt, $workCallback, 'sync', $cpIndex ); @@ -190,34 +196,6 @@ abstract class LBFactory implements ILBFactory { $this->commitMasterChanges( __METHOD__ ); // sanity } - /** - * @see ILBFactory::newMainLB() - * @param bool $domain - * @return ILoadBalancer - */ - abstract public function newMainLB( $domain = false ); - - /** - * @see ILBFactory::getMainLB() - * @param bool $domain - * @return ILoadBalancer - */ - abstract public function getMainLB( $domain = false ); - - /** - * @see ILBFactory::newExternalLB() - * @param string $cluster - * @return ILoadBalancer - */ - abstract public function newExternalLB( $cluster ); - - /** - * @see ILBFactory::getExternalLB() - * @param string $cluster - * @return ILoadBalancer - */ - abstract public function getExternalLB( $cluster ); - /** * Call a method of each tracked load balancer * @@ -240,17 +218,20 @@ abstract class LBFactory implements ILBFactory { [ 'trace' => ( new RuntimeException() )->getTraceAsString() ] ); } - $this->forEachLBCallMethod( 'flushReplicaSnapshots', [ $fname ] ); + $this->forEachLBCallMethod( 'flushReplicaSnapshots', [ $fname, $this->id ] ); } final public function commitAll( $fname = __METHOD__, array $options = [] ) { $this->commitMasterChanges( $fname, $options ); - $this->forEachLBCallMethod( 'flushMasterSnapshots', [ $fname ] ); - $this->forEachLBCallMethod( 'flushReplicaSnapshots', [ $fname ] ); + $this->forEachLBCallMethod( 'flushMasterSnapshots', [ $fname, $this->id ] ); + $this->forEachLBCallMethod( 'flushReplicaSnapshots', [ $fname, $this->id ] ); } final public function beginMasterChanges( $fname = __METHOD__ ) { $this->assertTransactionRoundStage( self::ROUND_CURSORY ); + /** @noinspection PhpUnusedLocalVariableInspection */ + $scope = ScopedCallback::newScopedIgnoreUserAbort(); + $this->trxRoundStage = self::ROUND_BEGINNING; if ( $this->trxRoundId !== false ) { throw new DBTransactionError( @@ -266,6 +247,9 @@ abstract class LBFactory implements ILBFactory { final public function commitMasterChanges( $fname = __METHOD__, array $options = [] ) { $this->assertTransactionRoundStage( self::ROUND_CURSORY ); + /** @noinspection PhpUnusedLocalVariableInspection */ + $scope = ScopedCallback::newScopedIgnoreUserAbort(); + $this->trxRoundStage = self::ROUND_COMMITTING; if ( $this->trxRoundId !== false && $this->trxRoundId !== $fname ) { throw new DBTransactionError( @@ -273,8 +257,6 @@ abstract class LBFactory implements ILBFactory { "$fname: transaction round '{$this->trxRoundId}' still running" ); } - /** @noinspection PhpUnusedLocalVariableInspection */ - $scope = ScopedCallback::newScopedIgnoreUserAbort(); // try to ignore client aborts // Run pre-commit callbacks and suppress post-commit callbacks, aborting on failure do { $count = 0; // number of callbacks executed this iteration @@ -300,6 +282,9 @@ abstract class LBFactory implements ILBFactory { } final public function rollbackMasterChanges( $fname = __METHOD__ ) { + /** @noinspection PhpUnusedLocalVariableInspection */ + $scope = ScopedCallback::newScopedIgnoreUserAbort(); + $this->trxRoundStage = self::ROUND_ROLLING_BACK; $this->trxRoundId = false; // Actually perform the rollback on all master DB connections and revert DBO_TRX @@ -592,10 +577,12 @@ abstract class LBFactory implements ILBFactory { } /** - * Base parameters to ILoadBalancer::__construct() + * Get parameters to ILoadBalancer::__construct() + * + * @param int|null $owner Use getOwnershipId() if this is for getMainLB()/getExternalLB() * @return array */ - final protected function baseLoadBalancerParams() { + final protected function baseLoadBalancerParams( $owner ) { if ( $this->trxRoundStage === self::ROUND_COMMIT_CALLBACKS ) { $initStage = ILoadBalancer::STAGE_POSTCOMMIT_CALLBACKS; } elseif ( $this->trxRoundStage === self::ROUND_ROLLBACK_CALLBACKS ) { @@ -627,7 +614,7 @@ abstract class LBFactory implements ILBFactory { $this->getChronologyProtector()->applySessionReplicationPosition( $lb ); }, 'roundStage' => $initStage, - 'ownerId' => $this->id + 'ownerId' => $owner ]; } @@ -674,7 +661,10 @@ abstract class LBFactory implements ILBFactory { } public function closeAll() { - $this->forEachLBCallMethod( 'closeAll' ); + /** @noinspection PhpUnusedLocalVariableInspection */ + $scope = ScopedCallback::newScopedIgnoreUserAbort(); + + $this->forEachLBCallMethod( 'closeAll', [ __METHOD__, $this->id ] ); } public function setAgentName( $agent ) { @@ -742,6 +732,14 @@ abstract class LBFactory implements ILBFactory { $this->requestInfo = $info + $this->requestInfo; } + /** + * @return int Internal instance ID used to assert ownership of ILoadBalancer instances + * @since 1.34 + */ + final protected function getOwnershipId() { + return $this->id; + } + /** * @param string $stage */ diff --git a/includes/libs/rdbms/lbfactory/LBFactoryMulti.php b/includes/libs/rdbms/lbfactory/LBFactoryMulti.php index f675b58778..5d33e698db 100644 --- a/includes/libs/rdbms/lbfactory/LBFactoryMulti.php +++ b/includes/libs/rdbms/lbfactory/LBFactoryMulti.php @@ -24,6 +24,7 @@ namespace Wikimedia\Rdbms; use InvalidArgumentException; +use UnexpectedValueException; /** * A multi-database, multi-master factory for Wikimedia and similar installations. @@ -32,64 +33,45 @@ use InvalidArgumentException; * @ingroup Database */ class LBFactoryMulti extends LBFactory { - /** @var array A map of database names to section names */ - private $sectionsByDB; - /** - * @var array A 2-d map. For each section, gives a map of server names to - * load ratios - */ - private $sectionLoads; - /** - * @var array[] Server info associative array - * @note The host, hostName and load entries will be overridden - */ - private $serverTemplate; + /** @var LoadBalancer[] */ + private $mainLBs = []; + /** @var LoadBalancer[] */ + private $externalLBs = []; - /** @var array A 3-d map giving server load ratios for each section and group */ + /** @var string[] Map of (hostname => IP address) */ + private $hostsByName = []; + /** @var string[] Map of (database name => section name) */ + private $sectionsByDB = []; + /** @var int[][][] Map of (section => group => host => load ratio) */ private $groupLoadsBySection = []; - /** @var array A 3-d map giving server load ratios by DB name */ + /** @var int[][][] Map of (database => group => host => load ratio) */ private $groupLoadsByDB = []; - /** @var array A map of hostname to IP address */ - private $hostsByName = []; - /** @var array A map of external storage cluster name to server load map */ + /** @var int[][] Map of (cluster => host => load ratio) */ private $externalLoads = []; - /** - * @var array A set of server info keys overriding serverTemplate for - * external storage - */ - private $externalTemplateOverrides; - /** - * @var array A 2-d map overriding serverTemplate and - * externalTemplateOverrides on a server-by-server basis. Applies to both - * core and external storage - */ - private $templateOverridesByServer; - /** @var array A 2-d map overriding the server info by section */ - private $templateOverridesBySection; - /** @var array A 2-d map overriding the server info by external storage cluster */ - private $templateOverridesByCluster; - /** @var array An override array for all master servers */ - private $masterTemplateOverrides; - /** - * @var array|bool A map of section name to read-only message. Missing or - * false for read/write - */ + /** @var array Server config map ("host", "hostName", "load", and "groupLoads" are ignored) */ + private $serverTemplate = []; + /** @var array Server config map overriding "serverTemplate" for external storage */ + private $externalTemplateOverrides = []; + /** @var array[] Map of (section => server config map overrides) */ + private $templateOverridesBySection = []; + /** @var array[] Map of (cluster => server config map overrides) for external storage */ + private $templateOverridesByCluster = []; + /** @var array Server config override map for all main and external master servers */ + private $masterTemplateOverrides = []; + /** @var array[] Map of (host => server config map overrides) for main and external servers */ + private $templateOverridesByServer = []; + /** @var string[]|bool[] A map of section name to read-only message */ private $readOnlyBySection = []; - /** @var LoadBalancer[] */ - private $mainLBs = []; - /** @var LoadBalancer[] */ - private $extLBs = []; - /** @var string */ - private $loadMonitorClass = 'LoadMonitor'; + /** @var string An ILoadMonitor class */ + private $loadMonitorClass; + /** @var string */ private $lastDomain; /** @var string */ private $lastSection; /** - * @see LBFactory::__construct() - * * Template override precedence (highest => lowest): * - templateOverridesByServer * - masterTemplateOverrides @@ -98,155 +80,140 @@ class LBFactoryMulti extends LBFactory { * - serverTemplate * Overrides only work on top level keys (so nested values will not be merged). * - * Server configuration maps should be of the format Database::factory() requires. + * Server config maps should be of the format Database::factory() requires. * Additionally, a 'max lag' key should also be set on server maps, indicating how stale the * data can be before the load balancer tries to avoid using it. The map can have 'is static' * set to disable blocking replication sync checks (intended for archive servers with * unchanging data). * - * @param array $conf Parameters of LBFactory::__construct() as well as: - * - sectionsByDB Map of database names to section names. - * - sectionLoads 2-d map. For each section, gives a map of server names to - * load ratios. For example: + * @see LBFactory::__construct() + * @param array $conf Additional parameters include: + * - hostsByName Optional (hostname => IP address) map. + * - sectionsByDB Optional map of (database => section name). + * For example: * [ - * 'section1' => [ - * 'db1' => 100, - * 'db2' => 100 - * ] + * 'DEFAULT' => 'section1', + * 'database1' => 'section2' * ] - * - serverTemplate Server configuration map intended for Database::factory(). - * Note that "host", "hostName" and "load" entries will be - * overridden by "sectionLoads" and "hostsByName". - * - groupLoadsBySection 3-d map giving server load ratios for each section/group. + * - sectionLoads Optional map of (section => host => load ratio); the first + * host in each section is the master server for that section. + * For example: + * [ + * 'dbmaser' => 0, + * 'dbreplica1' => 100, + * 'dbreplica2' => 100 + * ] + * - groupLoadsBySection Optional map of (section => group => host => load ratio); + * any ILoadBalancer::GROUP_GENERIC group will be ignored. * For example: * [ * 'section1' => [ * 'group1' => [ - * 'db1' => 100, - * 'db2' => 100 + * 'dbreplica3 => 100, + * 'dbreplica4' => 100 * ] * ] * ] - * - groupLoadsByDB 3-d map giving server load ratios by DB name. - * - hostsByName Map of hostname to IP address. - * - externalLoads Map of external storage cluster name to server load map. - * - externalTemplateOverrides Set of server configuration maps overriding - * "serverTemplate" for external storage. - * - templateOverridesByServer 2-d map overriding "serverTemplate" and - * "externalTemplateOverrides" on a server-by-server basis. - * Applies to both core and external storage. - * - templateOverridesBySection 2-d map overriding the server configuration maps by section. - * - templateOverridesByCluster 2-d map overriding the server configuration maps by external - * storage cluster. - * - masterTemplateOverrides Server configuration map overrides for all master servers. - * - loadMonitorClass Name of the LoadMonitor class to always use. - * - readOnlyBySection A map of section name to read-only message. - * Missing or false for read/write. + * - groupLoadsByDB Optional (database => group => host => load ratio) map. + * - externalLoads Optional (cluster => host => load ratio) map. + * - serverTemplate server config map for Database::factory(). + * Note that "host", "hostName" and "load" entries will be + * overridden by "groupLoadsBySection" and "hostsByName". + * - externalTemplateOverrides Optional server config map overrides for external + * stores; respects the override precedence described above. + * - templateOverridesBySection Optional (section => server config map overrides) map; + * respects the override precedence described above. + * - templateOverridesByCluster Optional (external cluster => server config map overrides) + * map; respects the override precedence described above. + * - masterTemplateOverrides Optional server config map overrides for masters; + * respects the override precedence described above. + * - templateOverridesByServer Optional (host => server config map overrides) map; + * respects the override precedence described above + * and applies to both core and external storage. + * - loadMonitorClass Name of the LoadMonitor class to always use. [optional] + * - readOnlyBySection Optional map of (section name => message text or false). + * String values make sections read only, whereas anything + * else does not restrict read/write mode. */ public function __construct( array $conf ) { parent::__construct( $conf ); - $required = [ 'sectionsByDB', 'sectionLoads', 'serverTemplate' ]; - $optional = [ 'groupLoadsBySection', 'groupLoadsByDB', 'hostsByName', - 'externalLoads', 'externalTemplateOverrides', 'templateOverridesByServer', - 'templateOverridesByCluster', 'templateOverridesBySection', 'masterTemplateOverrides', - 'readOnlyBySection', 'loadMonitorClass' ]; - - foreach ( $required as $key ) { - if ( !isset( $conf[$key] ) ) { - throw new InvalidArgumentException( __CLASS__ . ": $key is required." ); - } - $this->$key = $conf[$key]; - } - - foreach ( $optional as $key ) { - if ( isset( $conf[$key] ) ) { - $this->$key = $conf[$key]; - } + $this->hostsByName = $conf['hostsByName'] ?? []; + $this->sectionsByDB = $conf['sectionsByDB']; + $this->groupLoadsBySection = $conf['groupLoadsBySection'] ?? []; + foreach ( ( $conf['sectionLoads'] ?? [] ) as $section => $loadByHost ) { + $this->groupLoadsBySection[$section][ILoadBalancer::GROUP_GENERIC] = $loadByHost; } + $this->groupLoadsByDB = $conf['groupLoadsByDB'] ?? []; + $this->externalLoads = $conf['externalLoads'] ?? []; + $this->serverTemplate = $conf['serverTemplate'] ?? []; + $this->externalTemplateOverrides = $conf['externalTemplateOverrides'] ?? []; + $this->templateOverridesBySection = $conf['templateOverridesBySection'] ?? []; + $this->templateOverridesByCluster = $conf['templateOverridesByCluster'] ?? []; + $this->masterTemplateOverrides = $conf['masterTemplateOverrides'] ?? []; + $this->templateOverridesByServer = $conf['templateOverridesByServer'] ?? []; + $this->readOnlyBySection = $conf['readOnlyBySection'] ?? []; + + $this->loadMonitorClass = $conf['loadMonitorClass'] ?? LoadMonitor::class; } - /** - * @param bool|string $domain - * @return string - */ - private function getSectionForDomain( $domain = false ) { - if ( $this->lastDomain === $domain ) { - return $this->lastSection; - } - - $database = $this->getDatabaseFromDomain( $domain ); - $section = $this->sectionsByDB[$database] ?? 'DEFAULT'; - $this->lastSection = $section; - $this->lastDomain = $domain; - - return $section; - } - - public function newMainLB( $domain = false ) { - $database = $this->getDatabaseFromDomain( $domain ); + public function newMainLB( $domain = false, $owner = null ) { $section = $this->getSectionForDomain( $domain ); - $groupLoads = $this->groupLoadsByDB[$database] ?? []; - - if ( isset( $this->groupLoadsBySection[$section] ) ) { - $groupLoads = array_merge_recursive( - $groupLoads, $this->groupLoadsBySection[$section] ); + if ( !isset( $this->groupLoadsBySection[$section][ILoadBalancer::GROUP_GENERIC] ) ) { + throw new UnexpectedValueException( "Section '$section' has no hosts defined." ); } - $readOnlyReason = $this->readOnlyReason; - // Use the LB-specific read-only reason if everything isn't already read-only - if ( $readOnlyReason === false && isset( $this->readOnlyBySection[$section] ) ) { - $readOnlyReason = $this->readOnlyBySection[$section]; - } - - $template = $this->serverTemplate; - if ( isset( $this->templateOverridesBySection[$section] ) ) { - $template = $this->templateOverridesBySection[$section] + $template; - } + $dbGroupLoads = $this->groupLoadsByDB[$this->getDomainDatabase( $domain )] ?? []; + unset( $dbGroupLoads[ILoadBalancer::GROUP_GENERIC] ); // cannot override return $this->newLoadBalancer( - $template, - $this->sectionLoads[$section], - $groupLoads, - $readOnlyReason + array_merge( + $this->serverTemplate, + $this->templateOverridesBySection[$section] ?? [] + ), + array_merge( $this->groupLoadsBySection[$section], $dbGroupLoads ), + // Use the LB-specific read-only reason if everything isn't already read-only + is_string( $this->readOnlyReason ) + ? $this->readOnlyReason + : ( $this->readOnlyBySection[$section] ?? false ), + $owner ); } public function getMainLB( $domain = false ) { $section = $this->getSectionForDomain( $domain ); + if ( !isset( $this->mainLBs[$section] ) ) { - $this->mainLBs[$section] = $this->newMainLB( $domain ); + $this->mainLBs[$section] = $this->newMainLB( $domain, $this->getOwnershipId() ); } return $this->mainLBs[$section]; } - public function newExternalLB( $cluster ) { + public function newExternalLB( $cluster, $owner = null ) { if ( !isset( $this->externalLoads[$cluster] ) ) { - throw new InvalidArgumentException( __METHOD__ . ": Unknown cluster \"$cluster\"" ); - } - $template = $this->serverTemplate; - if ( $this->externalTemplateOverrides ) { - $template = $this->externalTemplateOverrides + $template; - } - if ( isset( $this->templateOverridesByCluster[$cluster] ) ) { - $template = $this->templateOverridesByCluster[$cluster] + $template; + throw new InvalidArgumentException( "Unknown cluster '$cluster'" ); } return $this->newLoadBalancer( - $template, - $this->externalLoads[$cluster], - [], - $this->readOnlyReason + array_merge( + $this->serverTemplate, + $this->externalTemplateOverrides, + $this->templateOverridesByCluster[$cluster] ?? [] + ), + [ ILoadBalancer::GROUP_GENERIC => $this->externalLoads[$cluster] ], + $this->readOnlyReason, + $owner ); } public function getExternalLB( $cluster ) { - if ( !isset( $this->extLBs[$cluster] ) ) { - $this->extLBs[$cluster] = $this->newExternalLB( $cluster ); + if ( !isset( $this->externalLBs[$cluster] ) ) { + $this->externalLBs[$cluster] = + $this->newExternalLB( $cluster, $this->getOwnershipId() ); } - return $this->extLBs[$cluster]; + return $this->externalLBs[$cluster]; } public function getAllMainLBs() { @@ -269,20 +236,46 @@ class LBFactoryMulti extends LBFactory { return $lbs; } + public function forEachLB( $callback, array $params = [] ) { + foreach ( $this->mainLBs as $lb ) { + $callback( $lb, ...$params ); + } + foreach ( $this->externalLBs as $lb ) { + $callback( $lb, ...$params ); + } + } + + /** + * @param bool|string $domain + * @return string + */ + private function getSectionForDomain( $domain = false ) { + if ( $this->lastDomain === $domain ) { + return $this->lastSection; + } + + $database = $this->getDomainDatabase( $domain ); + $section = $this->sectionsByDB[$database] ?? self::CLUSTER_MAIN_DEFAULT; + $this->lastSection = $section; + $this->lastDomain = $domain; + + return $section; + } + /** * Make a new load balancer object based on template and load array * - * @param array $template - * @param array $loads - * @param array $groupLoads + * @param array $serverTemplate Server config map + * @param int[][] $groupLoads Map of (group => host => load) * @param string|bool $readOnlyReason + * @param int|null $owner * @return LoadBalancer */ - private function newLoadBalancer( $template, $loads, $groupLoads, $readOnlyReason ) { + private function newLoadBalancer( $serverTemplate, $groupLoads, $readOnlyReason, $owner ) { $lb = new LoadBalancer( array_merge( - $this->baseLoadBalancerParams(), + $this->baseLoadBalancerParams( $owner ), [ - 'servers' => $this->makeServerArray( $template, $loads, $groupLoads ), + 'servers' => $this->makeServerArray( $serverTemplate, $groupLoads ), 'loadMonitor' => [ 'class' => $this->loadMonitorClass ], 'readOnlyReason' => $readOnlyReason ] @@ -293,45 +286,37 @@ class LBFactoryMulti extends LBFactory { } /** - * Make a server array as expected by LoadBalancer::__construct, using a template and load array + * Make a server array as expected by LoadBalancer::__construct() * - * @param array $template - * @param array $loads - * @param array $groupLoads - * @return array + * @param array $serverTemplate Server config map + * @param int[][] $groupLoads Map of (group => host => load) + * @return array[] List of server config maps */ - private function makeServerArray( $template, $loads, $groupLoads ) { - $servers = []; - $master = true; - $groupLoadsByServer = $this->reindexGroupLoads( $groupLoads ); - foreach ( $groupLoadsByServer as $server => $stuff ) { - if ( !isset( $loads[$server] ) ) { - $loads[$server] = 0; - } + private function makeServerArray( array $serverTemplate, array $groupLoads ) { + // The master server is the first host explicitly listed in the generic load group + if ( !$groupLoads[ILoadBalancer::GROUP_GENERIC] ) { + throw new UnexpectedValueException( "Empty generic load array; no master defined." ); } - foreach ( $loads as $serverName => $load ) { - $serverInfo = $template; - if ( $master ) { - $serverInfo['master'] = true; - if ( $this->masterTemplateOverrides ) { - $serverInfo = $this->masterTemplateOverrides + $serverInfo; - } - $master = false; - } else { - $serverInfo['replica'] = true; - } - if ( isset( $this->templateOverridesByServer[$serverName] ) ) { - $serverInfo = $this->templateOverridesByServer[$serverName] + $serverInfo; - } - if ( isset( $groupLoadsByServer[$serverName] ) ) { - $serverInfo['groupLoads'] = $groupLoadsByServer[$serverName]; - } - $serverInfo['host'] = $this->hostsByName[$serverName] ?? $serverName; - $serverInfo['hostName'] = $serverName; - $serverInfo['load'] = $load; - $serverInfo += [ 'flags' => IDatabase::DBO_DEFAULT ]; - $servers[] = $serverInfo; + $groupLoadsByHost = $this->reindexGroupLoads( $groupLoads ); + // Get the ordered map of (host => load); the master server is first + $genericLoads = $groupLoads[ILoadBalancer::GROUP_GENERIC]; + // Implictly append any hosts that only appear in custom load groups + $genericLoads += array_fill_keys( array_keys( $groupLoadsByHost ), 0 ); + + $servers = []; + foreach ( $genericLoads as $host => $load ) { + $servers[] = array_merge( + $serverTemplate, + $servers ? [] : $this->masterTemplateOverrides, + $this->templateOverridesByServer[$host] ?? [], + [ + 'host' => $this->hostsByName[$host] ?? $host, + 'hostName' => $host, + 'load' => $load, + 'groupLoads' => $groupLoadsByHost[$host] ?? [] + ] + ); } return $servers; @@ -339,14 +324,15 @@ class LBFactoryMulti extends LBFactory { /** * Take a group load array indexed by group then server, and reindex it by server then group - * @param array $groupLoads - * @return array + * @param int[][] $groupLoads Map of (group => host => load) + * @return int[][] Map of (host => group => load) */ - private function reindexGroupLoads( $groupLoads ) { + private function reindexGroupLoads( array $groupLoads ) { $reindexed = []; - foreach ( $groupLoads as $group => $loads ) { - foreach ( $loads as $server => $load ) { - $reindexed[$server][$group] = $load; + + foreach ( $groupLoads as $group => $loadByHost ) { + foreach ( $loadByHost as $host => $load ) { + $reindexed[$host][$group] = $load; } } @@ -357,18 +343,9 @@ class LBFactoryMulti extends LBFactory { * @param DatabaseDomain|string|bool $domain Domain ID, or false for the current domain * @return string */ - private function getDatabaseFromDomain( $domain = false ) { + private function getDomainDatabase( $domain = false ) { return ( $domain === false ) ? $this->localDomain->getDatabase() : DatabaseDomain::newFromId( $domain )->getDatabase(); } - - public function forEachLB( $callback, array $params = [] ) { - foreach ( $this->mainLBs as $lb ) { - $callback( $lb, ...$params ); - } - foreach ( $this->extLBs as $lb ) { - $callback( $lb, ...$params ); - } - } } diff --git a/includes/libs/rdbms/lbfactory/LBFactorySimple.php b/includes/libs/rdbms/lbfactory/LBFactorySimple.php index fd76d88dd0..cc79f99be0 100644 --- a/includes/libs/rdbms/lbfactory/LBFactorySimple.php +++ b/includes/libs/rdbms/lbfactory/LBFactorySimple.php @@ -32,20 +32,20 @@ class LBFactorySimple extends LBFactory { /** @var LoadBalancer */ private $mainLB; /** @var LoadBalancer[] */ - private $extLBs = []; + private $externalLBs = []; - /** @var array[] Map of (server index => server config) */ - private $servers = []; - /** @var array[] Map of (cluster => (server index => server config)) */ - private $externalClusters = []; + /** @var array[] Map of (server index => server config map) */ + private $mainServers = []; + /** @var array[][] Map of (cluster => server index => server config map) */ + private $externalServersByCluster = []; /** @var string */ private $loadMonitorClass; /** * @see LBFactory::__construct() - * @param array $conf Parameters of LBFactory::__construct() as well as: - * - servers : list of server configuration maps to Database::factory(). + * @param array $conf Additional parameters include: + * - servers : list of server config maps to Database::factory(). * Additionally, the server maps should have a 'load' key, which is used to decide * how often clients connect to one server verses the others. A 'max lag' key should * also be set on server maps, indicating how stale the data can be before the load @@ -57,63 +57,68 @@ class LBFactorySimple extends LBFactory { public function __construct( array $conf ) { parent::__construct( $conf ); - $this->servers = $conf['servers'] ?? []; - foreach ( $this->servers as $i => $server ) { + $this->mainServers = $conf['servers'] ?? []; + foreach ( $this->mainServers as $i => $server ) { if ( $i == 0 ) { - $this->servers[$i]['master'] = true; + $this->mainServers[$i]['master'] = true; } else { - $this->servers[$i]['replica'] = true; + $this->mainServers[$i]['replica'] = true; } } - $this->externalClusters = $conf['externalClusters'] ?? []; - $this->loadMonitorClass = $conf['loadMonitorClass'] ?? 'LoadMonitor'; + foreach ( ( $conf['externalClusters'] ?? [] ) as $cluster => $servers ) { + foreach ( $servers as $index => $server ) { + $this->externalServersByCluster[$cluster][$index] = $server; + } + } + + $this->loadMonitorClass = $conf['loadMonitorClass'] ?? LoadMonitor::class; } - public function newMainLB( $domain = false ) { - return $this->newLoadBalancer( $this->servers ); + public function newMainLB( $domain = false, $owner = null ) { + return $this->newLoadBalancer( $this->mainServers, $owner ); } public function getMainLB( $domain = false ) { - if ( !$this->mainLB ) { - $this->mainLB = $this->newMainLB( $domain ); + if ( $this->mainLB === null ) { + $this->mainLB = $this->newMainLB( $domain, $this->getOwnershipId() ); } return $this->mainLB; } - public function newExternalLB( $cluster ) { - if ( !isset( $this->externalClusters[$cluster] ) ) { - throw new InvalidArgumentException( __METHOD__ . ": Unknown cluster \"$cluster\"." ); + public function newExternalLB( $cluster, $owner = null ) { + if ( !isset( $this->externalServersByCluster[$cluster] ) ) { + throw new InvalidArgumentException( "Unknown cluster '$cluster'." ); } - return $this->newLoadBalancer( $this->externalClusters[$cluster] ); + return $this->newLoadBalancer( $this->externalServersByCluster[$cluster], $owner ); } public function getExternalLB( $cluster ) { - if ( !isset( $this->extLBs[$cluster] ) ) { - $this->extLBs[$cluster] = $this->newExternalLB( $cluster ); + if ( !isset( $this->externalLBs[$cluster] ) ) { + $this->externalLBs[$cluster] = $this->newExternalLB( $cluster, $this->getOwnershipId() ); } - return $this->extLBs[$cluster]; + return $this->externalLBs[$cluster]; } public function getAllMainLBs() { - return [ 'DEFAULT' => $this->getMainLB() ]; + return [ self::CLUSTER_MAIN_DEFAULT => $this->getMainLB() ]; } public function getAllExternalLBs() { $lbs = []; - foreach ( $this->externalClusters as $cluster => $unused ) { + foreach ( array_keys( $this->externalServersByCluster ) as $cluster ) { $lbs[$cluster] = $this->getExternalLB( $cluster ); } return $lbs; } - private function newLoadBalancer( array $servers ) { + private function newLoadBalancer( array $servers, $owner ) { $lb = new LoadBalancer( array_merge( - $this->baseLoadBalancerParams(), + $this->baseLoadBalancerParams( $owner ), [ 'servers' => $servers, 'loadMonitor' => [ 'class' => $this->loadMonitorClass ], @@ -125,10 +130,10 @@ class LBFactorySimple extends LBFactory { } public function forEachLB( $callback, array $params = [] ) { - if ( isset( $this->mainLB ) ) { + if ( $this->mainLB !== null ) { $callback( $this->mainLB, ...$params ); } - foreach ( $this->extLBs as $lb ) { + foreach ( $this->externalLBs as $lb ) { $callback( $lb, ...$params ); } } diff --git a/includes/libs/rdbms/lbfactory/LBFactorySingle.php b/includes/libs/rdbms/lbfactory/LBFactorySingle.php index 60044baa9c..97daf1092f 100644 --- a/includes/libs/rdbms/lbfactory/LBFactorySingle.php +++ b/includes/libs/rdbms/lbfactory/LBFactorySingle.php @@ -44,8 +44,12 @@ class LBFactorySingle extends LBFactory { throw new InvalidArgumentException( "Missing 'connection' argument." ); } - $lb = new LoadBalancerSingle( array_merge( $this->baseLoadBalancerParams(), $conf ) ); + $lb = new LoadBalancerSingle( array_merge( + $this->baseLoadBalancerParams( $this->getOwnershipId() ), + $conf + ) ); $this->initLoadBalancer( $lb ); + $this->lb = $lb; } @@ -63,23 +67,15 @@ class LBFactorySingle extends LBFactory { ) ); } - /** - * @param bool|string $domain (unused) - * @return LoadBalancerSingle - */ - public function newMainLB( $domain = false ) { - return $this->lb; + public function newMainLB( $domain = false, $owner = null ) { + throw new BadMethodCallException( "Method is not supported." ); } - /** - * @param bool|string $domain (unused) - * @return LoadBalancerSingle - */ public function getMainLB( $domain = false ) { return $this->lb; } - public function newExternalLB( $cluster ) { + public function newExternalLB( $cluster, $owner = null ) { throw new BadMethodCallException( "Method is not supported." ); } @@ -87,24 +83,14 @@ class LBFactorySingle extends LBFactory { throw new BadMethodCallException( "Method is not supported." ); } - /** - * @return LoadBalancerSingle[] Map of (cluster name => LoadBalancer) - */ public function getAllMainLBs() { return [ 'DEFAULT' => $this->lb ]; } - /** - * @return LoadBalancerSingle[] Map of (cluster name => LoadBalancer) - */ public function getAllExternalLBs() { return []; } - /** - * @param string|callable $callback - * @param array $params - */ public function forEachLB( $callback, array $params = [] ) { if ( isset( $this->lb ) ) { // may not be set during _destruct() $callback( $this->lb, ...$params ); diff --git a/includes/libs/rdbms/loadbalancer/ILoadBalancer.php b/includes/libs/rdbms/loadbalancer/ILoadBalancer.php index 990705c45f..4ca4250225 100644 --- a/includes/libs/rdbms/loadbalancer/ILoadBalancer.php +++ b/includes/libs/rdbms/loadbalancer/ILoadBalancer.php @@ -95,6 +95,8 @@ interface ILoadBalancer { const CONN_SILENCE_ERRORS = 2; /** @var int Caller is requesting the master DB server for possibly writes */ const CONN_INTENT_WRITABLE = 4; + /** @var int Bypass and update any server-side read-only mode state cache */ + const CONN_REFRESH_READ_ONLY = 8; /** @var string Manager of ILoadBalancer instances is running post-commit callbacks */ const STAGE_POSTCOMMIT_CALLBACKS = 'stage-postcommit-callbacks'; @@ -155,6 +157,18 @@ interface ILoadBalancer { */ public function redefineLocalDomain( $domain ); + /** + * Indicate whether the tables on this domain are only temporary tables for testing + * + * In "temporary tables mode", the ILoadBalancer::CONN_TRX_AUTOCOMMIT flag is ignored + * + * @param bool $value + * @param string $domain + * @return bool Whether "temporary tables mode" was active + * @since 1.34 + */ + public function setTempTablesOnlyMode( $value, $domain ); + /** * Get the server index of the reader connection for a given group * @@ -167,8 +181,7 @@ interface ILoadBalancer { * * @param string|bool $group Query group or false for the generic group * @param string|bool $domain DB domain ID or false for the local domain - * @throws DBError If no live handle can be obtained - * @return bool|int|string + * @return int|bool Returns false if no live handle can be obtained */ public function getReaderIndex( $group = false, $domain = false ); @@ -480,15 +493,22 @@ interface ILoadBalancer { public function getReplicaResumePos(); /** - * Disable this load balancer. All connections are closed, and any attempt to - * open a new connection will result in a DBAccessError. + * Close all connections and disable this load balancer + * + * Any attempt to open a new connection will result in a DBAccessError. + * + * @param string $fname Caller name + * @param int|null $owner ID of the calling instance (e.g. the LBFactory ID) */ - public function disable(); + public function disable( $fname = __METHOD__, $owner = null ); /** * Close all open connections + * + * @param string $fname Caller name + * @param int|null $owner ID of the calling instance (e.g. the LBFactory ID) */ - public function closeAll(); + public function closeAll( $fname = __METHOD__, $owner = null ); /** * Close a connection @@ -585,8 +605,9 @@ interface ILoadBalancer { * Commit all replica DB transactions so as to flush any REPEATABLE-READ or SSI snapshots * * @param string $fname Caller name + * @param int|null $owner ID of the calling instance (e.g. the LBFactory ID) */ - public function flushReplicaSnapshots( $fname = __METHOD__ ); + public function flushReplicaSnapshots( $fname = __METHOD__, $owner = null ); /** * Commit all master DB transactions so as to flush any REPEATABLE-READ or SSI snapshots @@ -594,8 +615,9 @@ interface ILoadBalancer { * An error will be thrown if a connection has pending writes or callbacks * * @param string $fname Caller name + * @param int|null $owner ID of the calling instance (e.g. the LBFactory ID) */ - public function flushMasterSnapshots( $fname = __METHOD__ ); + public function flushMasterSnapshots( $fname = __METHOD__, $owner = null ); /** * @return bool Whether a master connection is already open @@ -650,10 +672,9 @@ interface ILoadBalancer { /** * @note This method may trigger a DB connection if not yet done * @param string|bool $domain DB domain ID or false for the local domain - * @param IDatabase|null $conn DB master connection; used to avoid loops [optional] * @return string|bool Reason the master is read-only or false if it is not */ - public function getReadOnlyReason( $domain = false, IDatabase $conn = null ); + public function getReadOnlyReason( $domain = false ); /** * Disables/enables lag checks diff --git a/includes/libs/rdbms/loadbalancer/LoadBalancer.php b/includes/libs/rdbms/loadbalancer/LoadBalancer.php index 98607ce807..39d1bd6864 100644 --- a/includes/libs/rdbms/loadbalancer/LoadBalancer.php +++ b/includes/libs/rdbms/loadbalancer/LoadBalancer.php @@ -68,7 +68,9 @@ class LoadBalancer implements ILoadBalancer { /** @var DatabaseDomain Local DB domain ID and default for selectDB() calls */ private $localDomain; - /** @var Database[][][] Map of (connection category => server index => IDatabase[]) */ + /** + * @var IDatabase[][][]|Database[][][] Map of (connection category => server index => IDatabase[]) + */ private $conns; /** @var array[] Map of (server index => server config array) */ @@ -99,9 +101,15 @@ class LoadBalancer implements ILoadBalancer { private $tableAliases = []; /** @var string[] Map of (index alias => index) */ private $indexAliases = []; - /** @var array[] Map of (name => callable) */ + /** @var callable[] Map of (name => callable) */ private $trxRecurringCallbacks = []; + /** @var bool[] Map of (domain => whether to use "temp tables only" mode) */ + private $tempTablesOnlyMode = []; + /** @var string|bool Explicit DBO_TRX transaction round active or false if none */ + private $trxRoundId = false; + /** @var string Stage of the current transaction round in the transaction round life-cycle */ + private $trxRoundStage = self::ROUND_CURSORY; /** @var Database Connection handle that caused a problem */ private $errorConnection; /** @var int[] The group replica server indexes keyed by group */ @@ -121,12 +129,10 @@ class LoadBalancer implements ILoadBalancer { /** @var bool Whether any connection has been attempted yet */ private $connectionAttempted = false; + /** var int An identifier for this class instance */ + private $id; /** @var int|null Integer ID of the managing LBFactory instance or null if none */ private $ownerId; - /** @var string|bool Explicit DBO_TRX transaction round active or false if none */ - private $trxRoundId = false; - /** @var string Stage of the current transaction round in the transaction round life-cycle */ - private $trxRoundStage = self::ROUND_CURSORY; /** @var int Warn when this many connection are held */ const CONN_HELD_WARN_THRESHOLD = 10; @@ -214,7 +220,6 @@ class LoadBalancer implements ILoadBalancer { $this->deprecationLogger = $params['deprecationLogger'] ?? function ( $msg ) { trigger_error( $msg, E_USER_DEPRECATED ); }; - foreach ( [ 'replLogger', 'connLogger', 'queryLogger', 'perfLogger' ] as $key ) { $this->$key = $params[$key] ?? new NullLogger(); } @@ -238,6 +243,8 @@ class LoadBalancer implements ILoadBalancer { $group = $params['defaultGroup'] ?? self::GROUP_GENERIC; $this->defaultGroup = isset( $this->groupLoads[$group] ) ? $group : self::GROUP_GENERIC; + static $nextId; + $this->id = $nextId = ( is_int( $nextId ) ? $nextId++ : mt_rand() ); $this->ownerId = $params['ownerId'] ?? null; } @@ -297,9 +304,10 @@ class LoadBalancer implements ILoadBalancer { /** * @param int $flags Bitfield of class CONN_* constants * @param int $i Specific server index or DB_MASTER/DB_REPLICA + * @param string $domain Database domain * @return int Sanitized bitfield */ - private function sanitizeConnectionFlags( $flags, $i ) { + private function sanitizeConnectionFlags( $flags, $i, $domain ) { // Whether an outside caller is explicitly requesting the master database server if ( $i === self::DB_MASTER || $i === $this->getWriterIndex() ) { $flags |= self::CONN_INTENT_WRITABLE; @@ -320,6 +328,12 @@ class LoadBalancer implements ILoadBalancer { $flags &= ~self::CONN_TRX_AUTOCOMMIT; $type = $this->getServerType( $this->getWriterIndex() ); $this->connLogger->info( __METHOD__ . ": CONN_TRX_AUTOCOMMIT disallowed ($type)" ); + } elseif ( isset( $this->tempTablesOnlyMode[$domain] ) ) { + // T202116: integration tests are active and queries should be all be using + // temporary clone tables (via prefix). Such tables are not visible accross + // different connections nor can there be REPEATABLE-READ snapshot staleness, + // so use the same connection for everything. + $flags &= ~self::CONN_TRX_AUTOCOMMIT; } } @@ -592,8 +606,7 @@ class LoadBalancer implements ILoadBalancer { $this->connLogger->debug( __METHOD__ . ": Using reader #$i: $serverName..." ); // Get a connection to this server without triggering other server connections - $flags = self::CONN_SILENCE_ERRORS; - $conn = $this->getServerConnection( $i, $domain, $flags ); + $conn = $this->getServerConnection( $i, $domain, self::CONN_SILENCE_ERRORS ); if ( !$conn ) { $this->connLogger->warning( __METHOD__ . ": Failed connecting to $i/$domain" ); unset( $currentLoads[$i] ); // avoid this server next iteration @@ -875,7 +888,7 @@ class LoadBalancer implements ILoadBalancer { public function getConnection( $i, $groups = [], $domain = false, $flags = 0 ) { $domain = $this->resolveDomainID( $domain ); $groups = $this->resolveGroups( $groups, $i ); - $flags = $this->sanitizeConnectionFlags( $flags, $i ); + $flags = $this->sanitizeConnectionFlags( $flags, $i, $domain ); // If given DB_MASTER/DB_REPLICA, resolve it to a specific server index. Resolving // DB_REPLICA might trigger getServerConnection() calls due to the getReaderIndex() // connectivity checks or LoadMonitor::scaleLoads() server state cache regeneration. @@ -884,7 +897,11 @@ class LoadBalancer implements ILoadBalancer { // Get an open connection to that server (might trigger a new connection) $conn = $this->getServerConnection( $serverIndex, $domain, $flags ); // Set master DB handles as read-only if there is high replication lag - if ( $serverIndex === $this->getWriterIndex() && $this->getLaggedReplicaMode( $domain ) ) { + if ( + $serverIndex === $this->getWriterIndex() && + $this->getLaggedReplicaMode( $domain ) && + !is_string( $conn->getLBInfo( 'readOnlyReason' ) ) + ) { $reason = ( $this->getExistingReaderIndex( self::GROUP_GENERIC ) >= 0 ) ? 'The database is read-only until replication lag decreases.' : 'The database is read-only until replica database servers becomes reachable.'; @@ -940,15 +957,15 @@ class LoadBalancer implements ILoadBalancer { // or the master database server is running in server-side read-only mode. Note that // replica DB handles are always read-only via Database::assertIsWritableMaster(). // Read-only mode due to replication lag is *avoided* here to avoid recursion. - if ( $conn->getLBInfo( 'serverIndex' ) === $this->getWriterIndex() ) { + if ( $i === $this->getWriterIndex() ) { if ( $this->readOnlyReason !== false ) { - $conn->setLBInfo( 'readOnlyReason', $this->readOnlyReason ); - } elseif ( $this->masterRunningReadOnly( $domain, $conn ) ) { - $conn->setLBInfo( - 'readOnlyReason', - 'The master database server is running in read-only mode.' - ); + $readOnlyReason = $this->readOnlyReason; + } elseif ( $this->isMasterConnectionReadOnly( $conn, $flags ) ) { + $readOnlyReason = 'The master database server is running in read-only mode.'; + } else { + $readOnlyReason = false; } + $conn->setLBInfo( 'readOnlyReason', $readOnlyReason ); } return $conn; @@ -958,17 +975,7 @@ class LoadBalancer implements ILoadBalancer { $serverIndex = $conn->getLBInfo( 'serverIndex' ); $refCount = $conn->getLBInfo( 'foreignPoolRefCount' ); if ( $serverIndex === null || $refCount === null ) { - /** - * This can happen in code like: - * foreach ( $dbs as $db ) { - * $conn = $lb->getConnection( $lb::DB_REPLICA, [], $db ); - * ... - * $lb->reuseConnection( $conn ); - * } - * When a connection to the local DB is opened in this way, reuseConnection() - * should be ignored - */ - return; + return; // non-foreign connection; no domain-use tracking to update } elseif ( $conn instanceof DBConnRef ) { // DBConnRef already handles calling reuseConnection() and only passes the live // Database instance to this method. Any caller passing in a DBConnRef is broken. @@ -1297,44 +1304,49 @@ class LoadBalancer implements ILoadBalancer { // Use DBO_DEFAULT flags by default for LoadBalancer managed databases. Assume that the // application calls LoadBalancer::commitMasterChanges() before the PHP script completes. $server['flags'] = $server['flags'] ?? IDatabase::DBO_DEFAULT; + $server['ownerId'] = $this->id; // Create a live connection object + $conn = Database::factory( $server['type'], $server, Database::NEW_UNCONNECTED ); + $conn->setLBInfo( $server ); + $conn->setLazyMasterHandle( + $this->getLazyConnectionRef( self::DB_MASTER, [], $conn->getDomainID() ) + ); + $conn->setTableAliases( $this->tableAliases ); + $conn->setIndexAliases( $this->indexAliases ); + try { - $db = Database::factory( $server['type'], $server ); - // Log when many connection are made on requests + $conn->initConnection(); ++$this->connectionCounter; - $currentConnCount = $this->getCurrentConnectionCount(); - if ( $currentConnCount >= self::CONN_HELD_WARN_THRESHOLD ) { - $this->perfLogger->warning( - __METHOD__ . ": {connections}+ connections made (master={masterdb})", - [ 'connections' => $currentConnCount, 'masterdb' => $masterName ] - ); - } } catch ( DBConnectionError $e ) { - // FIXME: This is probably the ugliest thing I have ever done to - // PHP. I'm half-expecting it to segfault, just out of disgust. -- TS - $db = $e->db; + // ignore; let the DB handle the logging } - $db->setLBInfo( $server ); - $db->setLazyMasterHandle( - $this->getLazyConnectionRef( self::DB_MASTER, [], $db->getDomainID() ) - ); - $db->setTableAliases( $this->tableAliases ); - $db->setIndexAliases( $this->indexAliases ); - if ( $server['serverIndex'] === $this->getWriterIndex() ) { if ( $this->trxRoundId !== false ) { - $this->applyTransactionRoundFlags( $db ); + $this->applyTransactionRoundFlags( $conn ); } foreach ( $this->trxRecurringCallbacks as $name => $callback ) { - $db->setTransactionListener( $name, $callback ); + $conn->setTransactionListener( $name, $callback ); } } $this->lazyLoadReplicationPositions(); // session consistency - return $db; + // Log when many connection are made on requests + $count = $this->getCurrentConnectionCount(); + if ( $count >= self::CONN_HELD_WARN_THRESHOLD ) { + $this->perfLogger->warning( + __METHOD__ . ": {connections}+ connections made (master={masterdb})", + [ + 'connections' => $count, + 'dbserver' => $conn->getServer(), + 'masterdb' => $conn->getLBInfo( 'clusterMasterHost' ) + ] + ); + } + + return $conn; } /** @@ -1490,18 +1502,22 @@ class LoadBalancer implements ILoadBalancer { return $highestPos; } - public function disable() { - $this->closeAll(); + public function disable( $fname = __METHOD__, $owner = null ) { + $this->assertOwnership( $fname, $owner ); + $this->closeAll( $fname, $owner ); $this->disabled = true; } - public function closeAll() { - $fname = __METHOD__; + public function closeAll( $fname = __METHOD__, $owner = null ) { + $this->assertOwnership( $fname, $owner ); + if ( $this->ownerId === null ) { + /** @noinspection PhpUnusedLocalVariableInspection */ + $scope = ScopedCallback::newScopedIgnoreUserAbort(); + } $this->forEachOpenConnection( function ( IDatabase $conn ) use ( $fname ) { $host = $conn->getServer(); - $this->connLogger->debug( - $fname . ": closing connection to database '$host'." ); - $conn->close(); + $this->connLogger->debug( "$fname: closing connection to database '$host'." ); + $conn->close( $fname, $this->id ); } ); $this->conns = self::newTrackedConnectionsArray(); @@ -1530,18 +1546,22 @@ class LoadBalancer implements ILoadBalancer { } } - $conn->close(); + $conn->close( __METHOD__ ); } public function commitAll( $fname = __METHOD__, $owner = null ) { $this->commitMasterChanges( $fname, $owner ); - $this->flushMasterSnapshots( $fname ); - $this->flushReplicaSnapshots( $fname ); + $this->flushMasterSnapshots( $fname, $owner ); + $this->flushReplicaSnapshots( $fname, $owner ); } public function finalizeMasterChanges( $fname = __METHOD__, $owner = null ) { $this->assertOwnership( $fname, $owner ); $this->assertTransactionRoundStage( [ self::ROUND_CURSORY, self::ROUND_FINALIZED ] ); + if ( $this->ownerId === null ) { + /** @noinspection PhpUnusedLocalVariableInspection */ + $scope = ScopedCallback::newScopedIgnoreUserAbort(); + } $this->trxRoundStage = self::ROUND_ERROR; // "failed" until proven otherwise // Loop until callbacks stop adding callbacks on other connections @@ -1567,6 +1587,10 @@ class LoadBalancer implements ILoadBalancer { public function approveMasterChanges( array $options, $fname = __METHOD__, $owner = null ) { $this->assertOwnership( $fname, $owner ); $this->assertTransactionRoundStage( self::ROUND_FINALIZED ); + if ( $this->ownerId === null ) { + /** @noinspection PhpUnusedLocalVariableInspection */ + $scope = ScopedCallback::newScopedIgnoreUserAbort(); + } $limit = $options['maxWriteDuration'] ?? 0; @@ -1607,9 +1631,13 @@ class LoadBalancer implements ILoadBalancer { ); } $this->assertTransactionRoundStage( self::ROUND_CURSORY ); + if ( $this->ownerId === null ) { + /** @noinspection PhpUnusedLocalVariableInspection */ + $scope = ScopedCallback::newScopedIgnoreUserAbort(); + } // Clear any empty transactions (no writes/callbacks) from the implicit round - $this->flushMasterSnapshots( $fname ); + $this->flushMasterSnapshots( $fname, $owner ); $this->trxRoundId = $fname; $this->trxRoundStage = self::ROUND_ERROR; // "failed" until proven otherwise @@ -1626,12 +1654,13 @@ class LoadBalancer implements ILoadBalancer { public function commitMasterChanges( $fname = __METHOD__, $owner = null ) { $this->assertOwnership( $fname, $owner ); $this->assertTransactionRoundStage( self::ROUND_APPROVED ); + if ( $this->ownerId === null ) { + /** @noinspection PhpUnusedLocalVariableInspection */ + $scope = ScopedCallback::newScopedIgnoreUserAbort(); + } $failures = []; - /** @noinspection PhpUnusedLocalVariableInspection */ - $scope = ScopedCallback::newScopedIgnoreUserAbort(); // try to ignore client aborts - $restore = ( $this->trxRoundId !== false ); $this->trxRoundId = false; $this->trxRoundStage = self::ROUND_ERROR; // "failed" until proven otherwise @@ -1674,6 +1703,10 @@ class LoadBalancer implements ILoadBalancer { "Transaction should be in the callback stage (not '{$this->trxRoundStage}')" ); } + if ( $this->ownerId === null ) { + /** @noinspection PhpUnusedLocalVariableInspection */ + $scope = ScopedCallback::newScopedIgnoreUserAbort(); + } $oldStage = $this->trxRoundStage; $this->trxRoundStage = self::ROUND_ERROR; // "failed" until proven otherwise @@ -1708,7 +1741,7 @@ class LoadBalancer implements ILoadBalancer { $this->queryLogger->warning( $fname . ": found writes pending." ); $fnames = implode( ', ', $conn->pendingWriteAndCallbackCallers() ); $this->queryLogger->warning( - $fname . ": found writes pending ($fnames).", + "$fname: found writes pending ($fnames).", [ 'db_server' => $conn->getServer(), 'db_name' => $conn->getDBname() @@ -1717,7 +1750,7 @@ class LoadBalancer implements ILoadBalancer { } elseif ( $conn->trxLevel() ) { // A callback from another handle read from this one and DBO_TRX is set, // which can easily happen if there is only one DB (no replicas) - $this->queryLogger->debug( $fname . ": found empty transaction." ); + $this->queryLogger->debug( "$fname: found empty transaction." ); } try { $conn->commit( $fname, $conn::FLUSHING_ALL_PEERS ); @@ -1744,6 +1777,10 @@ class LoadBalancer implements ILoadBalancer { "Transaction should be in the callback stage (not '{$this->trxRoundStage}')" ); } + if ( $this->ownerId === null ) { + /** @noinspection PhpUnusedLocalVariableInspection */ + $scope = ScopedCallback::newScopedIgnoreUserAbort(); + } $e = null; @@ -1762,6 +1799,10 @@ class LoadBalancer implements ILoadBalancer { public function rollbackMasterChanges( $fname = __METHOD__, $owner = null ) { $this->assertOwnership( $fname, $owner ); + if ( $this->ownerId === null ) { + /** @noinspection PhpUnusedLocalVariableInspection */ + $scope = ScopedCallback::newScopedIgnoreUserAbort(); + } $restore = ( $this->trxRoundId !== false ); $this->trxRoundId = false; @@ -1800,15 +1841,22 @@ class LoadBalancer implements ILoadBalancer { } /** + * Assure that if this instance is owned, the caller is either the owner or is internal + * + * If an LBFactory owns the LoadBalancer, then certain methods should only called through + * that LBFactory to avoid broken contracts. Otherwise, those methods can publically be + * called by anything. In any case, internal methods from the LoadBalancer itself should + * always be allowed. + * * @param string $fname * @param int|null $owner Owner ID of the caller * @throws DBTransactionError */ private function assertOwnership( $fname, $owner ) { - if ( $this->ownerId !== null && $owner !== $this->ownerId ) { + if ( $this->ownerId !== null && $owner !== $this->ownerId && $owner !== $this->id ) { throw new DBTransactionError( null, - "$fname: LoadBalancer is owned by LBFactory #{$this->ownerId} (got '$owner')." + "$fname: LoadBalancer is owned by ID '{$this->ownerId}' (got '$owner')." ); } } @@ -1855,13 +1903,15 @@ class LoadBalancer implements ILoadBalancer { } } - public function flushReplicaSnapshots( $fname = __METHOD__ ) { + public function flushReplicaSnapshots( $fname = __METHOD__, $owner = null ) { + $this->assertOwnership( $fname, $owner ); $this->forEachOpenReplicaConnection( function ( IDatabase $conn ) use ( $fname ) { $conn->flushSnapshot( $fname ); } ); } - public function flushMasterSnapshots( $fname = __METHOD__ ) { + public function flushMasterSnapshots( $fname = __METHOD__, $owner = null ) { + $this->assertOwnership( $fname, $owner ); $this->forEachOpenMasterConnection( function ( IDatabase $conn ) use ( $fname ) { $conn->flushSnapshot( $fname ); } ); @@ -1919,13 +1969,8 @@ class LoadBalancer implements ILoadBalancer { } if ( $this->hasStreamingReplicaServers() ) { - try { - // Set "laggedReplicaMode" - $this->getReaderIndex( self::GROUP_GENERIC, $domain ); - } catch ( DBConnectionError $e ) { - // Sanity: avoid expensive re-connect attempts and failures - $this->laggedReplicaMode = true; - } + // This will set "laggedReplicaMode" as needed + $this->getReaderIndex( self::GROUP_GENERIC, $domain ); } return $this->laggedReplicaMode; @@ -1935,10 +1980,12 @@ class LoadBalancer implements ILoadBalancer { return $this->laggedReplicaMode; } - public function getReadOnlyReason( $domain = false, IDatabase $conn = null ) { + public function getReadOnlyReason( $domain = false ) { + $domainInstance = DatabaseDomain::newFromId( $this->resolveDomainID( $domain ) ); + if ( $this->readOnlyReason !== false ) { return $this->readOnlyReason; - } elseif ( $this->masterRunningReadOnly( $domain, $conn ) ) { + } elseif ( $this->isMasterRunningReadOnly( $domainInstance ) ) { return 'The master database server is running in read-only mode.'; } elseif ( $this->getLaggedReplicaMode( $domain ) ) { return ( $this->getExistingReaderIndex( self::GROUP_GENERIC ) >= 0 ) @@ -1950,26 +1997,68 @@ class LoadBalancer implements ILoadBalancer { } /** - * @param string $domain Domain ID, or false for the current domain - * @param IDatabase|null $conn DB master connectionl used to avoid loops [optional] - * @return bool + * @param IDatabase $conn Master connection + * @param int $flags Bitfield of class CONN_* constants + * @return bool Whether the entire server or currently selected DB/schema is read-only */ - private function masterRunningReadOnly( $domain, IDatabase $conn = null ) { - $cache = $this->wanCache; - $masterServer = $this->getServerName( $this->getWriterIndex() ); + private function isMasterConnectionReadOnly( IDatabase $conn, $flags = 0 ) { + // Note that table prefixes are not related to server-side read-only mode + $key = $this->srvCache->makeGlobalKey( + 'rdbms-server-readonly', + $conn->getServer(), + $conn->getDBname(), + $conn->dbSchema() + ); - return (bool)$cache->getWithSetCallback( - $cache->makeGlobalKey( __CLASS__, 'server-read-only', $masterServer ), + if ( ( $flags & self::CONN_REFRESH_READ_ONLY ) == self::CONN_REFRESH_READ_ONLY ) { + try { + $readOnly = (int)$conn->serverIsReadOnly(); + } catch ( DBError $e ) { + $readOnly = 0; + } + $this->srvCache->set( $key, $readOnly, BagOStuff::TTL_PROC_SHORT ); + } else { + $readOnly = $this->srvCache->getWithSetCallback( + $key, + BagOStuff::TTL_PROC_SHORT, + function () use ( $conn ) { + try { + return (int)$conn->serverIsReadOnly(); + } catch ( DBError $e ) { + return 0; + } + } + ); + } + + return (bool)$readOnly; + } + + /** + * @param DatabaseDomain $domain + * @return bool Whether the entire master server or the local domain DB is read-only + */ + private function isMasterRunningReadOnly( DatabaseDomain $domain ) { + // Context will often be HTTP GET/HEAD; heavily cache the results + return (bool)$this->wanCache->getWithSetCallback( + // Note that table prefixes are not related to server-side read-only mode + $this->wanCache->makeGlobalKey( + 'rdbms-server-readonly', + $this->getMasterServerName(), + $domain->getDatabase(), + $domain->getSchema() + ), self::TTL_CACHE_READONLY, - function () use ( $domain, $conn ) { + function () use ( $domain ) { $old = $this->trxProfiler->setSilenced( true ); try { $index = $this->getWriterIndex(); - $dbw = $conn ?: $this->getServerConnection( $index, $domain ); - $readOnly = (int)$dbw->serverIsReadOnly(); - if ( !$conn ) { - $this->reuseConnection( $dbw ); - } + // Reset the cache for isMasterConnectionReadOnly() + $flags = self::CONN_REFRESH_READ_ONLY; + $conn = $this->getServerConnection( $index, $domain->getId(), $flags ); + // Reuse the process cache set above + $readOnly = (int)$this->isMasterConnectionReadOnly( $conn ); + $this->reuseConnection( $conn ); } catch ( DBError $e ) { $readOnly = 0; } @@ -1977,7 +2066,7 @@ class LoadBalancer implements ILoadBalancer { return $readOnly; }, - [ 'pcTTL' => $cache::TTL_PROC_LONG, 'busyValue' => 0 ] + [ 'pcTTL' => WANObjectCache::TTL_PROC_LONG, 'lockTSE' => 10, 'busyValue' => 0 ] ); } @@ -2232,19 +2321,30 @@ class LoadBalancer implements ILoadBalancer { ) ); // Update the prefix for all local connections... - $this->forEachOpenConnection( function ( IDatabase $db ) use ( $prefix ) { - if ( !$db->getLBInfo( 'foreign' ) ) { - $db->tablePrefix( $prefix ); + $this->forEachOpenConnection( function ( IDatabase $conn ) use ( $prefix ) { + if ( !$conn->getLBInfo( 'foreign' ) ) { + $conn->tablePrefix( $prefix ); } } ); } public function redefineLocalDomain( $domain ) { - $this->closeAll(); + $this->closeAll( __METHOD__, $this->id ); $this->setLocalDomain( DatabaseDomain::newFromId( $domain ) ); } + public function setTempTablesOnlyMode( $value, $domain ) { + $old = $this->tempTablesOnlyMode[$domain] ?? false; + if ( $value ) { + $this->tempTablesOnlyMode[$domain] = true; + } else { + unset( $this->tempTablesOnlyMode[$domain] ); + } + + return $old; + } + /** * @param DatabaseDomain $domain */ @@ -2282,9 +2382,16 @@ class LoadBalancer implements ILoadBalancer { return $this->servers[$i]; } + /** + * @return string + */ + private function getMasterServerName() { + return $this->getServerName( $this->getWriterIndex() ); + } + function __destruct() { // Avoid connection leaks for sanity - $this->disable(); + $this->disable( __METHOD__, $this->ownerId ); } } diff --git a/includes/libs/rdbms/loadmonitor/LoadMonitor.php b/includes/libs/rdbms/loadmonitor/LoadMonitor.php index 19550f42a5..c3f8879217 100644 --- a/includes/libs/rdbms/loadmonitor/LoadMonitor.php +++ b/includes/libs/rdbms/loadmonitor/LoadMonitor.php @@ -107,6 +107,7 @@ class LoadMonitor implements ILoadMonitor { $key = $this->getCacheKey( $serverIndexes ); # Randomize TTLs to reduce stampedes (4.0 - 5.0 sec) + // @phan-suppress-next-line PhanTypeMismatchArgumentInternal $ttl = mt_rand( 4e6, 5e6 ) / 1e6; # Keep keys around longer as fallbacks $staleTTL = 60; diff --git a/includes/libs/redis/RedisConnectionPool.php b/includes/libs/redis/RedisConnectionPool.php index eb645cc149..343e35c41b 100644 --- a/includes/libs/redis/RedisConnectionPool.php +++ b/includes/libs/redis/RedisConnectionPool.php @@ -90,6 +90,10 @@ class RedisConnectionPool implements LoggerAwareInterface { if ( !isset( $options['serializer'] ) || $options['serializer'] === 'php' ) { $this->serializer = Redis::SERIALIZER_PHP; } elseif ( $options['serializer'] === 'igbinary' ) { + if ( !defined( 'Redis::SERIALIZER_IGBINARY' ) ) { + throw new InvalidArgumentException( + __CLASS__ . ': configured serializer "igbinary" not available' ); + } $this->serializer = Redis::SERIALIZER_IGBINARY; } elseif ( $options['serializer'] === 'none' ) { $this->serializer = Redis::SERIALIZER_NONE; diff --git a/includes/libs/services/ServiceContainer.php b/includes/libs/services/ServiceContainer.php index d1f10524d5..84755edb90 100644 --- a/includes/libs/services/ServiceContainer.php +++ b/includes/libs/services/ServiceContainer.php @@ -6,6 +6,7 @@ use InvalidArgumentException; use Psr\Container\ContainerInterface; use RuntimeException; use Wikimedia\Assert\Assert; +use Wikimedia\ScopedCallback; /** * Generic service container. @@ -77,6 +78,11 @@ class ServiceContainer implements ContainerInterface, DestructibleService { */ private $destroyed = false; + /** + * @var array Set of services currently being created, to detect loops + */ + private $servicesBeingCreated = []; + /** * @param array $extraInstantiationParams Any additional parameters to be passed to the * instantiator function when creating a service. This is typically used to provide @@ -433,10 +439,20 @@ class ServiceContainer implements ContainerInterface, DestructibleService { * @param string $name * * @throws InvalidArgumentException if $name is not a known service. + * @throws RuntimeException if a circular dependency is detected. * @return object */ private function createService( $name ) { if ( isset( $this->serviceInstantiators[$name] ) ) { + if ( isset( $this->servicesBeingCreated[$name] ) ) { + throw new RuntimeException( "Circular dependency when creating service! " . + implode( ' -> ', array_keys( $this->servicesBeingCreated ) ) . " -> $name" ); + } + $this->servicesBeingCreated[$name] = true; + $removeFromStack = new ScopedCallback( function () use ( $name ) { + unset( $this->servicesBeingCreated[$name] ); + } ); + $service = ( $this->serviceInstantiators[$name] )( $this, ...$this->extraInstantiationParams @@ -458,6 +474,8 @@ class ServiceContainer implements ContainerInterface, DestructibleService { } } + $removeFromStack->consume(); + // NOTE: when adding more wiring logic here, make sure importWiring() is kept in sync! } else { throw new NoSuchServiceException( $name ); diff --git a/includes/libs/stats/SamplingStatsdClient.php b/includes/libs/stats/SamplingStatsdClient.php index 6494c26302..172ead3812 100644 --- a/includes/libs/stats/SamplingStatsdClient.php +++ b/includes/libs/stats/SamplingStatsdClient.php @@ -84,7 +84,7 @@ class SamplingStatsdClient extends StatsdClient { $data = [ $data ]; } if ( !$data ) { - return; + return 0; } foreach ( $data as $item ) { if ( !( $item instanceof StatsdDataInterface ) ) { @@ -109,7 +109,7 @@ class SamplingStatsdClient extends StatsdClient { try { $fp = $this->getSender()->open(); if ( !$fp ) { - return; + return 0; } foreach ( $data as $message ) { $written += $this->getSender()->write( $fp, $message ); diff --git a/includes/logging/BlockLogFormatter.php b/includes/logging/BlockLogFormatter.php index ead290f062..d49e3c5710 100644 --- a/includes/logging/BlockLogFormatter.php +++ b/includes/logging/BlockLogFormatter.php @@ -22,6 +22,8 @@ * @since 1.25 */ +use MediaWiki\MediaWikiServices; + /** * This class formats block log entries. * @@ -138,7 +140,9 @@ class BlockLogFormatter extends LogFormatter { $linkRenderer = $this->getLinkRenderer(); if ( $this->entry->isDeleted( LogPage::DELETED_ACTION ) // Action is hidden || !( $subtype === 'block' || $subtype === 'reblock' ) - || !$this->context->getUser()->isAllowed( 'block' ) + || !MediaWikiServices::getInstance() + ->getPermissionManager() + ->userHasRight( $this->context->getUser(), 'block' ) ) { return ''; } @@ -262,6 +266,10 @@ class BlockLogFormatter extends LogFormatter { return $params; } + /** + * @inheritDoc + * @suppress PhanTypeInvalidDimOffset + */ public function formatParametersForApi() { $ret = parent::formatParametersForApi(); if ( isset( $ret['flags'] ) ) { diff --git a/includes/logging/ContentModelLogFormatter.php b/includes/logging/ContentModelLogFormatter.php index e05357cd68..23c582c30f 100644 --- a/includes/logging/ContentModelLogFormatter.php +++ b/includes/logging/ContentModelLogFormatter.php @@ -1,5 +1,7 @@ context->getLanguage(); @@ -12,7 +14,9 @@ class ContentModelLogFormatter extends LogFormatter { public function getActionLinks() { if ( $this->entry->isDeleted( LogPage::DELETED_ACTION ) // Action is hidden || $this->entry->getSubtype() !== 'change' - || !$this->context->getUser()->isAllowed( 'editcontentmodel' ) + || !MediaWikiServices::getInstance() + ->getPermissionManager() + ->userHasRight( $this->context->getUser(), 'editcontentmodel' ) ) { return ''; } diff --git a/includes/logging/DeleteLogFormatter.php b/includes/logging/DeleteLogFormatter.php index 3bc19ffd37..565a65f11b 100644 --- a/includes/logging/DeleteLogFormatter.php +++ b/includes/logging/DeleteLogFormatter.php @@ -23,7 +23,8 @@ * @since 1.22 */ -use MediaWiki\Storage\RevisionRecord; +use MediaWiki\MediaWikiServices; +use MediaWiki\Revision\RevisionRecord; /** * This class formats delete log entries. @@ -31,6 +32,12 @@ use MediaWiki\Storage\RevisionRecord; * @since 1.19 */ class DeleteLogFormatter extends LogFormatter { + /** @var array|null */ + private $parsedParametersDeleteLog; + + /** + * @inheritDoc + */ protected function getMessageKey() { $key = parent::getMessageKey(); if ( in_array( $this->entry->getSubtype(), [ 'event', 'revision' ] ) ) { @@ -50,8 +57,11 @@ class DeleteLogFormatter extends LogFormatter { return $key; } + /** + * @inheritDoc + */ protected function getMessageParameters() { - if ( isset( $this->parsedParametersDeleteLog ) ) { + if ( $this->parsedParametersDeleteLog !== null ) { return $this->parsedParametersDeleteLog; } @@ -136,7 +146,8 @@ class DeleteLogFormatter extends LogFormatter { public function getActionLinks() { $user = $this->context->getUser(); $linkRenderer = $this->getLinkRenderer(); - if ( !$user->isAllowed( 'deletedhistory' ) + $permissionManager = MediaWikiServices::getInstance()->getPermissionManager(); + if ( !$permissionManager->userHasRight( $user, 'deletedhistory' ) || $this->entry->isDeleted( LogPage::DELETED_ACTION ) ) { return ''; @@ -145,7 +156,7 @@ class DeleteLogFormatter extends LogFormatter { switch ( $this->entry->getSubtype() ) { case 'delete': // Show undelete link case 'delete_redir': - if ( $user->isAllowed( 'undelete' ) ) { + if ( $permissionManager->userHasRight( $user, 'undelete' ) ) { $message = 'undeletelink'; } else { $message = 'undeleteviewlink'; diff --git a/includes/logging/LogEntry.php b/includes/logging/LogEntry.php index 17f72bd574..ce68a91897 100644 --- a/includes/logging/LogEntry.php +++ b/includes/logging/LogEntry.php @@ -59,7 +59,7 @@ interface LogEntry { public function getParameters(); /** - * Get the user for performed this action. + * Get the user who performed this action. * * @return User */ diff --git a/includes/logging/LogEntryBase.php b/includes/logging/LogEntryBase.php index 170fc2975f..4fff1de3a2 100644 --- a/includes/logging/LogEntryBase.php +++ b/includes/logging/LogEntryBase.php @@ -64,7 +64,7 @@ abstract class LogEntryBase implements LogEntry { * * @since 1.26 * @param string $blob - * @return array + * @return array|false */ public static function extractParams( $blob ) { return unserialize( $blob ); diff --git a/includes/logging/LogEventsList.php b/includes/logging/LogEventsList.php index e66bd69cd5..2e7f065bf1 100644 --- a/includes/logging/LogEventsList.php +++ b/includes/logging/LogEventsList.php @@ -36,6 +36,7 @@ class LogEventsList extends ContextSource { /** * @var array + * @deprecated since 1.34, no longer used. */ protected $mDefaultQuery; @@ -220,21 +221,6 @@ class LogEventsList extends ContextSource { ]; } - private function getDefaultQuery() { - if ( !isset( $this->mDefaultQuery ) ) { - $this->mDefaultQuery = $this->getRequest()->getQueryValues(); - unset( $this->mDefaultQuery['title'] ); - unset( $this->mDefaultQuery['dir'] ); - unset( $this->mDefaultQuery['offset'] ); - unset( $this->mDefaultQuery['limit'] ); - unset( $this->mDefaultQuery['order'] ); - unset( $this->mDefaultQuery['month'] ); - unset( $this->mDefaultQuery['year'] ); - } - - return $this->mDefaultQuery; - } - /** * @param array $queryTypes * @return array Form descriptor @@ -247,7 +233,10 @@ class LogEventsList extends ContextSource { foreach ( LogPage::validTypes() as $type ) { $page = new LogPage( $type ); $restriction = $page->getRestriction(); - if ( $this->getUser()->isAllowed( $restriction ) ) { + if ( MediaWikiServices::getInstance() + ->getPermissionManager() + ->userHasRight( $this->getUser(), $restriction ) + ) { $typesByName[$type] = $page->getName()->text(); } } @@ -464,11 +453,12 @@ class LogEventsList extends ContextSource { } $del = ''; + $permissionManager = MediaWikiServices::getInstance()->getPermissionManager(); // Don't show useless checkbox to people who cannot hide log entries - if ( $user->isAllowed( 'deletedhistory' ) ) { - $canHide = $user->isAllowed( 'deletelogentry' ); - $canViewSuppressedOnly = $user->isAllowed( 'viewsuppressed' ) && - !$user->isAllowed( 'suppressrevision' ); + if ( $permissionManager->userHasRight( $user, 'deletedhistory' ) ) { + $canHide = $permissionManager->userHasRight( $user, 'deletelogentry' ); + $canViewSuppressedOnly = $permissionManager->userHasRight( $user, 'viewsuppressed' ) && + !$permissionManager->userHasRight( $user, 'suppressrevision' ); $entryIsSuppressed = self::isDeleted( $row, LogPage::DELETED_RESTRICTED ); $canViewThisSuppressedEntry = $canViewSuppressedOnly && $entryIsSuppressed; if ( $row->log_deleted || $canHide ) { @@ -522,7 +512,9 @@ class LogEventsList extends ContextSource { in_array( $row->log_action, $action ) : $row->log_action == $action; if ( $match && $right ) { global $wgUser; - $match = $wgUser->isAllowed( $right ); + $match = MediaWikiServices::getInstance() + ->getPermissionManager() + ->userHasRight( $wgUser, $right ); } } @@ -565,7 +557,9 @@ class LogEventsList extends ContextSource { } $permissionlist = implode( ', ', $permissions ); wfDebug( "Checking for $permissionlist due to $field match on $bitfield\n" ); - return $user->isAllowedAny( ...$permissions ); + return MediaWikiServices::getInstance() + ->getPermissionManager() + ->userHasAnyRight( $user, ...$permissions ); } return true; } @@ -584,7 +578,10 @@ class LogEventsList extends ContextSource { $user = $wgUser; } $logRestrictions = MediaWikiServices::getInstance()->getMainConfig()->get( 'LogRestrictions' ); - if ( isset( $logRestrictions[$type] ) && !$user->isAllowed( $logRestrictions[$type] ) ) { + if ( isset( $logRestrictions[$type] ) && !MediaWikiServices::getInstance() + ->getPermissionManager() + ->userHasRight( $user, $logRestrictions[$type] ) + ) { return false; } return true; @@ -795,7 +792,10 @@ class LogEventsList extends ContextSource { // Don't show private logs to unprivileged users foreach ( $wgLogRestrictions as $logType => $right ) { - if ( $audience == 'public' || !$user->isAllowed( $right ) ) { + if ( $audience == 'public' || !MediaWikiServices::getInstance() + ->getPermissionManager() + ->userHasRight( $user, $right ) + ) { $hiddenLogs[] = $logType; } } diff --git a/includes/logging/LogFormatter.php b/includes/logging/LogFormatter.php index 9e63ffee64..b4a682583c 100644 --- a/includes/logging/LogFormatter.php +++ b/includes/logging/LogFormatter.php @@ -163,7 +163,9 @@ class LogFormatter { $logRestrictions = $this->context->getConfig()->get( 'LogRestrictions' ); $type = $this->entry->getType(); return !isset( $logRestrictions[$type] ) - || $this->context->getUser()->isAllowed( $logRestrictions[$type] ); + || MediaWikiServices::getInstance() + ->getPermissionManager() + ->userHasRight( $this->context->getUser(), $logRestrictions[$type] ); } /** diff --git a/includes/logging/LogPage.php b/includes/logging/LogPage.php index 981aeb017d..d5f5de3000 100644 --- a/includes/logging/LogPage.php +++ b/includes/logging/LogPage.php @@ -94,8 +94,7 @@ class LogPage { $dbw = wfGetDB( DB_MASTER ); - // @todo FIXME private/protected/public property? - $this->timestamp = $now = wfTimestampNow(); + $now = wfTimestampNow(); $data = [ 'log_type' => $this->type, 'log_action' => $this->action, diff --git a/includes/logging/LogPager.php b/includes/logging/LogPager.php index 4ecc368d4e..0b78a36033 100644 --- a/includes/logging/LogPager.php +++ b/includes/logging/LogPager.php @@ -23,6 +23,8 @@ * @file */ +use MediaWiki\MediaWikiServices; + /** * @ingroup Pager */ @@ -51,6 +53,12 @@ class LogPager extends ReverseChronologicalPager { /** @var bool */ private $actionRestrictionsEnforced = false; + /** @var array */ + private $mConds; + + /** @var string */ + private $mTagFilter; + /** @var LogEventsList */ public $mLogEventsList; @@ -78,12 +86,13 @@ class LogPager extends ReverseChronologicalPager { $this->mLogEventsList = $list; $this->limitType( $types ); // also excludes hidden types + $this->limitLogId( $logId ); + $this->limitFilterTypes(); $this->limitPerformer( $performer ); $this->limitTitle( $title, $pattern ); $this->limitAction( $action ); $this->getDateCond( $year, $month, $day ); $this->mTagFilter = $tagFilter; - $this->limitLogId( $logId ); $this->mDb = wfGetDB( DB_REPLICA, 'logpager' ); } @@ -99,7 +108,18 @@ class LogPager extends ReverseChronologicalPager { return $query; } - // Call ONLY after calling $this->limitType() already! + private function limitFilterTypes() { + if ( $this->hasEqualsClause( 'log_id' ) ) { // T220834 + return; + } + $filterTypes = $this->getFilterParams(); + foreach ( $filterTypes as $type => $hide ) { + if ( $hide ) { + $this->mConds[] = 'log_type != ' . $this->mDb->addQuotes( $type ); + } + } + } + public function getFilterParams() { global $wgFilterLogTypes; $filters = []; @@ -119,9 +139,6 @@ class LogPager extends ReverseChronologicalPager { } $filters[$type] = $hide; - if ( $hide ) { - $this->mConds[] = 'log_type != ' . $this->mDb->addQuotes( $type ); - } } return $filters; @@ -144,7 +161,9 @@ class LogPager extends ReverseChronologicalPager { $needReindex = false; foreach ( $types as $type ) { if ( isset( $wgLogRestrictions[$type] ) - && !$user->isAllowed( $wgLogRestrictions[$type] ) + && !MediaWikiServices::getInstance() + ->getPermissionManager() + ->userHasRight( $user, $wgLogRestrictions[$type] ) ) { $needReindex = true; $types = array_diff( $types, [ $type ] ); @@ -256,7 +275,7 @@ class LogPager extends ReverseChronologicalPager { $params[] = $db->anyString(); } array_pop( $params ); // Get rid of the last % we added. - $this->mConds[] = 'log_title' . $db->buildLike( $params ); + $this->mConds[] = 'log_title' . $db->buildLike( ...$params ); } elseif ( $pattern && !$wgMiserMode ) { $this->mConds[] = 'log_title' . $db->buildLike( $title->getDBkey(), $db->anyString() ); $this->pattern = $pattern; @@ -342,6 +361,11 @@ class LogPager extends ReverseChronologicalPager { if ( !$this->mTagFilter && !array_key_exists( 'ls_field', $this->mConds ) ) { $options[] = 'STRAIGHT_JOIN'; } + if ( $this->performer !== '' ) { + // T223151: MariaDB's optimizer, at least 10.1, likes to choose a wildly bad plan for + // some reason for this code path. Tell it not to use the wrong index it wants to pick. + $options['IGNORE INDEX'] = [ 'logging' => [ 'times' ] ]; + } $info = [ 'tables' => $tables, @@ -460,9 +484,10 @@ class LogPager extends ReverseChronologicalPager { } $this->actionRestrictionsEnforced = true; $user = $this->getUser(); - if ( !$user->isAllowed( 'deletedhistory' ) ) { + $permissionManager = MediaWikiServices::getInstance()->getPermissionManager(); + if ( !$permissionManager->userHasRight( $user, 'deletedhistory' ) ) { $this->mConds[] = $this->mDb->bitAnd( 'log_deleted', LogPage::DELETED_ACTION ) . ' = 0'; - } elseif ( !$user->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) { + } elseif ( !$permissionManager->userHasAnyRight( $user, 'suppressrevision', 'viewsuppressed' ) ) { $this->mConds[] = $this->mDb->bitAnd( 'log_deleted', LogPage::SUPPRESSED_ACTION ) . ' != ' . LogPage::SUPPRESSED_USER; } @@ -478,9 +503,10 @@ class LogPager extends ReverseChronologicalPager { } $this->performerRestrictionsEnforced = true; $user = $this->getUser(); - if ( !$user->isAllowed( 'deletedhistory' ) ) { + $permissionManager = MediaWikiServices::getInstance()->getPermissionManager(); + if ( !$permissionManager->userHasRight( $user, 'deletedhistory' ) ) { $this->mConds[] = $this->mDb->bitAnd( 'log_deleted', LogPage::DELETED_USER ) . ' = 0'; - } elseif ( !$user->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) { + } elseif ( !$permissionManager->userHasAnyRight( $user, 'suppressrevision', 'viewsuppressed' ) ) { $this->mConds[] = $this->mDb->bitAnd( 'log_deleted', LogPage::SUPPRESSED_USER ) . ' != ' . LogPage::SUPPRESSED_ACTION; } diff --git a/includes/logging/ManualLogEntry.php b/includes/logging/ManualLogEntry.php index 1d0bbfd6fa..523a2f8ea1 100644 --- a/includes/logging/ManualLogEntry.php +++ b/includes/logging/ManualLogEntry.php @@ -191,18 +191,20 @@ class ManualLogEntry extends LogEntryBase implements Taggable { wfDebug( 'Overwriting existing ManualLogEntry tags' ); } $this->tags = []; - if ( $tags !== null ) { - $this->addTags( $tags ); - } + $this->addTags( $tags ); } /** * Add change tags for the log entry * * @since 1.33 - * @param string|string[] $tags Tags to apply + * @param string|string[]|null $tags Tags to apply */ public function addTags( $tags ) { + if ( $tags === null ) { + return; + } + if ( is_string( $tags ) ) { $tags = [ $tags ]; } @@ -251,8 +253,6 @@ class ManualLogEntry extends LogEntryBase implements Taggable { * @throws MWException */ public function insert( IDatabase $dbw = null ) { - global $wgActorTableSchemaMigrationStage; - $dbw = $dbw ?: wfGetDB( DB_MASTER ); if ( $this->timestamp === null ) { @@ -265,31 +265,6 @@ class ManualLogEntry extends LogEntryBase implements Taggable { $params = $this->getParameters(); $relations = $this->relations; - // Ensure actor relations are set - if ( ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_NEW ) && - empty( $relations['target_author_actor'] ) - ) { - $actorIds = []; - if ( !empty( $relations['target_author_id'] ) ) { - foreach ( $relations['target_author_id'] as $id ) { - $actorIds[] = User::newFromId( $id )->getActorId( $dbw ); - } - } - if ( !empty( $relations['target_author_ip'] ) ) { - foreach ( $relations['target_author_ip'] as $ip ) { - $actorIds[] = User::newFromName( $ip, false )->getActorId( $dbw ); - } - } - if ( $actorIds ) { - $relations['target_author_actor'] = $actorIds; - $params['authorActors'] = $actorIds; - } - } - if ( !( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_OLD ) ) { - unset( $relations['target_author_id'], $relations['target_author_ip'] ); - unset( $params['authorIds'], $params['authorIPs'] ); - } - // Additional fields for which there's no space in the database table schema $revId = $this->getAssociatedRevId(); if ( $revId ) { diff --git a/includes/logging/MergeLogFormatter.php b/includes/logging/MergeLogFormatter.php index 7a6fb9df48..925c976d7a 100644 --- a/includes/logging/MergeLogFormatter.php +++ b/includes/logging/MergeLogFormatter.php @@ -22,6 +22,8 @@ * @since 1.25 */ +use MediaWiki\MediaWikiServices; + /** * This class formats merge log entries. * @@ -47,7 +49,9 @@ class MergeLogFormatter extends LogFormatter { public function getActionLinks() { if ( $this->entry->isDeleted( LogPage::DELETED_ACTION ) // Action is hidden - || !$this->context->getUser()->isAllowed( 'mergehistory' ) + || !MediaWikiServices::getInstance() + ->getPermissionManager() + ->userHasRight( $this->context->getUser(), 'mergehistory' ) ) { return ''; } diff --git a/includes/logging/MoveLogFormatter.php b/includes/logging/MoveLogFormatter.php index 637a8e7821..6797f98f04 100644 --- a/includes/logging/MoveLogFormatter.php +++ b/includes/logging/MoveLogFormatter.php @@ -23,6 +23,8 @@ * @since 1.22 */ +use MediaWiki\MediaWikiServices; + /** * This class formats move log entries. * @@ -60,7 +62,9 @@ class MoveLogFormatter extends LogFormatter { public function getActionLinks() { if ( $this->entry->isDeleted( LogPage::DELETED_ACTION ) // Action is hidden || $this->entry->getSubtype() !== 'move' - || !$this->context->getUser()->isAllowed( 'move' ) + || !MediaWikiServices::getInstance() + ->getPermissionManager() + ->userHasRight( $this->context->getUser(), 'move' ) ) { return ''; } diff --git a/includes/logging/PatrolLog.php b/includes/logging/PatrolLog.php index d737a4b1e1..9392220eb8 100644 --- a/includes/logging/PatrolLog.php +++ b/includes/logging/PatrolLog.php @@ -63,7 +63,7 @@ class PatrolLog { $entry->setTarget( $rc->getTitle() ); $entry->setParameters( self::buildParams( $rc, $auto ) ); $entry->setPerformer( $user ); - $entry->setTags( $tags ); + $entry->addTags( $tags ); $logid = $entry->insert(); if ( !$auto ) { $entry->publish( $logid, 'udp' ); diff --git a/includes/logging/ProtectLogFormatter.php b/includes/logging/ProtectLogFormatter.php index ba02457319..6e3b26b17f 100644 --- a/includes/logging/ProtectLogFormatter.php +++ b/includes/logging/ProtectLogFormatter.php @@ -101,7 +101,10 @@ class ProtectLogFormatter extends LogFormatter { ]; // Show change protection link - if ( $this->context->getUser()->isAllowed( 'protect' ) ) { + if ( !MediaWikiServices::getInstance() + ->getPermissionManager() + ->userHasRight( $this->context->getUser(), 'protect' ) + ) { $links[] = $linkRenderer->makeKnownLink( $title, $this->msg( 'protect_change' )->text(), diff --git a/includes/mail/EmailNotification.php b/includes/mail/EmailNotification.php index 7361032937..4d176fb0bc 100644 --- a/includes/mail/EmailNotification.php +++ b/includes/mail/EmailNotification.php @@ -129,7 +129,10 @@ class EmailNotification { if ( $watchers === [] && !count( $wgUsersNotifiedOnAllChanges ) ) { $sendEmail = false; // Only send notification for non minor edits, unless $wgEnotifMinorEdits - if ( !$minorEdit || ( $wgEnotifMinorEdits && !$editor->isAllowed( 'nominornewtalk' ) ) ) { + if ( !$minorEdit || ( $wgEnotifMinorEdits && !MediaWikiServices::getInstance() + ->getPermissionManager() + ->userHasRight( $editor, 'nominornewtalk' ) ) + ) { $isUserTalkPage = ( $title->getNamespace() == NS_USER_TALK ); if ( $wgEnotifUserTalk && $isUserTalkPage @@ -173,13 +176,23 @@ class EmailNotification { * @param string $pageStatus * @throws MWException */ - public function actuallyNotifyOnPageChange( $editor, $title, $timestamp, $summary, $minorEdit, - $oldid, $watchers, $pageStatus = 'changed' ) { + public function actuallyNotifyOnPageChange( + $editor, + $title, + $timestamp, + $summary, + $minorEdit, + $oldid, + $watchers, + $pageStatus = 'changed' + ) { # we use $wgPasswordSender as sender's address global $wgUsersNotifiedOnAllChanges; global $wgEnotifWatchlist, $wgBlockDisablesLogin; global $wgEnotifMinorEdits, $wgEnotifUserTalk; + $messageCache = MediaWikiServices::getInstance()->getMessageCache(); + # The following code is only run, if several conditions are met: # 1. EmailNotification for pages (other than user_talk pages) must be enabled # 2. minor edits (changes) are only regarded if the global flag indicates so @@ -204,13 +217,16 @@ class EmailNotification { $userTalkId = false; - if ( !$minorEdit || ( $wgEnotifMinorEdits && !$editor->isAllowed( 'nominornewtalk' ) ) ) { + if ( !$minorEdit || ( $wgEnotifMinorEdits && !MediaWikiServices::getInstance() + ->getPermissionManager() + ->userHasRight( $editor, 'nominornewtalk' ) ) + ) { if ( $wgEnotifUserTalk && $isUserTalkPage && $this->canSendUserTalkEmail( $editor, $title, $minorEdit ) ) { $targetUser = User::newFromName( $title->getText() ); - $this->compose( $targetUser, self::USER_TALK ); + $this->compose( $targetUser, self::USER_TALK, $messageCache ); $userTalkId = $targetUser->getId(); } @@ -229,7 +245,7 @@ class EmailNotification { && !( $wgBlockDisablesLogin && $watchingUser->getBlock() ) && Hooks::run( 'SendWatchlistEmailNotification', [ $watchingUser, $title, $this ] ) ) { - $this->compose( $watchingUser, self::WATCHLIST ); + $this->compose( $watchingUser, self::WATCHLIST, $messageCache ); } } } @@ -241,7 +257,7 @@ class EmailNotification { continue; } $user = User::newFromName( $name ); - $this->compose( $user, self::ALL_CHANGES ); + $this->compose( $user, self::ALL_CHANGES, $messageCache ); } $this->sendMails(); @@ -288,8 +304,9 @@ class EmailNotification { /** * Generate the generic "this page has been changed" e-mail text. + * @param MessageCache $messageCache */ - private function composeCommonMailtext() { + private function composeCommonMailtext( MessageCache $messageCache ) { global $wgPasswordSender, $wgNoReplyAddress; global $wgEnotifFromEditor, $wgEnotifRevealEditorAddress; global $wgEnotifImpersonal, $wgEnotifUseRealName; @@ -374,7 +391,7 @@ class EmailNotification { $body = wfMessage( 'enotif_body' )->inContentLanguage()->plain(); $body = strtr( $body, $keys ); - $body = MessageCache::singleton()->transform( $body, false, null, $this->title ); + $body = $messageCache->transform( $body, false, null, $this->title ); $this->body = wordwrap( strtr( $body, $postTransformKeys ), 72 ); # Reveal the page editor's address as REPLY-TO address only if @@ -406,12 +423,13 @@ class EmailNotification { * Call sendMails() to send any mails that were queued. * @param User $user * @param string $source + * @param MessageCache $messageCache */ - private function compose( $user, $source ) { + private function compose( $user, $source, MessageCache $messageCache ) { global $wgEnotifImpersonal; if ( !$this->composed_common ) { - $this->composeCommonMailtext(); + $this->composeCommonMailtext( $messageCache ); } if ( $wgEnotifImpersonal ) { diff --git a/includes/mail/UserMailer.php b/includes/mail/UserMailer.php index 47fa16f87f..7b739027f8 100644 --- a/includes/mail/UserMailer.php +++ b/includes/mail/UserMailer.php @@ -34,8 +34,8 @@ class UserMailer { * Send mail using a PEAR mailer * * @param Mail_smtp $mailer - * @param string $dest - * @param string $headers + * @param string[]|string $dest + * @param array $headers * @param string $body * * @return Status @@ -382,6 +382,8 @@ class UserMailer { throw new MWException( 'PEAR mail package is not installed' ); } + $recips = array_map( 'strval', $to ); + Wikimedia\suppressWarnings(); // Create the mail object using the Mail::factory method @@ -391,19 +393,20 @@ class UserMailer { Wikimedia\restoreWarnings(); return Status::newFatal( 'pear-mail-error', $mail_object->getMessage() ); } + '@phan-var Mail_smtp $mail_object'; wfDebug( "Sending mail via PEAR::Mail\n" ); $headers['Subject'] = self::quotedPrintable( $subject ); // When sending only to one recipient, shows it its email using To: - if ( count( $to ) == 1 ) { - $headers['To'] = $to[0]->toString(); + if ( count( $recips ) == 1 ) { + $headers['To'] = $recips[0]; } // Split jobs since SMTP servers tends to limit the maximum // number of possible recipients. - $chunks = array_chunk( $to, $wgEnotifMaxRecips ); + $chunks = array_chunk( $recips, $wgEnotifMaxRecips ); foreach ( $chunks as $chunk ) { $status = self::sendWithPear( $mail_object, $chunk, $headers, $body ); // FIXME : some chunks might be sent while others are not! diff --git a/includes/media/DjVuHandler.php b/includes/media/DjVuHandler.php index 3b904e8418..d54dd6bc05 100644 --- a/includes/media/DjVuHandler.php +++ b/includes/media/DjVuHandler.php @@ -240,6 +240,7 @@ class DjVuHandler extends ImageHandler { * @param File|FSFile $image * @param string $path * @return DjVuImage + * @suppress PhanUndeclaredProperty Custom property */ function getDjVuImage( $image, $path ) { if ( !$image ) { @@ -290,6 +291,7 @@ class DjVuHandler extends ImageHandler { * @param File $image * @param bool $gettext DOCUMENT (Default: false) * @return bool|SimpleXMLElement + * @suppress PhanUndeclaredProperty Custom property */ public function getMetaTree( $image, $gettext = false ) { if ( $gettext && isset( $image->djvuTextTree ) ) { diff --git a/includes/media/DjVuImage.php b/includes/media/DjVuImage.php index 92fad528fe..50b13a413f 100644 --- a/includes/media/DjVuImage.php +++ b/includes/media/DjVuImage.php @@ -42,6 +42,9 @@ class DjVuImage { */ const DJVUTXT_MEMORY_LIMIT = 300000; + /** @var string */ + private $mFilename; + /** * @param string $filename The DjVu file name. */ diff --git a/includes/media/ExifBitmapHandler.php b/includes/media/ExifBitmapHandler.php index fa9e1dc90c..9058340fb6 100644 --- a/includes/media/ExifBitmapHandler.php +++ b/includes/media/ExifBitmapHandler.php @@ -80,7 +80,7 @@ class ExifBitmapHandler extends BitmapHandler { /** * @param File $image - * @param array $metadata + * @param string $metadata * @return bool|int */ public function isMetadataValid( $image, $metadata ) { diff --git a/includes/media/FormatMetadata.php b/includes/media/FormatMetadata.php index 333c610375..39937954e4 100644 --- a/includes/media/FormatMetadata.php +++ b/includes/media/FormatMetadata.php @@ -98,6 +98,7 @@ class FormatMetadata extends ContextSource { * Exif::getFilteredData() or BitmapMetadataHandler ) * @return array * @since 1.23 + * @suppress PhanTypeArraySuspiciousNullable */ public function makeFormattedData( $tags ) { $resolutionunit = !isset( $tags['ResolutionUnit'] ) || $tags['ResolutionUnit'] == 2 ? 2 : 3; @@ -490,8 +491,16 @@ class FormatMetadata extends ContextSource { case 'CustomRendered': switch ( $val ) { - case 0: - case 1: + case 0: /* normal */ + case 1: /* custom */ + /* The following are unofficial Apple additions */ + case 2: /* HDR (no original saved) */ + case 3: /* HDR (original saved) */ + case 4: /* Original (for HDR) */ + /* Yes 5 is not present ;) */ + case 6: /* Panorama */ + case 7: /* Portrait HDR */ + case 8: /* Portrait */ $val = $this->exifMsg( $tag, $val ); break; default: diff --git a/includes/media/GIFMetadataExtractor.php b/includes/media/GIFMetadataExtractor.php index 591ccf191f..4e5c4ea924 100644 --- a/includes/media/GIFMetadataExtractor.php +++ b/includes/media/GIFMetadataExtractor.php @@ -282,6 +282,7 @@ class GIFMetadataExtractor { } $buf = unpack( 'C', $data )[1]; $bpp = ( $buf & 7 ) + 1; + // @phan-suppress-next-line PhanTypeInvalidLeftOperandOfIntegerOp $buf >>= 7; $have_map = $buf & 1; diff --git a/includes/media/IPTC.php b/includes/media/IPTC.php index 683ded107a..c32db28a79 100644 --- a/includes/media/IPTC.php +++ b/includes/media/IPTC.php @@ -36,6 +36,7 @@ class IPTC { * * @param string $rawData The app13 block from jpeg containing iptc/iim data * @return array IPTC metadata array + * @suppress PhanTypeArraySuspicious */ static function parse( $rawData ) { $parsed = iptcparse( $rawData ); diff --git a/includes/media/JpegMetadataExtractor.php b/includes/media/JpegMetadataExtractor.php index 8a26f606f9..dffb1f988b 100644 --- a/includes/media/JpegMetadataExtractor.php +++ b/includes/media/JpegMetadataExtractor.php @@ -122,14 +122,17 @@ class JpegMetadataExtractor { $temp = self::jpegExtractMarker( $fh ); // check what type of app segment this is. if ( substr( $temp, 0, 29 ) === "http://ns.adobe.com/xap/1.0/\x00" && $showXMP ) { - $segments["XMP"] = substr( $temp, 29 ); + // use trim to remove trailing \0 chars + $segments["XMP"] = trim( substr( $temp, 29 ) ); } elseif ( substr( $temp, 0, 35 ) === "http://ns.adobe.com/xmp/extension/\x00" && $showXMP ) { - $segments["XMP_ext"][] = substr( $temp, 35 ); + // use trim to remove trailing \0 chars + $segments["XMP_ext"][] = trim( substr( $temp, 35 ) ); } elseif ( substr( $temp, 0, 29 ) === "XMP\x00://ns.adobe.com/xap/1.0/\x00" && $showXMP ) { // Some images (especially flickr images) seem to have this. // I really have no idea what the deal is with them, but // whatever... - $segments["XMP"] = substr( $temp, 29 ); + // use trim to remove trailing \0 chars + $segments["XMP"] = trim( substr( $temp, 29 ) ); wfDebug( __METHOD__ . ' Found XMP section with wrong app identifier ' . "Using anyways.\n" ); } elseif ( substr( $temp, 0, 6 ) === "Exif\0\0" ) { diff --git a/includes/media/ThumbnailImage.php b/includes/media/ThumbnailImage.php index 7ee6dcbee3..6e4412ca8f 100644 --- a/includes/media/ThumbnailImage.php +++ b/includes/media/ThumbnailImage.php @@ -110,7 +110,7 @@ class ThumbnailImage extends MediaTransformOutput { * @return string */ function toHtml( $options = [] ) { - global $wgPriorityHints, $wgPriorityHintsRatio, $wgElementTiming; + global $wgPriorityHints, $wgPriorityHintsRatio, $wgElementTiming, $wgNativeImageLazyLoading; if ( func_num_args() == 2 ) { throw new MWException( __METHOD__ . ' called in the old style' ); @@ -126,6 +126,10 @@ class ThumbnailImage extends MediaTransformOutput { 'decoding' => 'async', ]; + if ( $wgNativeImageLazyLoading ) { + $attribs['loading'] = 'lazy'; + } + $elementTimingName = 'thumbnail'; if ( $wgPriorityHints diff --git a/includes/media/TiffHandler.php b/includes/media/TiffHandler.php index 15c4dbf1e6..880d382b4f 100644 --- a/includes/media/TiffHandler.php +++ b/includes/media/TiffHandler.php @@ -62,7 +62,7 @@ class TiffHandler extends ExifBitmapHandler { * @param string $ext * @param string $mime * @param array|null $params - * @return bool + * @return array */ public function getThumbType( $ext, $mime, $params = null ) { global $wgTiffThumbnailType; diff --git a/includes/media/WebPHandler.php b/includes/media/WebPHandler.php index 295a9785b5..854c24949c 100644 --- a/includes/media/WebPHandler.php +++ b/includes/media/WebPHandler.php @@ -182,6 +182,7 @@ class WebPHandler extends BitmapHandler { * Decodes a lossless chunk header * @param string $header First few bytes of the header, expected to be at least 13 bytes long * @return bool|array See WebPHandler::decodeHeader + * @suppress PhanTypeInvalidLeftOperandOfIntegerOp */ public static function decodeLosslessChunkHeader( $header ) { // Bytes 0-3 are 'VP8L' diff --git a/includes/objectcache/ObjectCache.php b/includes/objectcache/ObjectCache.php index 3bb077173f..e49feae881 100644 --- a/includes/objectcache/ObjectCache.php +++ b/includes/objectcache/ObjectCache.php @@ -43,11 +43,6 @@ use MediaWiki\MediaWikiServices; * * Primary entry points: * - * - ObjectCache::getMainWANInstance() - * Purpose: Memory cache. - * Stored in the local data-center's main cache (keyspace different from local-cluster cache). - * Delete events are broadcasted to other DCs main cache. See WANObjectCache for details. - * * - ObjectCache::getLocalServerInstance( $fallbackType ) * Purpose: Memory cache for very hot keys. * Stored only on the individual web server (typically APC or APCu for web requests, @@ -60,13 +55,6 @@ use MediaWiki\MediaWikiServices; * Stored centrally within the local data-center. Not replicated to other DCs. * Configured by $wgMainCacheType. * - * - ObjectCache::getMainStashInstance() - * Purpose: Ephemeral global storage. - * Stored centrally within the primary data-center. - * Changes are applied there first and replicated to other DCs (best-effort). - * To retrieve the latest value (e.g. not from a replica DB), use BagOStuff::READ_LATEST. - * This store may be subject to LRU style evictions. - * * - ObjectCache::getInstance( $cacheType ) * Purpose: Special cases (like tiered memory/disk caches). * Get a specific cache type by key in $wgObjectCaches. @@ -119,7 +107,7 @@ class ObjectCache { * @return BagOStuff * @throws InvalidArgumentException */ - public static function newFromId( $id ) { + private static function newFromId( $id ) { global $wgObjectCaches; if ( !isset( $wgObjectCaches[$id] ) ) { @@ -146,7 +134,7 @@ class ObjectCache { * * @return string */ - public static function getDefaultKeyspace() { + private static function getDefaultKeyspace() { global $wgCachePrefix; $keyspace = $wgCachePrefix; @@ -204,9 +192,6 @@ class ObjectCache { if ( !isset( $params['servers'] ) ) { $params['servers'] = $GLOBALS['wgMemCachedServers']; } - if ( !isset( $params['debug'] ) ) { - $params['debug'] = $GLOBALS['wgMemCachedDebug']; - } if ( !isset( $params['persistent'] ) ) { $params['persistent'] = $GLOBALS['wgMemCachedPersistent']; } @@ -297,7 +282,7 @@ class ObjectCache { * @return WANObjectCache * @throws UnexpectedValueException */ - public static function newWANCacheFromId( $id ) { + private static function newWANCacheFromId( $id ) { global $wgWANObjectCaches, $wgObjectCaches; if ( !isset( $wgWANObjectCaches[$id] ) ) { @@ -322,6 +307,7 @@ class ObjectCache { * @param array $params * @return WANObjectCache * @throws UnexpectedValueException + * @suppress PhanTypeMismatchReturn */ public static function newWANCacheFromParams( array $params ) { global $wgCommandLineMode, $wgSecretKey; @@ -353,30 +339,6 @@ class ObjectCache { return self::getInstance( $wgMainCacheType ); } - /** - * Get the main WAN cache object. - * - * @since 1.26 - * @return WANObjectCache - * @deprecated Since 1.28 Use MediaWikiServices::getInstance()->getMainWANObjectCache() - */ - public static function getMainWANInstance() { - wfDeprecated( __METHOD__, '1.28' ); - return MediaWikiServices::getInstance()->getMainWANObjectCache(); - } - - /** - * Get the cache object for the main stash. - * - * @return BagOStuff - * @since 1.26 - * @deprecated Since 1.28 Use MediaWikiServices::getInstance()->getMainObjectStash() - */ - public static function getMainStashInstance() { - wfDeprecated( __METHOD__, '1.28' ); - return MediaWikiServices::getInstance()->getMainObjectStash(); - } - /** * Clear all the cached instances. */ @@ -393,12 +355,19 @@ class ObjectCache { */ public static function detectLocalServerCache() { if ( function_exists( 'apcu_fetch' ) ) { - return 'apcu'; + // Make sure the APCu methods actually store anything + if ( PHP_SAPI !== 'cli' || ini_get( 'apc.enable_cli' ) ) { + return 'apcu'; + } } elseif ( function_exists( 'apc_fetch' ) ) { - return 'apc'; + // Make sure the APC methods actually store anything + if ( PHP_SAPI !== 'cli' || ini_get( 'apc.enable_cli' ) ) { + return 'apc'; + } } elseif ( function_exists( 'wincache_ucache_get' ) ) { return 'wincache'; } + return CACHE_NONE; } } diff --git a/includes/objectcache/SqlBagOStuff.php b/includes/objectcache/SqlBagOStuff.php index e97dc41af7..4a3445e935 100644 --- a/includes/objectcache/SqlBagOStuff.php +++ b/includes/objectcache/SqlBagOStuff.php @@ -22,6 +22,7 @@ */ use MediaWiki\MediaWikiServices; +use Wikimedia\AtEase\AtEase; use Wikimedia\Rdbms\Database; use Wikimedia\Rdbms\IDatabase; use Wikimedia\Rdbms\DBError; @@ -30,6 +31,7 @@ use Wikimedia\Rdbms\DBConnectionError; use Wikimedia\Rdbms\IMaintainableDatabase; use Wikimedia\Rdbms\LoadBalancer; use Wikimedia\ScopedCallback; +use Wikimedia\Timestamp\ConvertibleTimestamp; use Wikimedia\WaitConditionLoop; /** @@ -43,7 +45,7 @@ class SqlBagOStuff extends MediumSpecificBagOStuff { /** @var string[] (server index => tag/host name) */ protected $serverTags; /** @var int */ - protected $numServers; + protected $numServerShards; /** @var int UNIX timestamp */ protected $lastGarbageCollect = 0; /** @var int */ @@ -51,7 +53,7 @@ class SqlBagOStuff extends MediumSpecificBagOStuff { /** @var int */ protected $purgeLimit = 100; /** @var int */ - protected $shards = 1; + protected $numTableShards = 1; /** @var string */ protected $tableName = 'objectcache'; /** @var bool */ @@ -126,7 +128,7 @@ class SqlBagOStuff extends MediumSpecificBagOStuff { if ( isset( $params['servers'] ) ) { $this->serverInfos = []; $this->serverTags = []; - $this->numServers = count( $params['servers'] ); + $this->numServerShards = count( $params['servers'] ); $index = 0; foreach ( $params['servers'] as $tag => $info ) { $this->serverInfos[$index] = $info; @@ -139,11 +141,11 @@ class SqlBagOStuff extends MediumSpecificBagOStuff { } } elseif ( isset( $params['server'] ) ) { $this->serverInfos = [ $params['server'] ]; - $this->numServers = count( $this->serverInfos ); + $this->numServerShards = count( $this->serverInfos ); } else { // Default to using the main wiki's database servers - $this->serverInfos = false; - $this->numServers = 1; + $this->serverInfos = []; + $this->numServerShards = 1; $this->attrMap[self::ATTR_SYNCWRITES] = self::QOS_SYNCWRITES_BE; } if ( isset( $params['purgePeriod'] ) ) { @@ -156,7 +158,7 @@ class SqlBagOStuff extends MediumSpecificBagOStuff { $this->tableName = $params['tableName']; } if ( isset( $params['shards'] ) ) { - $this->shards = intval( $params['shards'] ); + $this->numTableShards = intval( $params['shards'] ); } // Backwards-compatibility for < 1.34 $this->replicaOnly = $params['replicaOnly'] ?? ( $params['slaveOnly'] ?? false ); @@ -165,52 +167,54 @@ class SqlBagOStuff extends MediumSpecificBagOStuff { /** * Get a connection to the specified database * - * @param int $serverIndex + * @param int $shardIndex * @return IMaintainableDatabase * @throws MWException */ - protected function getDB( $serverIndex ) { - if ( $serverIndex >= $this->numServers ) { - throw new MWException( __METHOD__ . ": Invalid server index \"$serverIndex\"" ); + private function getConnection( $shardIndex ) { + if ( $shardIndex >= $this->numServerShards ) { + throw new MWException( __METHOD__ . ": Invalid server index \"$shardIndex\"" ); } # Don't keep timing out trying to connect for each call if the DB is down if ( - isset( $this->connFailureErrors[$serverIndex] ) && - ( $this->getCurrentTime() - $this->connFailureTimes[$serverIndex] ) < 60 + isset( $this->connFailureErrors[$shardIndex] ) && + ( $this->getCurrentTime() - $this->connFailureTimes[$shardIndex] ) < 60 ) { - throw $this->connFailureErrors[$serverIndex]; + throw $this->connFailureErrors[$shardIndex]; } if ( $this->serverInfos ) { - if ( !isset( $this->conns[$serverIndex] ) ) { + if ( !isset( $this->conns[$shardIndex] ) ) { // Use custom database defined by server connection info - $info = $this->serverInfos[$serverIndex]; + $info = $this->serverInfos[$shardIndex]; $type = $info['type'] ?? 'mysql'; $host = $info['host'] ?? '[unknown]'; $this->logger->debug( __CLASS__ . ": connecting to $host" ); - $db = Database::factory( $type, $info ); - $db->clearFlag( DBO_TRX ); // auto-commit mode - $this->conns[$serverIndex] = $db; + $conn = Database::factory( $type, $info ); + $conn->clearFlag( DBO_TRX ); // auto-commit mode + $this->conns[$shardIndex] = $conn; + // Automatically create the objectcache table for sqlite as needed + if ( $conn->getType() === 'sqlite' ) { + $this->initSqliteDatabase( $conn ); + } } - $db = $this->conns[$serverIndex]; + $conn = $this->conns[$shardIndex]; } else { // Use the main LB database $lb = MediaWikiServices::getInstance()->getDBLoadBalancer(); $index = $this->replicaOnly ? DB_REPLICA : DB_MASTER; - if ( $lb->getServerType( $lb->getWriterIndex() ) !== 'sqlite' ) { - // Keep a separate connection to avoid contention and deadlocks - $db = $lb->getConnectionRef( $index, [], false, $lb::CONN_TRX_AUTOCOMMIT ); - } else { - // However, SQLite has the opposite behavior due to DB-level locking. - // Stock sqlite MediaWiki installs use a separate sqlite cache DB instead. - $db = $lb->getConnectionRef( $index ); - } + // If the RDBMS has row-level locking, use the autocommit connection to avoid + // contention and deadlocks. Do not do this if it only has DB-level locking since + // that would just cause deadlocks. + $attribs = $lb->getServerAttributes( $lb->getWriterIndex() ); + $flags = $attribs[Database::ATTR_DB_LEVEL_LOCKING] ? 0 : $lb::CONN_TRX_AUTOCOMMIT; + $conn = $lb->getMaintenanceConnectionRef( $index, [], false, $flags ); } - $this->logger->debug( sprintf( "Connection %s will be used for SqlBagOStuff", $db ) ); + $this->logger->debug( sprintf( "Connection %s will be used for SqlBagOStuff", $conn ) ); - return $db; + return $conn; } /** @@ -218,22 +222,22 @@ class SqlBagOStuff extends MediumSpecificBagOStuff { * @param string $key * @return array Server index and table name */ - protected function getTableByKey( $key ) { - if ( $this->shards > 1 ) { + private function getTableByKey( $key ) { + if ( $this->numTableShards > 1 ) { $hash = hexdec( substr( md5( $key ), 0, 8 ) ) & 0x7fffffff; - $tableIndex = $hash % $this->shards; + $tableIndex = $hash % $this->numTableShards; } else { $tableIndex = 0; } - if ( $this->numServers > 1 ) { + if ( $this->numServerShards > 1 ) { $sortedServers = $this->serverTags; ArrayUtils::consistentHashSort( $sortedServers, $key ); reset( $sortedServers ); - $serverIndex = key( $sortedServers ); + $shardIndex = key( $sortedServers ); } else { - $serverIndex = 0; + $shardIndex = 0; } - return [ $serverIndex, $this->getTableNameByShard( $tableIndex ) ]; + return [ $shardIndex, $this->getTableNameByShard( $tableIndex ) ]; } /** @@ -241,9 +245,9 @@ class SqlBagOStuff extends MediumSpecificBagOStuff { * @param int $index * @return string */ - protected function getTableNameByShard( $index ) { - if ( $this->shards > 1 ) { - $decimals = strlen( $this->shards - 1 ); + private function getTableNameByShard( $index ) { + if ( $this->numTableShards > 1 ) { + $decimals = strlen( $this->numTableShards - 1 ); return $this->tableName . sprintf( "%0{$decimals}d", $index ); } else { @@ -278,19 +282,19 @@ class SqlBagOStuff extends MediumSpecificBagOStuff { return $values; } - protected function fetchBlobMulti( array $keys, $flags = 0 ) { + private function fetchBlobMulti( array $keys, $flags = 0 ) { $values = []; // array of (key => value) $keysByTable = []; foreach ( $keys as $key ) { - list( $serverIndex, $tableName ) = $this->getTableByKey( $key ); - $keysByTable[$serverIndex][$tableName][] = $key; + list( $shardIndex, $tableName ) = $this->getTableByKey( $key ); + $keysByTable[$shardIndex][$tableName][] = $key; } $dataRows = []; - foreach ( $keysByTable as $serverIndex => $serverKeys ) { + foreach ( $keysByTable as $shardIndex => $serverKeys ) { try { - $db = $this->getDB( $serverIndex ); + $db = $this->getConnection( $shardIndex ); foreach ( $serverKeys as $tableName => $tableKeys ) { $res = $db->select( $tableName, [ 'keyname', 'value', 'exptime' ], @@ -305,13 +309,13 @@ class SqlBagOStuff extends MediumSpecificBagOStuff { continue; } foreach ( $res as $row ) { - $row->serverIndex = $serverIndex; + $row->shardIndex = $shardIndex; $row->tableName = $tableName; $dataRows[$row->keyname] = $row; } } } catch ( DBError $e ) { - $this->handleReadError( $e, $serverIndex ); + $this->handleReadError( $e, $shardIndex ); } } @@ -321,14 +325,14 @@ class SqlBagOStuff extends MediumSpecificBagOStuff { $this->debug( "get: retrieved data; expiry time is " . $row->exptime ); $db = null; // in case of connection failure try { - $db = $this->getDB( $row->serverIndex ); + $db = $this->getConnection( $row->shardIndex ); if ( $this->isExpired( $db, $row->exptime ) ) { // MISS $this->debug( "get: key has expired" ); } else { // HIT $values[$key] = $db->decodeBlob( $row->value ); } } catch ( DBQueryError $e ) { - $this->handleWriteError( $e, $db, $row->serverIndex ); + $this->handleWriteError( $e, $db, $row->shardIndex ); } } else { // MISS $this->debug( 'get: no matching rows' ); @@ -352,8 +356,8 @@ class SqlBagOStuff extends MediumSpecificBagOStuff { private function modifyMulti( array $data, $exptime, $flags, $op ) { $keysByTable = []; foreach ( $data as $key => $value ) { - list( $serverIndex, $tableName ) = $this->getTableByKey( $key ); - $keysByTable[$serverIndex][$tableName][] = $key; + list( $shardIndex, $tableName ) = $this->getTableByKey( $key ); + $keysByTable[$shardIndex][$tableName][] = $key; } $exptime = $this->getExpirationAsTimestamp( $exptime ); @@ -361,14 +365,14 @@ class SqlBagOStuff extends MediumSpecificBagOStuff { $result = true; /** @noinspection PhpUnusedLocalVariableInspection */ $silenceScope = $this->silenceTransactionProfiler(); - foreach ( $keysByTable as $serverIndex => $serverKeys ) { + foreach ( $keysByTable as $shardIndex => $serverKeys ) { $db = null; // in case of connection failure try { - $db = $this->getDB( $serverIndex ); + $db = $this->getConnection( $shardIndex ); $this->occasionallyGarbageCollect( $db ); // expire old entries if any $dbExpiry = $exptime ? $db->timestamp( $exptime ) : $this->getMaxDateTime( $db ); } catch ( DBError $e ) { - $this->handleWriteError( $e, $db, $serverIndex ); + $this->handleWriteError( $e, $db, $shardIndex ); $result = false; continue; } @@ -384,14 +388,14 @@ class SqlBagOStuff extends MediumSpecificBagOStuff { $dbExpiry ) && $result; } catch ( DBError $e ) { - $this->handleWriteError( $e, $db, $serverIndex ); + $this->handleWriteError( $e, $db, $shardIndex ); $result = false; } } } - if ( ( $flags & self::WRITE_SYNC ) == self::WRITE_SYNC ) { + if ( $this->fieldHasFlags( $flags, self::WRITE_SYNC ) ) { $result = $this->waitForReplication() && $result; } @@ -467,19 +471,19 @@ class SqlBagOStuff extends MediumSpecificBagOStuff { return $this->modifyMulti( [ $key => $value ], $exptime, $flags, self::$OP_SET ); } - public function add( $key, $value, $exptime = 0, $flags = 0 ) { + protected function doAdd( $key, $value, $exptime = 0, $flags = 0 ) { return $this->modifyMulti( [ $key => $value ], $exptime, $flags, self::$OP_ADD ); } - protected function cas( $casToken, $key, $value, $exptime = 0, $flags = 0 ) { - list( $serverIndex, $tableName ) = $this->getTableByKey( $key ); + protected function doCas( $casToken, $key, $value, $exptime = 0, $flags = 0 ) { + list( $shardIndex, $tableName ) = $this->getTableByKey( $key ); $exptime = $this->getExpirationAsTimestamp( $exptime ); /** @noinspection PhpUnusedLocalVariableInspection */ $silenceScope = $this->silenceTransactionProfiler(); $db = null; // in case of connection failure try { - $db = $this->getDB( $serverIndex ); + $db = $this->getConnection( $shardIndex ); // (T26425) use a replace if the db supports it instead of // delete/insert to avoid clashes with conflicting keynames $db->update( @@ -499,12 +503,17 @@ class SqlBagOStuff extends MediumSpecificBagOStuff { __METHOD__ ); } catch ( DBQueryError $e ) { - $this->handleWriteError( $e, $db, $serverIndex ); + $this->handleWriteError( $e, $db, $shardIndex ); return false; } - return (bool)$db->affectedRows(); + $success = (bool)$db->affectedRows(); + if ( $this->fieldHasFlags( $flags, self::WRITE_SYNC ) ) { + $success = $this->waitForReplication() && $success; + } + + return $success; } protected function doDeleteMulti( array $keys, $flags = 0 ) { @@ -520,15 +529,15 @@ class SqlBagOStuff extends MediumSpecificBagOStuff { return $this->modifyMulti( [ $key => null ], 0, $flags, self::$OP_DELETE ); } - public function incr( $key, $step = 1 ) { - list( $serverIndex, $tableName ) = $this->getTableByKey( $key ); + public function incr( $key, $step = 1, $flags = 0 ) { + list( $shardIndex, $tableName ) = $this->getTableByKey( $key ); $newCount = false; /** @noinspection PhpUnusedLocalVariableInspection */ $silenceScope = $this->silenceTransactionProfiler(); $db = null; // in case of connection failure try { - $db = $this->getDB( $serverIndex ); + $db = $this->getConnection( $shardIndex ); $encTimestamp = $db->addQuotes( $db->timestamp() ); $db->update( $tableName, @@ -548,19 +557,14 @@ class SqlBagOStuff extends MediumSpecificBagOStuff { } } } catch ( DBError $e ) { - $this->handleWriteError( $e, $db, $serverIndex ); + $this->handleWriteError( $e, $db, $shardIndex ); } return $newCount; } - public function merge( $key, callable $callback, $exptime = 0, $attempts = 10, $flags = 0 ) { - $ok = $this->mergeViaCas( $key, $callback, $exptime, $attempts, $flags ); - if ( ( $flags & self::WRITE_SYNC ) == self::WRITE_SYNC ) { - $ok = $this->waitForReplication() && $ok; - } - - return $ok; + public function decr( $key, $value = 1, $flags = 0 ) { + return $this->incr( $key, -$value, $flags ); } public function changeTTLMulti( array $keys, $exptime, $flags = 0 ) { @@ -581,10 +585,10 @@ class SqlBagOStuff extends MediumSpecificBagOStuff { * @param string $exptime * @return bool */ - protected function isExpired( $db, $exptime ) { + private function isExpired( IDatabase $db, $exptime ) { return ( $exptime != $this->getMaxDateTime( $db ) && - wfTimestamp( TS_UNIX, $exptime ) < $this->getCurrentTime() + ConvertibleTimestamp::convert( TS_UNIX, $exptime ) < $this->getCurrentTime() ); } @@ -592,7 +596,7 @@ class SqlBagOStuff extends MediumSpecificBagOStuff { * @param IDatabase $db * @return string */ - protected function getMaxDateTime( $db ) { + private function getMaxDateTime( $db ) { if ( (int)$this->getCurrentTime() > 0x7fffffff ) { return $db->timestamp( 1 << 62 ); } else { @@ -604,7 +608,7 @@ class SqlBagOStuff extends MediumSpecificBagOStuff { * @param IDatabase $db * @throws DBError */ - protected function occasionallyGarbageCollect( IDatabase $db ) { + private function occasionallyGarbageCollect( IDatabase $db ) { if ( // Random purging is enabled $this->purgePeriod && @@ -642,16 +646,16 @@ class SqlBagOStuff extends MediumSpecificBagOStuff { /** @noinspection PhpUnusedLocalVariableInspection */ $silenceScope = $this->silenceTransactionProfiler(); - $serverIndexes = range( 0, $this->numServers - 1 ); - shuffle( $serverIndexes ); + $shardIndexes = range( 0, $this->numServerShards - 1 ); + shuffle( $shardIndexes ); $ok = true; $keysDeletedCount = 0; - foreach ( $serverIndexes as $numServersDone => $serverIndex ) { + foreach ( $shardIndexes as $numServersDone => $shardIndex ) { $db = null; // in case of connection failure try { - $db = $this->getDB( $serverIndex ); + $db = $this->getConnection( $shardIndex ); $this->deleteServerObjectsExpiringBefore( $db, $timestamp, @@ -661,7 +665,7 @@ class SqlBagOStuff extends MediumSpecificBagOStuff { $keysDeletedCount ); } catch ( DBError $e ) { - $this->handleWriteError( $e, $db, $serverIndex ); + $this->handleWriteError( $e, $db, $shardIndex ); $ok = false; } } @@ -686,8 +690,8 @@ class SqlBagOStuff extends MediumSpecificBagOStuff { $serversDoneCount = 0, &$keysDeletedCount = 0 ) { - $cutoffUnix = wfTimestamp( TS_UNIX, $timestamp ); - $shardIndexes = range( 0, $this->shards - 1 ); + $cutoffUnix = ConvertibleTimestamp::convert( TS_UNIX, $timestamp ); + $shardIndexes = range( 0, $this->numTableShards - 1 ); shuffle( $shardIndexes ); foreach ( $shardIndexes as $numShardsDone => $shardIndex ) { @@ -708,7 +712,8 @@ class SqlBagOStuff extends MediumSpecificBagOStuff { if ( $res->numRows() ) { $row = $res->current(); if ( $lag === null ) { - $lag = max( $cutoffUnix - wfTimestamp( TS_UNIX, $row->exptime ), 1 ); + $rowExpUnix = ConvertibleTimestamp::convert( TS_UNIX, $row->exptime ); + $lag = max( $cutoffUnix - $rowExpUnix, 1 ); } $keys = []; @@ -730,15 +735,16 @@ class SqlBagOStuff extends MediumSpecificBagOStuff { if ( is_callable( $progressCallback ) ) { if ( $lag ) { - $remainingLag = $cutoffUnix - wfTimestamp( TS_UNIX, $continue ); + $continueUnix = ConvertibleTimestamp::convert( TS_UNIX, $continue ); + $remainingLag = $cutoffUnix - $continueUnix; $processedLag = max( $lag - $remainingLag, 0 ); - $doneRatio = ( $numShardsDone + $processedLag / $lag ) / $this->shards; + $doneRatio = ( $numShardsDone + $processedLag / $lag ) / $this->numTableShards; } else { $doneRatio = 1; } - $overallRatio = ( $doneRatio / $this->numServers ) - + ( $serversDoneCount / $this->numServers ); + $overallRatio = ( $doneRatio / $this->numServerShards ) + + ( $serversDoneCount / $this->numServerShards ); call_user_func( $progressCallback, $overallRatio * 100 ); } } while ( $res->numRows() && $keysDeletedCount < $limit ); @@ -753,15 +759,15 @@ class SqlBagOStuff extends MediumSpecificBagOStuff { public function deleteAll() { /** @noinspection PhpUnusedLocalVariableInspection */ $silenceScope = $this->silenceTransactionProfiler(); - for ( $serverIndex = 0; $serverIndex < $this->numServers; $serverIndex++ ) { + for ( $shardIndex = 0; $shardIndex < $this->numServerShards; $shardIndex++ ) { $db = null; // in case of connection failure try { - $db = $this->getDB( $serverIndex ); - for ( $i = 0; $i < $this->shards; $i++ ) { + $db = $this->getConnection( $shardIndex ); + for ( $i = 0; $i < $this->numTableShards; $i++ ) { $db->delete( $this->getTableNameByShard( $i ), '*', __METHOD__ ); } } catch ( DBError $e ) { - $this->handleWriteError( $e, $db, $serverIndex ); + $this->handleWriteError( $e, $db, $shardIndex ); return false; } } @@ -779,11 +785,11 @@ class SqlBagOStuff extends MediumSpecificBagOStuff { } } - list( $serverIndex ) = $this->getTableByKey( $key ); + list( $shardIndex ) = $this->getTableByKey( $key ); $db = null; // in case of connection failure try { - $db = $this->getDB( $serverIndex ); + $db = $this->getConnection( $shardIndex ); $ok = $db->lock( $key, __METHOD__, $timeout ); if ( $ok ) { $this->locks[$key] = [ 'class' => $rclass, 'depth' => 1 ]; @@ -796,7 +802,7 @@ class SqlBagOStuff extends MediumSpecificBagOStuff { return $ok; } catch ( DBError $e ) { - $this->handleWriteError( $e, $db, $serverIndex ); + $this->handleWriteError( $e, $db, $shardIndex ); $ok = false; } @@ -811,11 +817,11 @@ class SqlBagOStuff extends MediumSpecificBagOStuff { if ( --$this->locks[$key]['depth'] <= 0 ) { unset( $this->locks[$key] ); - list( $serverIndex ) = $this->getTableByKey( $key ); + list( $shardIndex ) = $this->getTableByKey( $key ); $db = null; // in case of connection failure try { - $db = $this->getDB( $serverIndex ); + $db = $this->getConnection( $shardIndex ); $ok = $db->unlock( $key, __METHOD__ ); if ( !$ok ) { $this->logger->warning( @@ -824,7 +830,7 @@ class SqlBagOStuff extends MediumSpecificBagOStuff { ); } } catch ( DBError $e ) { - $this->handleWriteError( $e, $db, $serverIndex ); + $this->handleWriteError( $e, $db, $shardIndex ); $ok = false; } @@ -866,9 +872,9 @@ class SqlBagOStuff extends MediumSpecificBagOStuff { } if ( function_exists( 'gzinflate' ) ) { - Wikimedia\suppressWarnings(); + AtEase::suppressWarnings(); $decomp = gzinflate( $serial ); - Wikimedia\restoreWarnings(); + AtEase::restoreWarnings(); if ( $decomp !== false ) { $serial = $decomp; @@ -882,11 +888,11 @@ class SqlBagOStuff extends MediumSpecificBagOStuff { * Handle a DBError which occurred during a read operation. * * @param DBError $exception - * @param int $serverIndex + * @param int $shardIndex */ - protected function handleReadError( DBError $exception, $serverIndex ) { + private function handleReadError( DBError $exception, $shardIndex ) { if ( $exception instanceof DBConnectionError ) { - $this->markServerDown( $exception, $serverIndex ); + $this->markServerDown( $exception, $shardIndex ); } $this->setAndLogDBError( $exception ); @@ -897,12 +903,12 @@ class SqlBagOStuff extends MediumSpecificBagOStuff { * * @param DBError $exception * @param IDatabase|null $db DB handle or null if connection failed - * @param int $serverIndex + * @param int $shardIndex * @throws Exception */ - protected function handleWriteError( DBError $exception, $db, $serverIndex ) { + private function handleWriteError( DBError $exception, $db, $shardIndex ) { if ( !( $db instanceof IDatabase ) ) { - $this->markServerDown( $exception, $serverIndex ); + $this->markServerDown( $exception, $shardIndex ); } $this->setAndLogDBError( $exception ); @@ -926,41 +932,68 @@ class SqlBagOStuff extends MediumSpecificBagOStuff { * Mark a server down due to a DBConnectionError exception * * @param DBError $exception - * @param int $serverIndex + * @param int $shardIndex */ - protected function markServerDown( DBError $exception, $serverIndex ) { - unset( $this->conns[$serverIndex] ); // bug T103435 + private function markServerDown( DBError $exception, $shardIndex ) { + unset( $this->conns[$shardIndex] ); // bug T103435 $now = $this->getCurrentTime(); - if ( isset( $this->connFailureTimes[$serverIndex] ) ) { - if ( $now - $this->connFailureTimes[$serverIndex] >= 60 ) { - unset( $this->connFailureTimes[$serverIndex] ); - unset( $this->connFailureErrors[$serverIndex] ); + if ( isset( $this->connFailureTimes[$shardIndex] ) ) { + if ( $now - $this->connFailureTimes[$shardIndex] >= 60 ) { + unset( $this->connFailureTimes[$shardIndex] ); + unset( $this->connFailureErrors[$shardIndex] ); } else { - $this->logger->debug( __METHOD__ . ": Server #$serverIndex already down" ); + $this->logger->debug( __METHOD__ . ": Server #$shardIndex already down" ); return; } } - $this->logger->info( __METHOD__ . ": Server #$serverIndex down until " . ( $now + 60 ) ); - $this->connFailureTimes[$serverIndex] = $now; - $this->connFailureErrors[$serverIndex] = $exception; + $this->logger->info( __METHOD__ . ": Server #$shardIndex down until " . ( $now + 60 ) ); + $this->connFailureTimes[$shardIndex] = $now; + $this->connFailureErrors[$shardIndex] = $exception; } /** - * Create shard tables. For use from eval.php. + * @param IMaintainableDatabase $db + * @throws DBError */ - public function createTables() { - for ( $serverIndex = 0; $serverIndex < $this->numServers; $serverIndex++ ) { - $db = $this->getDB( $serverIndex ); - if ( $db->getType() !== 'mysql' ) { - throw new MWException( __METHOD__ . ' is not supported on this DB server' ); - } + private function initSqliteDatabase( IMaintainableDatabase $db ) { + if ( $db->tableExists( 'objectcache' ) ) { + return; + } + // Use one table for SQLite; sharding does not seem to have much benefit + $db->query( "PRAGMA journal_mode=WAL" ); // this is permanent + $db->startAtomic( __METHOD__ ); // atomic DDL + try { + $encTable = $db->tableName( 'objectcache' ); + $encExptimeIndex = $db->addIdentifierQuotes( $db->tablePrefix() . 'exptime' ); + $db->query( + "CREATE TABLE $encTable (\n" . + " keyname BLOB NOT NULL default '' PRIMARY KEY,\n" . + " value BLOB,\n" . + " exptime TEXT\n" . + ")", + __METHOD__ + ); + $db->query( "CREATE INDEX $encExptimeIndex ON $encTable (exptime)" ); + $db->endAtomic( __METHOD__ ); + } catch ( DBError $e ) { + $db->rollback( __METHOD__ ); + throw $e; + } + } - for ( $i = 0; $i < $this->shards; $i++ ) { - $db->query( - 'CREATE TABLE ' . $db->tableName( $this->getTableNameByShard( $i ) ) . - ' LIKE ' . $db->tableName( 'objectcache' ), - __METHOD__ ); + /** + * Create the shard tables on all databases (e.g. via eval.php/shell.php) + */ + public function createTables() { + for ( $shardIndex = 0; $shardIndex < $this->numServerShards; $shardIndex++ ) { + $db = $this->getConnection( $shardIndex ); + if ( in_array( $db->getType(), [ 'mysql', 'postgres' ], true ) ) { + for ( $i = 0; $i < $this->numTableShards; $i++ ) { + $encBaseTable = $db->tableName( 'objectcache' ); + $encShardTable = $db->tableName( $this->getTableNameByShard( $i ) ); + $db->query( "CREATE TABLE $encShardTable LIKE $encBaseTable" ); + } } } } @@ -968,11 +1001,11 @@ class SqlBagOStuff extends MediumSpecificBagOStuff { /** * @return bool Whether the main DB is used, e.g. wfGetDB( DB_MASTER ) */ - protected function usesMainDB() { + private function usesMainDB() { return !$this->serverInfos; } - protected function waitForReplication() { + private function waitForReplication() { if ( !$this->usesMainDB() ) { // Custom DB server list; probably doesn't use replication return true; @@ -1007,12 +1040,17 @@ class SqlBagOStuff extends MediumSpecificBagOStuff { } /** - * Returns a ScopedCallback which resets the silence flag in the transaction profiler when it is - * destroyed on the end of a scope, for example on return or throw - * @return ScopedCallback - * @since 1.32 + * Silence the transaction profiler until the return value falls out of scope + * + * @return ScopedCallback|null */ - protected function silenceTransactionProfiler() { + private function silenceTransactionProfiler() { + if ( !$this->usesMainDB() ) { + // Custom DB is configured which either has no TransactionProfiler injected, + // or has one specific for cache use, which we shouldn't silence + return null; + } + $trxProfiler = Profiler::instance()->getTransactionProfiler(); $oldSilenced = $trxProfiler->setSilenced( true ); return new ScopedCallback( function () use ( $trxProfiler, $oldSilenced ) { diff --git a/includes/page/Article.php b/includes/page/Article.php index fcfb83de69..b6e366e953 100644 --- a/includes/page/Article.php +++ b/includes/page/Article.php @@ -363,8 +363,16 @@ class Article implements Page { } } + $rl = MediaWikiServices::getInstance()->getRevisionLookup(); + $oldRev = $this->mRevision ? $this->mRevision->getRevisionRecord() : null; if ( $request->getVal( 'direction' ) == 'next' ) { - $nextid = $this->getTitle()->getNextRevisionID( $oldid ); + $nextid = 0; + if ( $oldRev ) { + $nextRev = $rl->getNextRevision( $oldRev ); + if ( $nextRev ) { + $nextid = $nextRev->getId(); + } + } if ( $nextid ) { $oldid = $nextid; $this->mRevision = null; @@ -372,7 +380,13 @@ class Article implements Page { $this->mRedirectUrl = $this->getTitle()->getFullURL( 'redirect=no' ); } } elseif ( $request->getVal( 'direction' ) == 'prev' ) { - $previd = $this->getTitle()->getPreviousRevisionID( $oldid ); + $previd = 0; + if ( $oldRev ) { + $prevRev = $rl->getPreviousRevision( $oldRev ); + if ( $prevRev ) { + $previd = $prevRev->getId(); + } + } if ( $previd ) { $oldid = $previd; $this->mRevision = null; @@ -587,7 +601,7 @@ class Article implements Page { * page of the given title. */ public function view() { - global $wgUseFileCache, $wgDebugToolbar; + global $wgUseFileCache; # Get variables from query string # As side effect this will load the revision and update the title @@ -635,14 +649,15 @@ class Article implements Page { if ( $outputPage->isPrintable() ) { $parserOptions->setIsPrintable( true ); $poOptions['enableSectionEditLinks'] = false; - } elseif ( $this->viewIsRenderAction - || !$this->isCurrent() || !$this->getTitle()->quickUserCan( 'edit', $user ) + } elseif ( $this->viewIsRenderAction || !$this->isCurrent() || + !MediaWikiServices::getInstance()->getPermissionManager() + ->quickUserCan( 'edit', $user, $this->getTitle() ) ) { $poOptions['enableSectionEditLinks'] = false; } # Try client and file cache - if ( !$wgDebugToolbar && $oldid === 0 && $this->mPage->checkTouched() ) { + if ( $oldid === 0 && $this->mPage->checkTouched() ) { # Try to stream the output from file cache if ( $wgUseFileCache && $this->tryFileCache() ) { wfDebug( __METHOD__ . ": done file cache\n" ); @@ -793,7 +808,9 @@ class Article implements Page { $outputPage->enableClientCache( false ); $outputPage->setRobotPolicy( 'noindex,nofollow' ); - $errortext = $error->getWikiText( false, 'view-pool-error' ); + $errortext = $error->getWikiText( + false, 'view-pool-error', $this->getContext()->getLanguage() + ); $outputPage->wrapWikiTextAsInterface( 'errorbox', $errortext ); } # Connection or timeout error @@ -1181,7 +1198,8 @@ class Article implements Page { $title = $this->getTitle(); $rc = false; - if ( !$title->quickUserCan( 'patrol', $user ) + if ( !MediaWikiServices::getInstance()->getPermissionManager() + ->quickUserCan( 'patrol', $user, $title ) || !( $wgUseRCPatrol || $wgUseNPPatrol || ( $wgUseFilePatrol && $title->inNamespace( NS_FILE ) ) ) ) { @@ -1308,7 +1326,10 @@ class Article implements Page { } $outputPage->preventClickjacking(); - if ( $user->isAllowed( 'writeapi' ) ) { + if ( MediaWikiServices::getInstance() + ->getPermissionManager() + ->userHasRight( $user, 'writeapi' ) + ) { $outputPage->addModules( 'mediawiki.page.patrol.ajax' ); } @@ -1450,6 +1471,7 @@ class Article implements Page { # Show error message $oldid = $this->getOldID(); + $pm = MediaWikiServices::getInstance()->getPermissionManager(); if ( !$oldid && $title->getNamespace() === NS_MEDIAWIKI && $title->hasSourceText() ) { // use fake Content object for system message $parserOptions = ParserOptions::newCanonical( 'canonical' ); @@ -1457,8 +1479,8 @@ class Article implements Page { } else { if ( $oldid ) { $text = wfMessage( 'missing-revision', $oldid )->plain(); - } elseif ( $title->quickUserCan( 'create', $this->getContext()->getUser() ) - && $title->quickUserCan( 'edit', $this->getContext()->getUser() ) + } elseif ( $pm->quickUserCan( 'create', $this->getContext()->getUser(), $title ) && + $pm->quickUserCan( 'edit', $this->getContext()->getUser(), $title ) ) { $message = $this->getContext()->getUser()->isLoggedIn() ? 'noarticletext' : 'noarticletextanon'; $text = wfMessage( $message )->plain(); @@ -1593,8 +1615,9 @@ class Article implements Page { 'oldid' => $oldid ] + $extraParams ); - $prev = $this->getTitle()->getPreviousRevisionID( $oldid ); - $prevlink = $prev + $rl = MediaWikiServices::getInstance()->getRevisionLookup(); + $prevExist = (bool)$rl->getPreviousRevision( $revision->getRevisionRecord() ); + $prevlink = $prevExist ? Linker::linkKnown( $this->getTitle(), $context->msg( 'previousrevision' )->escaped(), @@ -1605,7 +1628,7 @@ class Article implements Page { ] + $extraParams ) : $context->msg( 'previousrevision' )->escaped(); - $prevdiff = $prev + $prevdiff = $prevExist ? Linker::linkKnown( $this->getTitle(), $context->msg( 'diff' )->escaped(), @@ -1645,7 +1668,7 @@ class Article implements Page { } // the outer div is need for styling the revision info and nav in MobileFrontend - $outputPage->addSubtitle( "
" . $revisionInfo . + $outputPage->addSubtitle( "
" . $revisionInfo . "
" . $cdel . $context->msg( 'revision-nav' )->rawParams( $prevdiff, $prevlink, $lnk, $curdiff, $nextlink, $nextdiff @@ -1824,7 +1847,10 @@ class Article implements Page { [ 'delete', $this->getTitle()->getPrefixedText() ] ) ) { # Flag to hide all contents of the archived revisions - $suppress = $request->getCheck( 'wpSuppress' ) && $user->isAllowed( 'suppressrevision' ); + + $suppress = $request->getCheck( 'wpSuppress' ) && MediaWikiServices::getInstance() + ->getPermissionManager() + ->userHasRight( $user, 'suppressrevision' ); $this->doDelete( $reason, $suppress ); @@ -1926,6 +1952,8 @@ class Article implements Page { $outputPage->enableOOUI(); + $fields = []; + $options = Xml::listDropDownOptions( $ctx->msg( 'deletereason-dropdown' )->inContentLanguage()->text(), [ 'other' => $ctx->msg( 'deletereasonotherlist' )->inContentLanguage()->text() ] @@ -1981,8 +2009,8 @@ class Article implements Page { ] ); } - - if ( $user->isAllowed( 'suppressrevision' ) ) { + $permissionManager = MediaWikiServices::getInstance()->getPermissionManager(); + if ( $permissionManager->userHasRight( $user, 'suppressrevision' ) ) { $fields[] = new OOUI\FieldLayout( new OOUI\CheckboxInputWidget( [ 'name' => 'wpSuppress', @@ -2040,7 +2068,7 @@ class Article implements Page { ] ) ); - if ( $user->isAllowed( 'editinterface' ) ) { + if ( $permissionManager->userHasRight( $user, 'editinterface' ) ) { $link = Linker::linkKnown( $ctx->msg( 'deletereason-dropdown' )->inContentLanguage()->getTitle(), wfMessage( 'delete-edit-reasonlist' )->escaped(), @@ -2095,7 +2123,7 @@ class Article implements Page { if ( $error == '' ) { $outputPage->wrapWikiTextAsInterface( 'error mw-error-cannotdelete', - $status->getWikiText() + $status->getWikiText( false, false, $context->getLanguage() ) ); $deleteLogPage = new LogPage( 'delete' ); $outputPage->addHTML( Xml::element( 'h2', null, $deleteLogPage->getName()->text() ) ); diff --git a/includes/page/CategoryPage.php b/includes/page/CategoryPage.php index 491726bedd..dda13d3da9 100644 --- a/includes/page/CategoryPage.php +++ b/includes/page/CategoryPage.php @@ -24,16 +24,13 @@ /** * Special handling for category description pages, showing pages, * subcategories and file that belong to the category + * + * @property WikiCategoryPage $mPage Set by overwritten newPage() in this class */ class CategoryPage extends Article { # Subclasses can change this to override the viewer class. protected $mCategoryViewerClass = CategoryViewer::class; - /** - * @var WikiCategoryPage - */ - protected $mPage; - /** * @param Title $title * @return WikiCategoryPage diff --git a/includes/page/ImageHistoryList.php b/includes/page/ImageHistoryList.php index e488b6c1f7..cf2497f94e 100644 --- a/includes/page/ImageHistoryList.php +++ b/includes/page/ImageHistoryList.php @@ -91,7 +91,9 @@ class ImageHistoryList extends ContextSource { . Xml::openElement( 'table', [ 'class' => 'wikitable filehistory' ] ) . "\n" . '' . ( $this->current->isLocal() - && ( $this->getUser()->isAllowedAny( 'delete', 'deletedhistory' ) ) ? '' : '' ) + && ( MediaWikiServices::getInstance() + ->getPermissionManager() + ->userHasAnyRight( $this->getUser(), 'delete', 'deletedhistory' ) ) ? '' : '' ) . '' . $this->msg( 'filehist-datetime' )->escaped() . '' . ( $this->showThumb ? '' . $this->msg( 'filehist-thumb' )->escaped() . '' : '' ) . '' . $this->msg( 'filehist-dimensions' )->escaped() . '' @@ -116,7 +118,9 @@ class ImageHistoryList extends ContextSource { public function imageHistoryLine( $iscur, $file ) { $user = $this->getUser(); $lang = $this->getLanguage(); + $pm = MediaWikiServices::getInstance()->getPermissionManager(); $timestamp = wfTimestamp( TS_MW, $file->getTimestamp() ); + // @phan-suppress-next-line PhanUndeclaredMethod $img = $iscur ? $file->getName() : $file->getArchiveName(); $userId = $file->getUser( 'id' ); $userText = $file->getUser( 'text' ); @@ -126,10 +130,10 @@ class ImageHistoryList extends ContextSource { $row = $selected = ''; // Deletion link - if ( $local && ( $user->isAllowedAny( 'delete', 'deletedhistory' ) ) ) { + if ( $local && ( $pm->userHasAnyRight( $user, 'delete', 'deletedhistory' ) ) ) { $row .= ''; # Link to remove from history - if ( $user->isAllowed( 'delete' ) ) { + if ( $pm->userHasRight( $user, 'delete' ) ) { $q = [ 'action' => 'delete' ]; if ( !$iscur ) { $q['oldimage'] = $img; @@ -141,9 +145,10 @@ class ImageHistoryList extends ContextSource { ); } # Link to hide content. Don't show useless link to people who cannot hide revisions. - $canHide = $user->isAllowed( 'deleterevision' ); - if ( $canHide || ( $user->isAllowed( 'deletedhistory' ) && $file->getVisibility() ) ) { - if ( $user->isAllowed( 'delete' ) ) { + $canHide = $pm->userHasRight( $user, 'deleterevision' ); + if ( $canHide || ( $pm->userHasRight( $user, 'deletedhistory' ) + && $file->getVisibility() ) ) { + if ( $pm->userHasRight( $user, 'delete' ) ) { $row .= '
'; } // If file is top revision or locked from this user, don't link @@ -168,8 +173,8 @@ class ImageHistoryList extends ContextSource { $row .= ''; if ( $iscur ) { $row .= $this->msg( 'filehist-current' )->escaped(); - } elseif ( $local && $this->title->quickUserCan( 'edit', $user ) - && $this->title->quickUserCan( 'upload', $user ) + } elseif ( $local && $pm->quickUserCan( 'edit', $user, $this->title ) + && $pm->quickUserCan( 'upload', $user, $this->title ) ) { if ( $file->isDeleted( File::DELETED_FILE ) ) { $row .= $this->msg( 'filehist-revert' )->escaped(); diff --git a/includes/page/ImageHistoryPseudoPager.php b/includes/page/ImageHistoryPseudoPager.php index 799c33ad90..17a6d51a0b 100644 --- a/includes/page/ImageHistoryPseudoPager.php +++ b/includes/page/ImageHistoryPseudoPager.php @@ -77,7 +77,7 @@ class ImageHistoryPseudoPager extends ReverseChronologicalPager { } public function getQueryInfo() { - return false; + return []; } /** diff --git a/includes/page/ImagePage.php b/includes/page/ImagePage.php index 4f08995212..bb15532c1e 100644 --- a/includes/page/ImagePage.php +++ b/includes/page/ImagePage.php @@ -27,6 +27,9 @@ use Wikimedia\Rdbms\ResultWrapper; * Class for viewing MediaWiki file description pages * * @ingroup Media + * + * @property WikiFilePage $mPage Set by overwritten newPage() in this class + * @method WikiFilePage getPage() */ class ImagePage extends Article { /** @var File|false */ @@ -41,11 +44,6 @@ class ImagePage extends Article { /** @var bool */ protected $mExtraDescription = false; - /** - * @var WikiFilePage - */ - protected $mPage; - /** * @param Title $title * @return WikiFilePage @@ -605,7 +603,10 @@ EOT ); } - if ( $wgEnableUploads && $user->isAllowed( 'upload' ) ) { + if ( $wgEnableUploads && MediaWikiServices::getInstance() + ->getPermissionManager() + ->userHasRight( $user, 'upload' ) + ) { // Only show an upload link if the user can upload $uploadTitle = SpecialPage::getTitleFor( 'Upload' ); $nofile = [ @@ -748,7 +749,8 @@ EOT return; } - $canUpload = $this->getTitle()->quickUserCan( 'upload', $this->getContext()->getUser() ); + $canUpload = MediaWikiServices::getInstance()->getPermissionManager() + ->quickUserCan( 'upload', $this->getContext()->getUser(), $this->getTitle() ); if ( $canUpload && UploadBase::userCanReUpload( $this->getContext()->getUser(), $this->mPage->getFile() ) @@ -988,6 +990,7 @@ EOT parent::delete(); return; } + '@phan-var LocalFile $file'; $deleter = new FileDeleteForm( $file ); $deleter->execute(); diff --git a/includes/page/MovePageFactory.php b/includes/page/MovePageFactory.php new file mode 100644 index 0000000000..ff7ee4bd44 --- /dev/null +++ b/includes/page/MovePageFactory.php @@ -0,0 +1,91 @@ +assertRequiredOptions( self::$constructorOptions ); + + $this->options = $options; + $this->loadBalancer = $loadBalancer; + $this->nsInfo = $nsInfo; + $this->watchedItems = $watchedItems; + $this->permMgr = $permMgr; + $this->repoGroup = $repoGroup; + } + + /** + * @param Title $from + * @param Title $to + * @return MovePage + */ + public function newMovePage( Title $from, Title $to ) : MovePage { + return new MovePage( $from, $to, $this->options, $this->loadBalancer, $this->nsInfo, + $this->watchedItems, $this->permMgr, $this->repoGroup ); + } +} diff --git a/includes/page/Page.php b/includes/page/Page.php index 2cb1fc0389..52b2719735 100644 --- a/includes/page/Page.php +++ b/includes/page/Page.php @@ -20,6 +20,11 @@ /** * Interface for type hinting (accepts WikiPage, Article, ImagePage, CategoryPage) + * + * @method array getActionOverrides() + * @method string getUserText($audience=1,User $user=null) + * @method string getTimestamp() + * @method Title getTitle() */ interface Page { } diff --git a/includes/page/PageArchive.php b/includes/page/PageArchive.php index d69a433d9c..8b2ff6bca8 100644 --- a/includes/page/PageArchive.php +++ b/includes/page/PageArchive.php @@ -461,7 +461,7 @@ class PageArchive { $logEntry->setPerformer( $user ); $logEntry->setTarget( $this->title ); $logEntry->setComment( $comment ); - $logEntry->setTags( $tags ); + $logEntry->addTags( $tags ); $logEntry->setParameters( [ ':assoc:count' => [ 'revisions' => $textRestored, @@ -756,10 +756,14 @@ class PageArchive { Hooks::run( 'ArticleUndelete', [ &$this->title, $created, $comment, $oldPageId, $restoredPages ] ); + if ( $this->title->getNamespace() == NS_FILE ) { - DeferredUpdates::addUpdate( - new HTMLCacheUpdate( $this->title, 'imagelinks', 'file-restore' ) + $job = HTMLCacheUpdateJob::newForBacklinks( + $this->title, + 'imagelinks', + [ 'causeAction' => 'file-restore' ] ); + JobQueueGroup::singleton()->lazyPush( $job ); } } diff --git a/includes/page/WikiFilePage.php b/includes/page/WikiFilePage.php index acd506ba79..fd9f7b24d8 100644 --- a/includes/page/WikiFilePage.php +++ b/includes/page/WikiFilePage.php @@ -176,9 +176,12 @@ class WikiFilePage extends WikiPage { if ( $this->mFile->exists() ) { wfDebug( 'ImagePage::doPurge purging ' . $this->mFile->getName() . "\n" ); - DeferredUpdates::addUpdate( - new HTMLCacheUpdate( $this->mTitle, 'imagelinks', 'file-purge' ) + $job = HTMLCacheUpdateJob::newForBacklinks( + $this->mTitle, + 'imagelinks', + [ 'causeAction' => 'file-purge' ] ); + JobQueueGroup::singleton()->lazyPush( $job ); } else { wfDebug( 'ImagePage::doPurge no image for ' . $this->mFile->getName() . "; limiting purge to cache only\n" ); diff --git a/includes/page/WikiPage.php b/includes/page/WikiPage.php index 8cc5a39b51..c8566ac877 100644 --- a/includes/page/WikiPage.php +++ b/includes/page/WikiPage.php @@ -41,6 +41,8 @@ use Wikimedia\Rdbms\LoadBalancer; * * Some fields are public only for backwards-compatibility. Use accessors. * In the past, this class was part of Article.php and everything was public. + * + * @phan-file-suppress PhanAccessMethodInternal Due to the use of DerivedPageDataUpdater */ class WikiPage implements Page, IDBAccessObject { // Constants for $mDataLoadedFrom and related @@ -68,7 +70,9 @@ class WikiPage implements Page, IDBAccessObject { */ public $mLatest = false; - /** @var PreparedEdit Map of cache fields (text, parser output, ect) for a proposed/new edit */ + /** + * @var PreparedEdit|false Map of cache fields (text, parser output, ect) for a proposed/new edit + */ public $mPreparedEdit = false; /** @@ -722,7 +726,7 @@ class WikiPage implements Page, IDBAccessObject { // Try using the replica DB first, then try the master $rev = $this->mTitle->getFirstRevision(); if ( !$rev ) { - $rev = $this->mTitle->getFirstRevision( Title::GAID_FOR_UPDATE ); + $rev = $this->mTitle->getFirstRevision( Title::READ_LATEST ); } return $rev; } @@ -1892,7 +1896,8 @@ class WikiPage implements Page, IDBAccessObject { // TODO: this check is here for backwards-compatibility with 1.31 behavior. // Checking the minoredit right should be done in the same place the 'bot' right is // checked for the EDIT_FORCE_BOT flag, which is currently in EditPage::attemptSave. - if ( ( $flags & EDIT_MINOR ) && !$user->isAllowed( 'minoredit' ) ) { + $permissionManager = MediaWikiServices::getInstance()->getPermissionManager(); + if ( ( $flags & EDIT_MINOR ) && !$permissionManager->userHasRight( $user, 'minoredit' ) ) { $flags = ( $flags & ~EDIT_MINOR ); } @@ -1912,7 +1917,6 @@ class WikiPage implements Page, IDBAccessObject { // TODO: this logic should not be in the storage layer, it's here for compatibility // with 1.31 behavior. Applying the 'autopatrol' right should be done in the same // place the 'bot' right is handled, which is currently in EditPage::attemptSave. - $permissionManager = MediaWikiServices::getInstance()->getPermissionManager(); if ( $needsPatrol && $permissionManager->userCan( 'autopatrol', $user, $this->getTitle() @@ -2390,7 +2394,7 @@ class WikiPage implements Page, IDBAccessObject { if ( !is_null( $nullRevision ) ) { $logEntry->setAssociatedRevId( $nullRevision->getId() ); } - $logEntry->setTags( $tags ); + $logEntry->addTags( $tags ); if ( $logRelationsField !== null && count( $logRelationsValues ) ) { $logEntry->setRelations( [ $logRelationsField => $logRelationsValues ] ); } @@ -2791,7 +2795,7 @@ class WikiPage implements Page, IDBAccessObject { $logEntry->setPerformer( $deleter ); $logEntry->setTarget( $logTitle ); $logEntry->setComment( $reason ); - $logEntry->setTags( $tags ); + $logEntry->addTags( $tags ); $logid = $logEntry->insert(); $dbw->onTransactionPreCommitOrIdle( @@ -2837,7 +2841,7 @@ class WikiPage implements Page, IDBAccessObject { */ protected function archiveRevisions( $dbw, $id, $suppress ) { global $wgContentHandlerUseDB, $wgMultiContentRevisionSchemaMigrationStage, - $wgActorTableSchemaMigrationStage, $wgDeleteRevisionsBatchSize; + $wgDeleteRevisionsBatchSize; // Given the lock above, we can be confident in the title and page ID values $namespace = $this->getTitle()->getNamespace(); @@ -2964,9 +2968,7 @@ class WikiPage implements Page, IDBAccessObject { $dbw->delete( 'revision', [ 'rev_id' => $revids ], __METHOD__ ); $dbw->delete( 'revision_comment_temp', [ 'revcomment_rev' => $revids ], __METHOD__ ); - if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_NEW ) { - $dbw->delete( 'revision_actor_temp', [ 'revactor_rev' => $revids ], __METHOD__ ); - } + $dbw->delete( 'revision_actor_temp', [ 'revactor_rev' => $revids ], __METHOD__ ); // Also delete records from ip_changes as applicable. if ( count( $ipRevIds ) > 0 ) { @@ -3143,7 +3145,7 @@ class WikiPage implements Page, IDBAccessObject { public function commitRollback( $fromP, $summary, $bot, &$resultDetails, User $guser, $tags = null ) { - global $wgUseRCPatrol; + global $wgUseRCPatrol, $wgDisableAnonTalk; $dbw = wfGetDB( DB_MASTER ); @@ -3216,6 +3218,8 @@ class WikiPage implements Page, IDBAccessObject { if ( empty( $summary ) ) { if ( !$currentEditorForPublic ) { // no public user name $summary = wfMessage( 'revertpage-nouser' ); + } elseif ( $wgDisableAnonTalk && $current->getUser() === 0 ) { + $summary = wfMessage( 'revertpage-anon' ); } else { $summary = wfMessage( 'revertpage' ); } @@ -3245,11 +3249,12 @@ class WikiPage implements Page, IDBAccessObject { // Save $flags = EDIT_UPDATE | EDIT_INTERNAL; - if ( $guser->isAllowed( 'minoredit' ) ) { + $permissionManager = MediaWikiServices::getInstance()->getPermissionManager(); + if ( $permissionManager->userHasRight( $guser, 'minoredit' ) ) { $flags |= EDIT_MINOR; } - if ( $bot && ( $guser->isAllowedAny( 'markbotedits', 'bot' ) ) ) { + if ( $bot && ( $permissionManager->userHasAnyRight( $guser, 'markbotedits', 'bot' ) ) ) { $flags |= EDIT_FORCE_BOT; } @@ -3284,7 +3289,6 @@ class WikiPage implements Page, IDBAccessObject { // TODO: this logic should not be in the storage layer, it's here for compatibility // with 1.31 behavior. Applying the 'autopatrol' right should be done in the same // place the 'bot' right is handled, which is currently in EditPage::attemptSave. - $permissionManager = MediaWikiServices::getInstance()->getPermissionManager(); if ( $wgUseRCPatrol && $permissionManager->userCan( 'autopatrol', $guser, $this->getTitle() @@ -3301,7 +3305,7 @@ class WikiPage implements Page, IDBAccessObject { // Set patrolling and bot flag on the edits, which gets rollbacked. // This is done even on edit failure to have patrolling in that case (T64157). $set = []; - if ( $bot && $guser->isAllowed( 'markbotedits' ) ) { + if ( $bot && $permissionManager->userHasRight( $guser, 'markbotedits' ) ) { // Mark all reverted edits as bot $set['rc_bot'] = 1; } @@ -3400,9 +3404,12 @@ class WikiPage implements Page, IDBAccessObject { MediaWikiServices::getInstance()->getLinkCache()->invalidateTitle( $title ); // Invalidate caches of articles which include this page - DeferredUpdates::addUpdate( - new HTMLCacheUpdate( $title, 'templatelinks', 'page-create' ) + $job = HTMLCacheUpdateJob::newForBacklinks( + $title, + 'templatelinks', + [ 'causeAction' => 'page-create' ] ); + JobQueueGroup::singleton()->lazyPush( $job ); if ( $title->getNamespace() == NS_CATEGORY ) { // Load the Category object, which will schedule a job to create @@ -3444,9 +3451,12 @@ class WikiPage implements Page, IDBAccessObject { // Images if ( $title->getNamespace() == NS_FILE ) { - DeferredUpdates::addUpdate( - new HTMLCacheUpdate( $title, 'imagelinks', 'page-delete' ) + $job = HTMLCacheUpdateJob::newForBacklinks( + $title, + 'imagelinks', + [ 'causeAction' => 'page-delete' ] ); + JobQueueGroup::singleton()->lazyPush( $job ); } // User talk pages @@ -3478,20 +3488,24 @@ class WikiPage implements Page, IDBAccessObject { $slotsChanged = null ) { // TODO: move this into a PageEventEmitter service - - if ( $slotsChanged === null || in_array( SlotRecord::MAIN, $slotsChanged ) ) { + $jobs = []; + if ( $slotsChanged === null || in_array( SlotRecord::MAIN, $slotsChanged ) ) { // Invalidate caches of articles which include this page. // Only for the main slot, because only the main slot is transcluded. // TODO: MCR: not true for TemplateStyles! [SlotHandler] - DeferredUpdates::addUpdate( - new HTMLCacheUpdate( $title, 'templatelinks', 'page-edit' ) + $jobs[] = HTMLCacheUpdateJob::newForBacklinks( + $title, + 'templatelinks', + [ 'causeAction' => 'page-edit' ] ); } - // Invalidate the caches of all pages which redirect here - DeferredUpdates::addUpdate( - new HTMLCacheUpdate( $title, 'redirect', 'page-edit' ) + $jobs[] = HTMLCacheUpdateJob::newForBacklinks( + $title, + 'redirect', + [ 'causeAction' => 'page-edit' ] ); + JobQueueGroup::singleton()->lazyPush( $jobs ); MediaWikiServices::getInstance()->getLinkCache()->invalidateTitle( $title ); diff --git a/includes/pager/IndexPager.php b/includes/pager/IndexPager.php index 04021cc038..6af60a7b19 100644 --- a/includes/pager/IndexPager.php +++ b/includes/pager/IndexPager.php @@ -21,10 +21,11 @@ * @ingroup Pager */ -use Wikimedia\Rdbms\IResultWrapper; -use Wikimedia\Rdbms\IDatabase; -use MediaWiki\Linker\LinkTarget; +use MediaWiki\Linker\LinkRenderer; +use MediaWiki\MediaWikiServices; use MediaWiki\Navigation\PrevNextNavigationRenderer; +use Wikimedia\Rdbms\IDatabase; +use Wikimedia\Rdbms\IResultWrapper; /** * IndexPager is an efficient pager which uses a (roughly unique) index in the @@ -157,7 +158,10 @@ abstract class IndexPager extends ContextSource implements Pager { */ public $mResult; - public function __construct( IContextSource $context = null ) { + /** @var LinkRenderer */ + private $linkRenderer; + + public function __construct( IContextSource $context = null, LinkRenderer $linkRenderer = null ) { if ( $context ) { $this->setContext( $context ); } @@ -209,6 +213,7 @@ abstract class IndexPager extends ContextSource implements Pager { ? $dir[$this->mOrderType] : $dir; } + $this->linkRenderer = $linkRenderer; } /** @@ -526,9 +531,9 @@ abstract class IndexPager extends ContextSource implements Pager { $attrs['class'] = "mw-{$type}link"; } - return Linker::linkKnown( + return $this->getLinkRenderer()->makeKnownLink( $this->getTitle(), - $text, + new HtmlArmor( $text ), $attrs, $query + $this->getDefaultQuery() ); @@ -790,18 +795,25 @@ abstract class IndexPager extends ContextSource implements Pager { /** * Generate (prev x| next x) (20|50|100...) type links for paging * - * @param LinkTarget $title + * @param Title $title * @param int $offset * @param int $limit * @param array $query Optional URL query parameter string * @param bool $atend Optional param for specified if this is the last page * @return string */ - protected function buildPrevNextNavigation( LinkTarget $title, $offset, $limit, + protected function buildPrevNextNavigation( Title $title, $offset, $limit, array $query = [], $atend = false ) { $prevNext = new PrevNextNavigationRenderer( $this ); return $prevNext->buildPrevNextNavigation( $title, $offset, $limit, $query, $atend ); } + + protected function getLinkRenderer() { + if ( $this->linkRenderer === null ) { + $this->linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer(); + } + return $this->linkRenderer; + } } diff --git a/includes/pager/TablePager.php b/includes/pager/TablePager.php index d94104bda9..f611699b94 100644 --- a/includes/pager/TablePager.php +++ b/includes/pager/TablePager.php @@ -21,6 +21,8 @@ * @ingroup Pager */ +use MediaWiki\Linker\LinkRenderer; + /** * Table-based display with a user-selectable sort order * @ingroup Pager @@ -32,7 +34,7 @@ abstract class TablePager extends IndexPager { /** @var stdClass */ protected $mCurrentRow; - public function __construct( IContextSource $context = null ) { + public function __construct( IContextSource $context = null, LinkRenderer $linkRenderer = null ) { if ( $context ) { $this->setContext( $context ); } @@ -49,7 +51,8 @@ abstract class TablePager extends IndexPager { $this->mDefaultDirection = IndexPager::DIR_DESCENDING; } /* Else leave it at whatever the class default is */ - parent::__construct(); + // Parent constructor needs mSort set, so we call it last + parent::__construct( null, $linkRenderer ); } /** diff --git a/includes/parser/CoreParserFunctions.php b/includes/parser/CoreParserFunctions.php index a7916c5796..b803241956 100644 --- a/includes/parser/CoreParserFunctions.php +++ b/includes/parser/CoreParserFunctions.php @@ -434,7 +434,7 @@ class CoreParserFunctions { if ( !$wgRestrictDisplayTitle || ( $title instanceof Title && !$title->hasFragment() - && $title->equals( $parser->mTitle ) ) + && $title->equals( $parser->getTitle() ) ) ) { $old = $parser->mOutput->getProperty( 'displaytitle' ); if ( $old === false || $arg !== 'displaytitle_noreplace' ) { @@ -845,10 +845,7 @@ class CoreParserFunctions { * @return string */ public static function protectionlevel( $parser, $type = '', $title = '' ) { - $titleObject = Title::newFromText( $title ); - if ( !( $titleObject instanceof Title ) ) { - $titleObject = $parser->mTitle; - } + $titleObject = Title::newFromText( $title ) ?? $parser->getTitle(); if ( $titleObject->areRestrictionsLoaded() || $parser->incrementExpensiveFunctionCount() ) { $restrictions = $titleObject->getRestrictions( strtolower( $type ) ); # Title::getRestrictions returns an array, its possible it may have @@ -871,10 +868,7 @@ class CoreParserFunctions { * @return string */ public static function protectionexpiry( $parser, $type = '', $title = '' ) { - $titleObject = Title::newFromText( $title ); - if ( !( $titleObject instanceof Title ) ) { - $titleObject = $parser->mTitle; - } + $titleObject = Title::newFromText( $title ) ?? $parser->getTitle(); if ( $titleObject->areRestrictionsLoaded() || $parser->incrementExpensiveFunctionCount() ) { $expiry = $titleObject->getRestrictionExpiry( strtolower( $type ) ); // getRestrictionExpiry() returns false on invalid type; trying to @@ -1127,7 +1121,7 @@ class CoreParserFunctions { * @param Parser $parser * @param Title $title * @param string $vary ParserOuput vary-* flag - * @return Revision + * @return Revision|null * @since 1.23 */ private static function getCachedRevisionObject( $parser, $title, $vary ) { @@ -1377,10 +1371,7 @@ class CoreParserFunctions { * @since 1.23 */ public static function cascadingsources( $parser, $title = '' ) { - $titleObject = Title::newFromText( $title ); - if ( !( $titleObject instanceof Title ) ) { - $titleObject = $parser->mTitle; - } + $titleObject = Title::newFromText( $title ) ?? $parser->getTitle(); if ( $titleObject->areCascadeProtectionSourcesLoaded() || $parser->incrementExpensiveFunctionCount() ) { diff --git a/includes/parser/LinkHolderArray.php b/includes/parser/LinkHolderArray.php index 64164490c2..5eb799e9e6 100644 --- a/includes/parser/LinkHolderArray.php +++ b/includes/parser/LinkHolderArray.php @@ -48,6 +48,7 @@ class LinkHolderArray { * Reduce memory usage to reduce the impact of circular references */ public function __destruct() { + // @phan-suppress-next-line PhanTypeSuspiciousNonTraversableForeach foreach ( $this as $name => $value ) { unset( $this->$name ); } diff --git a/includes/parser/PPDPart.php b/includes/parser/PPDPart.php index 187373052c..6b63a0dd96 100644 --- a/includes/parser/PPDPart.php +++ b/includes/parser/PPDPart.php @@ -21,6 +21,10 @@ /** * @ingroup Parser + * + * @property int $eqpos + * @property int $commentEnd + * @property int $visualEnd */ class PPDPart { /** @@ -33,6 +37,9 @@ class PPDPart { // commentEnd Past-the-end input pointer for the last comment encountered // visualEnd Past-the-end input pointer for the end of the accumulator minus comments + /** + * @param string $out + */ public function __construct( $out = '' ) { $this->out = $out; } diff --git a/includes/parser/PPDPart_Hash.php b/includes/parser/PPDPart_Hash.php index 7507f06fe0..597f851d4a 100644 --- a/includes/parser/PPDPart_Hash.php +++ b/includes/parser/PPDPart_Hash.php @@ -21,16 +21,21 @@ /** * @ingroup Parser + * @property string[] $out */ // phpcs:ignore Squiz.Classes.ValidClassName.NotCamelCaps class PPDPart_Hash extends PPDPart { + /** + * @param string $out + */ public function __construct( $out = '' ) { if ( $out !== '' ) { $accum = [ $out ]; } else { $accum = []; } + // @phan-suppress-next-line PhanTypeMismatchArgument parent::__construct( $accum ); } } diff --git a/includes/parser/PPDStack.php b/includes/parser/PPDStack.php index adc0bc0014..b9d796d7a0 100644 --- a/includes/parser/PPDStack.php +++ b/includes/parser/PPDStack.php @@ -24,10 +24,14 @@ * @ingroup Parser */ class PPDStack { - public $stack, $rootAccum; + /** @var PPDStackElement[] */ + public $stack; + public $rootAccum; + /** @var string|array */ + public $accum; /** - * @var PPDStack|false + * @var PPDStackElement|false */ public $top; public $out; diff --git a/includes/parser/PPDStackElement.php b/includes/parser/PPDStackElement.php index 116244dbb7..fe2b04e223 100644 --- a/includes/parser/PPDStackElement.php +++ b/includes/parser/PPDStackElement.php @@ -21,6 +21,8 @@ /** * @ingroup Parser + * + * @property int $startPos */ class PPDStackElement { /** diff --git a/includes/parser/PPDStackElement_Hash.php b/includes/parser/PPDStackElement_Hash.php index 26351b21d3..750049de4d 100644 --- a/includes/parser/PPDStackElement_Hash.php +++ b/includes/parser/PPDStackElement_Hash.php @@ -21,6 +21,7 @@ /** * @ingroup Parser + * @property PPDPart_Hash[] $parts */ // phpcs:ignore Squiz.Classes.ValidClassName.NotCamelCaps class PPDStackElement_Hash extends PPDStackElement { @@ -35,6 +36,7 @@ class PPDStackElement_Hash extends PPDStackElement { * * @param int|bool $openingCount * @return array + * @suppress PhanParamSignatureMismatch */ public function breakSyntax( $openingCount = false ) { if ( $this->open == "\n" ) { @@ -59,6 +61,7 @@ class PPDStackElement_Hash extends PPDStackElement { } else { $accum[++$lastIndex] = '|'; } + foreach ( $part->out as $node ) { if ( is_string( $node ) && is_string( $accum[$lastIndex] ) ) { $accum[$lastIndex] .= $node; diff --git a/includes/parser/PPFrame.php b/includes/parser/PPFrame.php index 79c7c3b30a..b50fcfce05 100644 --- a/includes/parser/PPFrame.php +++ b/includes/parser/PPFrame.php @@ -21,6 +21,9 @@ /** * @ingroup Parser + * + * @property int $depth + * @property PPFrame $parent */ interface PPFrame { const NO_ARGS = 1; @@ -69,6 +72,7 @@ interface PPFrame { * @param string $sep * @param int $flags * @param string|PPNode $args,... + * @suppress PhanCommentParamWithoutRealParam HHVM bug T228695#5450847 * @return string */ public function implodeWithFlags( $sep, $flags /*, ... */ ); @@ -77,6 +81,7 @@ interface PPFrame { * Implode with no flags specified * @param string $sep * @param string|PPNode $args,... + * @suppress PhanCommentParamWithoutRealParam HHVM bug T228695#5450847 * @return string */ public function implode( $sep /*, ... */ ); @@ -85,20 +90,22 @@ interface PPFrame { * Makes an object that, when expand()ed, will be the same as one obtained * with implode() * @param string $sep - * @param string|PPNode $args,... + * @param string|PPNode ...$args + * @suppress PhanCommentParamWithoutRealParam HHVM bug T228695#5450847 * @return PPNode */ - public function virtualImplode( $sep /*, ... */ ); + public function virtualImplode( $sep /* ...$args */ ); /** * Virtual implode with brackets * @param string $start * @param string $sep * @param string $end - * @param string|PPNode $args,... + * @param string|PPNode ...$args + * @suppress PhanCommentParamWithoutRealParam HHVM bug T228695#5450847 * @return PPNode */ - public function virtualBracketedImplode( $start, $sep, $end /*, ... */ ); + public function virtualBracketedImplode( $start, $sep, $end /* ...$args */ ); /** * Returns true if there are no arguments in this frame diff --git a/includes/parser/PPFrame_DOM.php b/includes/parser/PPFrame_DOM.php index 452bab1259..a0ec326d95 100644 --- a/includes/parser/PPFrame_DOM.php +++ b/includes/parser/PPFrame_DOM.php @@ -23,6 +23,7 @@ * An expansion frame, used as a context to expand the result of preprocessToObj() * @deprecated since 1.34, use PPFrame_Hash * @ingroup Parser + * @phan-file-suppress PhanUndeclaredMethod */ // phpcs:ignore Squiz.Classes.ValidClassName.NotCamelCaps class PPFrame_DOM implements PPFrame { @@ -70,7 +71,7 @@ class PPFrame_DOM implements PPFrame { public function __construct( $preprocessor ) { $this->preprocessor = $preprocessor; $this->parser = $preprocessor->parser; - $this->title = $this->parser->mTitle; + $this->title = $this->parser->getTitle(); $this->titleCache = [ $this->title ? $this->title->getPrefixedDBkey() : false ]; $this->loopCheckHash = []; $this->depth = 0; @@ -81,7 +82,7 @@ class PPFrame_DOM implements PPFrame { * Create a new child frame * $args is optionally a multi-root PPNode or array containing the template arguments * - * @param bool|array $args + * @param bool|array|PPNode_DOM $args * @param Title|bool $title * @param int $indexOffset * @return PPTemplateFrame_DOM @@ -94,11 +95,12 @@ class PPFrame_DOM implements PPFrame { } if ( $args !== false ) { $xpath = false; - if ( $args instanceof PPNode ) { + if ( $args instanceof PPNode_DOM ) { $args = $args->node; } + // @phan-suppress-next-line PhanTypeSuspiciousNonTraversableForeach foreach ( $args as $arg ) { - if ( $arg instanceof PPNode ) { + if ( $arg instanceof PPNode_DOM ) { $arg = $arg->node; } if ( !$xpath || $xpath->document !== $arg->ownerDocument ) { @@ -152,7 +154,7 @@ class PPFrame_DOM implements PPFrame { /** * @throws MWException - * @param string|PPNode_DOM|DOMNode $root + * @param string|PPNode_DOM|DOMNode|DOMNodeList $root * @param int $flags * @return string */ @@ -458,6 +460,7 @@ class PPFrame_DOM implements PPFrame { * @param string $sep * @param string|PPNode_DOM|DOMNode ...$args * @return array + * @suppress PhanParamSignatureMismatch */ public function virtualImplode( $sep, ...$args ) { $out = []; @@ -489,6 +492,7 @@ class PPFrame_DOM implements PPFrame { * @param string $end * @param string|PPNode_DOM|DOMNode ...$args * @return array + * @suppress PhanParamSignatureMismatch */ public function virtualBracketedImplode( $start, $sep, $end, ...$args ) { $out = [ $start ]; diff --git a/includes/parser/PPFrame_Hash.php b/includes/parser/PPFrame_Hash.php index 845ec73c7e..f38cb06968 100644 --- a/includes/parser/PPFrame_Hash.php +++ b/includes/parser/PPFrame_Hash.php @@ -69,7 +69,7 @@ class PPFrame_Hash implements PPFrame { public function __construct( $preprocessor ) { $this->preprocessor = $preprocessor; $this->parser = $preprocessor->parser; - $this->title = $this->parser->mTitle; + $this->title = $this->parser->getTitle(); $this->titleCache = [ $this->title ? $this->title->getPrefixedDBkey() : false ]; $this->loopCheckHash = []; $this->depth = 0; diff --git a/includes/parser/PPNode_DOM.php b/includes/parser/PPNode_DOM.php index 53b17617bb..ae7f8a2ca6 100644 --- a/includes/parser/PPNode_DOM.php +++ b/includes/parser/PPNode_DOM.php @@ -22,6 +22,7 @@ /** * @deprecated since 1.34, use PPNode_Hash_{Tree,Text,Array,Attr} * @ingroup Parser + * @phan-file-suppress PhanUndeclaredMethod */ // phpcs:ignore Squiz.Classes.ValidClassName.NotCamelCaps class PPNode_DOM implements PPNode { diff --git a/includes/parser/PPTemplateFrame_Hash.php b/includes/parser/PPTemplateFrame_Hash.php index df740cf22d..902e4f17b8 100644 --- a/includes/parser/PPTemplateFrame_Hash.php +++ b/includes/parser/PPTemplateFrame_Hash.php @@ -40,6 +40,8 @@ class PPTemplateFrame_Hash extends PPFrame_Hash { $namedArgs = [], $title = false ) { parent::__construct( $preprocessor ); + /** @var PPFrame_Hash parent */ + '@phan-var PPFrame_Hash $parent'; $this->parent = $parent; $this->numberedArgs = $numberedArgs; diff --git a/includes/parser/Parser.php b/includes/parser/Parser.php index e5bf94a602..9ec58346cc 100644 --- a/includes/parser/Parser.php +++ b/includes/parser/Parser.php @@ -20,6 +20,7 @@ * @file * @ingroup Parser */ +use MediaWiki\BadFileLookup; use MediaWiki\Config\ServiceOptions; use MediaWiki\Linker\LinkRenderer; use MediaWiki\Linker\LinkRendererFactory; @@ -299,6 +300,9 @@ class Parser { /** @var LoggerInterface */ private $logger; + /** @var BadFileLookup */ + private $badFileLookup; + /** * TODO Make this a const when HHVM support is dropped (T192166) * @@ -339,6 +343,7 @@ class Parser { * @param LinkRendererFactory|null $linkRendererFactory * @param NamespaceInfo|null $nsInfo * @param LoggerInterface|null $logger + * @param BadFileLookup|null $badFileLookup */ public function __construct( $svcOptions = null, @@ -349,9 +354,9 @@ class Parser { SpecialPageFactory $spFactory = null, $linkRendererFactory = null, $nsInfo = null, - $logger = null + $logger = null, + BadFileLookup $badFileLookup = null ) { - $services = MediaWikiServices::getInstance(); if ( !$svcOptions || is_array( $svcOptions ) ) { // Pre-1.34 calling convention is the first parameter is just ParserConf, the seventh is // Config, and the eighth is LinkRendererFactory. @@ -363,8 +368,8 @@ class Parser { $this->mConf['preprocessorClass'] = self::getDefaultPreprocessorClass(); } $this->svcOptions = new ServiceOptions( self::$constructorOptions, - $this->mConf, - func_num_args() > 6 ? func_get_arg( 6 ) : $services->getMainConfig() + $this->mConf, func_num_args() > 6 + ? func_get_arg( 6 ) : MediaWikiServices::getInstance()->getMainConfig() ); $linkRendererFactory = func_num_args() > 7 ? func_get_arg( 7 ) : null; $nsInfo = func_num_args() > 8 ? func_get_arg( 8 ) : null; @@ -386,15 +391,19 @@ class Parser { self::EXT_LINK_URL_CLASS . '*)\p{Zs}*([^\]\\x00-\\x08\\x0a-\\x1F\\x{FFFD}]*?)\]/Su'; $this->magicWordFactory = $magicWordFactory ?? - $services->getMagicWordFactory(); + MediaWikiServices::getInstance()->getMagicWordFactory(); - $this->contLang = $contLang ?? $services->getContentLanguage(); + $this->contLang = $contLang ?? MediaWikiServices::getInstance()->getContentLanguage(); - $this->factory = $factory ?? $services->getParserFactory(); - $this->specialPageFactory = $spFactory ?? $services->getSpecialPageFactory(); - $this->linkRendererFactory = $linkRendererFactory ?? $services->getLinkRendererFactory(); - $this->nsInfo = $nsInfo ?? $services->getNamespaceInfo(); + $this->factory = $factory ?? MediaWikiServices::getInstance()->getParserFactory(); + $this->specialPageFactory = $spFactory ?? + MediaWikiServices::getInstance()->getSpecialPageFactory(); + $this->linkRendererFactory = $linkRendererFactory ?? + MediaWikiServices::getInstance()->getLinkRendererFactory(); + $this->nsInfo = $nsInfo ?? MediaWikiServices::getInstance()->getNamespaceInfo(); $this->logger = $logger ?: new NullLogger(); + $this->badFileLookup = $badFileLookup ?? + MediaWikiServices::getInstance()->getBadFileLookup(); } /** @@ -402,8 +411,10 @@ class Parser { */ public function __destruct() { if ( isset( $this->mLinkHolders ) ) { + // @phan-suppress-next-line PhanTypeObjectUnsetDeclaredProperty unset( $this->mLinkHolders ); } + // @phan-suppress-next-line PhanTypeSuspiciousNonTraversableForeach foreach ( $this as $name => $value ) { unset( $this->$name ); } @@ -468,8 +479,7 @@ class Parser { */ public function clearState() { $this->firstCallInit(); - $this->mOutput = new ParserOutput; - $this->mOptions->registerWatcher( [ $this->mOutput, 'recordOption' ] ); + $this->resetOutput(); $this->mAutonumber = 0; $this->mIncludeCount = []; $this->mLinkHolders = new LinkHolderArray( $this ); @@ -512,6 +522,14 @@ class Parser { Hooks::run( 'ParserClearState', [ &$parser ] ); } + /** + * Reset the ParserOutput + */ + public function resetOutput() { + $this->mOutput = new ParserOutput; + $this->mOptions->registerWatcher( [ $this->mOutput, 'recordOption' ] ); + } + /** * Convert wikitext to HTML * Do not call this function recursively. @@ -522,7 +540,10 @@ class Parser { * @param ParserOptions $options * @param bool $linestart * @param bool $clearState - * @param int|null $revid Number to pass in {{REVISIONID}} + * @param int|null $revid ID of the revision being rendered. This is used to render + * REVISION* magic words. 0 means that any current revision will be used. Null means + * that {{REVISIONID}}/{{REVISIONUSER}} will be empty and {{REVISIONTIMESTAMP}} will + * use the current timestamp. * @return ParserOutput A ParserOutput * @return-taint escaped */ @@ -889,7 +910,7 @@ class Parser { */ public function setTitle( $t ) { if ( !$t ) { - $t = Title::newFromText( 'NO TITLE' ); + $t = Title::makeTitle( NS_SPECIAL, 'Badtitle/Parser' ); } if ( $t->hasFragment() ) { @@ -1177,6 +1198,15 @@ class Parser { return $this->mStripList; } + /** + * Get the StripState + * + * @return StripState + */ + public function getStripState() { + return $this->mStripState; + } + /** * Add an item to the strip state * Returns the unique tag which must be inserted into the stripped text @@ -1980,6 +2010,7 @@ class Parser { */ public function replaceExternalLinks( $text ) { $bits = preg_split( $this->mExtLinkBracketedRegex, $text, -1, PREG_SPLIT_DELIM_CAPTURE ); + // @phan-suppress-next-line PhanTypeComparisonFromArray See phan issue #3161 if ( $bits === false ) { throw new MWException( "PCRE needs to be compiled with " . "--enable-unicode-properties in order for MediaWiki to function" ); @@ -2281,6 +2312,11 @@ class Parser { $line = $a->current(); # Workaround for broken ArrayIterator::next() that returns "void" $s = substr( $s, 1 ); + if ( is_null( $this->mTitle ) ) { + throw new MWException( __METHOD__ . ": \$this->mTitle is null\n" ); + } + $nottalk = !$this->mTitle->isTalkPage(); + $useLinkPrefixExtension = $this->getTargetLanguage()->linkPrefixExtension(); $e2 = null; if ( $useLinkPrefixExtension ) { @@ -2288,14 +2324,6 @@ class Parser { # e.g. in the case of 'The Arab al[[Razi]]', 'al' will be matched $charset = $this->contLang->linkPrefixCharset(); $e2 = "/^((?>.*[^$charset]|))(.+)$/sDu"; - } - - if ( is_null( $this->mTitle ) ) { - throw new MWException( __METHOD__ . ": \$this->mTitle is null\n" ); - } - $nottalk = !$this->mTitle->isTalkPage(); - - if ( $useLinkPrefixExtension ) { $m = []; if ( preg_match( $e2, $s, $m ) ) { $first_prefix = $m[2]; @@ -2481,7 +2509,7 @@ class Parser { } if ( $ns == NS_FILE ) { - if ( !wfIsBadImage( $nt->getDBkey(), $this->mTitle ) ) { + if ( !$this->badFileLookup->isBadFile( $nt->getDBkey(), $this->mTitle ) ) { if ( $wasblank ) { # if no parameters were passed, $text # becomes something like "File:Foo.png", @@ -3742,21 +3770,22 @@ class Parser { // Defaults to Parser::statelessFetchTemplate() $templateCb = $this->mOptions->getTemplateCallback(); $stuff = call_user_func( $templateCb, $title, $this ); - // We use U+007F DELETE to distinguish strip markers from regular text. + $rev = $stuff['revision'] ?? null; $text = $stuff['text']; if ( is_string( $stuff['text'] ) ) { + // We use U+007F DELETE to distinguish strip markers from regular text $text = strtr( $text, "\x7f", "?" ); } $finalTitle = $stuff['finalTitle'] ?? $title; - if ( isset( $stuff['deps'] ) ) { - foreach ( $stuff['deps'] as $dep ) { - $this->mOutput->addTemplate( $dep['title'], $dep['page_id'], $dep['rev_id'] ); - if ( $dep['title']->equals( $this->getTitle() ) ) { - // Self-transclusion; final result may change based on the new page version - $this->setOutputFlag( 'vary-revision', 'Self transclusion' ); - } + foreach ( ( $stuff['deps'] ?? [] ) as $dep ) { + $this->mOutput->addTemplate( $dep['title'], $dep['page_id'], $dep['rev_id'] ); + if ( $dep['title']->equals( $this->getTitle() ) && $rev instanceof Revision ) { + // Self-transclusion; final result may change based on the new page version + $this->setOutputFlag( 'vary-revision-sha1', 'Self transclusion' ); + $this->getOutput()->setRevisionUsedSha1Base36( $rev->getSha1() ); } } + return [ $text, $finalTitle ]; } @@ -3782,6 +3811,7 @@ class Parser { $text = $skip = false; $finalTitle = $title; $deps = []; + $rev = null; # Loop to fetch the article, with up to 1 redirect for ( $i = 0; $i < 2 && is_object( $title ); $i++ ) { @@ -3817,13 +3847,15 @@ class Parser { $deps[] = [ 'title' => $title, 'page_id' => $title->getArticleID(), - 'rev_id' => $rev_id ]; + 'rev_id' => $rev_id + ]; if ( $rev && !$title->equals( $rev->getTitle() ) ) { # We fetched a rev from a different title; register it too... $deps[] = [ 'title' => $rev->getTitle(), 'page_id' => $rev->getPage(), - 'rev_id' => $rev_id ]; + 'rev_id' => $rev_id + ]; } if ( $rev ) { @@ -3857,9 +3889,11 @@ class Parser { $title = $content->getRedirectTarget(); } return [ + 'revision' => $rev, 'text' => $text, 'finalTitle' => $finalTitle, - 'deps' => $deps ]; + 'deps' => $deps + ]; } /** @@ -4857,11 +4891,15 @@ class Parser { * @param ParserOptions $options * @param int $outputType * @param bool $clearState + * @param int|null $revId */ public function startExternalParse( Title $title = null, ParserOptions $options, - $outputType, $clearState = true + $outputType, $clearState = true, $revId = null ) { $this->startParse( $title, $options, $outputType, $clearState ); + if ( $revId !== null ) { + $this->mRevisionId = $revId; + } } /** @@ -6149,7 +6187,9 @@ class Parser { */ private static function normalizeSectionName( $text ) { # T90902: ensure the same normalization is applied for IDs as to links + /** @var MediaWikiTitleCodec $titleParser */ $titleParser = MediaWikiServices::getInstance()->getTitleParser(); + '@phan-var MediaWikiTitleCodec $titleParser'; try { $parts = $titleParser->splitTitleString( "#$text" ); diff --git a/includes/parser/ParserCache.php b/includes/parser/ParserCache.php index 2585872c2f..f87135892f 100644 --- a/includes/parser/ParserCache.php +++ b/includes/parser/ParserCache.php @@ -49,7 +49,7 @@ class ParserCache { const USE_ANYTHING = 3; /** @var BagOStuff */ - private $mMemc; + private $cache; /** * Anything cached prior to this is invalidated @@ -79,7 +79,7 @@ class ParserCache { * @throws MWException */ public function __construct( BagOStuff $cache, $cacheEpoch = '20030516000000' ) { - $this->mMemc = $cache; + $this->cache = $cache; $this->cacheEpoch = $cacheEpoch; } @@ -95,7 +95,7 @@ class ParserCache { $pageid = $article->getId(); $renderkey = (int)( $wgRequest->getVal( 'action' ) == 'render' ); - $key = $this->mMemc->makeKey( 'pcache', 'idhash', "{$pageid}-{$renderkey}!{$hash}" ); + $key = $this->cache->makeKey( 'pcache', 'idhash', "{$pageid}-{$renderkey}!{$hash}" ); return $key; } @@ -104,7 +104,7 @@ class ParserCache { * @return mixed|string */ protected function getOptionsKey( $page ) { - return $this->mMemc->makeKey( 'pcache', 'idoptions', $page->getId() ); + return $this->cache->makeKey( 'pcache', 'idoptions', $page->getId() ); } /** @@ -112,7 +112,7 @@ class ParserCache { * @since 1.28 */ public function deleteOptionsKey( $page ) { - $this->mMemc->delete( $this->getOptionsKey( $page ) ); + $this->cache->delete( $this->getOptionsKey( $page ) ); } /** @@ -190,7 +190,7 @@ class ParserCache { } // Determine the options which affect this article - $optionsKey = $this->mMemc->get( + $optionsKey = $this->cache->get( $this->getOptionsKey( $article ), BagOStuff::READ_VERIFIED ); if ( $optionsKey instanceof CacheTime ) { if ( $useOutdated < self::USE_EXPIRED && $optionsKey->expired( $article->getTouched() ) ) { @@ -257,7 +257,7 @@ class ParserCache { $casToken = null; /** @var ParserOutput $value */ - $value = $this->mMemc->get( $parserOutputKey, BagOStuff::READ_VERIFIED ); + $value = $this->cache->get( $parserOutputKey, BagOStuff::READ_VERIFIED ); if ( !$value ) { wfDebug( "ParserOutput cache miss.\n" ); $this->incrementStats( $article, "miss.absent" ); @@ -319,7 +319,7 @@ class ParserCache { } $expire = $parserOutput->getCacheExpiry(); - if ( $expire > 0 && !$this->mMemc instanceof EmptyBagOStuff ) { + if ( $expire > 0 && !$this->cache instanceof EmptyBagOStuff ) { $cacheTime = $cacheTime ?: wfTimestampNow(); if ( !$revId ) { $revision = $page->getRevision(); @@ -350,10 +350,15 @@ class ParserCache { wfDebug( $msg ); // Save the parser output - $this->mMemc->set( $parserOutputKey, $parserOutput, $expire ); + $this->cache->set( + $parserOutputKey, + $parserOutput, + $expire, + BagOStuff::WRITE_ALLOW_SEGMENTS + ); // ...and its pointer - $this->mMemc->set( $this->getOptionsKey( $page ), $optionsKey, $expire ); + $this->cache->set( $this->getOptionsKey( $page ), $optionsKey, $expire ); Hooks::run( 'ParserCacheSaveComplete', @@ -372,6 +377,6 @@ class ParserCache { * @return BagOStuff */ public function getCacheStorage() { - return $this->mMemc; + return $this->cache; } } diff --git a/includes/parser/ParserDiffTest.php b/includes/parser/ParserDiffTest.php index 93f4246e02..bd610de67f 100644 --- a/includes/parser/ParserDiffTest.php +++ b/includes/parser/ParserDiffTest.php @@ -40,6 +40,7 @@ class ParserDiffTest { if ( !is_null( $this->parsers ) ) { return; } + $this->parsers = []; if ( isset( $this->conf['shortOutput'] ) ) { $this->shortOutput = $this->conf['shortOutput']; diff --git a/includes/parser/ParserFactory.php b/includes/parser/ParserFactory.php index 3d15e86fd5..bab1f364f6 100644 --- a/includes/parser/ParserFactory.php +++ b/includes/parser/ParserFactory.php @@ -19,6 +19,7 @@ * @ingroup Parser */ +use MediaWiki\BadFileLookup; use MediaWiki\Config\ServiceOptions; use MediaWiki\Linker\LinkRendererFactory; use MediaWiki\MediaWikiServices; @@ -54,6 +55,9 @@ class ParserFactory { /** @var LoggerInterface */ private $logger; + /** @var BadFileLookup */ + private $badFileLookup; + /** * Old parameter list, which we support for backwards compatibility, were: * array $parserConf See $wgParserConf documentation @@ -77,6 +81,7 @@ class ParserFactory { * @param LinkRendererFactory $linkRendererFactory * @param NamespaceInfo|LinkRendererFactory|null $nsInfo * @param LoggerInterface|null $logger + * @param BadFileLookup|null $badFileLookup * @since 1.32 */ public function __construct( @@ -87,7 +92,8 @@ class ParserFactory { SpecialPageFactory $spFactory, $linkRendererFactory, $nsInfo = null, - $logger = null + $logger = null, + BadFileLookup $badFileLookup = null ) { // @todo Do we need to retain compat for constructing this class directly? if ( !$nsInfo ) { @@ -119,6 +125,8 @@ class ParserFactory { $this->linkRendererFactory = $linkRendererFactory; $this->nsInfo = $nsInfo; $this->logger = $logger ?: new NullLogger(); + $this->badFileLookup = $badFileLookup ?? + MediaWikiServices::getInstance()->getBadFileLookup(); } /** @@ -135,7 +143,8 @@ class ParserFactory { $this->specialPageFactory, $this->linkRendererFactory, $this->nsInfo, - $this->logger + $this->logger, + $this->badFileLookup ); } } diff --git a/includes/parser/Preprocessor.php b/includes/parser/Preprocessor.php index b321078dcd..19dd96e885 100644 --- a/includes/parser/Preprocessor.php +++ b/includes/parser/Preprocessor.php @@ -31,6 +31,11 @@ abstract class Preprocessor { const CACHE_VERSION = 1; + /** + * @var Parser + */ + public $parser; + /** * @var array Brace matching rules. */ @@ -76,6 +81,7 @@ abstract class Preprocessor { $cache = MediaWikiServices::getInstance()->getMainWANObjectCache(); $key = $cache->makeKey( + // @phan-suppress-next-line PhanUndeclaredConstant defined( 'static::CACHE_PREFIX' ) ? static::CACHE_PREFIX : static::class, md5( $text ), $flags @@ -108,6 +114,7 @@ abstract class Preprocessor { $cache = MediaWikiServices::getInstance()->getMainWANObjectCache(); $key = $cache->makeKey( + // @phan-suppress-next-line PhanUndeclaredConstant defined( 'static::CACHE_PREFIX' ) ? static::CACHE_PREFIX : static::class, md5( $text ), $flags diff --git a/includes/parser/Preprocessor_Hash.php b/includes/parser/Preprocessor_Hash.php index f7f37ac044..7c372ee3d7 100644 --- a/includes/parser/Preprocessor_Hash.php +++ b/includes/parser/Preprocessor_Hash.php @@ -41,12 +41,6 @@ */ // phpcs:ignore Squiz.Classes.ValidClassName.NotCamelCaps class Preprocessor_Hash extends Preprocessor { - - /** - * @var Parser - */ - public $parser; - const CACHE_PREFIX = 'preprocess-hash'; const CACHE_VERSION = 2; @@ -628,7 +622,9 @@ class Preprocessor_Hash extends Preprocessor { } $i += $count; } elseif ( $found == 'close' ) { + /** @var PPDStackElement_Hash $piece */ $piece = $stack->top; + '@phan-var PPDStackElement_Hash $piece'; # lets check if there are enough characters for closing brace $maxCount = $piece->count; if ( $piece->close === '}-' && $curChar === '}' ) { diff --git a/includes/parser/Sanitizer.php b/includes/parser/Sanitizer.php index 8e0cf5c877..d4110468b6 100644 --- a/includes/parser/Sanitizer.php +++ b/includes/parser/Sanitizer.php @@ -2036,6 +2036,7 @@ class Sanitizer { * * @param string $html HTML fragment * @return string + * @return-taint tainted */ static function stripAllTags( $html ) { // Use RemexHtml to tokenize $html and extract the text diff --git a/includes/password/LayeredParameterizedPassword.php b/includes/password/LayeredParameterizedPassword.php index f3d8d03e30..389b8c3865 100644 --- a/includes/password/LayeredParameterizedPassword.php +++ b/includes/password/LayeredParameterizedPassword.php @@ -59,6 +59,7 @@ class LayeredParameterizedPassword extends ParameterizedPassword { // Construct pseudo-hash based on params and arguments /** @var ParameterizedPassword $passObj */ $passObj = $this->factory->newFromType( $type ); + '@phan-var ParameterizedPassword $passObj'; $params = ''; $args = ''; @@ -72,6 +73,7 @@ class LayeredParameterizedPassword extends ParameterizedPassword { // Hash the last hash with the next type in the layer $passObj = $this->factory->newFromCiphertext( $existingHash ); + '@phan-var ParameterizedPassword $passObj'; $passObj->crypt( $lastHash ); // Move over the params and args @@ -114,6 +116,7 @@ class LayeredParameterizedPassword extends ParameterizedPassword { // Construct pseudo-hash based on params and arguments /** @var ParameterizedPassword $passObj */ $passObj = $this->factory->newFromType( $type ); + '@phan-var ParameterizedPassword $passObj'; $params = ''; $args = ''; @@ -127,6 +130,7 @@ class LayeredParameterizedPassword extends ParameterizedPassword { // Hash the last hash with the next type in the layer $passObj = $this->factory->newFromCiphertext( $existingHash ); + '@phan-var ParameterizedPassword $passObj'; $passObj->crypt( $lastHash ); // Move over the params and args diff --git a/includes/password/PasswordPolicyChecks.php b/includes/password/PasswordPolicyChecks.php index c3af88f07d..1475c2053c 100644 --- a/includes/password/PasswordPolicyChecks.php +++ b/includes/password/PasswordPolicyChecks.php @@ -54,6 +54,8 @@ class PasswordPolicyChecks { /** * Check password is longer than minimum, fatal. + * Intended for locking out users with passwords too short to trust, requiring them + * to recover their account by some other means. * @param int $policyVal minimal length * @param User $user * @param string $password @@ -151,8 +153,6 @@ class PasswordPolicyChecks { global $wgPopularPasswordFile, $wgSitename; $status = Status::newGood(); if ( $policyVal > 0 ) { - wfDeprecated( __METHOD__, '1.33' ); - $langEn = Language::factory( 'en' ); $passwordKey = $langEn->lc( trim( $password ) ); diff --git a/includes/poolcounter/PoolCounterRedis.php b/includes/poolcounter/PoolCounterRedis.php index f5fa4c7319..c89dc15693 100644 --- a/includes/poolcounter/PoolCounterRedis.php +++ b/includes/poolcounter/PoolCounterRedis.php @@ -152,7 +152,9 @@ class PoolCounterRedis extends PoolCounter { if ( !$status->isOK() ) { return $status; } + /** @var RedisConnRef $conn */ $conn = $status->value; + '@phan-var RedisConnRef $conn'; // phpcs:disable Generic.Files.LineLength static $script = @@ -238,7 +240,9 @@ LUA; if ( !$status->isOK() ) { return $status; } + /** @var RedisConnRef $conn */ $conn = $status->value; + '@phan-var RedisConnRef $conn'; $now = microtime( true ); try { diff --git a/includes/preferences/DefaultPreferencesFactory.php b/includes/preferences/DefaultPreferencesFactory.php index 001c975dbc..68236e5f2e 100644 --- a/includes/preferences/DefaultPreferencesFactory.php +++ b/includes/preferences/DefaultPreferencesFactory.php @@ -20,7 +20,6 @@ namespace MediaWiki\Preferences; -use Config; use DateTime; use DateTimeZone; use Exception; @@ -37,6 +36,7 @@ use MediaWiki\Auth\PasswordAuthenticationRequest; use MediaWiki\Config\ServiceOptions; use MediaWiki\Linker\LinkRenderer; use MediaWiki\MediaWikiServices; +use MediaWiki\Permissions\PermissionManager; use MessageLocalizer; use MWException; use MWTimestamp; @@ -77,6 +77,9 @@ class DefaultPreferencesFactory implements PreferencesFactory { /** @var NamespaceInfo */ protected $nsInfo; + /** @var PermissionManager */ + protected $permissionManager; + /** * TODO Make this a const when we drop HHVM support (T192166) * @@ -84,6 +87,7 @@ class DefaultPreferencesFactory implements PreferencesFactory { * @since 1.34 */ public static $constructorOptions = [ + 'AllowRequiringEmailForResets', 'AllowUserCss', 'AllowUserCssPrefs', 'AllowUserJs', @@ -113,35 +117,29 @@ class DefaultPreferencesFactory implements PreferencesFactory { /** * Do not call this directly. Get it from MediaWikiServices. * - * @param ServiceOptions|Config $options Config accepted for backwards compatibility + * @param ServiceOptions $options * @param Language $contLang * @param AuthManager $authManager * @param LinkRenderer $linkRenderer - * @param NamespaceInfo|null $nsInfo + * @param NamespaceInfo $nsInfo + * @param PermissionManager $permissionManager */ public function __construct( - $options, + ServiceOptions $options, Language $contLang, AuthManager $authManager, LinkRenderer $linkRenderer, - NamespaceInfo $nsInfo = null + NamespaceInfo $nsInfo, + PermissionManager $permissionManager ) { - if ( $options instanceof Config ) { - wfDeprecated( __METHOD__ . ' with Config parameter', '1.34' ); - $options = new ServiceOptions( self::$constructorOptions, $options ); - } - $options->assertRequiredOptions( self::$constructorOptions ); - if ( !$nsInfo ) { - wfDeprecated( __METHOD__ . ' with no NamespaceInfo argument', '1.34' ); - $nsInfo = MediaWikiServices::getInstance()->getNamespaceInfo(); - } $this->options = $options; $this->contLang = $contLang; $this->authManager = $authManager; $this->linkRenderer = $linkRenderer; $this->nsInfo = $nsInfo; + $this->permissionManager = $permissionManager; $this->logger = new NullLogger(); } @@ -208,7 +206,7 @@ class DefaultPreferencesFactory implements PreferencesFactory { # # Make sure that form fields have their parent set. See T43337. $dummyForm = new HTMLForm( [], $context ); - $disable = !$user->isAllowed( 'editmyoptions' ); + $disable = !$this->permissionManager->userHasRight( $user, 'editmyoptions' ); $defaultOptions = User::getDefaultOptions(); $userOptions = $user->getOptions(); @@ -232,7 +230,10 @@ class DefaultPreferencesFactory implements PreferencesFactory { } elseif ( $field->validate( $globalDefault, $user->getOptions() ) === true ) { $info['default'] = $globalDefault; } else { - throw new MWException( "Global default '$globalDefault' is invalid for field $name" ); + $globalDefault = json_encode( $globalDefault ); + throw new MWException( + "Default '$globalDefault' is invalid for preference $name of user $user" + ); } } @@ -386,8 +387,8 @@ class DefaultPreferencesFactory implements PreferencesFactory { ]; } - $canViewPrivateInfo = $user->isAllowed( 'viewmyprivateinfo' ); - $canEditPrivateInfo = $user->isAllowed( 'editmyprivateinfo' ); + $canViewPrivateInfo = $this->permissionManager->userHasRight( $user, 'viewmyprivateinfo' ); + $canEditPrivateInfo = $this->permissionManager->userHasRight( $user, 'editmyprivateinfo' ); // Actually changeable stuff $defaultPreferences['realname'] = [ @@ -537,6 +538,7 @@ class DefaultPreferencesFactory implements PreferencesFactory { if ( $this->options->get( 'EnableEmail' ) ) { if ( $canViewPrivateInfo ) { + $helpMessages = []; $helpMessages[] = $this->options->get( 'EmailConfirmToEdit' ) ? 'prefs-help-email-required' : 'prefs-help-email'; @@ -616,7 +618,19 @@ class DefaultPreferencesFactory implements PreferencesFactory { } } - if ( $this->options->get( 'EnableUserEmail' ) && $user->isAllowed( 'sendemail' ) ) { + if ( $this->options->get( 'AllowRequiringEmailForResets' ) ) { + $defaultPreferences['requireemail'] = [ + 'type' => 'toggle', + 'label-message' => 'tog-requireemail', + 'help-message' => 'prefs-help-requireemail', + 'section' => 'personal/email', + 'disabled' => $disableEmailPrefs, + ]; + } + + if ( $this->options->get( 'EnableUserEmail' ) && + $this->permissionManager->userHasRight( $user, 'sendemail' ) + ) { $defaultPreferences['disablemail'] = [ 'id' => 'wpAllowEmail', 'type' => 'toggle', @@ -906,7 +920,7 @@ class DefaultPreferencesFactory implements PreferencesFactory { 'label-message' => 'tog-numberheadings', ]; - if ( $user->isAllowed( 'rollback' ) ) { + if ( $this->permissionManager->userHasRight( $user, 'rollback' ) ) { $defaultPreferences['showrollbackconfirmation'] = [ 'type' => 'toggle', 'section' => 'rendering/advancedrendering', @@ -946,7 +960,7 @@ class DefaultPreferencesFactory implements PreferencesFactory { ]; } - if ( $user->isAllowed( 'minoredit' ) ) { + if ( $this->permissionManager->userHasRight( $user, 'minoredit' ) ) { $defaultPreferences['minordefault'] = [ 'type' => 'toggle', 'section' => 'editing/editor', @@ -1092,7 +1106,7 @@ class DefaultPreferencesFactory implements PreferencesFactory { $watchlistdaysMax = ceil( $this->options->get( 'RCMaxAge' ) / ( 3600 * 24 ) ); # # Watchlist ##################################### - if ( $user->isAllowed( 'editmywatchlist' ) ) { + if ( $this->permissionManager->userHasRight( $user, 'editmywatchlist' ) ) { $editWatchlistLinks = ''; $editWatchlistModes = [ 'edit' => [ 'subpage' => false, 'flags' => [] ], @@ -1206,20 +1220,20 @@ class DefaultPreferencesFactory implements PreferencesFactory { ]; // Kinda hacky - if ( $user->isAllowed( 'createpage' ) || $user->isAllowed( 'createtalk' ) ) { + if ( $this->permissionManager->userHasAnyRight( $user, 'createpage', 'createtalk' ) ) { $watchTypes['read'] = 'watchcreations'; } - if ( $user->isAllowed( 'rollback' ) ) { + if ( $this->permissionManager->userHasRight( $user, 'rollback' ) ) { $watchTypes['rollback'] = 'watchrollback'; } - if ( $user->isAllowed( 'upload' ) ) { + if ( $this->permissionManager->userHasRight( $user, 'upload' ) ) { $watchTypes['upload'] = 'watchuploads'; } foreach ( $watchTypes as $action => $pref ) { - if ( $user->isAllowed( $action ) ) { + if ( $this->permissionManager->userHasRight( $user, $action ) ) { // Messages: // tog-watchdefault, tog-watchmoves, tog-watchdeletion, tog-watchcreations, tog-watchuploads // tog-watchrollback @@ -1506,7 +1520,7 @@ class DefaultPreferencesFactory implements PreferencesFactory { } /** - * @var HTMLForm $htmlForm + * @var PreferencesFormOOUI $htmlForm */ $htmlForm = new $formClass( $formDescriptor, $context, 'prefs' ); @@ -1519,6 +1533,10 @@ class DefaultPreferencesFactory implements PreferencesFactory { ] ) ); $htmlForm->setModifiedUser( $user ); + $htmlForm->setOptionsEditable( $this->permissionManager + ->userHasRight( $user, 'editmyoptions' ) ); + $htmlForm->setPrivateInfoEditable( $this->permissionManager + ->userHasRight( $user, 'editmyprivateinfo' ) ); $htmlForm->setId( 'mw-prefs-form' ); $htmlForm->setAutocomplete( 'off' ); $htmlForm->setSubmitText( $context->msg( 'saveprefs' )->text() ); @@ -1526,7 +1544,7 @@ class DefaultPreferencesFactory implements PreferencesFactory { $htmlForm->setSubmitTooltip( 'preferences-save' ); $htmlForm->setSubmitID( 'prefcontrol' ); $htmlForm->setSubmitCallback( - function ( array $formData, HTMLForm $form ) use ( $formDescriptor ) { + function ( array $formData, PreferencesFormOOUI $form ) use ( $formDescriptor ) { return $this->submitForm( $formData, $form, $formDescriptor ); } ); @@ -1582,17 +1600,18 @@ class DefaultPreferencesFactory implements PreferencesFactory { * Handle the form submission if everything validated properly * * @param array $formData - * @param HTMLForm $form + * @param PreferencesFormOOUI $form * @param array[] $formDescriptor * @return bool|Status|string */ - protected function saveFormData( $formData, HTMLForm $form, array $formDescriptor ) { - /** @var \User $user */ + protected function saveFormData( $formData, PreferencesFormOOUI $form, array $formDescriptor ) { $user = $form->getModifiedUser(); $hiddenPrefs = $this->options->get( 'HiddenPrefs' ); $result = true; - if ( !$user->isAllowedAny( 'editmyprivateinfo', 'editmyoptions' ) ) { + if ( !$this->permissionManager + ->userHasAnyRight( $user, 'editmyprivateinfo', 'editmyoptions' ) + ) { return Status::newFatal( 'mypreferencesprotected' ); } @@ -1603,14 +1622,14 @@ class DefaultPreferencesFactory implements PreferencesFactory { // (not really "private", but still shouldn't be edited without permission) if ( !in_array( 'realname', $hiddenPrefs ) - && $user->isAllowed( 'editmyprivateinfo' ) + && $this->permissionManager->userHasRight( $user, 'editmyprivateinfo' ) && array_key_exists( 'realname', $formData ) ) { $realName = $formData['realname']; $user->setRealName( $realName ); } - if ( $user->isAllowed( 'editmyoptions' ) ) { + if ( $this->permissionManager->userHasRight( $user, 'editmyoptions' ) ) { $oldUserOptions = $user->getOptions(); foreach ( $this->getSaveBlacklist() as $b ) { @@ -1685,11 +1704,15 @@ class DefaultPreferencesFactory implements PreferencesFactory { * Save the form data and reload the page * * @param array $formData - * @param HTMLForm $form + * @param PreferencesFormOOUI $form * @param array $formDescriptor * @return Status */ - protected function submitForm( array $formData, HTMLForm $form, array $formDescriptor ) { + protected function submitForm( + array $formData, + PreferencesFormOOUI $form, + array $formDescriptor + ) { $res = $this->saveFormData( $formData, $form, $formDescriptor ); if ( $res === true ) { @@ -1723,6 +1746,7 @@ class DefaultPreferencesFactory implements PreferencesFactory { */ protected function getTimeZoneList( Language $language ) { $identifiers = DateTimeZone::listIdentifiers(); + // @phan-suppress-next-line PhanTypeComparisonFromArray See phan issue #3162 if ( $identifiers === false ) { return []; } diff --git a/includes/profiler/Profiler.php b/includes/profiler/Profiler.php index 554ca084bd..2eb558603e 100644 --- a/includes/profiler/Profiler.php +++ b/includes/profiler/Profiler.php @@ -33,14 +33,15 @@ use Wikimedia\Rdbms\TransactionProfiler; abstract class Profiler { /** @var string|bool Profiler ID for bucketing data */ protected $profileID = false; - /** @var bool Whether MediaWiki is in a SkinTemplate output context */ - protected $templated = false; /** @var array All of the params passed from $wgProfiler */ protected $params = []; /** @var IContextSource Current request context */ protected $context = null; /** @var TransactionProfiler */ protected $trxProfiler; + /** @var bool */ + private $allowOutput = false; + /** @var Profiler */ private static $instance = null; @@ -248,6 +249,10 @@ abstract class Profiler { * @since 1.26 */ public function logDataPageOutputOnly() { + if ( !$this->allowOutput ) { + return; + } + $outputs = []; foreach ( $this->getOutputs() as $output ) { if ( $output->logsToOutput() ) { @@ -264,15 +269,20 @@ abstract class Profiler { } /** - * Get the content type sent out to the client. - * Used for profilers that output instead of store data. - * @return string + * Get the Content-Type for deciding how to format appended profile output. + * + * Disabled by default. Enable via setAllowOutput(). + * + * @see ProfilerOutputText * @since 1.25 + * @return string|null Returns null if disabled or no Content-Type found. */ public function getContentType() { - foreach ( headers_list() as $header ) { - if ( preg_match( '#^content-type: (\w+/\w+);?#i', $header, $m ) ) { - return $m[1]; + if ( $this->allowOutput ) { + foreach ( headers_list() as $header ) { + if ( preg_match( '#^content-type: (\w+/\w+);?#i', $header, $m ) ) { + return $m[1]; + } } } return null; @@ -281,19 +291,42 @@ abstract class Profiler { /** * Mark this call as templated or not * + * @deprecated since 1.34 Use setAllowOutput() instead. * @param bool $t */ public function setTemplated( $t ) { - $this->templated = $t; + wfDeprecated( __METHOD__, '1.34' ); + $this->allowOutput = ( $t === true ); } /** * Was this call as templated or not * + * @deprecated since 1.34 Use getAllowOutput() instead. * @return bool */ public function getTemplated() { - return $this->templated; + wfDeprecated( __METHOD__, '1.34' ); + return $this->getAllowOutput(); + } + + /** + * Enable appending profiles to standard output. + * + * @since 1.34 + */ + public function setAllowOutput() { + $this->allowOutput = true; + } + + /** + * Whether appending profiles is allowed. + * + * @since 1.34 + * @return bool + */ + public function getAllowOutput() { + return $this->allowOutput; } /** diff --git a/includes/profiler/ProfilerExcimer.php b/includes/profiler/ProfilerExcimer.php index 20f9a78508..ab59efed72 100644 --- a/includes/profiler/ProfilerExcimer.php +++ b/includes/profiler/ProfilerExcimer.php @@ -1,7 +1,9 @@ collateData(); - $totalCpu = max( $this->end['cpu'] - $this->start['cpu'], 0 ); - $totalReal = max( $this->end['real'] - $this->start['real'], 0 ); - $totalMem = max( $this->end['memory'] - $this->start['memory'], 0 ); + if ( is_array( $this->start ) ) { + $totalCpu = max( $this->end['cpu'] - $this->start['cpu'], 0 ); + $totalReal = max( $this->end['real'] - $this->start['real'], 0 ); + $totalMem = max( $this->end['memory'] - $this->start['memory'], 0 ); + } else { + $totalCpu = 0; + $totalReal = 0; + $totalMem = 0; + } $profile = []; foreach ( $this->collated as $fname => $data ) { diff --git a/includes/profiler/output/ProfilerOutput.php b/includes/profiler/output/ProfilerOutput.php index fe27c046e7..bc14f4bf68 100644 --- a/includes/profiler/output/ProfilerOutput.php +++ b/includes/profiler/output/ProfilerOutput.php @@ -48,7 +48,7 @@ abstract class ProfilerOutput { } /** - * Does log() just send the data to the request/script output? + * May the log() try to write to standard output? * @return bool * @since 1.33 */ @@ -57,7 +57,10 @@ abstract class ProfilerOutput { } /** - * Log MediaWiki-style profiling data + * Log MediaWiki-style profiling data. + * + * For classes that enable logsToOutput(), this must not + * be called unless Profiler::setAllowOutput is enabled. * * @param array $stats Result of Profiler::getFunctionStats() */ diff --git a/includes/profiler/output/ProfilerOutputDump.php b/includes/profiler/output/ProfilerOutputDump.php index 09f5688785..64a504a3eb 100644 --- a/includes/profiler/output/ProfilerOutputDump.php +++ b/includes/profiler/output/ProfilerOutputDump.php @@ -26,6 +26,7 @@ * @ingroup Profiler * * @since 1.25 + * @property ProfilerXhprof $collector */ class ProfilerOutputDump extends ProfilerOutput { diff --git a/includes/profiler/output/ProfilerOutputText.php b/includes/profiler/output/ProfilerOutputText.php index 95b5ff95bc..ee06b59ace 100644 --- a/includes/profiler/output/ProfilerOutputText.php +++ b/includes/profiler/output/ProfilerOutputText.php @@ -29,11 +29,15 @@ */ class ProfilerOutputText extends ProfilerOutput { /** @var float Min real time display threshold */ - protected $thresholdMs; + private $thresholdMs; + + /** @var bool Whether to use visible text or a comment (for HTML responses) */ + private $visible; function __construct( Profiler $collector, array $params ) { parent::__construct( $collector, $params ); $this->thresholdMs = $params['thresholdMs'] ?? 1.0; + $this->visible = $params['visible'] ?? false; } public function logsToOutput() { @@ -41,39 +45,36 @@ class ProfilerOutputText extends ProfilerOutput { } public function log( array $stats ) { - if ( $this->collector->getTemplated() ) { - $out = ''; + $out = ''; - // Filter out really tiny entries - $min = $this->thresholdMs; - $stats = array_filter( $stats, function ( $a ) use ( $min ) { - return $a['real'] > $min; - } ); - // Sort descending by time elapsed - usort( $stats, function ( $a, $b ) { - return $b['real'] <=> $a['real']; - } ); + // Filter out really tiny entries + $min = $this->thresholdMs; + $stats = array_filter( $stats, function ( $a ) use ( $min ) { + return $a['real'] > $min; + } ); + // Sort descending by time elapsed + usort( $stats, function ( $a, $b ) { + return $b['real'] <=> $a['real']; + } ); - array_walk( $stats, - function ( $item ) use ( &$out ) { - $out .= sprintf( "%6.2f%% %3.3f %6d - %s\n", - $item['%real'], $item['real'], $item['calls'], $item['name'] ); - } - ); + array_walk( $stats, + function ( $item ) use ( &$out ) { + $out .= sprintf( "%6.2f%% %3.3f %6d - %s\n", + $item['%real'], $item['real'], $item['calls'], $item['name'] ); + } + ); - $contentType = $this->collector->getContentType(); - if ( wfIsCLI() ) { + $contentType = $this->collector->getContentType(); + if ( wfIsCLI() ) { + print "\n"; + } elseif ( $contentType === 'text/html' ) { + if ( $this->visible ) { + print "
{$out}
"; + } else { print "\n"; - } elseif ( $contentType === 'text/html' ) { - $visible = $this->params['visible'] ?? false; - if ( $visible ) { - print "
{$out}
"; - } else { - print "\n"; - } - } elseif ( $contentType === 'text/javascript' || $contentType === 'text/css' ) { - print "\n/*\n{$out}*/\n"; } + } elseif ( $contentType === 'text/javascript' || $contentType === 'text/css' ) { + print "\n/*\n{$out}*/\n"; } } } diff --git a/includes/rcfeed/IRCColourfulRCFeedFormatter.php b/includes/rcfeed/IRCColourfulRCFeedFormatter.php index ff85c90614..4bdaca51c2 100644 --- a/includes/rcfeed/IRCColourfulRCFeedFormatter.php +++ b/includes/rcfeed/IRCColourfulRCFeedFormatter.php @@ -132,7 +132,7 @@ class IRCColourfulRCFeedFormatter implements RCFeedFormatter { } /** - * Remove newlines, carriage returns and decode html entites + * Remove newlines, carriage returns and decode html entities * @param string $text * @return string */ diff --git a/includes/registration/ExtensionRegistry.php b/includes/registration/ExtensionRegistry.php index 9cae73c907..c5d4b4a7c3 100644 --- a/includes/registration/ExtensionRegistry.php +++ b/includes/registration/ExtensionRegistry.php @@ -1,6 +1,7 @@ queued ) { return; } @@ -169,10 +168,9 @@ class ExtensionRegistry { // We use a try/catch because we don't want to fail here // if $wgObjectCaches is not configured properly for APC setup try { - // Don't use MediaWikiServices here to prevent instantiating it before extensions have - // been loaded + // Avoid MediaWikiServices to prevent instantiating it before extensions have loaded $cacheId = ObjectCache::detectLocalServerCache(); - $cache = ObjectCache::newFromId( $cacheId ); + $cache = ObjectCache::newFromParams( $wgObjectCaches[$cacheId] ); } catch ( InvalidArgumentException $e ) { $cache = new EmptyBagOStuff(); } @@ -307,16 +305,6 @@ class ExtensionRegistry { $autoloadNamespaces ); - if ( isset( $info['AutoloadClasses'] ) ) { - $autoload = $this->processAutoLoader( $dir, $info['AutoloadClasses'] ); - $GLOBALS['wgAutoloadClasses'] += $autoload; - $autoloadClasses += $autoload; - } - if ( isset( $info['AutoloadNamespaces'] ) ) { - $autoloadNamespaces += $this->processAutoLoader( $dir, $info['AutoloadNamespaces'] ); - AutoLoader::$psr4Namespaces += $autoloadNamespaces; - } - // get all requirements/dependencies for this extension $requires = $processor->getRequirements( $info, $this->checkDev ); diff --git a/includes/resourceloader/DerivativeResourceLoaderContext.php b/includes/resourceloader/DerivativeResourceLoaderContext.php index b11bd6fd33..0a1d5f7789 100644 --- a/includes/resourceloader/DerivativeResourceLoaderContext.php +++ b/includes/resourceloader/DerivativeResourceLoaderContext.php @@ -1,7 +1,5 @@ modules === self::INHERIT_VALUE ) { return $this->context->getModules(); } + return $this->modules; } diff --git a/includes/resourceloader/MessageBlobStore.php b/includes/resourceloader/MessageBlobStore.php index d0f67294a7..b4f0b7c25b 100644 --- a/includes/resourceloader/MessageBlobStore.php +++ b/includes/resourceloader/MessageBlobStore.php @@ -27,10 +27,13 @@ use Psr\Log\NullLogger; use Wikimedia\Rdbms\Database; /** - * This class generates message blobs for use by ResourceLoader modules. + * This class generates message blobs for use by ResourceLoader. * - * A message blob is a JSON object containing the interface messages for a certain module in - * a certain language. + * A message blob is a JSON object containing the interface messages for a + * certain module in a certain language. + * + * @ingroup ResourceLoader + * @since 1.17 */ class MessageBlobStore implements LoggerAwareInterface { diff --git a/includes/resourceloader/ResourceLoader.php b/includes/resourceloader/ResourceLoader.php index 9892b15705..d959ff6313 100644 --- a/includes/resourceloader/ResourceLoader.php +++ b/includes/resourceloader/ResourceLoader.php @@ -1,7 +1,5 @@ . + */ + +/** + * ResourceLoader is a loading system for JavaScript and CSS resources. + * + * For higher level documentation, see . * - * Most of the documentation is on the MediaWiki documentation wiki starting at: - * https://www.mediawiki.org/wiki/ResourceLoader + * @ingroup ResourceLoader + * @since 1.17 */ class ResourceLoader implements LoggerAwareInterface { - /** @var Config $config */ + /** @var Config */ protected $config; /** @var MessageBlobStore */ protected $blobStore; @@ -547,13 +553,81 @@ class ResourceLoader implements LoggerAwareInterface { } /** + * @internal For use by ResourceLoaderStartUpModule only. + */ + const HASH_LENGTH = 5; + + /** + * Create a hash for module versioning purposes. + * + * This hash is used in three ways: + * + * - To differentiate between the current version and a past version + * of a module by the same name. + * + * In the cache key of localStorage in the browser (mw.loader.store). + * This store keeps only one version of any given module. As long as the + * next version the client encounters has a different hash from the last + * version it saw, it will correctly discard it in favour of a network fetch. + * + * A browser may evict a site's storage container for any reason (e.g. when + * the user hasn't visited a site for some time, and/or when the device is + * low on storage space). Anecdotally it seems devices rarely keep unused + * storage beyond 2 weeks on mobile devices and 4 weeks on desktop. + * But, there is no hard limit or expiration on localStorage. + * ResourceLoader's Client also clears localStorage when the user changes + * their language preference or when they (temporarily) use Debug Mode. + * + * The only hard factors that reduce the range of possible versions are + * 1) the name and existence of a given module, and + * 2) the TTL for mw.loader.store, and + * 3) the `$wgResourceLoaderStorageVersion` configuration variable. + * + * - To identify a batch response of modules from load.php in an HTTP cache. + * + * When fetching modules in a batch from load.php, a combined hash + * is created by the JS code, and appended as query parameter. + * + * In cache proxies (e.g. Varnish, Nginx) and in the browser's HTTP cache, + * these urls are used to identify other previously cached responses. + * The range of possible versions a given version has to be unique amongst + * is determined by the maximum duration each response is stored for, which + * is controlled by `$wgResourceLoaderMaxage['versioned']`. + * + * - To detect race conditions between multiple web servers in a MediaWiki + * deployment of which some have the newer version and some still the older + * version. + * + * An HTTP request from a browser for the Startup manifest may be responded + * to by a server with the newer version. The browser may then use that to + * request a given module, which may then be responded to by a server with + * the older version. To avoid caching this for too long (which would pollute + * all other users without repairing itself), the combined hash that the JS + * client adds to the url is verified by the server (in ::sendResponseHeaders). + * If they don't match, we instruct cache proxies and clients to not cache + * this response as long as they normally would. This is also the reason + * that the algorithm used here in PHP must match the one used in JS. + * + * The fnv132 digest creates a 32-bit integer, which goes upto 4 Giga and + * needs up to 7 chars in base 36. + * Within 7 characters, base 36 can count up to 78,364,164,096 (78 Giga), + * (but with fnv132 we'd use very little of this range, mostly padding). + * Within 6 characters, base 36 can count up to 2,176,782,336 (2 Giga). + * Within 5 characters, base 36 can count up to 60,466,176 (60 Mega). + * * @since 1.26 * @param string $value * @return string Hash */ public static function makeHash( $value ) { $hash = hash( 'fnv132', $value ); - return Wikimedia\base_convert( $hash, 16, 36, 7 ); + // The base_convert will pad it (if too short), + // then substr() will trim it (if too long). + return substr( + Wikimedia\base_convert( $hash, 16, 36, self::HASH_LENGTH ), + 0, + self::HASH_LENGTH + ); } /** @@ -661,8 +735,9 @@ class ResourceLoader implements LoggerAwareInterface { // Do not allow private modules to be loaded from the web. // This is a security issue, see T36907. if ( $module->getGroup() === 'private' ) { + // Not a serious error, just means something is trying to access it (T101806) $this->logger->debug( "Request for private module '$name' denied" ); - $this->errors[] = "Cannot show private module \"$name\""; + $this->errors[] = "Cannot build private module \"$name\""; continue; } $modules[$name] = $module; @@ -743,9 +818,9 @@ class ResourceLoader implements LoggerAwareInterface { $errorText = implode( "\n\n", $this->errors ); $errorResponse = self::makeComment( $errorText ); if ( $context->shouldIncludeScripts() ) { - $errorResponse .= 'if (window.console && console.error) {' - . Xml::encodeJsCall( 'console.error', [ $errorText ] ) - . "}\n"; + $errorResponse .= 'if (window.console && console.error) { console.error(' + . self::encodeJsonForScript( $errorText ) + . "); }\n"; } // Prepend error info to the response @@ -1204,8 +1279,7 @@ MESSAGE; /** * Returns JS code which, when called, will register a given list of messages. * - * @param mixed $messages Either an associative array mapping message key to value, or a - * JSON-encoded message blob containing the same data, wrapped in an XmlJsCode object. + * @param mixed $messages Associative array mapping message key to value. * @return string JavaScript code */ public static function makeMessageSetScript( $messages ) { @@ -1254,8 +1328,8 @@ MESSAGE; * * @internal * @since 1.32 - * @param bool|string|array $data - * @return string JSON + * @param mixed $data + * @return string|false JSON string, false on error */ public static function encodeJsonForScript( $data ) { // Keep output as small as possible by disabling needless escape modes @@ -1476,20 +1550,16 @@ MESSAGE; * @throws Exception */ public static function makeConfigSetScript( array $configuration ) { - $js = Xml::encodeJsCall( - 'mw.config.set', - [ $configuration ], - self::inDebugMode() - ); - if ( $js === false ) { + $json = self::encodeJsonForScript( $configuration ); + if ( $json === false ) { $e = new Exception( 'JSON serialization of config data failed. ' . 'This usually means the config data is not valid UTF-8.' ); MWExceptionHandler::logException( $e ); - $js = Xml::encodeJsCall( 'mw.log.error', [ $e->__toString() ] ); + return 'mw.log.error(' . self::encodeJsonForScript( $e->__toString() ) . ');'; } - return $js; + return "mw.config.set($json);"; } /** diff --git a/includes/resourceloader/ResourceLoaderCircularDependencyError.php b/includes/resourceloader/ResourceLoaderCircularDependencyError.php index 7cd53fe126..5a62861963 100644 --- a/includes/resourceloader/ResourceLoaderCircularDependencyError.php +++ b/includes/resourceloader/ResourceLoaderCircularDependencyError.php @@ -1,6 +1,5 @@ getGroup(); $context = $this->getContext( $group, ResourceLoaderModule::TYPE_COMBINED ); - if ( $module->isKnownEmpty( $context ) ) { - // Avoid needless request or embed for empty module - $data['states'][$name] = 'ready'; - continue; - } + $shouldEmbed = $module->shouldEmbedModule( $this->context ); - if ( $group === 'user' || $module->shouldEmbedModule( $this->context ) ) { - // Call makeLoad() to decide how to load these, instead of - // loading via mw.loader.load(). + if ( ( $group === 'user' || $shouldEmbed ) && $module->isKnownEmpty( $context ) ) { + // This is a user-specific or embedded module, which means its output + // can be specific to the current page or user. As such, we can optimise + // the way we load it based on the current version of the module. + // Avoid needless embed for empty module, preset ready state. + $data['states'][$name] = 'ready'; + } elseif ( $group === 'user' || $shouldEmbed ) { // - For group=user: We need to provide a pre-generated load.php // url to the client that has the 'user' and 'version' parameters // filled in. Without this, the client would wrongly use the static @@ -187,15 +188,30 @@ class ResourceLoaderClientHtml { $group = $module->getGroup(); $context = $this->getContext( $group, ResourceLoaderModule::TYPE_STYLES ); - // Avoid needless request for empty module - if ( !$module->isKnownEmpty( $context ) ) { - if ( $module->shouldEmbedModule( $this->context ) ) { - // Embed via style element + if ( $module->shouldEmbedModule( $this->context ) ) { + // Avoid needless embed for private embeds we know are empty. + // (Set "ready" state directly instead, which we do a few lines above.) + if ( !$module->isKnownEmpty( $context ) ) { + // Embed via