From: jenkins-bot Date: Fri, 8 Dec 2017 18:50:35 +0000 (+0000) Subject: Merge "ApiFeedWatchlist: Use guessSectionNameFromWikiText()" X-Git-Tag: 1.31.0-rc.0~1242 X-Git-Url: https://git.cyclocoop.org/?a=commitdiff_plain;h=565558f4ef6762df13613d3ef03804b39423cf2e;hp=4dbb6b2d778ec98bcf7b8de8f908ac5db8459c49;p=lhc%2Fweb%2Fwiklou.git Merge "ApiFeedWatchlist: Use guessSectionNameFromWikiText()" --- diff --git a/.gitignore b/.gitignore index b991e115a3..bb3a946593 100644 --- a/.gitignore +++ b/.gitignore @@ -33,6 +33,8 @@ sftp-config.json /images/thumb ## Extension:EasyTimeline /images/timeline +## Extension:Score +/images/lilypond /images/tmp /maintenance/.mweval_history /maintenance/.mwsql_history diff --git a/.travis.yml b/.travis.yml index cde7193424..64414b5a98 100644 --- a/.travis.yml +++ b/.travis.yml @@ -33,6 +33,10 @@ matrix: php: hhvm-3.18 - env: dbtype=mysql dbuser=root php: 7 + - env: dbtype=mysql dbuser=root + php: 7.1 + - env: dbtype=mysql dbuser=root + php: 7.2 services: - mysql diff --git a/HISTORY b/HISTORY index 0a2869d0d1..1f30b7068e 100644 --- a/HISTORY +++ b/HISTORY @@ -2,6 +2,45 @@ Change notes from older releases. For current info see RELEASE-NOTES-1.30. = MediaWiki 1.29 = +== MediaWiki 1.29.2 == + +This is a security and maintenance release of the MediaWiki 1.29 branch. + +=== Changes since 1.29.1 === +* (T166757) Avoid scoped lock errors in Category::refreshCounts() due to nesting. +* (T175439) Unbreak Postgres Updater when setting defaults for a column. +* (T160298) Remove use of implicitGroupBy() in ActiveUsersPager. +* Fixed login button label to accept RawMessage. +* Fixed case of SpecialRecentChanges class usage. +* (T174255) Declare uploadCount property in importDump.php. +* (T163646) Pass a string not an int to mysql_real_escape_string(). +* (T180143) Bump justinrainbow/json-schema development dependency to ~5.2. +* Updated dev dependancy phpunit/phpunit from v4.8.35 to v4.8.36. +* (T178451) SECURITY: Potential XSS when $wgShowExceptionDetails = false and browser + sends non-standard url escaping. +* (T165846) SECURITY: BotPassword login attempts weren't throttled. +* (T128209) SECURITY: Reflected File Download from api.php. +* (T134100) SECURITY: Do not reveal if user exists during login failure. +* (T176247) SECURITY: Ensure Message::rawParams can't lead to XSS. +* (T125163) SECURITY: Make anchor for headlines escape > and <. +* (T180237) SECURITY: Protect vendor folder with .htaccess. +* (T180231) SECURITY: Remove PHPUnit file with known RCE if exists in update.php. +* (T124404) SECURITY: XSS in langconverter when regex hits pcre.backtrack_limit. +* (T119158) SECURITY: Handle -{}- syntax in attributes safely. +* (T180488) (T125177) "api.log contains passwords in plaintext" wasn't correctly fixed in all + branches in the previous security release. + +== MediaWiki 1.29.1 == + +This is a maintenance release of the MediaWiki 1.29 branch. + +The SpamBlacklist and PdfHandler extensions were missing from the generated +packages. + +=== Changes since 1.29.1 === +* (T164999) Define mw.Upload.Dialog.static.name in mediawiki.Upload.Dialog.js. +* (T172061) Fix fatal when passing a category to refreshLinks.php. + == MediaWiki 1.29.0 == === Configuration changes in 1.29 === @@ -336,6 +375,45 @@ changes to languages because of Phabricator reports. = MediaWiki 1.28 = +== MediaWiki 1.28.3 == + +This is a security and maintenance release of the MediaWiki 1.28 branch. + +=== Changes since 1.28.2 == +* (T168856) Allow SVGs created by Dia to be uploaded. +* (T157545) Add missing doUpdates() call to refreshLinks.php. +* (T165714) (T100085) Better handling of jobs execution in post-connection shutdown. +* (T154425) (T154438) (T157679) Use AutoCommitUpdate instead of Database->onTransactionIdle. +* (T154425) Make DeferredUpdates detect LBFactory transaction rounds. +* (T149454) Restore erroneously removed realTableName call from DatabasePostgres. +* (T167798) Fix phrase search and highlighting for phrase queries. +* (T151136) Provide credits information to callbacks in extension registration. +* (T160462) Allow namespaces defined in extension.json to be overwritten locally. +* (T168337) Fix ErrorPageError to work from non-UI contexts. +* (T143788) Backports for PHP 7.0 and 7.1 support. +* (T175439) Unbreak Postgres Updater when setting defaults for a column. +* (T160298) Remove use of implicitGroupBy() in ActiveUsersPager. +* (T174255) Declare uploadCount property in importDump.php. +* (T180231) SECURITY: Updated dev dependancy phpunit/phpunit from v4.8.24 to v4.8.36. +* (T178451) SECURITY: Potential XSS when $wgShowExceptionDetails = false and browser + sends non-standard url escaping. +* (T165846) SECURITY: BotPassword login attempts weren't throttled. +* (T128209) SECURITY: Reflected File Download from api.php. +* (T134100) SECURITY: Do not reveal if user exists during login failure. +* (T176247) SECURITY: Ensure Message::rawParams can't lead to XSS. +* (T125163) SECURITY: Make anchor for headlines escape > and <. +* (T180237) SECURITY: Protect vendor folder with .htaccess. +* (T180231) SECURITY: Remove PHPUnit file with known RCE if exists in update.php. +* (T124404) SECURITY: XSS in langconverter when regex hits pcre.backtrack_limit. +* (T119158) SECURITY: Handle -{}- syntax in attributes safely. + +== MediaWiki 1.28.2 == + +Due to a packaging error, the wrong version of the SyntaxHighlight extension was +included in the tarball version of MediaWiki 1.28.1. The version included had a +serious security issue in it (T158689). There was also some minor code fixes in +MediaWiki itself since 1.28.1, but none of them were security relevant. + == MediaWiki 1.28.1 == This is a security and maintenance release of the MediaWiki 1.28 branch. @@ -699,6 +777,38 @@ There's usually someone online in #mediawiki on irc.freenode.net. = MediaWiki 1.27 = +== MediaWiki 1.27.4 == +This is a security and maintenance release of the MediaWiki 1.27 branch. + +=== Changes since 1.27.3 === +* (T100085) Better handling of jobs execution in post-connection shutdown. +* (T141604) Support conditionally registered namespaces. +* (T167798) Fix highlighting for phrase queries and phrase search. +* (T151136) Provide credits information to callbacks. +* (T160462) Allow namespaces defined in extension.json to be overwritten locally. +* (T168856) Allow SVGs created by Dia to be uploaded. +* (T144705) (T148662) Password reset link is no longer shown when no reset options are + available. +* (T143788) (T174262) Various backports for PHP 7.0 and 7.1 support. +* (T66795) $wgUserEmailUseReplyTo is now true by default to work around restrictive DMARC + policies. +* DB_REPLICA constant added from REL1_28+ to ease backports to extensions and core. +* (T175439) Unbreak Postgres Updater when setting defaults for a column. +* (T160298) Remove use of implicitGroupBy() in ActiveUsersPager. +* (T142304) Allow putting the app ID in the password for bot passwords. +* Updated dev dependancy phpunit/phpunit from v4.8.24 to v4.8.36. +* (T178451) SECURITY: Potential XSS when $wgShowExceptionDetails = false and browser + sends non-standard url escaping. +* (T165846) SECURITY: BotPassword login attempts weren't throttled. +* (T128209) SECURITY: Reflected File Download from api.php. +* (T134100) SECURITY: Do not reveal if user exists during login failure. +* (T176247) SECURITY: Ensure Message::rawParams can't lead to XSS. +* (T125163) SECURITY: Make anchor for headlines escape > and <. +* (T180237) SECURITY: Protect vendor folder with .htaccess. +* (T180231) SECURITY: Remove PHPUnit file with known RCE if exists in update.php. +* (T124404) SECURITY: XSS in langconverter when regex hits pcre.backtrack_limit. +* (T119158) SECURITY: Handle -{}- syntax in attributes safely. + == MediaWiki 1.27.3 == Due to a packaging error, the wrong version of the SyntaxHighlight extension was included in the tarball version of MediaWiki 1.27.2. The version included had a diff --git a/RELEASE-NOTES-1.31 b/RELEASE-NOTES-1.31 index 5f91df67c9..4a2876d500 100644 --- a/RELEASE-NOTES-1.31 +++ b/RELEASE-NOTES-1.31 @@ -15,6 +15,10 @@ production. possible for fallback images such as png. * (T44246) $wgFilterLogTypes will no longer ignore 'patrol' when user does not have the right to mark things patrolled. +* Wikis that contain imported revisions or CentralAuth global blocks should run + maintenance/cleanupUsersWithNoId.php. +* $wgResourceLoaderMinifierStatementsOnOwnLine and $wgResourceLoaderMinifierMaxLineLength + were removed (deprecated since 1.27). === New features in 1.31 === * Wikimedia\Rdbms\IDatabase->select() and similar methods now support @@ -22,6 +26,15 @@ production. * As a first pass in standardizing dialog boxes across the MediaWiki product, Html class now provides helper methods for messageBox, successBox, errorBox and warningBox generation. +* (T9240) Imports will now record unknown (and, optionally, known) usernames in + a format like "iw>Example". +* (T20209) Linker (used on history pages, log pages, and so on) will display + usernames formed like "iw>Example" as interwiki links, as if by wikitext like + [[iw:User:Example|iw>Example]]. +* (T111605) The 'ImportHandleUnknownUser' hook allows extensions to auto-create + users during an import. +* Added a hook, ParserOutputPostCacheTransform, to allow extensions to affect + the ParserOutput::getText() post-cache transformations. === External library changes in 1.31 === @@ -104,6 +117,24 @@ changes to languages because of Phabricator reports. DifferenceEngine::MW_DIFF_VERSION should be used instead. * Use of Maintenance::error( $err, $die ) to exit script was deprecated. Use Maintenance::fatalError() instead. +* Passing a ParserOptions object to OutputPage::parserOptions() is deprecated. +* Browser support for Opera 12 and older was removed. + Opera 15+ continues at Grade A support. +* The Block class will no longer accept usable-but-missing usernames for + 'byText' or ->setBlocker(). Callers should either ensure the blocker exists + locally or use a new interwiki-format username like "iw>Example". +* The following methods that get and set ParserOutput state are deprecated. + Callers should use the new stateless $options parameter to + ParserOutput::getText() instead. + * ParserOptions::getEditSection() + * ParserOptions::setEditSection() + * ParserOutput::getEditSectionTokens() + * ParserOutput::setEditSectionTokens() + * ParserOutput::getTOCEnabled() + * ParserOutput::setTOCEnabled() + * OutputPage::enableSectionEditLinks() + * OutputPage::sectionEditLinksEnabled() + * The public ParserOutput state fields $mTOCEnabled and $mEditSectionTokens are also deprecated. == Compatibility == MediaWiki 1.31 requires PHP 5.5.9 or later. There is experimental support for diff --git a/autoload.php b/autoload.php index 5a2156ac92..cd01828a6c 100644 --- a/autoload.php +++ b/autoload.php @@ -264,7 +264,9 @@ $wgAutoloadLocalClasses = [ 'CleanupPreferences' => __DIR__ . '/maintenance/cleanupPreferences.php', 'CleanupRemovedModules' => __DIR__ . '/maintenance/cleanupRemovedModules.php', 'CleanupSpam' => __DIR__ . '/maintenance/cleanupSpam.php', + 'CleanupUsersWithNoId' => __DIR__ . '/maintenance/cleanupUsersWithNoId.php', 'ClearInterwikiCache' => __DIR__ . '/maintenance/clearInterwikiCache.php', + 'ClearUserWatchlistJob' => __DIR__ . '/includes/jobqueue/jobs/ClearUserWatchlistJob.php', 'CliInstaller' => __DIR__ . '/includes/installer/CliInstaller.php', 'CloneDatabase' => __DIR__ . '/includes/db/CloneDatabase.php', 'CodeCleanerGlobalsPass' => __DIR__ . '/maintenance/CodeCleanerGlobalsPass.inc', @@ -939,6 +941,7 @@ $wgAutoloadLocalClasses = [ 'MediaWiki\\ShellDisabledError' => __DIR__ . '/includes/exception/ShellDisabledError.php', 'MediaWiki\\Shell\\Command' => __DIR__ . '/includes/shell/Command.php', 'MediaWiki\\Shell\\CommandFactory' => __DIR__ . '/includes/shell/CommandFactory.php', + 'MediaWiki\\Shell\\FirejailCommand' => __DIR__ . '/includes/shell/FirejailCommand.php', 'MediaWiki\\Shell\\Result' => __DIR__ . '/includes/shell/Result.php', 'MediaWiki\\Shell\\Shell' => __DIR__ . '/includes/shell/Shell.php', 'MediaWiki\\Site\\MediaWikiPageNameNormalizer' => __DIR__ . '/includes/site/MediaWikiPageNameNormalizer.php', @@ -1037,6 +1040,7 @@ $wgAutoloadLocalClasses = [ 'NewPagesPager' => __DIR__ . '/includes/specials/pagers/NewPagesPager.php', 'NewUsersLogFormatter' => __DIR__ . '/includes/logging/NewUsersLogFormatter.php', 'NolinesImageGallery' => __DIR__ . '/includes/gallery/NolinesImageGallery.php', + 'NorthernSamiUppercaseCollation' => __DIR__ . '/includes/collation/NorthernSamiUppercaseCollation.php', 'NotRecursiveIterator' => __DIR__ . '/includes/libs/iterators/NotRecursiveIterator.php', 'NukeNS' => __DIR__ . '/maintenance/nukeNS.php', 'NukePage' => __DIR__ . '/maintenance/nukePage.php', diff --git a/composer.json b/composer.json index a5501d080a..35783f2313 100644 --- a/composer.json +++ b/composer.json @@ -25,7 +25,7 @@ "ext-xml": "*", "liuggio/statsd-php-client": "1.0.18", "mediawiki/at-ease": "1.1.0", - "oojs/oojs-ui": "0.24.2", + "oojs/oojs-ui": "0.24.3", "oyejorge/less.php": "1.7.0.14", "php": ">=5.5.9", "psr/log": "1.0.2", diff --git a/docs/hooks.txt b/docs/hooks.txt index 6c1597f3de..29883b25a6 100644 --- a/docs/hooks.txt +++ b/docs/hooks.txt @@ -1840,6 +1840,11 @@ $revisionInfo: Array of revision information Return false to stop further processing of the tag $reader: XMLReader object +'ImportHandleUnknownUser': When a user does exist locally, this hook is called +to give extensions an opportunity to auto-create it. If the auto-creation is +successful, return false. +$name: User name + 'ImportHandleUploadXMLTag': When parsing a XML tag in a file upload. Return false to stop further processing of the tag $reader: XMLReader object @@ -2589,6 +2594,12 @@ RejectParserCacheValue hook) because MediaWiki won't do it for you. callable here. The callable is passed the ParserOptions object and the option name. +'ParserOutputPostCacheTransform': Called from ParserOutput::getText() to do +post-cache transforms. +$parserOutput: The ParserOutput object. +&$text: The text being transformed, before core transformations are done. +&$options: The options array being used for the transformation. + 'ParserSectionCreate': Called each time the parser creates a document section from wikitext. Use this to apply per-section modifications to HTML (like wrapping the section in a DIV). Caveat: DIVs are valid wikitext, and a DIV diff --git a/includes/Block.php b/includes/Block.php index d1e78bb6cf..0999ad2063 100644 --- a/includes/Block.php +++ b/includes/Block.php @@ -1479,9 +1479,19 @@ class Block { /** * Set the user who implemented (or will implement) this block - * @param User|string $user Local User object or username string for foreign users + * @param User|string $user Local User object or username string */ public function setBlocker( $user ) { + if ( is_string( $user ) ) { + $user = User::newFromName( $user, false ); + } + + if ( $user->isAnon() && User::isUsableName( $user->getName() ) ) { + throw new InvalidArgumentException( + 'Blocker must be a local user or a name that cannot be a local user' + ); + } + $this->blocker = $user; } diff --git a/includes/Category.php b/includes/Category.php index 629962d2a9..9241730a04 100644 --- a/includes/Category.php +++ b/includes/Category.php @@ -119,9 +119,9 @@ class Category { /** * Factory function. * - * @param array $name A category name (no "Category:" prefix). It need + * @param string $name A category name (no "Category:" prefix). It need * not be normalized, with spaces replaced by underscores. - * @return mixed Category, or false on a totally invalid name + * @return Category|bool Category, or false on a totally invalid name */ public static function newFromName( $name ) { $cat = new self(); diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php index 3cd7ef181a..6fe74fa15c 100644 --- a/includes/DefaultSettings.php +++ b/includes/DefaultSettings.php @@ -38,10 +38,6 @@ * @file */ -/** - * @defgroup Globalsettings Global settings - */ - /** * @cond file_level_code * This is not a valid entry point, perform no further processing unless @@ -1810,7 +1806,7 @@ $wgDBtype = 'mysql'; /** * Whether to use SSL in DB connection. * - * This setting is only used $wgLBFactoryConf['class'] is set to + * This setting is only used if $wgLBFactoryConf['class'] is set to * 'LBFactorySimple' and $wgDBservers is an empty array; otherwise * the DBO_SSL flag must be set in the 'flags' option of the database * connection to achieve the same functionality. @@ -2554,6 +2550,8 @@ $wgGitInfoCacheDirectory = false; * It should be appended in the query string of static CSS and JS includes, * to ensure that client-side caches do not keep obsolete copies of global * styles. + * + * @deprecated since 1.31 */ $wgStyleVersion = '303'; @@ -3683,23 +3681,6 @@ $wgResourceLoaderMaxage = [ */ $wgResourceLoaderDebug = false; -/** - * Put each statement on its own line when minifying JavaScript. This makes - * debugging in non-debug mode a bit easier. - * - * @deprecated since 1.27: Always false; no longer configurable. - */ -$wgResourceLoaderMinifierStatementsOnOwnLine = false; - -/** - * Maximum line length when minifying JavaScript. This is not a hard maximum: - * the minifier will try not to produce lines longer than this, but may be - * forced to do so in certain cases. - * - * @deprecated since 1.27: Always 1,000; no longer configurable. - */ -$wgResourceLoaderMinifierMaxLineLength = 1000; - /** * Whether to ensure the mediawiki.legacy library is loaded before other modules. * @@ -4850,6 +4831,7 @@ $wgReservedUsernames = [ 'msg:double-redirect-fixer', // Automatic double redirect fix 'msg:usermessage-editor', // Default user for leaving user messages 'msg:proxyblocker', // For $wgProxyList and Special:Blockme (removed in 1.22) + 'msg:sorbs', // For $wgEnableDnsBlacklist etc. 'msg:spambot_username', // Used by cleanupSpam.php 'msg:autochange-username', // Used by anon category RC entries (parser functions, Lua & purges) ]; @@ -4881,7 +4863,6 @@ $wgDefaultUserOptions = [ 'hidepatrolled' => 0, 'hidecategorization' => 1, 'imagesize' => 2, - 'math' => 1, 'minordefault' => 0, 'newpageshidepatrolled' => 0, 'nickname' => '', @@ -6961,6 +6942,29 @@ $wgAllowCategorizedRecentChanges = false; */ $wgUseTagFilter = true; +/** + * List of core tags to enable. Available tags are: + * - 'mw-contentmodelchange': Edit changes content model of a page + * - 'mw-new-redirect': Edit makes new redirect page (new page or by changing content page) + * - 'mw-removed-redirect': Edit changes an existing redirect into a non-redirect + * - 'mw-changed-redirect-target': Edit changes redirect target + * - 'mw-blank': Edit completely blanks the page + * - 'mw-replace': Edit removes more than 90% of the content + * - 'mw-rollback': Edit is a rollback, made through the rollback link or rollback API + * + * @var array + * @since 1.31 + */ +$wgSoftwareTags = [ + 'mw-contentmodelchange' => true, + 'mw-new-redirect' => true, + 'mw-removed-redirect' => true, + 'mw-changed-redirect-target' => true, + 'mw-blank' => true, + 'mw-replace' => true, + 'mw-rollback' => true +]; + /** * If set to an integer, pages that are watched by this many users or more * will not require the unwatchedpages permission to view the number of @@ -7428,6 +7432,7 @@ $wgJobClasses = [ 'refreshLinksDynamic' => 'RefreshLinksJob', 'activityUpdateJob' => 'ActivityUpdateJob', 'categoryMembershipChange' => 'CategoryMembershipChangeJob', + 'clearUserWatchlist' => 'ClearUserWatchlistJob', 'cdnPurge' => 'CdnPurgeJob', 'enqueue' => 'EnqueueJob', // local queue for multi-DC setups 'null' => 'NullJob' @@ -8270,6 +8275,22 @@ $wgPhpCli = '/usr/bin/php'; */ $wgShellLocale = 'C.UTF-8'; +/** + * Method to use to restrict shell commands + * + * Supported options: + * - 'autodetect': Autodetect if any restriction methods are available + * - 'firejail': Use firejail + * - false: Don't use any restrictions + * + * @note If using firejail with MediaWiki running in a home directory different + * from the webserver user, firejail 0.9.44+ is required. + * + * @since 1.31 + * @var string|bool + */ +$wgShellRestrictionMethod = false; + /** @} */ # End shell } /************************************************************************//** diff --git a/includes/EditPage.php b/includes/EditPage.php index ff224c5598..bcaab3a3d7 100644 --- a/includes/EditPage.php +++ b/includes/EditPage.php @@ -4012,7 +4012,10 @@ class EditPage { $parserOutput->setEditSectionTokens( false ); // no section edit links return [ 'parserOutput' => $parserOutput, - 'html' => $parserOutput->getText() ]; + 'html' => $parserOutput->getText( [ + 'enableSectionEditLinks' => false + ] ) + ]; } /** diff --git a/includes/Feed.php b/includes/Feed.php index fd223e63dd..35f2ce9438 100644 --- a/includes/Feed.php +++ b/includes/Feed.php @@ -232,7 +232,8 @@ abstract class ChannelFeed extends FeedItem { header( "Content-type: $mimetype; charset=UTF-8" ); // Set a sane filename - $exts = MimeMagic::singleton()->getExtensionsForType( $mimetype ); + $exts = MediaWiki\MediaWikiServices::getInstance()->getMimeAnalyzer() + ->getExtensionsForType( $mimetype ); $ext = $exts ? strtok( $exts, ' ' ) : 'xml'; header( "Content-Disposition: inline; filename=\"feed.{$ext}\"" ); diff --git a/includes/GitInfo.php b/includes/GitInfo.php index 8095fd7308..fb75c256d6 100644 --- a/includes/GitInfo.php +++ b/includes/GitInfo.php @@ -37,6 +37,11 @@ class GitInfo { */ protected $basedir; + /** + * Location of the repository + */ + protected $repoDir; + /** * Path to JSON cache file for pre-computed git information. */ @@ -58,6 +63,7 @@ class GitInfo { * @see precomputeValues */ public function __construct( $repoDir, $usePrecomputed = true ) { + $this->repoDir = $repoDir; $this->cacheFile = self::getCacheFilePath( $repoDir ); wfDebugLog( 'gitinfo', "Computed cacheFile={$this->cacheFile} for {$repoDir}" @@ -230,8 +236,11 @@ class GitInfo { '--format=format:%ct', 'HEAD', ]; + $gitDir = realpath( $this->basedir ); $result = Shell::command( $cmd ) - ->environment( [ 'GIT_DIR' => $this->basedir ] ) + ->environment( [ 'GIT_DIR' => $gitDir ] ) + ->restrict( Shell::RESTRICT_DEFAULT | Shell::NO_NETWORK ) + ->whitelistPaths( [ $gitDir, $this->repoDir ] ) ->execute(); if ( $result->getExitCode() === 0 ) { diff --git a/includes/GlobalFunctions.php b/includes/GlobalFunctions.php index bb1951d528..1a33b76357 100644 --- a/includes/GlobalFunctions.php +++ b/includes/GlobalFunctions.php @@ -2404,9 +2404,10 @@ function wfShellWikiCmd( $script, array $parameters = [], array $options = [] ) * @param string $mine * @param string $yours * @param string &$result + * @param string &$mergeAttemptResult * @return bool */ -function wfMerge( $old, $mine, $yours, &$result ) { +function wfMerge( $old, $mine, $yours, &$result, &$mergeAttemptResult = null ) { global $wgDiff3; # This check may also protect against code injection in @@ -2442,13 +2443,18 @@ function wfMerge( $old, $mine, $yours, &$result ) { $oldtextName, $yourtextName ); $handle = popen( $cmd, 'r' ); - if ( fgets( $handle, 1024 ) ) { - $conflict = true; - } else { - $conflict = false; - } + $mergeAttemptResult = ''; + do { + $data = fread( $handle, 8192 ); + if ( strlen( $data ) == 0 ) { + break; + } + $mergeAttemptResult .= $data; + } while ( true ); pclose( $handle ); + $conflict = $mergeAttemptResult !== ''; + # Merge differences $cmd = Shell::escape( $wgDiff3, '-a', '-e', '--merge', $mytextName, $oldtextName, $yourtextName ); diff --git a/includes/Linker.php b/includes/Linker.php index 403b10a149..a0332cf615 100644 --- a/includes/Linker.php +++ b/includes/Linker.php @@ -892,10 +892,26 @@ class Linker { */ public static function userLink( $userId, $userName, $altUserName = false ) { $classes = 'mw-userlink'; + $page = null; if ( $userId == 0 ) { - $page = SpecialPage::getTitleFor( 'Contributions', $userName ); - if ( $altUserName === false ) { - $altUserName = IP::prettifyIP( $userName ); + $pos = strpos( $userName, '>' ); + if ( $pos !== false ) { + $iw = explode( ':', substr( $userName, 0, $pos ) ); + $firstIw = array_shift( $iw ); + $interwikiLookup = MediaWikiServices::getInstance()->getInterwikiLookup(); + if ( $interwikiLookup->isValidInterwiki( $firstIw ) ) { + $title = MWNamespace::getCanonicalName( NS_USER ) . ':' . substr( $userName, $pos + 1 ); + if ( $iw ) { + $title = join( ':', $iw ) . ':' . $title; + } + $page = Title::makeTitle( NS_MAIN, $title, '', $firstIw ); + } + $classes .= ' mw-extuserlink'; + } else { + $page = SpecialPage::getTitleFor( 'Contributions', $userName ); + if ( $altUserName === false ) { + $altUserName = IP::prettifyIP( $userName ); + } } $classes .= ' mw-anonuserlink'; // Separate link class for anons (T45179) } else { @@ -903,11 +919,12 @@ class Linker { } // Wrap the output with tags for directionality isolation - return self::link( - $page, - '' . htmlspecialchars( $altUserName !== false ? $altUserName : $userName ) . '', - [ 'class' => $classes ] - ); + $linkText = + '' . htmlspecialchars( $altUserName !== false ? $altUserName : $userName ) . ''; + + return $page + ? self::link( $page, $linkText, [ 'class' => $classes ] ) + : Html::rawElement( 'span', [ 'class' => $classes ], $linkText ); } /** @@ -931,6 +948,11 @@ class Linker { $blockable = !( $flags & self::TOOL_LINKS_NOBLOCK ); $addEmailLink = $flags & self::TOOL_LINKS_EMAIL && $userId; + if ( $userId == 0 && strpos( $userText, '>' ) !== false ) { + // No tools for an external user + return ''; + } + $items = []; if ( $talkable ) { $items[] = self::userTalkLink( $userId, $userText ); diff --git a/includes/Message.php b/includes/Message.php index 3b2f3ccc7b..16ae839e82 100644 --- a/includes/Message.php +++ b/includes/Message.php @@ -1244,7 +1244,9 @@ class Message implements MessageSpecifier, Serializable { $this->getLanguage() ); - return $out instanceof ParserOutput ? $out->getText() : $out; + return $out instanceof ParserOutput + ? $out->getText( [ 'enableSectionEditLinks' => false ] ) + : $out; } /** diff --git a/includes/MimeMagic.php b/includes/MimeMagic.php index a2a44bb868..6152d2262f 100644 --- a/includes/MimeMagic.php +++ b/includes/MimeMagic.php @@ -31,6 +31,7 @@ class MimeMagic extends MimeAnalyzer { * @deprecated since 1.28 get a MimeAnalyzer instance from MediaWikiServices */ public static function singleton() { + wfDeprecated( __METHOD__, '1.28' ); // XXX: We know that the MimeAnalyzer is currently an instance of MimeMagic $instance = MediaWikiServices::getInstance()->getMimeAnalyzer(); Assert::postcondition( diff --git a/includes/OutputPage.php b/includes/OutputPage.php index 4635f991c2..92963fd18b 100644 --- a/includes/OutputPage.php +++ b/includes/OutputPage.php @@ -1573,10 +1573,14 @@ class OutputPage extends ContextSource { * Get/set the ParserOptions object to use for wikitext parsing * * @param ParserOptions|null $options Either the ParserOption to use or null to only get the - * current ParserOption object + * current ParserOption object. This parameter is deprecated since 1.31. * @return ParserOptions */ public function parserOptions( $options = null ) { + if ( $options !== null ) { + wfDeprecated( __METHOD__ . ' with non-null $options', '1.31' ); + } + if ( $options !== null && !empty( $options->isBogus ) ) { // Someone is trying to set a bogus pre-$wgUser PO. Check if it has // been changed somehow, and keep it if so. @@ -1779,7 +1783,9 @@ class OutputPage extends ContextSource { $popts->setTidy( $oldTidy ); - $this->addParserOutput( $parserOutput ); + $this->addParserOutput( $parserOutput, [ + 'enableSectionEditLinks' => false, + ] ); } /** @@ -1864,9 +1870,10 @@ class OutputPage extends ContextSource { * * @since 1.24 * @param ParserOutput $parserOutput + * @param array $poOptions Options to ParserOutput::getText() */ - public function addParserOutputContent( $parserOutput ) { - $this->addParserOutputText( $parserOutput ); + public function addParserOutputContent( $parserOutput, $poOptions = [] ) { + $this->addParserOutputText( $parserOutput, $poOptions ); $this->addModules( $parserOutput->getModules() ); $this->addModuleScripts( $parserOutput->getModuleScripts() ); @@ -1880,9 +1887,10 @@ class OutputPage extends ContextSource { * * @since 1.24 * @param ParserOutput $parserOutput + * @param array $poOptions Options to ParserOutput::getText() */ - public function addParserOutputText( $parserOutput ) { - $text = $parserOutput->getText(); + public function addParserOutputText( $parserOutput, $poOptions = [] ) { + $text = $parserOutput->getText( $poOptions ); // Avoid PHP 7.1 warning of passing $this by reference $outputPage = $this; Hooks::runWithoutAbort( 'OutputPageBeforeHTML', [ &$outputPage, &$text ] ); @@ -1893,16 +1901,22 @@ class OutputPage extends ContextSource { * Add everything from a ParserOutput object. * * @param ParserOutput $parserOutput + * @param array $poOptions Options to ParserOutput::getText() */ - function addParserOutput( $parserOutput ) { + function addParserOutput( $parserOutput, $poOptions = [] ) { $this->addParserOutputMetadata( $parserOutput ); // Touch section edit links only if not previously disabled if ( $parserOutput->getEditSectionTokens() ) { $parserOutput->setEditSectionTokens( $this->mEnableSectionEditLinks ); } + if ( !$this->mEnableSectionEditLinks + && !array_key_exists( 'enableSectionEditLinks', $poOptions ) + ) { + $poOptions['enableSectionEditLinks'] = false; + } - $this->addParserOutputText( $parserOutput ); + $this->addParserOutputText( $parserOutput, $poOptions ); } /** @@ -1953,7 +1967,9 @@ class OutputPage extends ContextSource { $popts->setTargetLanguage( $oldLang ); } - return $parserOutput->getText(); + return $parserOutput->getText( [ + 'enableSectionEditLinks' => false, + ] ); } /** @@ -3953,6 +3969,7 @@ class OutputPage extends ContextSource { * Enables/disables section edit links, doesn't override __NOEDITSECTION__ * @param bool $flag * @since 1.23 + * @deprecated since 1.31, use $poOptions to addParserOutput() instead. */ public function enableSectionEditLinks( $flag = true ) { $this->mEnableSectionEditLinks = $flag; @@ -3961,6 +3978,7 @@ class OutputPage extends ContextSource { /** * @return bool * @since 1.23 + * @deprecated since 1.31, use $poOptions to addParserOutput() instead. */ public function sectionEditLinksEnabled() { return $this->mEnableSectionEditLinks; diff --git a/includes/Preferences.php b/includes/Preferences.php index e383f03f6b..878462db21 100644 --- a/includes/Preferences.php +++ b/includes/Preferences.php @@ -75,11 +75,6 @@ class Preferences { * @return array|null */ static function getPreferences( $user, IContextSource $context ) { - OutputPage::setupOOUI( - strtolower( $context->getSkin()->getSkinName() ), - $context->getLanguage()->getDir() - ); - $defaultPreferences = []; self::profilePreferences( $user, $context, $defaultPreferences ); @@ -317,17 +312,14 @@ class Preferences { if ( $canEditPrivateInfo && $authManager->allowsAuthenticationDataChange( new PasswordAuthenticationRequest(), false )->isGood() ) { - $link = new OOUI\ButtonWidget( [ - 'href' => SpecialPage::getTitleFor( 'ChangePassword' )->getLinkURL( [ - 'returnto' => SpecialPage::getTitleFor( 'Preferences' )->getPrefixedText() - ] ), - 'label' => $context->msg( 'prefs-resetpass' )->text(), - ] ); + $link = $linkRenderer->makeLink( SpecialPage::getTitleFor( 'ChangePassword' ), + $context->msg( 'prefs-resetpass' )->text(), [], + [ 'returnto' => SpecialPage::getTitleFor( 'Preferences' )->getPrefixedText() ] ); $defaultPreferences['password'] = [ 'type' => 'info', 'raw' => true, - 'default' => (string)$link, + 'default' => $link, 'label-message' => 'yourpassword', 'section' => 'personal/info', ]; @@ -471,15 +463,16 @@ class Preferences { $emailAddress = $user->getEmail() ? htmlspecialchars( $user->getEmail() ) : ''; if ( $canEditPrivateInfo && $authManager->allowsPropertyChange( 'emailaddress' ) ) { - $link = new OOUI\ButtonWidget( [ - 'href' => SpecialPage::getTitleFor( 'ChangeEmail' )->getLinkURL( [ - 'returnto' => SpecialPage::getTitleFor( 'Preferences' )->getPrefixedText() - ] ), - 'label' => - $context->msg( $user->getEmail() ? 'prefs-changeemail' : 'prefs-setemail' )->text(), - ] ); - - $emailAddress .= $emailAddress == '' ? $link : ( '
' . $link ); + $link = $linkRenderer->makeLink( + SpecialPage::getTitleFor( 'ChangeEmail' ), + $context->msg( $user->getEmail() ? 'prefs-changeemail' : 'prefs-setemail' )->text(), + [], + [ 'returnto' => SpecialPage::getTitleFor( 'Preferences' )->getPrefixedText() ] ); + + $emailAddress .= $emailAddress == '' ? $link : ( + $context->msg( 'word-separator' )->escaped() + . $context->msg( 'parentheses' )->rawParams( $link )->escaped() + ); } $defaultPreferences['emailaddress'] = [ @@ -514,10 +507,10 @@ class Preferences { } else { $disableEmailPrefs = true; $emailauthenticated = $context->msg( 'emailnotauthenticated' )->parse() . '
' . - new OOUI\ButtonWidget( [ - 'href' => SpecialPage::getTitleFor( 'Confirmemail' )->getLinkURL(), - 'label' => $context->msg( 'emailconfirmlink' )->text(), - ] ); + $linkRenderer->makeKnownLink( + SpecialPage::getTitleFor( 'Confirmemail' ), + $context->msg( 'emailconfirmlink' )->text() + ) . '
'; $emailauthenticationclass = "mw-email-not-authenticated"; } } else { @@ -754,7 +747,6 @@ class Preferences { 'default' => $tzSetting, 'size' => 20, 'section' => 'rendering/timeoffset', - 'id' => 'wpTimeCorrection', ]; } @@ -934,16 +926,16 @@ class Preferences { $defaultPreferences['rcfilters-wl-saved-queries'] = [ 'type' => 'api', ]; - $defaultPreferences['rcfilters-saved-queries-versionbackup'] = [ + // Override RCFilters preferences for RecentChanges 'limit' + $defaultPreferences['rcfilters-limit'] = [ 'type' => 'api', ]; - $defaultPreferences['rcfilters-wl-saved-queries-versionbackup'] = [ + $defaultPreferences['rcfilters-saved-queries-versionbackup'] = [ 'type' => 'api', ]; - $defaultPreferences['rcfilters-rclimit'] = [ + $defaultPreferences['rcfilters-wl-saved-queries-versionbackup'] = [ 'type' => 'api', ]; - if ( $config->get( 'RCWatchCategoryMembership' ) ) { $defaultPreferences['hidecategorization'] = [ 'type' => 'toggle', @@ -997,7 +989,7 @@ class Preferences { # # Watchlist ##################################### if ( $user->isAllowed( 'editmywatchlist' ) ) { - $editWatchlistLinks = ''; + $editWatchlistLinks = []; $editWatchlistModes = [ 'edit' => [ 'EditWatchlist', false ], 'raw' => [ 'EditWatchlist', 'raw' ], @@ -1006,19 +998,16 @@ class Preferences { $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer(); foreach ( $editWatchlistModes as $editWatchlistMode => $mode ) { // Messages: prefs-editwatchlist-edit, prefs-editwatchlist-raw, prefs-editwatchlist-clear - $editWatchlistLinks .= - new OOUI\ButtonWidget( [ - 'href' => SpecialPage::getTitleFor( $mode[0], $mode[1] )->getLinkURL(), - 'label' => new OOUI\HtmlSnippet( - $context->msg( "prefs-editwatchlist-{$editWatchlistMode}" )->parse() - ), - ] ); + $editWatchlistLinks[] = $linkRenderer->makeKnownLink( + SpecialPage::getTitleFor( $mode[0], $mode[1] ), + new HtmlArmor( $context->msg( "prefs-editwatchlist-{$editWatchlistMode}" )->parse() ) + ); } $defaultPreferences['editwatchlist'] = [ 'type' => 'info', 'raw' => true, - 'default' => $editWatchlistLinks, + 'default' => $context->getLanguage()->pipeList( $editWatchlistLinks ), 'label-message' => 'prefs-editwatchlist-label', 'section' => 'watchlist/editwatchlist', ]; @@ -1141,12 +1130,6 @@ class Preferences { 'default' => $user->getTokenFromOption( 'watchlisttoken' ), 'help-message' => 'prefs-help-watchlist-token2', ]; - $defaultPreferences['watchlisttoken-info2'] = [ - 'type' => 'info', - 'section' => 'watchlist/tokenwatchlist', - 'raw' => true, - 'default' => $context->msg( 'prefs-help-watchlist-token2' )->parse(), - ]; } } @@ -1188,21 +1171,31 @@ class Preferences { # Only show skins that aren't disabled in $wgSkipSkins $validSkinNames = Skin::getAllowedSkins(); - # Sort by UI skin name. First though need to update validSkinNames as sometimes - # the skinkey & UI skinname differ (e.g. "standard" skinkey is "Classic" in the UI). foreach ( $validSkinNames as $skinkey => &$skinname ) { $msg = $context->msg( "skinname-{$skinkey}" ); if ( $msg->exists() ) { $skinname = htmlspecialchars( $msg->text() ); } } - asort( $validSkinNames ); $config = $context->getConfig(); $defaultSkin = $config->get( 'DefaultSkin' ); $allowUserCss = $config->get( 'AllowUserCss' ); $allowUserJs = $config->get( 'AllowUserJs' ); + # Sort by the internal name, so that the ordering is the same for each display language, + # especially if some skin names are translated to use a different alphabet and some are not. + uksort( $validSkinNames, function ( $a, $b ) use ( $defaultSkin ) { + # Display the default first in the list by comparing it as lesser than any other. + if ( strcasecmp( $a, $defaultSkin ) === 0 ) { + return -1; + } + if ( strcasecmp( $b, $defaultSkin ) === 0 ) { + return 1; + } + return strcasecmp( $a, $b ); + } ); + $foundDefault = false; foreach ( $validSkinNames as $skinkey => $sn ) { $linkTools = []; @@ -1367,9 +1360,6 @@ class Preferences { $formClass = 'PreferencesForm', array $remove = [] ) { - // We use ButtonWidgets in some of the getPreferences() functions - $context->getOutput()->enableOOUI(); - $formDescriptor = self::getPreferences( $user, $context ); if ( count( $remove ) ) { $removeKeys = array_flip( $remove ); @@ -1554,6 +1544,14 @@ class Preferences { $formData[$pref] = $user->getOption( $pref, null, true ); } + // If the user changed the rclimit preference, also change the rcfilters-rclimit preference + if ( + isset( $formData['rclimit'] ) && + intval( $formData[ 'rclimit' ] ) !== $user->getIntOption( 'rclimit' ) + ) { + $formData['rcfilters-limit'] = $formData['rclimit']; + } + // Keep old preferences from interfering due to back-compat code, etc. $user->resetOptions( 'unused', $form->getContext() ); diff --git a/includes/ServiceWiring.php b/includes/ServiceWiring.php index ae88d375cc..dad0630edf 100644 --- a/includes/ServiceWiring.php +++ b/includes/ServiceWiring.php @@ -439,8 +439,9 @@ return [ 'filesize' => $config->get( 'MaxShellFileSize' ), ]; $cgroup = $config->get( 'ShellCgroup' ); + $restrictionMethod = $config->get( 'ShellRestrictionMethod' ); - $factory = new CommandFactory( $limits, $cgroup ); + $factory = new CommandFactory( $limits, $cgroup, $restrictionMethod ); $factory->setLogger( LoggerFactory::getInstance( 'exec' ) ); $factory->logStderr(); diff --git a/includes/Setup.php b/includes/Setup.php index 4c281b13ce..d6f4b2fe4c 100644 --- a/includes/Setup.php +++ b/includes/Setup.php @@ -734,14 +734,22 @@ if ( !$wgDBerrorLogTZ ) { $wgDBerrorLogTZ = $wgLocaltimezone; } -// initialize the request object in $wgRequest +// Initialize the request object in $wgRequest $wgRequest = RequestContext::getMain()->getRequest(); // BackCompat -// Set user IP/agent information for causal consistency purposes +// Set user IP/agent information for causal consistency purposes. +// The cpPosTime cookie has no prefix and is set by MediaWiki::preOutputCommit(). +$cpPosTime = $wgRequest->getFloat( 'cpPosTime', $wgRequest->getCookie( 'cpPosTime', '' ) ); MediaWikiServices::getInstance()->getDBLoadBalancerFactory()->setRequestInfo( [ 'IPAddress' => $wgRequest->getIP(), 'UserAgent' => $wgRequest->getHeader( 'User-Agent' ), - 'ChronologyProtection' => $wgRequest->getHeader( 'ChronologyProtection' ) + 'ChronologyProtection' => $wgRequest->getHeader( 'ChronologyProtection' ), + 'ChronologyPositionTime' => $cpPosTime ] ); +// Make sure that caching does not compromise the consistency improvements +if ( $cpPosTime ) { + MediaWikiServices::getInstance()->getMainWANObjectCache()->useInterimHoldOffCaching( false ); +} +unset( $cpPosTime ); // Useful debug output if ( $wgCommandLineMode ) { diff --git a/includes/Status.php b/includes/Status.php index a35af6e8c6..f17f173edc 100644 --- a/includes/Status.php +++ b/includes/Status.php @@ -316,7 +316,9 @@ class Status extends StatusValue { $lang = $this->languageFromParam( $lang ); $text = $this->getWikiText( $shortContext, $longContext, $lang ); $out = MessageCache::singleton()->parse( $text, null, true, true, $lang ); - return $out instanceof ParserOutput ? $out->getText() : $out; + return $out instanceof ParserOutput + ? $out->getText( [ 'enableSectionEditLinks' => false ] ) + : $out; } /** diff --git a/includes/StreamFile.php b/includes/StreamFile.php index 71113a8691..2ad42e5616 100644 --- a/includes/StreamFile.php +++ b/includes/StreamFile.php @@ -113,7 +113,7 @@ class StreamFile { return 'unknown/unknown'; } - $magic = MimeMagic::singleton(); + $magic = MediaWiki\MediaWikiServices::getInstance()->getMimeAnalyzer(); // Use the extension only, rather than magic numbers, to avoid opening // up vulnerabilities due to uploads of files with allowed extensions // but disallowed types. diff --git a/includes/Title.php b/includes/Title.php index 829be448b2..b23bd5a364 100644 --- a/includes/Title.php +++ b/includes/Title.php @@ -108,7 +108,12 @@ class Title implements LinkTarget { /** @var array Array of groups allowed to edit this article */ public $mRestrictions = []; - /** @var string|bool */ + /** + * @var string|bool Comma-separated set of permission keys + * indicating who can move or edit the page from the page table, (pre 1.10) rows. + * Edit and move sections are separated by a colon + * Example: "edit=autoconfirmed,sysop:move=sysop" + */ protected $mOldRestrictions = false; /** @var bool Cascade restrictions on this page to included templates and images? */ @@ -1464,7 +1469,9 @@ class Title implements LinkTarget { public function getFragmentForURL() { if ( !$this->hasFragment() ) { return ''; - } elseif ( $this->isExternal() && !$this->getTransWikiID() ) { + } elseif ( $this->isExternal() + && !self::getInterwikiLookup()->fetch( $this->mInterwiki )->isLocal() + ) { return '#' . Sanitizer::escapeIdForExternalInterwiki( $this->getFragment() ); } return '#' . Sanitizer::escapeIdForLink( $this->getFragment() ); @@ -3044,8 +3051,10 @@ class Title implements LinkTarget { * Public for usage by LiquidThreads. * * @param array $rows Array of db result objects - * @param string $oldFashionedRestrictions Comma-separated list of page - * restrictions from page table (pre 1.10) + * @param string $oldFashionedRestrictions Comma-separated set of permission keys + * indicating who can move or edit the page from the page table, (pre 1.10) rows. + * Edit and move sections are separated by a colon + * Example: "edit=autoconfirmed,sysop:move=sysop" */ public function loadRestrictionsFromRows( $rows, $oldFashionedRestrictions = null ) { $dbr = wfGetDB( DB_REPLICA ); @@ -3114,8 +3123,10 @@ class Title implements LinkTarget { /** * Load restrictions from the page_restrictions table * - * @param string $oldFashionedRestrictions Comma-separated list of page - * restrictions from page table (pre 1.10) + * @param string $oldFashionedRestrictions Comma-separated set of permission keys + * indicating who can move or edit the page from the page table, (pre 1.10) rows. + * Edit and move sections are separated by a colon + * Example: "edit=autoconfirmed,sysop:move=sysop" */ public function loadRestrictions( $oldFashionedRestrictions = null ) { if ( $this->mRestrictionsLoaded ) { diff --git a/includes/WebStart.php b/includes/WebStart.php index e4d93f9a30..be95779af3 100644 --- a/includes/WebStart.php +++ b/includes/WebStart.php @@ -50,13 +50,10 @@ unset( $IP ); # its purpose. define( 'MEDIAWIKI', true ); -# Full path to working directory. -# Makes it possible to for example to have effective exclude path in apc. -# __DIR__ breaks symlinked includes, but realpath() returns false -# if we don't have permissions on parent directories. +# Full path to the installation directory. $IP = getenv( 'MW_INSTALL_PATH' ); if ( $IP === false ) { - $IP = realpath( '.' ) ?: dirname( __DIR__ ); + $IP = dirname( __DIR__ ); } // If no LocalSettings file exists, try to display an error page diff --git a/includes/actions/HistoryAction.php b/includes/actions/HistoryAction.php index a9e3d6accd..0e964bf5cc 100644 --- a/includes/actions/HistoryAction.php +++ b/includes/actions/HistoryAction.php @@ -154,7 +154,7 @@ class HistoryAction extends FormlessAction { # show deletion/move log if there is an entry LogEventsList::showLogExtract( $out, - [ 'delete', 'move' ], + [ 'delete', 'move', 'protect' ], $this->getTitle(), '', [ 'lim' => 10, diff --git a/includes/api/ApiDelete.php b/includes/api/ApiDelete.php index 7766acd363..96c291c660 100644 --- a/includes/api/ApiDelete.php +++ b/includes/api/ApiDelete.php @@ -59,7 +59,7 @@ class ApiDelete extends ApiBase { // If change tagging was requested, check that the user is allowed to tag, // and the tags are valid - if ( count( $params['tags'] ) ) { + if ( $params['tags'] ) { $tagStatus = ChangeTags::canAddTagsAccompanyingChange( $params['tags'], $user ); if ( !$tagStatus->isOK() ) { $this->dieStatus( $tagStatus ); diff --git a/includes/api/ApiEditPage.php b/includes/api/ApiEditPage.php index 94d6e97b24..26d4fd1e43 100644 --- a/includes/api/ApiEditPage.php +++ b/includes/api/ApiEditPage.php @@ -334,7 +334,7 @@ class ApiEditPage extends ApiBase { } // Apply change tags - if ( count( $params['tags'] ) ) { + if ( $params['tags'] ) { $tagStatus = ChangeTags::canAddTagsAccompanyingChange( $params['tags'], $user ); if ( $tagStatus->isOK() ) { $requestArray['wpChangeTags'] = implode( ',', $params['tags'] ); diff --git a/includes/api/ApiFormatBase.php b/includes/api/ApiFormatBase.php index c5f2fcfaa7..4348fc881d 100644 --- a/includes/api/ApiFormatBase.php +++ b/includes/api/ApiFormatBase.php @@ -78,7 +78,8 @@ abstract class ApiFormatBase extends ApiBase { } elseif ( $this->getIsHtml() ) { return 'api-result.html'; } else { - $exts = MimeMagic::singleton()->getExtensionsForType( $this->getMimeType() ); + $exts = MediaWiki\MediaWikiServices::getInstance()->getMimeAnalyzer() + ->getExtensionsForType( $this->getMimeType() ); $ext = $exts ? strtok( $exts, ' ' ) : strtolower( $this->mFormat ); return "api-result.$ext"; } diff --git a/includes/api/ApiImageRotate.php b/includes/api/ApiImageRotate.php index 71bda6d7e4..05684036e9 100644 --- a/includes/api/ApiImageRotate.php +++ b/includes/api/ApiImageRotate.php @@ -43,7 +43,7 @@ class ApiImageRotate extends ApiBase { ] ); // Check if user can add tags - if ( count( $params['tags'] ) ) { + if ( $params['tags'] ) { $ableToTag = ChangeTags::canAddTagsAccompanyingChange( $params['tags'], $this->getUser() ); if ( !$ableToTag->isOK() ) { $this->dieStatus( $ableToTag ); diff --git a/includes/api/ApiImport.php b/includes/api/ApiImport.php index b46f0b1e51..822711aa02 100644 --- a/includes/api/ApiImport.php +++ b/includes/api/ApiImport.php @@ -1,9 +1,5 @@ .@gmail.com" * * This program is free software; you can redistribute it and/or modify @@ -53,12 +49,18 @@ class ApiImport extends ApiBase { $params['fullhistory'], $params['templates'] ); + $usernamePrefix = $params['interwikisource']; } else { $isUpload = true; if ( !$user->isAllowed( 'importupload' ) ) { $this->dieWithError( 'apierror-cantimport-upload' ); } $source = ImportStreamSource::newFromUpload( 'xml' ); + $usernamePrefix = (string)$params['interwikiprefix']; + if ( $usernamePrefix === '' ) { + $encParamName = $this->encodeParamName( 'interwikiprefix' ); + $this->dieWithError( [ 'apierror-missingparam', $encParamName ] ); + } } if ( !$source->isOK() ) { $this->dieStatus( $source ); @@ -81,6 +83,7 @@ class ApiImport extends ApiBase { $this->dieStatus( $statusRootPage ); } } + $importer->setUsernamePrefix( $usernamePrefix, $params['assignknownusers'] ); $reporter = new ApiImportReporter( $importer, $isUpload, @@ -141,6 +144,9 @@ class ApiImport extends ApiBase { 'xml' => [ ApiBase::PARAM_TYPE => 'upload', ], + 'interwikiprefix' => [ + ApiBase::PARAM_TYPE => 'string', + ], 'interwikisource' => [ ApiBase::PARAM_TYPE => $this->getAllowedImportSources(), ], @@ -150,6 +156,7 @@ class ApiImport extends ApiBase { 'namespace' => [ ApiBase::PARAM_TYPE => 'namespace' ], + 'assignknownusers' => false, 'rootpage' => null, 'tags' => [ ApiBase::PARAM_TYPE => 'tags', diff --git a/includes/api/ApiOpenSearch.php b/includes/api/ApiOpenSearch.php index 419fd140d7..416fc7f6e4 100644 --- a/includes/api/ApiOpenSearch.php +++ b/includes/api/ApiOpenSearch.php @@ -1,7 +1,5 @@ @gmail.com" * Copyright © 2008 Brion Vibber * Copyright © 2014 Wikimedia Foundation and contributors @@ -382,6 +380,9 @@ class ApiOpenSearch extends ApiBase { } } +/** + * @ingroup API + */ class ApiOpenSearchFormatJson extends ApiFormatJson { private $warningsAsError = false; diff --git a/includes/api/ApiOptions.php b/includes/api/ApiOptions.php index 5b0d86a7f6..14bd089929 100644 --- a/includes/api/ApiOptions.php +++ b/includes/api/ApiOptions.php @@ -64,7 +64,7 @@ class ApiOptions extends ApiBase { } $changes = []; - if ( count( $params['change'] ) ) { + if ( $params['change'] ) { foreach ( $params['change'] as $entry ) { $array = explode( '=', $entry, 2 ); $changes[$array[0]] = isset( $array[1] ) ? $array[1] : null; diff --git a/includes/api/ApiParse.php b/includes/api/ApiParse.php index 15b94fb952..ec015da712 100644 --- a/includes/api/ApiParse.php +++ b/includes/api/ApiParse.php @@ -288,10 +288,6 @@ class ApiParse extends ApiBase { $result_array['textsuppressed'] = true; } - if ( $params['disabletoc'] ) { - $p_result->setTOCEnabled( false ); - } - if ( isset( $params['useskin'] ) ) { $factory = MediaWikiServices::getInstance()->getSkinFactory(); $skin = $factory->makeSkin( Skin::normalizeKey( $params['useskin'] ) ); @@ -347,7 +343,10 @@ class ApiParse extends ApiBase { } if ( isset( $prop['text'] ) ) { - $result_array['text'] = $p_result->getText(); + $result_array['text'] = $p_result->getText( [ + 'allowTOC' => !$params['disabletoc'], + 'enableSectionEditLinks' => !$params['disableeditsection'], + ] ); $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'text'; } diff --git a/includes/api/ApiPurge.php b/includes/api/ApiPurge.php index 4b8ce7fce1..b7cfc2c6a2 100644 --- a/includes/api/ApiPurge.php +++ b/includes/api/ApiPurge.php @@ -1,12 +1,6 @@ addTables( 'page_restrictions' ); $this->addWhere( 'page_id=pr_page' ); $this->addWhere( "pr_expiry > {$db->addQuotes( $db->timestamp() )} OR pr_expiry IS NULL" ); - if ( count( $params['prtype'] ) ) { + if ( $params['prtype'] ) { $this->addWhereFld( 'pr_type', $params['prtype'] ); if ( isset( $params['prlevel'] ) ) { diff --git a/includes/api/ApiQueryBacklinks.php b/includes/api/ApiQueryBacklinks.php index 54be254d59..830cc48477 100644 --- a/includes/api/ApiQueryBacklinks.php +++ b/includes/api/ApiQueryBacklinks.php @@ -138,7 +138,7 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase { if ( count( $this->cont ) >= 2 ) { $op = $this->params['dir'] == 'descending' ? '<' : '>'; - if ( count( $this->params['namespace'] ) > 1 ) { + if ( $this->params['namespace'] !== null && count( $this->params['namespace'] ) > 1 ) { $this->addWhere( "{$this->bl_from_ns} $op {$this->cont[0]} OR " . "({$this->bl_from_ns} = {$this->cont[0]} AND " . @@ -160,7 +160,7 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase { $this->addOption( 'LIMIT', $this->params['limit'] + 1 ); $sort = ( $this->params['dir'] == 'descending' ? ' DESC' : '' ); $orderBy = []; - if ( count( $this->params['namespace'] ) > 1 ) { + if ( $this->params['namespace'] !== null && count( $this->params['namespace'] ) > 1 ) { $orderBy[] = $this->bl_from_ns . $sort; } $orderBy[] = $this->bl_from . $sort; @@ -246,7 +246,7 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase { $where = "{$this->bl_from} $op= {$this->cont[5]}"; // Don't bother with namespace, title, or from_namespace if it's // otherwise constant in the where clause. - if ( count( $this->params['namespace'] ) > 1 ) { + if ( $this->params['namespace'] !== null && count( $this->params['namespace'] ) > 1 ) { $where = "{$this->bl_from_ns} $op {$this->cont[4]} OR " . "({$this->bl_from_ns} = {$this->cont[4]} AND ($where))"; } @@ -278,7 +278,7 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase { if ( count( $allRedirDBkey ) > 1 ) { $orderBy[] = $this->bl_title . $sort; } - if ( count( $this->params['namespace'] ) > 1 ) { + if ( $this->params['namespace'] !== null && count( $this->params['namespace'] ) > 1 ) { $orderBy[] = $this->bl_from_ns . $sort; } $orderBy[] = $this->bl_from . $sort; diff --git a/includes/api/ApiQueryBacklinksprop.php b/includes/api/ApiQueryBacklinksprop.php index 1db15f87e8..ef02d095c8 100644 --- a/includes/api/ApiQueryBacklinksprop.php +++ b/includes/api/ApiQueryBacklinksprop.php @@ -161,7 +161,9 @@ class ApiQueryBacklinksprop extends ApiQueryGeneratorBase { } } else { $this->addWhereFld( "{$p}_from_namespace", $params['namespace'] ); - if ( !empty( $settings['from_namespace'] ) && count( $params['namespace'] ) > 1 ) { + if ( !empty( $settings['from_namespace'] ) + && $params['namespace'] !== null && count( $params['namespace'] ) > 1 + ) { $sortby["{$p}_from_namespace"] = 'int'; } } diff --git a/includes/api/ApiQueryBase.php b/includes/api/ApiQueryBase.php index 6987dfb13f..179e6f7b25 100644 --- a/includes/api/ApiQueryBase.php +++ b/includes/api/ApiQueryBase.php @@ -262,9 +262,7 @@ abstract class ApiQueryBase extends ApiBase { * @param string|string[] $value Value; ignored if null or empty array; */ protected function addWhereFld( $field, $value ) { - // Use count() to its full documented capabilities to simultaneously - // test for null, empty array or empty countable object - if ( count( $value ) ) { + if ( $value !== null && !( is_array( $value ) && !$value ) ) { $this->where[$field] = $value; } } diff --git a/includes/api/ApiQueryCategoryMembers.php b/includes/api/ApiQueryCategoryMembers.php index c570ec997e..e3265d1cdc 100644 --- a/includes/api/ApiQueryCategoryMembers.php +++ b/includes/api/ApiQueryCategoryMembers.php @@ -97,7 +97,7 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase { // how to have efficient subcategory access :-) ~~~~ (oh well, domas) $miser_ns = []; if ( $this->getConfig()->get( 'MiserMode' ) ) { - $miser_ns = $params['namespace']; + $miser_ns = $params['namespace'] ?: []; } else { $this->addWhereFld( 'page_namespace', $params['namespace'] ); } diff --git a/includes/api/ApiQueryExtLinksUsage.php b/includes/api/ApiQueryExtLinksUsage.php index 6c29b6030f..43f41312fd 100644 --- a/includes/api/ApiQueryExtLinksUsage.php +++ b/includes/api/ApiQueryExtLinksUsage.php @@ -61,7 +61,7 @@ class ApiQueryExtLinksUsage extends ApiQueryGeneratorBase { $miser_ns = []; if ( $this->getConfig()->get( 'MiserMode' ) ) { - $miser_ns = $params['namespace']; + $miser_ns = $params['namespace'] ?: []; } else { $this->addWhereFld( 'page_namespace', $params['namespace'] ); } diff --git a/includes/api/ApiQueryLinks.php b/includes/api/ApiQueryLinks.php index 508bdf3f9d..119db3e6a9 100644 --- a/includes/api/ApiQueryLinks.php +++ b/includes/api/ApiQueryLinks.php @@ -114,7 +114,7 @@ class ApiQueryLinks extends ApiQueryGeneratorBase { } } elseif ( $params['namespace'] ) { $this->addWhereFld( $this->prefix . '_namespace', $params['namespace'] ); - $multiNS = count( $params['namespace'] ) !== 1; + $multiNS = $params['namespace'] === null || count( $params['namespace'] ) !== 1; } if ( !is_null( $params['continue'] ) ) { diff --git a/includes/api/ApiQuerySiteinfo.php b/includes/api/ApiQuerySiteinfo.php index 6b896c95ad..2e9e69c9a5 100644 --- a/includes/api/ApiQuerySiteinfo.php +++ b/includes/api/ApiQuerySiteinfo.php @@ -283,6 +283,8 @@ class ApiQuerySiteinfo extends ApiQueryBase { $data['interwikimagic'] = (bool)$config->get( 'InterwikiMagic' ); $data['magiclinks'] = $config->get( 'EnableMagicLinks' ); + $data['categorycollation'] = $config->get( 'CategoryCollation' ); + Hooks::run( 'APIQuerySiteInfoGeneralInfo', [ $this, &$data ] ); return $this->getResult()->addValue( 'query', $property, $data ); diff --git a/includes/api/ApiRevisionDelete.php b/includes/api/ApiRevisionDelete.php index 9d71a7db7e..5a51b2843a 100644 --- a/includes/api/ApiRevisionDelete.php +++ b/includes/api/ApiRevisionDelete.php @@ -47,7 +47,7 @@ class ApiRevisionDelete extends ApiBase { } // Check if user can add tags - if ( count( $params['tags'] ) ) { + if ( $params['tags'] ) { $ableToTag = ChangeTags::canAddTagsAccompanyingChange( $params['tags'], $user ); if ( !$ableToTag->isOK() ) { $this->dieStatus( $ableToTag ); diff --git a/includes/api/ApiRollback.php b/includes/api/ApiRollback.php index 76b6cc6722..4ca2955079 100644 --- a/includes/api/ApiRollback.php +++ b/includes/api/ApiRollback.php @@ -52,7 +52,7 @@ class ApiRollback extends ApiBase { // If change tagging was requested, check that the user is allowed to tag, // and the tags are valid - if ( count( $params['tags'] ) ) { + if ( $params['tags'] ) { $tagStatus = ChangeTags::canAddTagsAccompanyingChange( $params['tags'], $user ); if ( !$tagStatus->isOK() ) { $this->dieStatus( $tagStatus ); diff --git a/includes/api/ApiRsd.php b/includes/api/ApiRsd.php index fdc62a8ea9..f20d1c6c8b 100644 --- a/includes/api/ApiRsd.php +++ b/includes/api/ApiRsd.php @@ -3,8 +3,6 @@ /** * API for MediaWiki 1.17+ * - * Created on October 26, 2010 - * * Copyright © 2010 Bryan Tong Minh and Brion Vibber * * This program is free software; you can redistribute it and/or modify diff --git a/includes/api/ApiSetPageLanguage.php b/includes/api/ApiSetPageLanguage.php index 7e3f1acf96..54394a57d9 100644 --- a/includes/api/ApiSetPageLanguage.php +++ b/includes/api/ApiSetPageLanguage.php @@ -73,7 +73,7 @@ class ApiSetPageLanguage extends ApiBase { // If change tagging was requested, check that the user is allowed to tag, // and the tags are valid - if ( count( $params['tags'] ) ) { + if ( $params['tags'] ) { $tagStatus = ChangeTags::canAddTagsAccompanyingChange( $params['tags'], $user ); if ( !$tagStatus->isOK() ) { $this->dieStatus( $tagStatus ); diff --git a/includes/api/ApiStashEdit.php b/includes/api/ApiStashEdit.php index 4bd6a3fb6f..b4b9321787 100644 --- a/includes/api/ApiStashEdit.php +++ b/includes/api/ApiStashEdit.php @@ -181,9 +181,14 @@ class ApiStashEdit extends ApiBase { $title = $page->getTitle(); $key = self::getStashKey( $title, self::getContentHash( $content ), $user ); - // Use the master DB for fast blocking locks + // Use the master DB to allow for fast blocking locks on the "save path" where this + // value might actually be used to complete a page edit. If the edit submission request + // happens before this edit stash requests finishes, then the submission will block until + // the stash request finishes parsing. For the lock acquisition below, there is not much + // need to duplicate parsing of the same content/user/summary bundle, so try to avoid + // blocking at all here. $dbw = wfGetDB( DB_MASTER ); - if ( !$dbw->lock( $key, __METHOD__, 1 ) ) { + if ( !$dbw->lock( $key, __METHOD__, 0 ) ) { // De-duplicate requests on the same key return self::ERROR_BUSY; } diff --git a/includes/api/ApiTag.php b/includes/api/ApiTag.php index 76c676293f..9304c2b414 100644 --- a/includes/api/ApiTag.php +++ b/includes/api/ApiTag.php @@ -37,7 +37,7 @@ class ApiTag extends ApiBase { } // Check if user can add tags - if ( count( $params['tags'] ) ) { + if ( $params['tags'] ) { $ableToTag = ChangeTags::canAddTagsAccompanyingChange( $params['tags'], $user ); if ( !$ableToTag->isOk() ) { $this->dieStatus( $ableToTag ); diff --git a/includes/api/ApiUsageException.php b/includes/api/ApiUsageException.php index 4196add2c3..c200dcba6f 100644 --- a/includes/api/ApiUsageException.php +++ b/includes/api/ApiUsageException.php @@ -16,7 +16,6 @@ * http://www.gnu.org/copyleft/gpl.html * * @file - * @defgroup API API */ /** diff --git a/includes/api/ApiUserrights.php b/includes/api/ApiUserrights.php index 2a364d9756..3813aba7a1 100644 --- a/includes/api/ApiUserrights.php +++ b/includes/api/ApiUserrights.php @@ -64,14 +64,15 @@ class ApiUserrights extends ApiBase { } else { $expiry = [ 'infinity' ]; } - if ( count( $expiry ) !== count( $params['add'] ) ) { + $add = (array)$params['add']; + if ( count( $expiry ) !== count( $add ) ) { if ( count( $expiry ) === 1 ) { - $expiry = array_fill( 0, count( $params['add'] ), $expiry[0] ); + $expiry = array_fill( 0, count( $add ), $expiry[0] ); } else { $this->dieWithError( [ 'apierror-toofewexpiries', count( $expiry ), - count( $params['add'] ) + count( $add ) ] ); } } @@ -79,7 +80,7 @@ class ApiUserrights extends ApiBase { // Validate the expiries $groupExpiries = []; foreach ( $expiry as $index => $expiryValue ) { - $group = $params['add'][$index]; + $group = $add[$index]; $groupExpiries[$group] = UserrightsPage::expiryToTimestamp( $expiryValue ); if ( $groupExpiries[$group] === false ) { @@ -109,7 +110,7 @@ class ApiUserrights extends ApiBase { $r['user'] = $user->getName(); $r['userid'] = $user->getId(); list( $r['added'], $r['removed'] ) = $form->doSaveUserGroups( - $user, (array)$params['add'], (array)$params['remove'], + $user, (array)$add, (array)$params['remove'], $params['reason'], $tags, $groupExpiries ); diff --git a/includes/api/i18n/de.json b/includes/api/i18n/de.json index d3273db479..ba8d2f989d 100644 --- a/includes/api/i18n/de.json +++ b/includes/api/i18n/de.json @@ -220,6 +220,8 @@ "apihelp-import-extended-description": "Bitte beachte, dass der HTTP-POST-Vorgang als Dateiupload ausgeführt werden muss (z.B. durch multipart/form-data), um eine Datei über den xml-Parameter zu senden.", "apihelp-import-param-summary": "Importzusammenfassung des Logbucheintrags.", "apihelp-import-param-xml": "Hochgeladene XML-Datei.", + "apihelp-import-param-interwikiprefix": "Für hochgeladene Importe: Auf unbekannte Benutzernamen anzuwendendes Interwiki-Präfix (und bekannte Benutzer, falls $1assignknownusers festgelegt ist).", + "apihelp-import-param-assignknownusers": "Weist Bearbeitungen lokalen Benutzern zu, wo der benannte Benutzer lokal vorhanden ist.", "apihelp-import-param-interwikisource": "Für Interwiki-Importe: Wiki, von dem importiert werden soll.", "apihelp-import-param-interwikipage": "Für Interwiki-Importe: zu importierende Seite.", "apihelp-import-param-fullhistory": "Für Interwiki-Importe: importiere die komplette Versionsgeschichte, nicht nur die aktuelle Version.", @@ -1035,6 +1037,8 @@ "api-help-param-disabled-in-miser-mode": "Deaktiviert aufgrund des [[mw:Special:MyLanguage/Manual:$wgMiserMode|Miser-Modus]].", "api-help-param-continue": "Falls weitere Ergebnisse verfügbar sind, dies zum Fortfahren verwenden.", "api-help-param-no-description": "(keine Beschreibung)", + "api-help-param-maxbytes": "Kann nicht länger sein als {{PLURAL:$1|ein Byte|$1 Bytes}}.", + "api-help-param-maxchars": "Kann nicht länger sein als {{PLURAL:$1|ein|$1}} Zeichen.", "api-help-examples": "{{PLURAL:$1|Beispiel|Beispiele}}:", "api-help-permissions": "{{PLURAL:$1|Berechtigung|Berechtigungen}}:", "api-help-permissions-granted-to": "{{PLURAL:$1|Gewährt an}}: $2", @@ -1055,6 +1059,8 @@ "apierror-invalid-file-key": "Kein gültiger Dateischlüssel.", "apierror-invalidsection": "Der Parameter section muss eine gültige Abschnittskennung oder new sein.", "apierror-invaliduserid": "Die Benutzerkennung $1 ist nicht gültig.", + "apierror-maxbytes": "Der Parameter $1 kann nicht länger sein als {{PLURAL:$2|ein Byte|$2 Bytes}}", + "apierror-maxchars": "Der Parameter $1 kann nicht länger sein als {{PLURAL:$2|ein|$2}} Zeichen", "apierror-nosuchsection": "Es gibt keinen Abschnitt $1.", "apierror-nosuchuserid": "Es gibt keinen Benutzer mit der Kennung $1.", "apierror-offline": "Aufgrund von Problemen bei der Netzwerkverbindung kannst du nicht weitermachen. Stelle sicher, dass du eine funktionierende Internetverbindung hast und versuche es erneut.", diff --git a/includes/api/i18n/en.json b/includes/api/i18n/en.json index 85f17debc2..91c3e185b0 100644 --- a/includes/api/i18n/en.json +++ b/includes/api/i18n/en.json @@ -250,6 +250,8 @@ "apihelp-import-extended-description": "Note that the HTTP POST must be done as a file upload (i.e. using multipart/form-data) when sending a file for the xml parameter.", "apihelp-import-param-summary": "Log entry import summary.", "apihelp-import-param-xml": "Uploaded XML file.", + "apihelp-import-param-interwikiprefix": "For uploaded imports: interwiki prefix to apply to unknown user names (and known users if $1assignknownusers is set).", + "apihelp-import-param-assignknownusers": "Assign edits to local users where the named user exists locally.", "apihelp-import-param-interwikisource": "For interwiki imports: wiki to import from.", "apihelp-import-param-interwikipage": "For interwiki imports: page to import.", "apihelp-import-param-fullhistory": "For interwiki imports: import the full history, not just the current version.", diff --git a/includes/api/i18n/es.json b/includes/api/i18n/es.json index 06cd64f3fd..b84057ea6f 100644 --- a/includes/api/i18n/es.json +++ b/includes/api/i18n/es.json @@ -29,7 +29,8 @@ "Igv", "Fortega", "Luzcaru", - "Javiersanp" + "Javiersanp", + "KATRINE1992" ] }, "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 Mailing list]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce API Announcements]\n* [https://phabricator.wikimedia.org/maniphest/query/GebfyV4uCaLd/#R Bugs & requests]\n
\nStatus: Todas las funciones mostradas en esta página deberían estar funcionando, pero la API aún está en desarrollo activo, y puede cambiar en cualquier momento. Suscribase a [https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ the mediawiki-api-announce mailing list] para aviso de actualizaciones.\n\nErroneous requests: Cuando se envían solicitudes erróneas a la API, se enviará un encabezado HTTP con la clave \"MediaWiki-API-Error\" y, luego, el valor del encabezado y el código de error devuelto se establecerán en el mismo valor. Para más información ver [[mw:Special:MyLanguage/API:Errors_and_warnings|API: Errors and warnings]].\n\nTesting: Para facilitar la comprobación de las solicitudes de API, consulte [[Special:ApiSandbox]].", @@ -239,6 +240,8 @@ "apihelp-import-extended-description": "Tenga en cuenta que el HTTP POST debe hacerse como una carga de archivos (es decir, el uso de multipart/form-data) al enviar un archivo para el parámetro xml.", "apihelp-import-param-summary": "Resumen de importación de entrada del registro.", "apihelp-import-param-xml": "Se cargó el archivo XML.", + "apihelp-import-param-interwikiprefix": "Para importaciones cargadas: el prefijo de interwiki debe aplicarse a los nombres de usuario desconocidos (y a los conocidos si se define $1assignknownusers).", + "apihelp-import-param-assignknownusers": "Asignar ediciones a usuarios locales cuando sus nombres de usuario existan localmente.", "apihelp-import-param-interwikisource": "Para importaciones interwiki: wiki desde la que importar.", "apihelp-import-param-interwikipage": "Para importaciones interwiki: página a importar.", "apihelp-import-param-fullhistory": "Para importaciones interwiki: importar todo el historial, no solo la versión actual.", @@ -1429,6 +1432,8 @@ "api-help-param-direction": "En qué sentido hacer la enumeración:\n;newer: De más antiguos a más recientes. Nota: $1start debe ser anterior a $1end.\n;older: De más recientes a más antiguos (orden predefinido). Nota: $1start debe ser posterior a $1end.", "api-help-param-continue": "Cuando haya más resultados disponibles, utiliza esto para continuar.", "api-help-param-no-description": "(sin descripción)", + "api-help-param-maxbytes": "No puede sobrepasar $1 {{PLURAL:$1|byte|bytes}} de longitud.", + "api-help-param-maxchars": "No puede sobrepasar $1 {{PLURAL:$1|carácter|caracteres}} de longitud.", "api-help-examples": "{{PLURAL:$1|Ejemplo|Ejemplos}}:", "api-help-permissions": "{{PLURAL:$1|Permiso|Permisos}}:", "api-help-permissions-granted-to": "{{PLURAL:$1|Concedido a|Concedidos a}}: $2", @@ -1515,6 +1520,8 @@ "apierror-invalidurlparam": "Valor no válido para $1urlparam ($2=$3).", "apierror-invaliduser": "Nombre de usuario «$1» no válido.", "apierror-invaliduserid": "El identificador de usuario $1 no es válido.", + "apierror-maxbytes": "El parámetro $1 no puede sobrepasar $2 {{PLURAL:$2|byte|bytes}}", + "apierror-maxchars": "El parámetro $1 no puede sobrepasar $2 {{PLURAL:$2|carácter|caracteres}} de longitud.", "apierror-mimesearchdisabled": "La búsqueda MIME está deshabilitada en el modo avaro.", "apierror-missingcontent-pageid": "Contenido faltante para la página con identificador $1.", "apierror-missingparam-at-least-one-of": "{{PLURAL:$2|El parámetro|Al menos uno de los parámetros}} $1 es necesario.", diff --git a/includes/api/i18n/fr.json b/includes/api/i18n/fr.json index bdaf58c7bd..a56b42f083 100644 --- a/includes/api/i18n/fr.json +++ b/includes/api/i18n/fr.json @@ -254,6 +254,8 @@ "apihelp-import-extended-description": "Noter que le POST HTTP doit être effectué comme un import de fichier (c’est-à-dire en utilisant multipart/form-data) lors de l’envoi d’un fichier pour le paramètre xml.", "apihelp-import-param-summary": "Résumé de l’importation de l’entrée de journal.", "apihelp-import-param-xml": "Fichier XML téléversé.", + "apihelp-import-param-interwikiprefix": "Pour les importations téléchargées : le préfixe interwiki à appliquer aux noms d’utilisateur inconnus (et aux utilisateurs connus si $1assignknownusers est positionné).", + "apihelp-import-param-assignknownusers": "Affecter les modifications aux utilisateurs locaux quand l’utilisateur nommé existe localement.", "apihelp-import-param-interwikisource": "Pour les importations interwiki : wiki depuis lequel importer.", "apihelp-import-param-interwikipage": "Pour les importations interwiki : page à importer.", "apihelp-import-param-fullhistory": "Pour les importations interwiki : importer tout l’historique, et pas seulement la version courante.", @@ -1504,6 +1506,8 @@ "api-help-param-direction": "Dans quelle direction énumérer :\n;newer:Lister les plus anciens en premier. Note : $1start doit être avant $1end.\n;older:Lister les nouveaux en premier (par défaut). Note : $1start doit être postérieur à $1end.", "api-help-param-continue": "Quand plus de résultats sont disponibles, utiliser cela pour continuer.", "api-help-param-no-description": "(aucune description)", + "api-help-param-maxbytes": "Ne peut excéder $1 octet{{PLURAL:$1||s}}.", + "api-help-param-maxchars": "Ne peut excéder $1 caractères{{PLURAL:$1||s}}.", "api-help-examples": "{{PLURAL:$1|Exemple|Exemples}} :", "api-help-permissions": "{{PLURAL:$1|Droit|Droits}} :", "api-help-permissions-granted-to": "{{PLURAL:$1|Accordé à}} : $2", @@ -1606,6 +1610,8 @@ "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.", + "apierror-maxbytes": "Le paramètre $1 ne peut excéder $2 octets{{PLURAL:$2||s}}", + "apierror-maxchars": "Le paramètre $1 ne peut excéder $2 catactères{{PLURAL:$2||s}}", "apierror-maxlag-generic": "Attente d’un serveur de base de données : $1 {{PLURAL:$1|seconde|secondes}} de délai.", "apierror-maxlag": "Attente de $2 : $1 {{PLURAL:$1|seconed|secondes}} de délai.", "apierror-mimesearchdisabled": "La recherche MIME est désactivée en mode Misère.", diff --git a/includes/api/i18n/it.json b/includes/api/i18n/it.json index fd88c186fb..568203622f 100644 --- a/includes/api/i18n/it.json +++ b/includes/api/i18n/it.json @@ -676,6 +676,8 @@ "api-help-param-token": "Un token \"$1\" recuperato da [[Special:ApiHelp/query+tokens|action=query&meta=tokens]]", "api-help-param-continue": "Quando più risultati sono disponibili, usa questo per continuare.", "api-help-param-no-description": "(nessuna descrizione)", + "api-help-param-maxbytes": "Non può essere più lungo di $1 {{PLURAL:$1|byte}}.", + "api-help-param-maxchars": "Non può essere più lungo di $1 {{PLURAL:$1|carattere|caratteri}}.", "api-help-examples": "{{PLURAL:$1|Esempio|Esempi}}:", "api-help-permissions": "{{PLURAL:$1|Permesso|Permessi}}:", "api-help-open-in-apisandbox": "[apri in una sandbox]", @@ -687,6 +689,8 @@ "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-invalidoldimage": "Il parametro oldimage ha un formato non valido.", "apierror-invaliduserid": "L'ID utente $1 non è valido.", + "apierror-maxbytes": "Il parametro $1 non può essere più lungo di $2 {{PLURAL:$2|byte}}", + "apierror-maxchars": "Il parametro $1 non può essere più lungo di $2 {{PLURAL:$2|carattere|caratteri}}", "apierror-nosuchuserid": "Non c'è alcun utente con ID $1.", "apierror-timeout": "Il server non ha risposto entro il tempo previsto.", "api-credits-header": "Crediti" diff --git a/includes/api/i18n/ja.json b/includes/api/i18n/ja.json index 4e102cf507..094c406164 100644 --- a/includes/api/i18n/ja.json +++ b/includes/api/i18n/ja.json @@ -11,10 +11,11 @@ "Macofe", "Suchichi02", "Kkairri", - "ネイ" + "ネイ", + "Omotecho" ] }, - "apihelp-main-extended-description": "
\n* [[mw:API:Main_page|説明文書]]\n* [[mw: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状態: このページに表示されている機能は全て動作するはずですが、この API は未だ活発に開発されており、変更される可能性があります。アップデートの通知を受け取るには、[https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ the mediawiki-api-announce メーリングリスト]に参加してください。\n\n誤ったリクエスト: 誤ったリクエストが API に送られた場合、\"MediaWiki-API-Error\" HTTP ヘッダーが送信され、そのヘッダーの値と送り返されるエラーコードは同じ値にセットされます。より詳しい情報は [[mw:API:Errors_and_warnings|API: Errors and warnings]] を参照してください。\n\nテスト: API のリクエストのテストは、[[Special:ApiSandbox]]で簡単に行えます。", + "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状態: このページに表示されている機能は全て動作するはずですが、この API は未だ活発に開発されており、変更される可能性があります。アップデートの通知を受け取るには、[https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ the mediawiki-api-announce メーリングリスト]に参加してください。\n\n誤ったリクエスト: 誤ったリクエストが API に送られた場合、\"MediaWiki-API-Error\" HTTP ヘッダーが送信され、そのヘッダーの値と送り返されるエラーコードは同じ値にセットされます。より詳しい情報は [[mw:Special:MyLanguage/API:Errors_and_warnings|API: Errors and warnings]] を参照してください。\n\nテスト: API のリクエストのテストは、[[Special:ApiSandbox]]で簡単に行えます。", "apihelp-main-param-action": "実行する操作です。", "apihelp-main-param-format": "出力する形式です。", "apihelp-main-param-smaxage": "s-maxage HTTP キャッシュ コントロール ヘッダー に、この秒数を設定します。エラーがキャッシュされることはありません。", @@ -955,6 +956,8 @@ "api-help-param-limited-in-miser-mode": "注意: [[mw:Special:MyLanguage/Manual:$wgMiserMode|miser mode]] により、これを使用すると継続する前に $1limit より返される結果が少なくなることがあります; 極端な場合では、ゼロ件の結果が返ることもあります。", "api-help-param-direction": "列挙の方向:\n;newer:古いものを先に表示します。注意: $1start は $1end 以前でなければなりません。\n;older:新しいものを先に表示します (既定)。注意: $1start は $1end 以降でなければなりません。", "api-help-param-no-description": "(説明なし)", + "api-help-param-maxbytes": "$1 {{PLURAL:$1|byte|バイト}}以下で入力してください。", + "api-help-param-maxchars": "$1 {{PLURAL:$1|character|文字}}以下で入力してください。", "api-help-examples": "{{PLURAL:$1|例}}:", "api-help-permissions": "{{PLURAL:$1|権限}}:", "api-help-permissions-granted-to": "{{PLURAL:$1|権限を持つグループ}}: $2", diff --git a/includes/api/i18n/ko.json b/includes/api/i18n/ko.json index a2dc344205..034a03303c 100644 --- a/includes/api/i18n/ko.json +++ b/includes/api/i18n/ko.json @@ -734,6 +734,8 @@ "api-help-param-token-webui": "호환성을 위해, 웹 UI에 사용된 토큰도 허용합니다.", "api-help-param-continue": "더 많은 결과를 이용할 수 있을 때, 계속하려면 이것을 사용하십시오.", "api-help-param-no-description": "(설명 없음)", + "api-help-param-maxbytes": "$1{{PLURAL:$1|바이트}}를 초과할 수 없습니다.", + "api-help-param-maxchars": "$1{{PLURAL:$1|자}}를 초과할 수 없습니다.", "api-help-examples": "{{PLURAL:$1|예시}}:", "api-help-permissions": "{{PLURAL:$1|권한}}:", "api-help-permissions-granted-to": "{{PLURAL:$1|다음 그룹에 부여됨}}: $2", @@ -786,6 +788,8 @@ "apierror-invalidtitle": "잘못된 제목 \"$1\".", "apierror-invaliduser": "잘못된 사용자 이름 \"$1\".", "apierror-invaliduserid": "$1 사용자 ID는 유효하지 않습니다.", + "apierror-maxbytes": "$1 변수는 $2{{PLURAL:$2|바이트}}를 초과할 수 없습니다", + "apierror-maxchars": "$1 변수는 $2{{PLURAL:$2|자}}를 초과할 수 없습니다", "apierror-maxlag-generic": "데이터베이스 서버 대기 중: $1 {{PLURAL:$1|초}} 지연되었습니다.", "apierror-maxlag": "$2 대기 중: $1 {{PLURAL:$1|초}} 지연되었습니다.", "apierror-missingcontent-revid": "ID $1 판에 해당하는 내용이 없습니다.", diff --git a/includes/api/i18n/lb.json b/includes/api/i18n/lb.json index 0d5f22481e..c54c6225df 100644 --- a/includes/api/i18n/lb.json +++ b/includes/api/i18n/lb.json @@ -203,6 +203,7 @@ "api-help-datatypes-header": "Datentypen", "api-help-param-type-user": "Typ: {{PLURAL:$1|1=Benotzernumm|2=Lëscht vu Benotzernimm}}", "api-help-param-multi-max-simple": "Maximal Zuel vun de Wäerter ass {{PLURAL:$1|$1}}.", + "api-help-param-maxbytes": "Däerf net méi laang si wéi {{PLURAL:$1|ee Byte|$1 Byten}}.", "api-help-examples": "{{PLURAL:$1|Beispill|Beispiler}}:", "api-help-permissions": "{{PLURAL:$1|Autorisatioun|Autorisatiounen}}:", "api-help-open-in-apisandbox": "[an der Sandkëscht opmaachen]", diff --git a/includes/api/i18n/mk.json b/includes/api/i18n/mk.json index ce32e3f322..a0129009c6 100644 --- a/includes/api/i18n/mk.json +++ b/includes/api/i18n/mk.json @@ -26,9 +26,9 @@ "apihelp-block-param-autoblock": "Автоматски блокирај ја последно употребената IP-адреса и сите понатамошни IP-адреси од кои лицето ќе се обиде да се најави.", "apihelp-block-param-noemail": "Оневозможи му на корисникот да испаќа е-пошта преку викито. (Го бара правото code>blockemail).", "apihelp-block-param-hidename": "Скриј го корисничкото име од дневникот на блокирања. (Го бара правото hideuser)", - "apihelp-block-param-allowusertalk": "Овозможи му на корисникот да си ја уредува сопствената страница за разговор (зависи од [[mw:Manual:$wgBlockAllowsUTEdit|$wgBlockAllowsUTEdit]]).", + "apihelp-block-param-allowusertalk": "Овозможи му на корисникот да ја уредува неговата разговорна страница (зависи од [[mw:Special:MyLanguage/Manual:$wgBlockAllowsUTEdit|$wgBlockAllowsUTEdit]]).", "apihelp-block-param-reblock": "Ако корисникот е веќе блокиран, наметни врз постоечкиот блок.", - "apihelp-block-param-watchuser": "Набљудувај ја корисничката страница и страницата за разговор на овој корисник или IP-адреса", + "apihelp-block-param-watchuser": "Набљудувај ја корисничката страница и разговорна страница на овој корисник или IP-адреса", "apihelp-block-example-ip-simple": "Блокирај ја IP-адресата 192.0.2.5 три дена со причината Прва опомена.", "apihelp-block-example-user-complex": "Блокирај го корисникот Vandal (Вандал) бесконечно со причината Vandal (Вандализам) и оневозможи создавање на нови сметки и праќање е-пошта.", "apihelp-checktoken-summary": "Проверка на полноважноста на шифрата од [[Special:ApiHelp/query+tokens|action=query&meta=tokens]].", @@ -116,8 +116,8 @@ "apihelp-expandtemplates-param-includecomments": "Дали во изводот да се вклучени HTML-коментари.", "apihelp-expandtemplates-param-generatexml": "Создај XML-дрво на расчленување (заменето со $1prop=parsetree).", "apihelp-expandtemplates-example-simple": "Прошири го викитекстот {{Project:Sandbox}}.", - "apihelp-feedcontributions-summary": "Дава канал со придонеси на корисник.", - "apihelp-feedcontributions-param-feedformat": "Формат на каналот.", + "apihelp-feedcontributions-summary": "Дава тековник со придонеси на корисник.", + "apihelp-feedcontributions-param-feedformat": "Формат на тековникот.", "apihelp-feedcontributions-param-user": "За кои корисници да се прикажуваат придонесите.", "apihelp-feedcontributions-param-namespace": "По кој именски простор да се филтрираат придонесите:", "apihelp-feedcontributions-param-year": "Од година (и порано):", @@ -129,8 +129,8 @@ "apihelp-feedcontributions-param-hideminor": "Сокриј ситни уредувања.", "apihelp-feedcontributions-param-showsizediff": "Покажувај ја големинската разлика меѓу преработките.", "apihelp-feedcontributions-example-simple": "Покажувај придонеси на Пример.", - "apihelp-feedrecentchanges-summary": "Дава канал со скорешни промени.", - "apihelp-feedrecentchanges-param-feedformat": "Форматот на каналот.", + "apihelp-feedrecentchanges-summary": "Дава тековник со скорешни промени.", + "apihelp-feedrecentchanges-param-feedformat": "Форматот на тековникот.", "apihelp-feedrecentchanges-param-namespace": "На кој именски простор да се ограничи исходот.", "apihelp-feedrecentchanges-param-invert": "Сите именски простори освен избраниот.", "apihelp-feedrecentchanges-param-associated": "Вклучи придружни именски простори (разговор или главен).", @@ -151,11 +151,11 @@ "apihelp-feedrecentchanges-param-categories_any": "Прикажи само промени на страниците во било која од категориите.", "apihelp-feedrecentchanges-example-simple": "Прикажи скорешни промени", "apihelp-feedrecentchanges-example-30days": "Прикажувај скорешни промени 30 дена", - "apihelp-feedwatchlist-summary": "Дава канал од набљудуваните.", - "apihelp-feedwatchlist-param-feedformat": "Форматот на каналот.", + "apihelp-feedwatchlist-summary": "Дава тековник со набљудуваните.", + "apihelp-feedwatchlist-param-feedformat": "Форматот на тековникот.", "apihelp-feedwatchlist-param-hours": "Испиши страници изменети во рок од олку часови отсега.", "apihelp-feedwatchlist-param-linktosections": "Давај ме право на изменетите делови, ако е можно.", - "apihelp-feedwatchlist-example-default": "Прикажи го каналот од набљудуваните.", + "apihelp-feedwatchlist-example-default": "Прикажи го тековникот на набљудуваните.", "apihelp-feedwatchlist-example-all6hrs": "Прикажи ги сите промени во набљудуваните во последните 6 часа", "apihelp-filerevert-summary": "Врати податотека на претходна верзија.", "apihelp-filerevert-param-filename": "Име на целната податотека, без претставката „Податотека:“.", @@ -205,7 +205,7 @@ "apihelp-move-param-fromid": "Назнака на страницата што треба да се премести. Не може да се користи заедно со $1from.", "apihelp-move-param-to": "Како да гласи новата страница.", "apihelp-move-param-reason": "Причина за преименувањето.", - "apihelp-move-param-movetalk": "Преименувај ја и страницата за разговор, ако ја има.", + "apihelp-move-param-movetalk": "Преименувај ја и разговорната страница, ако ја има.", "apihelp-move-param-movesubpages": "Преименувај потстраници, ако има.", "apihelp-move-param-noredirect": "Не прави пренасочување.", "apihelp-move-param-watch": "Додај ги страницата и пренасочувањето во набљудуваните на тековниот корисник.", diff --git a/includes/api/i18n/nl.json b/includes/api/i18n/nl.json index ff99dff7c6..5a14cda646 100644 --- a/includes/api/i18n/nl.json +++ b/includes/api/i18n/nl.json @@ -66,7 +66,7 @@ "apihelp-compare-param-totitle": "Tweede paginanaam om te vergelijken.", "apihelp-compare-param-toid": "Tweede pagina-ID om te vergelijken.", "apihelp-compare-param-torev": "Tweede versie om te vergelijken.", - "apihelp-createaccount-summary": "Nieuwe gebruikersaccount aanmaken.", + "apihelp-createaccount-summary": "Nieuw gebruikersaccount aanmaken.", "apihelp-createaccount-example-create": "Start het proces voor het aanmaken van de gebruiker Example met het wachtwoord ExamplePassword.", "apihelp-createaccount-param-name": "Gebruikersnaam.", "apihelp-createaccount-param-password": "Wachtwoord (genegeerd als $1mailpassword is ingesteld).", @@ -262,7 +262,7 @@ "apihelp-query+blocks-summary": "Toon alle geblokkeerde gebruikers en IP-adressen.", "apihelp-query+blocks-param-limit": "Het maximum aantal blokkades te tonen.", "apihelp-query+blocks-paramvalue-prop-id": "Voegt de blokkade ID toe.", - "apihelp-query+blocks-paramvalue-prop-user": "Voegt de gebruikernaam van de geblokeerde gebruiker toe.", + "apihelp-query+blocks-paramvalue-prop-user": "Voegt de gebruikersnaam van de geblokkeerde gebruiker toe.", "apihelp-query+blocks-paramvalue-prop-userid": "Voegt de gebruiker-ID van de geblokkeerde gebruiker toe.", "apihelp-query+blocks-paramvalue-prop-flags": "Labelt de blokkade met (automatische blokkade, alleen anoniem, enzovoort).", "apihelp-query+blocks-example-simple": "Toon blokkades.", @@ -359,7 +359,7 @@ "api-help-datatypes-header": "Gegevenstypen", "api-help-param-default": "Standaard: $1", "api-help-examples": "{{PLURAL:$1|Voorbeeld|Voorbeelden}}:", - "apierror-autoblocked": "Uw IP-adres is automatisch geblokeerd, omdat het gebruikt is door een geblokkeerde gebruiker.", + "apierror-autoblocked": "Uw IP-adres is automatisch geblokkeerd, omdat het gebruikt is door een geblokkeerde gebruiker.", "apierror-badmodule-nosubmodules": "De module $1 heeft geen submodules.", "apierror-blockedfrommail": "U bent geblokkeerd en kunt geen emails verzenden.", "apierror-blocked": "U bent geblokkeerd en kunt niet bewerken.", diff --git a/includes/api/i18n/pt.json b/includes/api/i18n/pt.json index 8d4cedc9fb..b85ddc96b1 100644 --- a/includes/api/i18n/pt.json +++ b/includes/api/i18n/pt.json @@ -233,6 +233,8 @@ "apihelp-import-extended-description": "Note que o pedido POST de HTTP tem de ser feito como um carregamento de ficheiro (isto é, usando \"multipart/form-data\") ao enviar um ficheiro para o parâmetro xml.", "apihelp-import-param-summary": "Resumo da importação para a entrada do registo.", "apihelp-import-param-xml": "Ficheiro XML carregado.", + "apihelp-import-param-interwikiprefix": "Para importações carregadas: o prefixo interwikis a ser aplicado aos nomes de utilizador desconhecidos (e aos conhecidos se $1assignknownusers estiver definido).", + "apihelp-import-param-assignknownusers": "Atribuir as edições aos utilizadores locais se o utilizador nomeado existir localmente.", "apihelp-import-param-interwikisource": "Para importações interwikis: a wiki de onde importar.", "apihelp-import-param-interwikipage": "Para importações interwikis: a página a importar.", "apihelp-import-param-fullhistory": "Para importações interwikis: importar o historial completo, não apenas a versão atual.", @@ -1483,6 +1485,8 @@ "api-help-param-direction": "A direção da enumeração:\n;newer:Listar o mais antigo primeiro. Nota: $1start tem de estar antes de $1end.\n;older:Listar o mais recente primeiro (padrão). Nota: $1start tem de estar depois de $1end.", "api-help-param-continue": "Quando houver mais resultados disponíveis, usar isto para continuar", "api-help-param-no-description": "(sem descrição)", + "api-help-param-maxbytes": "Não pode exceder $1 {{PLURAL:$1|byte|bytes}}.", + "api-help-param-maxchars": "Não pode exceder $1 {{PLURAL:$1|carácter|caracteres}}.", "api-help-examples": "{{PLURAL:$1|Exemplo|Exemplos}}:", "api-help-permissions": "{{PLURAL:$1|Permissão|Permissões}}:", "api-help-permissions-granted-to": "{{PLURAL:$1|Concedida a|Concedidas a}}: $2", @@ -1585,6 +1589,8 @@ "apierror-invalidurlparam": "Valor inválido para $1urlparam ($2=$3).", "apierror-invaliduser": "Nome de utilizador inválido \"$1\".", "apierror-invaliduserid": "O identificador de utilizador $1 não é válido.", + "apierror-maxbytes": "O parâmetro $1 não pode exceder $2 {{PLURAL:$2|byte|bytes}}", + "apierror-maxchars": "O parâmetro $1 não pode exceder $2 {{PLURAL:$2|carácter|caracteres}}", "apierror-maxlag-generic": "À espera de um servidor de base de dados: $1 {{PLURAL:$1|segundo|segundos}} de atraso.", "apierror-maxlag": "À espera de $2: $1 {{PLURAL:$1|segundo|segundos}} de atraso.", "apierror-mimesearchdisabled": "A pesquisa MIME é desativada no modo avarento.", diff --git a/includes/api/i18n/qqq.json b/includes/api/i18n/qqq.json index 3bdf7c6d1d..47afdc12b9 100644 --- a/includes/api/i18n/qqq.json +++ b/includes/api/i18n/qqq.json @@ -240,6 +240,8 @@ "apihelp-import-extended-description": "{{doc-apihelp-extended-description|import}}", "apihelp-import-param-summary": "{{doc-apihelp-param|import|summary|info=The parameter being documented here provides the summary used on the log messages about the import. The phrase \"Import summary\" here is grammatically equivalent to a phrase such as \"science book\", not \"eat food\".}}", "apihelp-import-param-xml": "{{doc-apihelp-param|import|xml}}", + "apihelp-import-param-interwikiprefix": "{{doc-apihelp-param|import|interwikiprefix}}", + "apihelp-import-param-assignknownusers": "{{doc-apihelp-param|import|assignknownusers}}", "apihelp-import-param-interwikisource": "{{doc-apihelp-param|import|interwikisource}}", "apihelp-import-param-interwikipage": "{{doc-apihelp-param|import|interwikipage}}", "apihelp-import-param-fullhistory": "{{doc-apihelp-param|import|fullhistory}}", diff --git a/includes/api/i18n/sv.json b/includes/api/i18n/sv.json index 546bbf5d60..b1df86efee 100644 --- a/includes/api/i18n/sv.json +++ b/includes/api/i18n/sv.json @@ -270,6 +270,7 @@ "apihelp-parse-param-pageid": "Tolka innehållet på denna sida. Åsidosätter $1sidan.", "apihelp-parse-param-prop": "Vilka bitar av information att få:", "apihelp-parse-paramvalue-prop-categorieshtml": "Ger HTML-version av kategorierna.", + "apihelp-parse-param-disablepp": "Använd $1disablelimitreport istället.", "apihelp-parse-param-preview": "Tolka i preview-läget.", "apihelp-parse-example-page": "Tolka en sida.", "apihelp-parse-example-text": "Tolka wikitext.", @@ -282,6 +283,7 @@ "apihelp-protect-summary": "Ändra skyddsnivån för en sida.", "apihelp-protect-example-protect": "Skydda en sida", "apihelp-purge-summary": "Rensa cachen för angivna titlar.", + "apihelp-purge-param-forcelinkupdate": "Uppdatera länktabellerna.", "apihelp-query-param-list": "Vilka listor att hämta.", "apihelp-query-param-meta": "Vilka metadata att hämta.", "apihelp-query-example-allpages": "Hämta sidversioner av sidor som börjar med API/.", @@ -290,6 +292,7 @@ "apihelp-query+allcategories-param-min": "Returnera endast kategorier med minst så här många medlemmar.", "apihelp-query+allcategories-param-max": "Returnera endast kategorier med som mest så här många medlemmar.", "apihelp-query+allcategories-param-limit": "Hur många kategorier att returnera.", + "apihelp-query+allcategories-param-prop": "Vilka egenskaper att hämta:", "apihelp-query+allcategories-paramvalue-prop-size": "Lägger till antal sidor i kategorin.", "apihelp-query+allcategories-paramvalue-prop-hidden": "Märker kategorier som är dolda med __HIDDENCAT__.", "apihelp-query+alldeletedrevisions-summary": "Lista alla raderade revisioner av en användare or inom en namnrymd.", @@ -306,6 +309,7 @@ "apihelp-query+alldeletedrevisions-example-ns-main": "Lista dem första 50 revideringarna i huvud-namnrymden", "apihelp-query+allfileusages-summary": "Lista all fil användningsområden, inklusive icke-existerande.", "apihelp-query+allfileusages-param-prefix": "Sök för all fil-titlar som börjar med detta värde.", + "apihelp-query+allfileusages-paramvalue-prop-title": "Lägger till filens titel.", "apihelp-query+allfileusages-param-limit": "Hur många saker att returnera totalt.", "apihelp-query+allfileusages-param-dir": "Riktningen att lista mot.", "apihelp-query+allfileusages-example-unique": "Lista unika filtitlar", @@ -399,6 +403,7 @@ "apihelp-query+categorymembers-summary": "Lista alla sidor i en angiven kategori.", "apihelp-query+categorymembers-paramvalue-prop-ids": "Lägger till sid-ID.", "apihelp-query+categorymembers-paramvalue-prop-title": "Lägger till titeln och namnrymds-ID för sidan.", + "apihelp-query+categorymembers-param-sort": "Egenskap att sortera efter.", "apihelp-query+categorymembers-param-dir": "I vilken riktning att sortera.", "apihelp-query+categorymembers-param-startsortkey": "Använd $1starthexsortkey istället.", "apihelp-query+categorymembers-param-endsortkey": "Använd $1endhexsortkey istället.", @@ -420,18 +425,29 @@ "apihelp-query+embeddedin-summary": "Hitta alla sidor som bäddar in (inkluderar) angiven titel.", "apihelp-query+embeddedin-param-dir": "Riktningen att lista mot.", "apihelp-query+embeddedin-param-limit": "Hur många sidor att returnera totalt.", + "apihelp-query+extlinks-param-limit": "Hur många länkar som ska returneras.", "apihelp-query+extlinks-example-simple": "Hämta en lista över externa länkar på Main Page.", + "apihelp-query+exturlusage-param-limit": "Hur många sidor att returnera.", + "apihelp-query+filearchive-param-limit": "Hur många bilder att returnera totalt.", "apihelp-query+filearchive-param-dir": "Riktningen att lista mot.", "apihelp-query+filearchive-paramvalue-prop-timestamp": "Lägger till tidsstämpel för den uppladdade versionen.", "apihelp-query+filearchive-paramvalue-prop-user": "Lägger till användaren som laddade upp bildversionen.", + "apihelp-query+filearchive-paramvalue-prop-dimensions": "Alias för storlek.", + "apihelp-query+filearchive-paramvalue-prop-description": "Lägger till beskrivning till bildversionen.", "apihelp-query+filearchive-example-simple": "Visa en lista över alla borttagna filer.", "apihelp-query+filerepoinfo-summary": "Returnera metainformation om bildegenskaper som konfigureras på wikin.", "apihelp-query+fileusage-summary": "Hitta alla sidor som använder angivna filer.", + "apihelp-query+fileusage-param-prop": "Vilka egenskaper att hämta:", "apihelp-query+fileusage-paramvalue-prop-title": "Titel för varje sida.", "apihelp-query+fileusage-paramvalue-prop-redirect": "Flagga om sidan är en omdirigering.", "apihelp-query+imageinfo-summary": "Returnerar filinformation och uppladdningshistorik.", + "apihelp-query+imageinfo-param-prop": "Vilka filinformation att hämta:", + "apihelp-query+imageinfo-paramvalue-prop-timestamp": "Lägger till tidsstämpel för den uppladdade versionen.", "apihelp-query+imageinfo-paramvalue-prop-userid": "Lägg till det användar-ID som laddade upp varje filversion.", + "apihelp-query+imageinfo-paramvalue-prop-dimensions": "Alias för storlek.", + "apihelp-query+images-param-limit": "Hur många filer att returnera.", "apihelp-query+images-param-dir": "Riktningen att lista mot.", + "apihelp-query+images-example-simple": "Hämta en lista över filer som används på [[Main Page]].", "apihelp-query+imageusage-summary": "Hitta alla sidor som användare angiven bildtitel.", "apihelp-query+imageusage-param-dir": "Riktningen att lista mot.", "apihelp-query+imageusage-example-simple": "Visa sidor med hjälp av [[:File:Albert Einstein Head.jpg]].", @@ -521,10 +537,14 @@ "api-help-param-limit": "Inte mer än $1 tillåts.", "api-help-param-limit2": "Inte mer än $1 ($2 för robotar) tillåts.", "api-help-param-multi-separate": "Separera värden med | eller [[Special:ApiHelp/main#main/datatypes|alternativ]].", + "api-help-param-maxbytes": "Kan inte vara längre än $1 {{PLURAL:$1|byte}}.", + "api-help-param-maxchars": "Kan inte vara längre än $1 {{PLURAL:$1|tecken}}.", "apierror-articleexists": "Artikeln du försökte skapa har redan skapats.", "apierror-baddiff": "Diff kan inte hämtas. En eller båda sidversioner finns inte eller du har inte behörighet för att visa dem.", "apierror-invalidoldimage": "Parametern oldimage har ett ogiltigt format.", "apierror-invalidsection": "Parametern section måste vara ett giltigt avsnitts-ID eller new.", + "apierror-maxbytes": "Parametern $1 kan inte var längre än $2 {{PLURAL:$2|byte}}", + "apierror-maxchars": "Parametern $1 kan inte vara längre än $2 {{PLURAL:$2|tecken}}", "apierror-nosuchuserid": "Det finns ingen användare med ID $1.", "apierror-offline": "Kunde inte fortsätta p.g.a. problem med nätverksanslutningen. Se till att du har en fungerande Internetanslutning och försök igen.", "apierror-protect-invalidaction": "Ogiltig skyddstyp \"$1\".", diff --git a/includes/api/i18n/zh-hans.json b/includes/api/i18n/zh-hans.json index d09c65110d..2fb6178b5d 100644 --- a/includes/api/i18n/zh-hans.json +++ b/includes/api/i18n/zh-hans.json @@ -249,6 +249,8 @@ "apihelp-import-extended-description": "注意当发送用于xml参数的文件时,HTTP POST必须作为文件上传完成(即使用multipart/form-data)", "apihelp-import-param-summary": "日志记录导入摘要。", "apihelp-import-param-xml": "上传的XML文件。", + "apihelp-import-param-interwikiprefix": "对于上传导入:要应用到位置用户名的跨wiki前缀(如果设置了$1assignknownusers的话,则也包含已知用户)。", + "apihelp-import-param-assignknownusers": "分配编辑至本地用户,只要命名用户存在于本地。", "apihelp-import-param-interwikisource": "用于跨wiki导入:导入的来源wiki。", "apihelp-import-param-interwikipage": "用于跨wiki导入:导入的页面。", "apihelp-import-param-fullhistory": "用于跨wiki导入:完整导入历史,而不只是最新版本。", @@ -1499,6 +1501,8 @@ "api-help-param-direction": "列举的方向:\n;newer:最早的优先。注意:$1start应早于$1end。\n;older:最新的优先(默认)。注意:$1start应晚于$1end。", "api-help-param-continue": "当更多结果可用时,使用这个继续。", "api-help-param-no-description": "(没有说明)", + "api-help-param-maxbytes": "不能超过$1{{PLURAL:$1|字节}}。", + "api-help-param-maxchars": "不能超过$1个{{PLURAL:$1|字符}}。", "api-help-examples": "{{PLURAL:$1|例子}}:", "api-help-permissions": "{{PLURAL:$1|权限}}:", "api-help-permissions-granted-to": "{{PLURAL:$1|授予}}:$2", @@ -1601,6 +1605,8 @@ "apierror-invalidurlparam": "$1urlparam的值无效($2=$3)。", "apierror-invaliduser": "无效用户名“$1”。", "apierror-invaliduserid": "用户ID$1无效。", + "apierror-maxbytes": "参数$1不能超过$2{{PLURAL:$2|字节}}", + "apierror-maxchars": "参数$1不能超过$2个{{PLURAL:$2|字符}}", "apierror-maxlag-generic": "正在等待数据库服务器:已延迟$1{{PLURAL:$1|秒}}。", "apierror-maxlag": "正在等待$2:已延迟$1{{PLURAL:$1|秒}}。", "apierror-mimesearchdisabled": "MIME搜索在Miser模式中被禁用。", diff --git a/includes/api/i18n/zh-hant.json b/includes/api/i18n/zh-hant.json index ea5f2dd98e..75baaaabf0 100644 --- a/includes/api/i18n/zh-hant.json +++ b/includes/api/i18n/zh-hant.json @@ -283,6 +283,7 @@ "apihelp-xml-summary": "使用 XML 格式輸出資料。", "apihelp-xmlfm-summary": "使用 XML 格式輸出資料 (使用 HTML 格式顯示)。", "api-format-title": "MediaWiki API 結果", + "api-format-prettyprint-header": "這是$1格式的HTML呈現。HTML適合用於除錯,但不適合應用程式使用。\n\n指定format參數以更改輸出格式。要檢視$1格式的非HTML呈現,設定format=$2。\n\n參考 [[mw:Special:MyLanguage/API|完整說明文件]] 或 [[Special:ApiHelp/main|API說明]] 以取得更多資訊。", "api-pageset-param-titles": "要使用的標題清單。", "api-pageset-param-pageids": "要使用的頁面 ID 清單。", "api-pageset-param-revids": "要使用的修訂 ID 清單。", diff --git a/includes/changes/ChangesFeed.php b/includes/changes/ChangesFeed.php index df964e0a2b..7ac8cd0ed0 100644 --- a/includes/changes/ChangesFeed.php +++ b/includes/changes/ChangesFeed.php @@ -82,10 +82,11 @@ class ChangesFeed { return null; } + $cache = ObjectCache::getMainWANInstance(); $optionsHash = md5( serialize( $opts->getAllValues() ) ) . $wgRenderHashAppend; - $timekey = wfMemcKey( + $timekey = $cache->makeKey( $this->type, $this->format, $wgLang->getCode(), $optionsHash, 'timestamp' ); - $key = wfMemcKey( $this->type, $this->format, $wgLang->getCode(), $optionsHash ); + $key = $cache->makeKey( $this->type, $this->format, $wgLang->getCode(), $optionsHash ); FeedUtils::checkPurge( $timekey, $key ); diff --git a/includes/changetags/ChangeTags.php b/includes/changetags/ChangeTags.php index fa981247c2..b4a8ca8028 100644 --- a/includes/changetags/ChangeTags.php +++ b/includes/changetags/ChangeTags.php @@ -32,10 +32,44 @@ class ChangeTags { */ const MAX_DELETE_USES = 5000; + private static $definedSoftwareTags = [ + 'mw-contentmodelchange', + 'mw-new-redirect', + 'mw-removed-redirect', + 'mw-changed-redirect-target', + 'mw-blank', + 'mw-replace', + 'mw-rollback' + ]; + /** - * @var string[] + * Loads defined core tags, checks for invalid types (if not array), + * and filters for supported and enabled (if $all is false) tags only. + * + * @param bool $all If true, return all valid defined tags. Otherwise, return only enabled ones. + * @return array Array of all defined/enabled tags. */ - private static $coreTags = [ 'mw-contentmodelchange' ]; + public static function getSoftwareTags( $all = false ) { + global $wgSoftwareTags; + $softwareTags = []; + + if ( !is_array( $wgSoftwareTags ) ) { + wfWarn( 'wgSoftwareTags should be associative array of enabled tags. + Please refer to documentation for the list of tags you can enable' ); + return $softwareTags; + } + + $availableSoftwareTags = !$all ? + array_keys( array_filter( $wgSoftwareTags ) ) : + array_keys( $wgSoftwareTags ); + + $softwareTags = array_intersect( + $availableSoftwareTags, + self::$definedSoftwareTags + ); + + return $softwareTags; + } /** * Creates HTML for the given tags @@ -1210,7 +1244,7 @@ class ChangeTags { */ public static function listSoftwareActivatedTags() { // core active tags - $tags = self::$coreTags; + $tags = self::getSoftwareTags(); if ( !Hooks::isRegistered( 'ChangeTagsListActive' ) ) { return $tags; } @@ -1301,7 +1335,7 @@ class ChangeTags { */ public static function listSoftwareDefinedTags() { // core defined tags - $tags = self::$coreTags; + $tags = self::getSoftwareTags( true ); if ( !Hooks::isRegistered( 'ListDefinedTags' ) ) { return $tags; } diff --git a/includes/collation/Collation.php b/includes/collation/Collation.php index d009168d5e..7171a218ae 100644 --- a/includes/collation/Collation.php +++ b/includes/collation/Collation.php @@ -67,6 +67,8 @@ abstract class Collation { return new CollationFa; case 'uppercase-ba': return new BashkirUppercaseCollation; + case 'uppercase-se': + return new NorthernSamiUppercaseCollation; default: $match = []; if ( preg_match( '/^uca-([A-Za-z@=-]+)$/', $collationName, $match ) ) { diff --git a/includes/collation/NorthernSamiUppercaseCollation.php b/includes/collation/NorthernSamiUppercaseCollation.php new file mode 100644 index 0000000000..d373749e54 --- /dev/null +++ b/includes/collation/NorthernSamiUppercaseCollation.php @@ -0,0 +1,83 @@ +getRedirectTarget() : null; + $newTarget = $newContent !== null ? $newContent->getRedirectTarget() : null; + + // We check for the type of change in the given edit, and return string key accordingly + + // Blanking of a page + if ( $oldContent && $oldContent->getSize() > 0 && + $newContent && $newContent->getSize() === 0 + ) { + return 'blank'; + } + + // Redirects + if ( $newTarget ) { + if ( !$oldTarget ) { + // New redirect page (by creating new page or by changing content page) + return 'new-redirect'; + } elseif ( !$newTarget->equals( $oldTarget ) || + $oldTarget->getFragment() !== $newTarget->getFragment() + ) { + // Redirect target changed + return 'changed-redirect-target'; + } + } elseif ( $oldTarget ) { + // Changing an existing redirect into a non-redirect + return 'removed-redirect'; + } + + // New page created + if ( $flags & EDIT_NEW && $newContent && $newContent->getSize() > 0 ) { + return 'newpage'; + } + + // New blank page + if ( $flags & EDIT_NEW && $newContent && $newContent->getSize() === 0 ) { + return 'newblank'; + } + + // Removing more than 90% of the page + if ( $oldContent && $newContent && $oldContent->getSize() > 10 * $newContent->getSize() ) { + return 'replace'; + } + + // Content model changed + if ( $oldContent && $newContent && $oldContent->getModel() !== $newContent->getModel() ) { + return 'contentmodelchange'; + } + + return null; + } + /** * Return an applicable auto-summary if one exists for the given edit. * * @since 1.21 * - * @param Content $oldContent The previous text of the page. - * @param Content $newContent The submitted text of the page. + * @param Content|null $oldContent The previous text of the page. + * @param Content|null $newContent The submitted text of the page. * @param int $flags Bit mask: a bit mask of flags submitted for the edit. * * @return string An appropriate auto-summary, or an empty string. */ - public function getAutosummary( Content $oldContent = null, Content $newContent = null, - $flags ) { - // Decide what kind of auto-summary is needed. - - // Redirect auto-summaries - - /** - * @var $ot Title - * @var $rt Title - */ + public function getAutosummary( + Content $oldContent = null, + Content $newContent = null, + $flags = 0 + ) { + $changeType = $this->getChangeType( $oldContent, $newContent, $flags ); - $ot = !is_null( $oldContent ) ? $oldContent->getRedirectTarget() : null; - $rt = !is_null( $newContent ) ? $newContent->getRedirectTarget() : null; + // There's no applicable auto-summary for our case, so our auto-summary is empty. + if ( !$changeType ) { + return ''; + } - if ( is_object( $rt ) ) { - if ( !is_object( $ot ) - || !$rt->equals( $ot ) - || $ot->getFragment() != $rt->getFragment() - ) { + // Decide what kind of auto-summary is needed. + switch ( $changeType ) { + case 'new-redirect': + $newTarget = $newContent->getRedirectTarget(); $truncatedtext = $newContent->getTextForSummary( 250 - strlen( wfMessage( 'autoredircomment' )->inContentLanguage()->text() ) - - strlen( $rt->getFullText() ) ); + - strlen( $newTarget->getFullText() ) + ); - return wfMessage( 'autoredircomment', $rt->getFullText() ) - ->rawParams( $truncatedtext )->inContentLanguage()->text(); - } - } + return wfMessage( 'autoredircomment', $newTarget->getFullText() ) + ->plaintextParams( $truncatedtext )->inContentLanguage()->text(); + case 'changed-redirect-target': + $oldTarget = $oldContent->getRedirectTarget(); + $newTarget = $newContent->getRedirectTarget(); - // New page auto-summaries - if ( $flags & EDIT_NEW && $newContent->getSize() > 0 ) { - // If they're making a new article, give its text, truncated, in - // the summary. + $truncatedtext = $newContent->getTextForSummary( + 250 + - strlen( wfMessage( 'autosumm-changed-redirect-target' ) + ->inContentLanguage()->text() ) + - strlen( $oldTarget->getFullText() ) + - strlen( $newTarget->getFullText() ) + ); + + return wfMessage( 'autosumm-changed-redirect-target', + $oldTarget->getFullText(), + $newTarget->getFullText() ) + ->rawParams( $truncatedtext )->inContentLanguage()->text(); + case 'removed-redirect': + $oldTarget = $oldContent->getRedirectTarget(); + $truncatedtext = $newContent->getTextForSummary( + 250 + - strlen( wfMessage( 'autosumm-removed-redirect' ) + ->inContentLanguage()->text() ) + - strlen( $oldTarget->getFullText() ) ); - $truncatedtext = $newContent->getTextForSummary( - 200 - strlen( wfMessage( 'autosumm-new' )->inContentLanguage()->text() ) ); + return wfMessage( 'autosumm-removed-redirect', $oldTarget->getFullText() ) + ->rawParams( $truncatedtext )->inContentLanguage()->text(); + case 'newpage': + // If they're making a new article, give its text, truncated, in the summary. + $truncatedtext = $newContent->getTextForSummary( + 200 - strlen( wfMessage( 'autosumm-new' )->inContentLanguage()->text() ) ); - return wfMessage( 'autosumm-new' )->rawParams( $truncatedtext ) - ->inContentLanguage()->text(); + return wfMessage( 'autosumm-new' )->rawParams( $truncatedtext ) + ->inContentLanguage()->text(); + case 'blank': + return wfMessage( 'autosumm-blank' )->inContentLanguage()->text(); + case 'replace': + $truncatedtext = $newContent->getTextForSummary( + 200 - strlen( wfMessage( 'autosumm-replace' )->inContentLanguage()->text() ) ); + + return wfMessage( 'autosumm-replace' )->rawParams( $truncatedtext ) + ->inContentLanguage()->text(); + case 'newblank': + return wfMessage( 'autosumm-newblank' )->inContentLanguage()->text(); + default: + return ''; } + } - // Blanking auto-summaries - if ( !empty( $oldContent ) && $oldContent->getSize() > 0 && $newContent->getSize() == 0 ) { - return wfMessage( 'autosumm-blank' )->inContentLanguage()->text(); - } elseif ( !empty( $oldContent ) - && $oldContent->getSize() > 10 * $newContent->getSize() - && $newContent->getSize() < 500 - ) { - // Removing more than 90% of the article - - $truncatedtext = $newContent->getTextForSummary( - 200 - strlen( wfMessage( 'autosumm-replace' )->inContentLanguage()->text() ) ); + /** + * Return an applicable tag if one exists for the given edit or return null. + * + * @since 1.31 + * + * @param Content|null $oldContent The previous text of the page. + * @param Content|null $newContent The submitted text of the page. + * @param int $flags Bit mask: a bit mask of flags submitted for the edit. + * + * @return string|null An appropriate tag, or null. + */ + public function getChangeTag( + Content $oldContent = null, + Content $newContent = null, + $flags = 0 + ) { + $changeType = $this->getChangeType( $oldContent, $newContent, $flags ); - return wfMessage( 'autosumm-replace' )->rawParams( $truncatedtext ) - ->inContentLanguage()->text(); + // There's no applicable tag for this change. + if ( !$changeType ) { + return null; } - // New blank article auto-summary - if ( $flags & EDIT_NEW && $newContent->isEmpty() ) { - return wfMessage( 'autosumm-newblank' )->inContentLanguage()->text(); + // Core tags use the same keys as ones returned from $this->getChangeType() + // but prefixed with pseudo namespace 'mw-', so we add the prefix before checking + // if this type of change should be tagged + $tag = 'mw-' . $changeType; + + // Not all change types are tagged, so we check against the list of defined tags. + if ( in_array( $tag, ChangeTags::getSoftwareTags() ) ) { + return $tag; } - // If we reach this point, there's no applicable auto-summary for our - // case, so our auto-summary is empty. - return ''; + return null; } /** diff --git a/includes/content/WikiTextStructure.php b/includes/content/WikiTextStructure.php index aeb96b6531..0eadc3c67a 100644 --- a/includes/content/WikiTextStructure.php +++ b/includes/content/WikiTextStructure.php @@ -146,9 +146,10 @@ class WikiTextStructure { if ( !is_null( $this->allText ) ) { return; } - $this->parserOutput->setEditSectionTokens( false ); - $this->parserOutput->setTOCEnabled( false ); - $text = $this->parserOutput->getText(); + $text = $this->parserOutput->getText( [ + 'enableSectionEditTokens' => false, + 'allowTOC' => false, + ] ); if ( strlen( $text ) == 0 ) { $this->allText = ""; // empty text - nothing to seek here diff --git a/includes/content/WikitextContent.php b/includes/content/WikitextContent.php index 942390f68f..bc20aa0020 100644 --- a/includes/content/WikitextContent.php +++ b/includes/content/WikitextContent.php @@ -87,7 +87,7 @@ class WikitextContent extends TextContent { if ( $sectionId === 'new' ) { # Inserting a new section $subject = $sectionTitle ? wfMessage( 'newsectionheaderdefaultlevel' ) - ->rawParams( $sectionTitle )->inContentLanguage()->text() . "\n\n" : ''; + ->plaintextParams( $sectionTitle )->inContentLanguage()->text() . "\n\n" : ''; if ( Hooks::run( 'PlaceNewSection', [ $this, $oldtext, $subject, &$text ] ) ) { $text = strlen( trim( $oldtext ) ) > 0 ? "{$oldtext}\n\n{$subject}{$text}" diff --git a/includes/context/RequestContext.php b/includes/context/RequestContext.php index 7cabd405cc..c2d0de1dc9 100644 --- a/includes/context/RequestContext.php +++ b/includes/context/RequestContext.php @@ -308,7 +308,6 @@ class RequestContext implements IContextSource, MutableContext { # Validate $code if ( !$code || !Language::isValidCode( $code ) || $code === 'qqq' ) { - wfDebug( "Invalid user language code\n" ); $code = $wgLanguageCode; } diff --git a/includes/db/CloneDatabase.php b/includes/db/CloneDatabase.php index 3d22c037ae..16d10d1d54 100644 --- a/includes/db/CloneDatabase.php +++ b/includes/db/CloneDatabase.php @@ -2,9 +2,6 @@ /** * Helper class for making a copy of the database, mostly for unit testing. * - * Copyright © 2010 Chad Horohoe - * https://www.mediawiki.org/ - * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or diff --git a/includes/deferred/CdnCacheUpdate.php b/includes/deferred/CdnCacheUpdate.php index 7fafc0ebca..301c4f3b7e 100644 --- a/includes/deferred/CdnCacheUpdate.php +++ b/includes/deferred/CdnCacheUpdate.php @@ -18,7 +18,6 @@ * http://www.gnu.org/copyleft/gpl.html * * @file - * @ingroup Cache */ use Wikimedia\Assert\Assert; diff --git a/includes/diff/DifferenceEngine.php b/includes/diff/DifferenceEngine.php index 51b9f15a60..7e05be6675 100644 --- a/includes/diff/DifferenceEngine.php +++ b/includes/diff/DifferenceEngine.php @@ -634,7 +634,10 @@ class DifferenceEngine extends ContextSource { if ( Hooks::run( 'DifferenceEngineRenderRevisionAddParserOutput', [ $this, $out, $parserOutput, $wikiPage ] ) ) { - $out->addParserOutput( $parserOutput ); + $out->addParserOutput( $parserOutput, [ + 'enableSectionEditLinks' => $this->mNewRev->isCurrent() + && $this->mNewRev->getTitle()->quickUserCan( 'edit', $this->getUser() ), + ] ); } } } diff --git a/includes/filebackend/FileBackendGroup.php b/includes/filebackend/FileBackendGroup.php index 5d0da6d32a..0b61979409 100644 --- a/includes/filebackend/FileBackendGroup.php +++ b/includes/filebackend/FileBackendGroup.php @@ -229,7 +229,7 @@ class FileBackendGroup { * @since 1.27 */ public function guessMimeInternal( $storagePath, $content, $fsPath ) { - $magic = MimeMagic::singleton(); + $magic = MediaWiki\MediaWikiServices::getInstance()->getMimeAnalyzer(); // Trust the extension of the storage path (caller must validate) $ext = FileBackend::extensionFromPath( $storagePath ); $type = $magic->guessTypesForExtension( $ext ); diff --git a/includes/filerepo/FileRepo.php b/includes/filerepo/FileRepo.php index 5005280732..5d22b8d80d 100644 --- a/includes/filerepo/FileRepo.php +++ b/includes/filerepo/FileRepo.php @@ -1543,7 +1543,7 @@ class FileRepo { */ public function getFileProps( $virtualUrl ) { $fsFile = $this->getLocalReference( $virtualUrl ); - $mwProps = new MWFileProps( MimeMagic::singleton() ); + $mwProps = new MWFileProps( MediaWiki\MediaWikiServices::getInstance()->getMimeAnalyzer() ); if ( $fsFile ) { $props = $mwProps->getPropsFromPath( $fsFile->getPath(), true ); } else { diff --git a/includes/filerepo/RepoGroup.php b/includes/filerepo/RepoGroup.php index 2edd6d0914..5e37d676ef 100644 --- a/includes/filerepo/RepoGroup.php +++ b/includes/filerepo/RepoGroup.php @@ -452,7 +452,7 @@ class RepoGroup { return $repo->getFileProps( $fileName ); } else { - $mwProps = new MWFileProps( MimeMagic::singleton() ); + $mwProps = new MWFileProps( MediaWiki\MediaWikiServices::getInstance()->getMimeAnalyzer() ); return $mwProps->getPropsFromPath( $fileName, true ); } diff --git a/includes/filerepo/file/File.php b/includes/filerepo/file/File.php index 827f4caa8f..4e79de2d56 100644 --- a/includes/filerepo/file/File.php +++ b/includes/filerepo/file/File.php @@ -250,7 +250,7 @@ abstract class File implements IDBAccessObject { $oldMime = $old->getMimeType(); $n = strrpos( $new, '.' ); $newExt = self::normalizeExtension( $n ? substr( $new, $n + 1 ) : '' ); - $mimeMagic = MimeMagic::singleton(); + $mimeMagic = MediaWiki\MediaWikiServices::getInstance()->getMimeAnalyzer(); return $mimeMagic->isMatchingExtension( $newExt, $oldMime ); } diff --git a/includes/filerepo/file/ForeignAPIFile.php b/includes/filerepo/file/ForeignAPIFile.php index 43b6855f82..16c154f788 100644 --- a/includes/filerepo/file/ForeignAPIFile.php +++ b/includes/filerepo/file/ForeignAPIFile.php @@ -286,7 +286,7 @@ class ForeignAPIFile extends File { */ function getMimeType() { if ( !isset( $this->mInfo['mime'] ) ) { - $magic = MimeMagic::singleton(); + $magic = MediaWiki\MediaWikiServices::getInstance()->getMimeAnalyzer(); $this->mInfo['mime'] = $magic->guessTypesForExtension( $this->getExtension() ); } @@ -300,7 +300,7 @@ class ForeignAPIFile extends File { if ( isset( $this->mInfo['mediatype'] ) ) { return $this->mInfo['mediatype']; } - $magic = MimeMagic::singleton(); + $magic = MediaWiki\MediaWikiServices::getInstance()->getMimeAnalyzer(); return $magic->getMediaType( null, $this->getMimeType() ); } diff --git a/includes/filerepo/file/LocalFile.php b/includes/filerepo/file/LocalFile.php index bb12515056..4248f953e1 100644 --- a/includes/filerepo/file/LocalFile.php +++ b/includes/filerepo/file/LocalFile.php @@ -1284,7 +1284,7 @@ class LocalFile extends File { ) { $props = $this->repo->getFileProps( $srcPath ); } else { - $mwProps = new MWFileProps( MimeMagic::singleton() ); + $mwProps = new MWFileProps( MediaWiki\MediaWikiServices::getInstance()->getMimeAnalyzer() ); $props = $mwProps->getPropsFromPath( $srcPath, true ); } } diff --git a/includes/filerepo/file/UnregisteredLocalFile.php b/includes/filerepo/file/UnregisteredLocalFile.php index cdad5fcec8..fde68bbe74 100644 --- a/includes/filerepo/file/UnregisteredLocalFile.php +++ b/includes/filerepo/file/UnregisteredLocalFile.php @@ -151,7 +151,7 @@ class UnregisteredLocalFile extends File { */ function getMimeType() { if ( !isset( $this->mime ) ) { - $magic = MimeMagic::singleton(); + $magic = MediaWiki\MediaWikiServices::getInstance()->getMimeAnalyzer(); $this->mime = $magic->guessMimeType( $this->getLocalRefPath() ); } diff --git a/includes/gallery/PackedOverlayImageGallery.php b/includes/gallery/PackedOverlayImageGallery.php index db8ce68b9a..0a5a457fea 100644 --- a/includes/gallery/PackedOverlayImageGallery.php +++ b/includes/gallery/PackedOverlayImageGallery.php @@ -1,8 +1,5 @@ [ $widget ], - 'label' => $sectionLabel, + 'label' => new OOUI\HtmlSnippet( $sectionLabel ), ] ); } else { $out[] = $widget; diff --git a/includes/htmlform/fields/HTMLTextAreaField.php b/includes/htmlform/fields/HTMLTextAreaField.php index e6963d5cde..466a2511a3 100644 --- a/includes/htmlform/fields/HTMLTextAreaField.php +++ b/includes/htmlform/fields/HTMLTextAreaField.php @@ -16,7 +16,7 @@ class HTMLTextAreaField extends HTMLFormField { parent::__construct( $params ); if ( isset( $params['placeholder-message'] ) ) { - $this->mPlaceholder = $this->getMessage( $params['placeholder-message'] )->parse(); + $this->mPlaceholder = $this->getMessage( $params['placeholder-message'] )->text(); } elseif ( isset( $params['placeholder'] ) ) { $this->mPlaceholder = $params['placeholder']; } diff --git a/includes/htmlform/fields/HTMLTextField.php b/includes/htmlform/fields/HTMLTextField.php index 1c5a43ddad..b2e4f2a559 100644 --- a/includes/htmlform/fields/HTMLTextField.php +++ b/includes/htmlform/fields/HTMLTextField.php @@ -31,7 +31,7 @@ class HTMLTextField extends HTMLFormField { parent::__construct( $params ); if ( isset( $params['placeholder-message'] ) ) { - $this->mPlaceholder = $this->getMessage( $params['placeholder-message'] )->parse(); + $this->mPlaceholder = $this->getMessage( $params['placeholder-message'] )->text(); } elseif ( isset( $params['placeholder'] ) ) { $this->mPlaceholder = $params['placeholder']; } diff --git a/includes/import/WikiImporter.php b/includes/import/WikiImporter.php index a1f7e0c002..1424f33637 100644 --- a/includes/import/WikiImporter.php +++ b/includes/import/WikiImporter.php @@ -47,6 +47,9 @@ class WikiImporter { private $countableCache = []; /** @var bool */ private $disableStatisticsUpdate = false; + private $usernamePrefix = 'imported'; + private $assignKnownUsers = false; + private $triedCreations = []; /** * Creates an ImportXMLReader drawing from the source provided @@ -311,6 +314,16 @@ class WikiImporter { $this->mImportUploads = $import; } + /** + * @since 1.31 + * @param string $usernamePrefix Prefix to apply to unknown (and possibly also known) usernames + * @param bool $assignKnownUsers Whether to apply the prefix to usernames that exist locally + */ + public function setUsernamePrefix( $usernamePrefix, $assignKnownUsers ) { + $this->usernamePrefix = rtrim( (string)$usernamePrefix, ':>' ); + $this->assignKnownUsers = (bool)$assignKnownUsers; + } + /** * Statistics update can cause a lot of time * @since 1.29 @@ -546,6 +559,7 @@ class WikiImporter { /** * Primary entry point + * @throws Exception * @throws MWException * @return bool */ @@ -716,9 +730,9 @@ class WikiImporter { } if ( !isset( $logInfo['contributor']['username'] ) ) { - $revision->setUsername( 'Unknown user' ); + $revision->setUsername( $this->usernamePrefix . '>Unknown user' ); } else { - $revision->setUsername( $logInfo['contributor']['username'] ); + $revision->setUsername( $this->prefixUsername( $logInfo['contributor']['username'] ) ); } return $this->logItemCallback( $revision ); @@ -847,6 +861,7 @@ class WikiImporter { /** * @param array $pageInfo * @param array $revisionInfo + * @throws MWException * @return bool|mixed */ private function processRevision( $pageInfo, $revisionInfo ) { @@ -911,9 +926,9 @@ class WikiImporter { if ( isset( $revisionInfo['contributor']['ip'] ) ) { $revision->setUserIP( $revisionInfo['contributor']['ip'] ); } elseif ( isset( $revisionInfo['contributor']['username'] ) ) { - $revision->setUsername( $revisionInfo['contributor']['username'] ); + $revision->setUsername( $this->prefixUsername( $revisionInfo['contributor']['username'] ) ); } else { - $revision->setUsername( 'Unknown user' ); + $revision->setUsername( $this->usernamePrefix . '>Unknown user' ); } if ( isset( $revisionInfo['sha1'] ) ) { $revision->setSha1Base36( $revisionInfo['sha1'] ); @@ -1020,13 +1035,43 @@ class WikiImporter { $revision->setUserIP( $uploadInfo['contributor']['ip'] ); } if ( isset( $uploadInfo['contributor']['username'] ) ) { - $revision->setUsername( $uploadInfo['contributor']['username'] ); + $revision->setUsername( $this->prefixUsername( $uploadInfo['contributor']['username'] ) ); } $revision->setNoUpdates( $this->mNoUpdates ); return call_user_func( $this->mUploadCallback, $revision ); } + /** + * Add an interwiki prefix to the username, if appropriate + * @since 1.31 + * @param string $name Name being imported + * @return string Name, possibly with the prefix prepended. + */ + protected function prefixUsername( $name ) { + if ( !User::isUsableName( $name ) ) { + return $name; + } + + if ( $this->assignKnownUsers ) { + if ( User::idFromName( $name ) ) { + return $name; + } + + // See if any extension wants to create it. + if ( !isset( $this->triedCreations[$name] ) ) { + $this->triedCreations[$name] = true; + if ( !Hooks::run( 'ImportHandleUnknownUser', [ $name ] ) && + User::idFromName( $name, User::READ_LATEST ) + ) { + return $name; + } + } + } + + return substr( $this->usernamePrefix . '>' . $name, 0, 255 ); + } + /** * @return array */ diff --git a/includes/installer/InstallDocFormatter.php b/includes/installer/InstallDocFormatter.php index 4163e2f958..08cfd8689a 100644 --- a/includes/installer/InstallDocFormatter.php +++ b/includes/installer/InstallDocFormatter.php @@ -21,7 +21,7 @@ */ class InstallDocFormatter { - static function format( $text ) { + public static function format( $text ) { $obj = new self( $text ); return $obj->execute(); diff --git a/includes/installer/Installer.php b/includes/installer/Installer.php index e99ea7c049..46978e1ba1 100644 --- a/includes/installer/Installer.php +++ b/includes/installer/Installer.php @@ -688,7 +688,9 @@ abstract class Installer { try { $out = $wgParser->parse( $text, $this->parserTitle, $this->parserOptions, $lineStart ); - $html = $out->getText(); + $html = $out->getText( [ + 'enableSectionEditLinks' => false, + ] ); } catch ( MediaWiki\Services\ServiceDisabledException $e ) { $html = ' ' . htmlspecialchars( $text ); @@ -1485,6 +1487,11 @@ abstract class Installer { } } if ( $status->isOk() ) { + $this->showMessage( + 'config-install-success', + $this->getVar( 'wgServer' ), + $this->getVar( 'wgScriptPath' ) + ); $this->setVar( '_InstallDone', true ); } diff --git a/includes/installer/LocalSettingsGenerator.php b/includes/installer/LocalSettingsGenerator.php index bdaeaca86c..b4ef49d7c6 100644 --- a/includes/installer/LocalSettingsGenerator.php +++ b/includes/installer/LocalSettingsGenerator.php @@ -185,7 +185,7 @@ class LocalSettingsGenerator { $jsonFile = 'skin.json'; $function = 'wfLoadSkin'; } else { - throw new InvalidArgumentException( '$dir was not "extensions" or "skins' ); + throw new InvalidArgumentException( '$dir was not "extensions" or "skins"' ); } $encName = self::escapePhpString( $name ); diff --git a/includes/installer/PhpBugTests.php b/includes/installer/PhpBugTests.php index d412216aaa..4e1e365dbf 100644 --- a/includes/installer/PhpBugTests.php +++ b/includes/installer/PhpBugTests.php @@ -18,9 +18,11 @@ * http://www.gnu.org/copyleft/gpl.html * * @file - * @defgroup PHPBugTests PHP known bugs tests */ +/** + * @defgroup PHPBugTests PHP known bugs tests + */ /** * Test for PHP+libxml2 bug which breaks XML input subtly with certain versions. * Known fixed with PHP 5.2.9 + libxml2-2.7.3 diff --git a/includes/installer/PostgresUpdater.php b/includes/installer/PostgresUpdater.php index 91f569f0be..c38eb6aabc 100644 --- a/includes/installer/PostgresUpdater.php +++ b/includes/installer/PostgresUpdater.php @@ -482,6 +482,7 @@ class PostgresUpdater extends DatabaseUpdater { [ 'addPgField', 'protected_titles', 'pt_reason_id', 'INTEGER NOT NULL DEFAULT 0' ], [ 'addTable', 'comment', 'patch-comment-table.sql' ], [ 'addIndex', 'site_stats', 'site_stats_pkey', 'patch-site_stats-pk.sql' ], + [ 'addTable', 'ip_changes', 'patch-ip_changes.sql' ], ]; } diff --git a/includes/installer/i18n/be-tarask.json b/includes/installer/i18n/be-tarask.json index b427c19566..ffcbe39433 100644 --- a/includes/installer/i18n/be-tarask.json +++ b/includes/installer/i18n/be-tarask.json @@ -312,6 +312,7 @@ "config-install-mainpage-failed": "Немагчыма ўставіць галоўную старонку: $1", "config-install-done": "Віншуем!\nВы ўсталявалі MediaWiki.\n\nПраграма ўсталяваньня стварыла файл LocalSettings.php.\nЁн утрымлівае ўсе Вашыя налады.\n\nВам неабходна загрузіць яго і захаваць у карэнную дырэкторыю Вашай вікі (у тую ж самую дырэкторыю, дзе знаходзіцца index.php). Загрузка павінна пачацца аўтаматычна.\n\nКалі загрузка не пачалася, ці Вы яе адмянілі, Вы можаце перазапусьціць яе націснуўшы на спасылку ніжэй:\n\n$3\n\nЗаўвага: калі Вы гэтага ня зробіце зараз, то створаны файл ня будзе даступны Вам потым, калі Вы выйдзеце з праграмы ўсталяваньня безь яго загрузкі.\n\nКалі Вы гэта зробіце, Вы можаце [$2 ўвайсьці ў Вашую вікі].", "config-install-done-path": "Віншуем!\nВы ўсталявалі MediaWiki.\n\nПраграма ўсталёўкі стварыла файл LocalSettings.php. Ён утрымлівае ўсе вашыя налады.\n\nВам трэба спампаваць яго і пакласьці ў $4. Спампоўка павінна пачацца аўтаматычна.\n\nКалі спампоўка не пачалася або вы адмянілі яе, вы можаце пачаць яе наноў, калі націсьніце на наступную спасылку:\n\n$3\n\nЗаўвага: Калі вы ня зробіце гэта зараз, то створаны файл ня будзе даступны вам па выхадзе з праграмы безь яго спампоўкі.\n\nКалі вы зробіце гэта, вы можаце [$2 ўвайсьці ў вашую вікі].", + "config-install-success": "MediaWiki была пасьпяхова ўсталяваная. Цяпер вы можаце наведаць <$1$2>, каб пабачыць вашую вікі. Калі вы маеце пытаньні, сьпярша паглядзіце сьпіс адказаў на частыя пытаньні: ці скарыстайцеся адным з форумаў падтрымкі, пазначаных на гэтай старонцы.", "config-download-localsettings": "Загрузіць LocalSettings.php", "config-help": "дапамога", "config-help-tooltip": "націсьніце, каб разгарнуць", diff --git a/includes/installer/i18n/ckb.json b/includes/installer/i18n/ckb.json index 3bfa8a6f54..20f2f24bd2 100644 --- a/includes/installer/i18n/ckb.json +++ b/includes/installer/i18n/ckb.json @@ -4,7 +4,8 @@ "Asoxor", "Calak", "Muhammed taha", - "Lost Whispers" + "Lost Whispers", + "Épine" ] }, "config-desc": "دامەزرێنەرەکە بۆ میدیاویکی", diff --git a/includes/installer/i18n/de.json b/includes/installer/i18n/de.json index ca649dcab5..9a79e344cd 100644 --- a/includes/installer/i18n/de.json +++ b/includes/installer/i18n/de.json @@ -321,6 +321,7 @@ "config-install-mainpage-failed": "Die Hauptseite konnte nicht erstellt werden: $1", "config-install-done": "'''Herzlichen Glückwunsch!'''\nMediaWiki wurde erfolgreich installiert.\n\nDas Installationsprogramm hat die Datei LocalSettings.php erzeugt.\nSie enthält alle vorgenommenen Konfigurationseinstellungen.\n\nDiese Datei muss nun heruntergeladen und anschließend in das Stammverzeichnis der MediaWiki-Installation hochgeladen werden. Dies ist dasselbe Verzeichnis, in dem sich auch die Datei index.php befindet. Das Herunterladen sollte inzwischen automatisch gestartet worden sein.\n\nSofern dies nicht der Fall war, oder das Herunterladen unterbrochen wurde, kann der Vorgang durch einen Klick auf den folgenden Link erneut gestartet werden:\n\n$3\n\n'''Hinweis:''' Die Konfigurationsdatei sollte jetzt unbedingt heruntergeladen werden. Sie wird nach Beenden des Installationsprogramms, nicht mehr zur Verfügung stehen.\n\nSobald alles erledigt wurde, kann auf das '''[$2 Wiki zugegriffen werden]'''. Wir wünschen viel Spaß und Erfolg mit dem Wiki.", "config-install-done-path": "Herzlichen Glückwunsch!\nDu hast MediaWiki installiert.\n\nDas Installationsprogramm hat eine Datei „LocalSettings.php“ erzeugt.\nSie enthält deine gesamte Konfiguration.\n\nDu musst sie herunterladen und unter $4 ablegen. Der Download sollte automatisch gestartet sein.\n\nFalls der Download nicht angeboten wird oder du ihn abgebrochen hast, kannst du ihn durch Anklicken des folgenden Links neu starten:\n\n$3\n\nHinweis: Falls du dies jetzt nicht tust, wird die erzeugte Konfigurationsdatei später nicht verfügbar sein, wenn du die Installation ohne Herunterladen verlässt.\n\nBei Fertigstellung kannst du [$2 dein Wiki aufrufen].", + "config-install-success": "MediaWiki wurde erfolgreich installiert. Du kannst jetzt\n<$1$2> aufrufen, um dein Wiki anzusehen.\nFalls du Fragen hast, lies unsere Liste der häufig gestellten Fragen:\n oder benutze eines der\nSupport-Foren, die auf dieser Seite verlinkt sind.", "config-download-localsettings": "LocalSettings.php herunterladen", "config-help": "Hilfe", "config-help-tooltip": "Zum Expandieren klicken", diff --git a/includes/installer/i18n/el.json b/includes/installer/i18n/el.json index f9bab3edd0..0c057fc9ac 100644 --- a/includes/installer/i18n/el.json +++ b/includes/installer/i18n/el.json @@ -9,7 +9,8 @@ "Stam.nikos", "Giorgos456", "Badseed", - "Macofe" + "Macofe", + "KATRINE1992" ] }, "config-desc": "Το πρόγραμμα εγκατάστασης για το MediaWiki", @@ -227,6 +228,7 @@ "config-install-extension-tables": "Γίνεται δημιουργία πινάκων για τις εγκατεστημένες επεκτάσεις", "config-install-mainpage-failed": "Δεν ήταν δυνατή η εισαγωγή της αρχικής σελίδας: $1", "config-install-done": "Συγχαρητήρια!\nΈχετε εγκαταστήσει με επιτυχία το MediaWiki.\n\nΤο πρόγραμμα εγκατάστασης έχει δημιουργήσει το αρχείο LocalSettings.php.\nΠεριέχει όλες τις ρυθμίσεις παραμέτρων σας.\n\nΘα πρέπει να το κατεβάσετε και να το βάλετε στη βάση της εγκατάστασης του wiki σας (στον ίδιο κατάλογο όπως το index.php). Η λήψη θα αρχίσει αυτόματα.\n\nΑν η λήψη δεν προσφέφθηκε, ή αν την ακυρώσατε, μπορείτε να επανεκκινήσετε τη λήψη κάνοντας κλικ στο παρακάτω link:\n\n$3\n\nΣημείωση: Εάν δεν το κάνετε αυτό τώρα, αυτό το αρχείο ρύθμισης παραμέτρων δεν θα είναι διαθέσιμο για σας αργότερα, αν βγείτε από την εγκατάσταση, χωρίς να το κατεβάσετε!\n\nΌταν γίνει αυτό, μπορείτε να [$2 μπείτε στο wiki σας].", + "config-install-success": " Το σύστημα της MediaWiki έχει εγκατασταθεί με επιτυχία. Μπορείτε τώρα να επισκεφθείτε το \n <$1$2> για να δείτε το wiki σας.\nΑν έχετε ερωτήσεις, ελέγξετε την λίστα με τις πιο συχνές ερωτήσεις:\n ή χρησιμοποιήστε ένα από τα φόρουμ υποστήριξης που είναι συνδεδεμένα σε αυτήν την σελίδα.", "config-download-localsettings": "Λήψη του LocalSettings.php", "config-help": "βοήθεια", "config-help-tooltip": "κλικ για ανάπτυξη", diff --git a/includes/installer/i18n/en.json b/includes/installer/i18n/en.json index 6319b76da7..6d4c485263 100644 --- a/includes/installer/i18n/en.json +++ b/includes/installer/i18n/en.json @@ -304,6 +304,7 @@ "config-install-mainpage-failed": "Could not insert main page: $1", "config-install-done": "Congratulations!\nYou have installed MediaWiki.\n\nThe installer has generated a LocalSettings.php file.\nIt contains all your configuration.\n\nYou will need to download it and put it in the base of your wiki installation (the same directory as index.php). The download should have started automatically.\n\nIf the download was not offered, or if you cancelled it, you can restart the download by clicking the link below:\n\n$3\n\nNote: If you do not do this now, this generated configuration file will not be available to you later if you exit the installation without downloading it.\n\nWhen that has been done, you can [$2 enter your wiki].", "config-install-done-path": "Congratulations!\nYou have installed MediaWiki.\n\nThe installer has generated a LocalSettings.php file.\nIt contains all your configuration.\n\nYou will need to download it and put it at $4. The download should have started automatically.\n\nIf the download was not offered, or if you cancelled it, you can restart the download by clicking the link below:\n\n$3\n\nNote: If you do not do this now, this generated configuration file will not be available to you later if you exit the installation without downloading it.\n\nWhen that has been done, you can [$2 enter your wiki].", + "config-install-success": "MediaWiki has been successfully installed. You can now\nvisit <$1$2> to view your wiki.\nIf you have questions, check out our frequently asked questions list:\n or use one of the\nsupport forums linked on that page.", "config-download-localsettings": "Download LocalSettings.php", "config-help": "help", "config-help-tooltip": "click to expand", diff --git a/includes/installer/i18n/es.json b/includes/installer/i18n/es.json index 0eecddb904..d7ed72b870 100644 --- a/includes/installer/i18n/es.json +++ b/includes/installer/i18n/es.json @@ -33,7 +33,8 @@ "Peter Bowman", "Dgstranz", "Irus", - "Tinss" + "Tinss", + "KATRINE1992" ] }, "config-desc": "El instalador de MediaWiki", @@ -338,6 +339,7 @@ "config-install-mainpage-failed": "No se pudo insertar la página principal: $1", "config-install-done": "¡Felicidades!\nHas instalado MediaWiki.\n\nEl instalador ha generado un archivo LocalSettings.php.\nEste contiene toda su configuración.\n\nDeberás descargarlo y ponerlo en la base de la instalación de wiki (el mismo directorio que index.php). La descarga debería haber comenzado automáticamente.\n\nSi no comenzó la descarga, o si se ha cancelado, puedes reiniciar la descarga haciendo clic en el siguiente enlace:\n\n$3\n\nNota: si no haces esto ahora, este archivo de configuración generado no estará disponible más tarde si sales de la instalación sin descargarlo.\n\nCuando lo hayas hecho, podrás [$2 entrar en tu wiki].", "config-install-done-path": "¡Felicidades!\nHas instalado MediaWiki.\n\nEl instalador ha generado un archivo LocalSettings.php.\nEste contiene toda su configuración.\n\nDeberás descargarlo y ponerlo en $4. La descarga debería haber comenzado automáticamente.\n\nSi no comenzó la descarga, o si se ha cancelado, puedes reiniciar la descarga haciendo clic en el siguiente enlace:\n\n$3\n\nNota: si no haces esto ahora, este archivo de configuración generado no estará disponible más tarde si sales de la instalación sin descargarlo.\n\nCuando lo hayas hecho, podrás [$2 entrar en tu wiki].", + "config-install-success": "Se instaló MediaWiki correctamente. Ahora puedes visitar\n<$1$2> para ver el wiki.\nSi tienes dudas, echa un vistazo a la lista de preguntas frecuentes:\n, o bien, utiliza uno de\nlos foros de asistencia que se enumeran en esa página.", "config-download-localsettings": "Descargar LocalSettings.php", "config-help": "ayuda", "config-help-tooltip": "haz clic para ampliar", diff --git a/includes/installer/i18n/eu.json b/includes/installer/i18n/eu.json index ae97154cc0..030b45ebf7 100644 --- a/includes/installer/i18n/eu.json +++ b/includes/installer/i18n/eu.json @@ -77,7 +77,9 @@ "config-no-cli-uri": "Oharra. Ez da zehaztu --scriptpath, erabiltzen estandar $1 .", "config-using-server": "\"$1\" zerbitzari-izena erabiltzen.", "config-using-uri": "\"$1$2\" zerbitzariaren URLa erabiltzen.", + "config-uploads-not-safe": "Oharra: Zure igoerak egiteko $1 direktorio lehenetsia gidoi arbitrarioen exekuzioek kaltetu dezakete.\nMediaWiki-k segurtasunerako kargatutako fitxategi guztiak egiaztatzen dituen arren, oso gomendagarria da [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Security#Upload_security segurtasun-ahultasun hau itxi] erabiltzea gaitu aurretik.", "config-brokenlibxml": "Zure sistemak dauka PHP-ko eta libxml2-ko konbinazio akastun bat eta eragin ahal du korrupzioa datarekin MediaWikin eta beste web aplikazioetan.\nAktualizatu libxml2 2.7.3-era edo berrietara ([https://bugs.php.net/bug.php?id=45996 bug filed with PHP]).\nInstalazioa geldiarazi egin da.", + "config-using-32bit": "Oharra: zure sistemak 32 bit-ekin jarduten duela dirudi. Hau da [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:32-bit not advised].", "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.", @@ -143,6 +145,7 @@ "config-sqlite-cant-create-db": "Ezin izan da $1 datu-basearen artxiboa sortu.", "config-sqlite-fts3-downgrade": "PHPn FTS3 laguntza falta da, taulen gradua jeisten.", "config-can-upgrade": "Datu base honetan MediaWiki taulak daude.\nMediaWiki $1ra graduz igotzeko, Jarraitu klikatu.", + "config-upgrade-done": "Berritzea burutu da.\n\nOrain [$1 zure wiki-a erabiltzen hasi] ahal zara.\n\nZure LocalSettings.php fitxategia birsortzea nahi baduzu, egin klik beheko botoian.\nHau ez da gomendagarria zure wikiarekin arazoak izan ezean.", "config-upgrade-done-no-regenerate": "Eguneratze prozesua amaitu egin da.\n\nHasi ahal zara [ $1 wikia arabiltzen]", "config-regenerate": "Birsortu LocalSettings.php →", "config-show-table-status": "SHOW TABLE STATUS kontsulta huts egin du!", @@ -161,6 +164,8 @@ "config-mysql-binary": "Bitarra", "config-mysql-utf8": "UTF-8", "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:", @@ -172,6 +177,7 @@ "config-ns-other": "Bestelakoa (zehaztu)", "config-ns-other-default": "MyWiki", "config-project-namespace-help": "Wikipedia-ren adibidea jarraitzen, wiki askok beren orrien politika mantentzen dute beren edukien orrialdeetatik bereizita, '' 'proiektuaren izen-eremuan' ''.\nOrrialde honetako izenburu guztiek aurrizki jakin batekin hasten dira, hemen zehaztu ahal direnak.\nNormalean, aurrizkia wikiaren izenetik dator, baina ezin du \"#\" edo \":\" puntuazio-karaktereak eduki.", + "config-ns-invalid": "Zehaztutako \"$1\" izena baliogabea da.\nZehaztu beste proiektu baten izenaren eremua.", "config-admin-box": "Administratzaile kontua", "config-admin-name": "Zure erabiltzaile-izena:", "config-admin-password": "Pasahitza:", diff --git a/includes/installer/i18n/fr.json b/includes/installer/i18n/fr.json index 2bfe36f574..1ad2cbd3b4 100644 --- a/includes/installer/i18n/fr.json +++ b/includes/installer/i18n/fr.json @@ -28,7 +28,8 @@ "C13m3n7", "The RedBurn", "Trial", - "Tinss" + "Tinss", + "Thibaut120094" ] }, "config-desc": "Le programme d’installation de MediaWiki", @@ -68,37 +69,37 @@ "config-help-restart": "Voulez-vous effacer toutes les données enregistrées que vous avez entrées et relancer le processus d'installation ?", "config-restart": "Oui, le relancer", "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-copyright": "=== Droit d’auteur et conditions ===\n\n$1\n\nCe programme est un logiciel gratuit : 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 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 [http://www.gnu.org/copyleft/gpl.html lisez-la en ligne].", + "config-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 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 [http://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]\n----\n* Lisez-moi\n* Notes de publication\n* Copie\n* 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 [http://pecl.php.net/intl l'extension PECL intl] pour la normalisation Unicode.", - "config-unicode-pure-php-warning": "Attention : L’[http://pecl.php.net/intl extension PECL intl] n’est pas disponible pour la normalisation d’Unicode, retour à la version lente implémentée en PHP.\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 [http://site.icu-project.org/ bibliothèque logicielle ''ICU Project''].\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-unicode-using-intl": "Utilisation de [http://pecl.php.net/intl l’extension PECL intl] pour la normalisation Unicode.", + "config-unicode-pure-php-warning": "Attention : L’[http://pecl.php.net/intl extension PECL intl] 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 actif, par exemple en utilisant ./configure --with-mysqli. Si vous avez installé PHP depuis un paquet Debian ou Ubuntu, alors vous devrez aussi installer, par exemple, le paquet php5-mysql.", - "config-outdated-sqlite": "'''Attention''': vous avez SQLite $1, qui est inférieur à la version minimale requise $2. SQLite sera indisponible.", - "config-no-fts3": "'''Attention :''' SQLite est compilé sans le module [//sqlite.org/fts3.html FTS3] ; les fonctions de recherche ne seront pas disponibles sur ce moteur.", - "config-pcre-old": "'''Fatal :''' 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.\nMédiaWiki a besoin de la gestion d’UTF-8 pour fonctionner correctement.", + "config-outdated-sqlite": "Attention : vous avez SQLite $1, qui est inférieur à la version minimale requise $2. SQLite sera indisponible.", + "config-no-fts3": "Attention : SQLite est compilé sans le module [//sqlite.org/fts3.html 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.", - "config-memory-bad": "'''Attention :''' Le paramètre memory_limit de PHP est à $1.\nCette valeur est probablement trop faible.\nIl est possible que l’installation échoue !", + "config-memory-bad": "Attention : Le paramètre memory_limit de PHP est à $1.\nCette valeur est probablement trop faible.\nIl est possible que l’installation échoue !", "config-xcache": "[http://xcache.lighttpd.net/ XCache] est installé", "config-apc": "[http://www.php.net/apc APC] est installé", "config-apcu": "[http://www.php.net/apcu APCu] est installé", "config-wincache": "[http://www.iis.net/download/WinCacheForPhp WinCache] est installé", - "config-no-cache-apcu": "Attention : Impossible de trouver [http://www.php.net/apcu APCu], [http://xcache.lighttpd.net/ XCache] ou [http://www.iis.net/download/WinCacheForPhp WinCache].\nLa mise en cache d'objets n'est pas activée.", - "config-mod-security": "'''Attention''': Votre serveur web a [http://modsecurity.org/ mod_security] activé. S’il est mal configuré, cela peut poser des problèmes à MediaWiki ou à d’autres applications qui permettent aux utilisateurs de publier un contenu quelconque.\nReportez-vous à [http://modsecurity.org/documentation/ la documentation de mod_security] ou contactez le soutien de votre hébergeur si vous rencontrez des erreurs aléatoires.", + "config-no-cache-apcu": "Attention : impossible de trouver [http://www.php.net/apcu APCu], [http://xcache.lighttpd.net/ XCache] ou [http://www.iis.net/download/WinCacheForPhp WinCache].\nLa mise en cache d’objets n’est pas activée.", + "config-mod-security": "Attention : votre serveur web a [http://modsecurity.org/ mod_security] activé. S’il est mal configuré, cela peut poser des problèmes à MediaWiki ou à d’autres applications qui permettent aux utilisateurs de publier un contenu quelconque. Si possible, ceci devrait être désactivé. Sinon, reportez-vous à [http://modsecurity.org/documentation/ la documentation de mod_security] ou contactez l’assistance de votre hébergeur si vous rencontrez des erreurs aléatoires.", "config-diff3-bad": "GNU diff3 introuvable.", "config-git": "Logiciel de contrôle de version Git trouvé : $1.", "config-git-bad": "Logiciel de contrôle de version Git non trouvé.", - "config-imagemagick": "ImageMagick trouvé : $1.\nLa miniaturisation d'images sera activée si vous activez le téléversement de fichiers.", + "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-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-using-server": "Utilisation du nom de serveur \"$1\".", + "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-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.", @@ -333,6 +334,7 @@ "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 bien été installé. Vous pouvez maintenant\nvisiter <$1$2> pour voir votre wiki.\nSi vous avez des questions, consultez notre liste de questions fréquemment posées :\n ou utilisez un des\nforums de soutien liés sur cette page.", "config-download-localsettings": "Télécharger LocalSettings.php", "config-help": "aide", "config-help-tooltip": "cliquer pour agrandir", diff --git a/includes/installer/i18n/ko.json b/includes/installer/i18n/ko.json index f268215f1d..fcc54689e8 100644 --- a/includes/installer/i18n/ko.json +++ b/includes/installer/i18n/ko.json @@ -315,6 +315,7 @@ "config-install-mainpage-failed": "대문을 삽입할 수 없습니다: $1", "config-install-done": "축하합니다!\n미디어위키를 설치했습니다.\n\n설치 관리자가 LocalSettings.php 파일을 만들었습니다.\n여기에 모든 설정이 포함되어 있습니다.\n\n파일을 다운로드하여 위키 설치의 거점에 넣어야 합니다. (index.php와 같은 디렉터리) 다운로드가 자동으로 시작됩니다.\n\n다운로드가 제공되지 않을 경우나 그것을 취소한 경우에는 아래의 링크를 클릭하여 다운로드를 다시 시작할 수 있습니다:\n\n$3\n\n참고: 이 생성한 설정 파일을 다운로드하지 않고 설치를 끝내면 이 파일은 나중에 사용할 수 없습니다.\n\n완료되었으면 [$2 위키에 들어갈 수 있습니다].", "config-install-done-path": "축하합니다!\n미디어위키가 설치되었습니다.\n\n설치 관리자가 LocalSettings.php 파일을 생성했습니다.\n이 파일에 모든 설정이 포함되어 있습니다.\n\n이 파일을 다운로드하여 $4 위치에 넣으세요. 다운로드가 자동으로 시작되었을 것입니다.\n\n다운로드가 시작되지 않았거나 취소했다면, 아래 링크를 클릭하여 다운로드를 재시작할 수 있습니다.\n\n$3\n\n알림: 지금 다운로드하지 않는다면, 이후에는 이 설정 파일을 다운로드할 수 없습니다.\n\n모든 작업이 완료되었다면, [$2 위키에 들어갈 수 있습니다].", + "config-install-success": "미디어위키가 성공적으로 설치되었습니다. 이제\n<$1$2>에 방문하여 위키를 볼 수 있습니다.\n질문이 있으시다면 자주 묻는 질문 목록을 살펴보십시오:\n 아니면\n해당 문서에 연결된 지원 포럼 중 한곳을 이용하십시오.", "config-download-localsettings": "LocalSettings.php 다운로드", "config-help": "도움말", "config-help-tooltip": "확장하려면 클릭", diff --git a/includes/installer/i18n/mk.json b/includes/installer/i18n/mk.json index d028a8c4d8..ee5bb8de23 100644 --- a/includes/installer/i18n/mk.json +++ b/includes/installer/i18n/mk.json @@ -230,7 +230,7 @@ "config-email-user": "Овозможи е-пошта од корисник до корисник", "config-email-user-help": "Дозволи сите корисници да можат да си праќаат е-пошта ако ја имаат овозможено во нагодувањата.", "config-email-usertalk": "Овозможи известувања за промени во кориснички страници за разговор", - "config-email-usertalk-help": "Овозможи корисниците да добиваат известувања за промени во нивните кориснички страници за разговор ако ги имаат овозможено во нагодувањата.", + "config-email-usertalk-help": "Овозможи корисниците да добиваат известувања за промени во нивните кориснички разговорни страници ако ги имаат овозможено во нагодувањата.", "config-email-watchlist": "Овозможи известувања за список на набљудувања", "config-email-watchlist-help": "Овозможи корисниците да добиваат известувања за нивните набљудувани страници ако ги имаат овозможено во нагодувањата.", "config-email-auth": "Овозможи потврдување на е-пошта", @@ -309,6 +309,7 @@ "config-install-mainpage-failed": "Не можев да вметнам главна страница: $1", "config-install-done": "Честитаме!\nУспешно го воспоставивте МедијаВики.\n\nВоспоставувачот создаде податотека LocalSettings.php.\nТаму се содржат сите ваши нагодувања.\n\nЌе треба да ја преземете и да ја ставите во основата на воспоставката (истата папка во која се наоѓа index.php). Преземањето треба да е започнато автоматски.\n\nАко не ви е понудено преземање, или пак ако сте го откажале, можете да го почнете одново стискајќи на следнава врска:\n\n$3\n\nНапомена: Ако ова не го направите сега, податотеката со поставки повеќе нема да биде на достапна.\n\nОткога ќе завршите со тоа, можете да [$2 влезете на вашето вики].", "config-install-done-path": "Честитаме!\nГо воспоставивте МедијаВики.\n\nВоспоставувачот создаде податотека LocalSettings.php.\nТаму се содржат сите ваши нагодувања.\n\nЌе треба да ја преземете и да ја ставите во $4. Преземањето треба да е започнато автоматски.\n\nАко не ви е понудено преземање, или пак ако сте го откажале, можете да го почнете одново стискајќи на следнава врска:\n\n$3\n\nНапомена: Ако ова не го направите сега, создадената податотека со поставки повеќе нема да биде на достапна, освен ако не ја преземете пред да излезете.\n\nОткога ќе завршите со тоа, можете да [$2 влезете на вашето вики].", + "config-install-success": "МедијаВики е успешно воспоставен. Сега можете да појдете на <$1$2> за да го погледате вашето вики.\nАко имате било какви прашања, погледајте го списокот на често поставувани прашања:\n или појдете на еден од форумите за поддршка наведени на таа страница.", "config-download-localsettings": "Преземи го LocalSettings.php", "config-help": "помош", "config-help-tooltip": "стиснете да расклопите", diff --git a/includes/installer/i18n/nb.json b/includes/installer/i18n/nb.json index 1eb418ed45..c271a90ad3 100644 --- a/includes/installer/i18n/nb.json +++ b/includes/installer/i18n/nb.json @@ -52,7 +52,7 @@ "config-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 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 [http://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-env-good": "Miljøet har blitt sjekket.\nDu kan installere MediaWiki.", - "config-env-bad": "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 [http://pecl.php.net/intl intl PECL-utvidelsen] for Unicode-normalisering.", @@ -314,6 +314,7 @@ "config-install-mainpage-failed": "Kunne ikke sette inn hovedside: $1", "config-install-done": "Gratulrerer!\nDu har lykkes i å installere MediaWiki.\n\nInstallasjonsprogrammet har generert en LocalSettings.php-fil.\nDen inneholder alle dine konfigureringer.\n\nDu må laste den ned og legge den på hovedfolderen for din wiki-installasjon (der index.php ligger). Nedlastingen skulle ha startet automatisk.\n\nHvis ingen nedlasting ble tilbudt, eller du avbrøt den, kan du få den i gang ved å klikke på lenken under:\n\n$3\n\nOBS: Hvis du ikke gjør dette nå, vil den genererte konfigurasjonsfilen ikke være tilgjengelig for deg senere.\n\nNår dette er gjort, kan du [$2 gå inn i wikien].", "config-install-done-path": "Gratulerer!\nDu har installert MediaWiki.\n\nInstallereren har generert en LocalSettings.php-fil.\nDen inneholder all konfigurasjonen for wikien.\n\nDu må laste den ned og legge den i $4. Nedlastingen skal ha startet automatisk.\n\nOm nedlastingen ikke ble startet, eller om du avbrøt den, kan du starte på nytt ved å klikke lenken nedenfor:\n\n$3\n\nMerk: Om du ikke gjør dette nå vil den genererte konfigurasjonen ikke være tilgjengelig senere.\n\nNår dette er gjort kan du [$2 gå til wikien din].", + "config-install-success": "MediaWiki har blitt installert. Du kan nå\nbesøke <$1$2> for å se wikien din.\nOm du har spørsmål, sjekk de ofte stilte spørsmålene:\n eller bruk et av\nsupportforumene som lenkes til fra den siden.", "config-download-localsettings": "Last ned LocalSettings.php", "config-help": "hjelp", "config-help-tooltip": "klikk for å utvide", diff --git a/includes/installer/i18n/pt.json b/includes/installer/i18n/pt.json index a6ebd92a56..5f3db68a6e 100644 --- a/includes/installer/i18n/pt.json +++ b/includes/installer/i18n/pt.json @@ -18,7 +18,8 @@ "Macofe", "Diniscoelho", "Ruila", - "Seb35" + "Seb35", + "MokaAkashiyaPT" ] }, "config-desc": "O instalador do MediaWiki", @@ -35,7 +36,7 @@ "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 o idioma que será usado durante o processo de instalação.", + "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-back": "← Voltar", @@ -323,6 +324,7 @@ "config-install-mainpage-failed": "Não foi possível inserir a página principal: $1", "config-install-done": "Parabéns!\nTerminou a instalação do MediaWiki.\n\nO instalador gerou um ficheiro LocalSettings.php.\nEste ficheiro contém todas as configurações.\n\nPrecisa de descarregar o ficheiro e colocá-lo no diretório de raiz da sua instalação (o mesmo diretório onde está o ficheiro index.php). Este descarregamento deverá ter sido iniciado automaticamente.\n\nSe o descarregamento não foi iniciado, ou se o cancelou, pode recomeçá-lo clicando na ligação abaixo:\n\n$3\n\nNota: Se não o descarregar agora, o ficheiro que foi gerado deixará de estar disponível quando sair do processo de instalação.\n\nDepois de terminar o passo anterior, pode [$2 entrar na wiki].", "config-install-done-path": "Parabéns!\nTerminou a instalação do MediaWiki.\n\nO instalador gerou um ficheiro LocalSettings.php.\nEste ficheiro contém todas as configurações.\n\nPrecisa de descarregar o ficheiro e colocá-lo no diretório $4. Este descarregamento deverá ter sido iniciado automaticamente.\n\nSe o descarregamento não foi iniciado, ou se o cancelou, pode recomeçá-lo clicando na ligação abaixo:\n\n$3\n\nNota: Se não fizer o descarregamento agora, o ficheiro que foi gerado deixará de estar disponível quando sair do processo de instalação.\n\nDepois de terminar o passo anterior, pode [$2 entrar na wiki].", + "config-install-success": "O MediaWiki foi instalado com sucesso. Já pode consultar <$1$2> para visualizar a sua wiki.\nSe tiver dúvidas, veja a nossa lista de perguntas frequentes:\n ou utilize um dos fóruns de suporte vinculados nessa página.", "config-download-localsettings": "Descarregar LocalSettings.php", "config-help": "ajuda", "config-help-tooltip": "clique para expandir", diff --git a/includes/installer/i18n/qqq.json b/includes/installer/i18n/qqq.json index a5c679036f..2fc95ceaa7 100644 --- a/includes/installer/i18n/qqq.json +++ b/includes/installer/i18n/qqq.json @@ -324,6 +324,7 @@ "config-install-mainpage-failed": "Used as error message. Parameters:\n* $1 - detailed error message", "config-install-done": "Parameters:\n* $1 is the URL to LocalSettings download\n* $2 is a link to the wiki.\n* $3 is a download link with attached download icon. The config-download-localsettings message will be used as the link text.", "config-install-done-path": "Parameters:\n* $1 is the URL to LocalSettings download\n* $2 is a link to the wiki.\n* $3 is a download link with attached download icon. The config-download-localsettings message will be used as the link text.\n* $4 is the filesystem location of where the LocalSettings.php file should be saved to.", + "config-install-success": "Gives user information that installation was successful. Parameters:\n* $1 - server name\n* $2 - script path", "config-download-localsettings": "The link text used in the download link in config-install-done.", "config-help": "This is used in help boxes.\n{{Identical|Help}}", "config-help-tooltip": "Tooltip for the 'help' links ({{msg-mw|config-help}}), to make it clear they'll expand in place rather than open a new page", diff --git a/includes/installer/i18n/ru.json b/includes/installer/i18n/ru.json index 904560556f..3142e71d12 100644 --- a/includes/installer/i18n/ru.json +++ b/includes/installer/i18n/ru.json @@ -329,6 +329,7 @@ "config-install-mainpage-failed": "Не удаётся вставить главную страницу: $1", "config-install-done": "Поздравляем!\nВы установили MediaWiki.\n\nВо время установки был создан файл LocalSettings.php.\nОн содержит все ваши настройки.\n\nВам необходимо скачать его и положить в корневую директорию вашей вики (ту же директорию, где находится файл index.php). Его загрузка должна начаться автоматически.\n\nЕсли автоматическая загрузка не началась или вы её отменили, вы можете скачать по ссылке ниже:\n\n$3\n\nПримечание: Если вы не сделаете этого сейчас, то сгенерированный файл конфигурации не будет доступен вам в дальнейшем, если вы выйдете из установки, не скачивая его.\n\nПо окончании действий, описанных выше, вы сможете [$2 войти в вашу вики].", "config-install-done-path": "Поздравляем!\nВы установили MediaWiki.\n\nВо время установки был создан файл LocalSettings.php.\nОн содержит все ваши настройки.\n\nВам необходимо скачать его и положить в $4. Его загрузка должна начаться автоматически.\n\nЕсли автоматическая загрузка не началась или вы её отменили, вы можете скачать по ссылке ниже:\n\n$3\n\nПримечание: Если вы не сделаете этого сейчас, то сгенерированный файл конфигурации не будет доступен вам в дальнейшем, если вы выйдете из установки, не скачивая его.\n\nПо окончании действий, описанных выше, вы сможете [$2 войти в вашу вики].", + "config-install-success": "MediaWiki успешно установлена. Сейчас вы можете перейти на <$1 $2>, чтобы просмотреть свою вики\nЕсли у вас есть вопросы, ознакомьтесь с нашим часто задаваемыми вопросами:\n или используйте один из форумов поддержки, указанный на этой странице.", "config-download-localsettings": "Загрузить LocalSettings.php", "config-help": "справка", "config-help-tooltip": "нажмите, чтобы развернуть", diff --git a/includes/installer/i18n/sv.json b/includes/installer/i18n/sv.json index a338387a64..861e3e62e4 100644 --- a/includes/installer/i18n/sv.json +++ b/includes/installer/i18n/sv.json @@ -311,6 +311,7 @@ "config-install-mainpage-failed": "Kunde inte infoga huvudsidan: $1", "config-install-done": "Grattis!\nDu har installerat MediaWiki.\n\nInstallationsprogrammet har genererat filen LocalSettings.php.\nDet innehåller alla dina konfigurationer.\n\nDu kommer att behöva ladda ner den och placera den i roten för din wiki-installation (samma katalog som index.php). Nedladdningen borde ha startats automatiskt.\n\nOm ingen nedladdning erbjöds, eller om du har avbrutit det kan du starta om nedladdningen genom att klicka på länken nedan:\n\n$3\n\nOBS: Om du inte gör detta nu, kommer denna genererade konfigurationsfil inte vara tillgänglig för dig senare om du avslutar installationen utan att ladda ned den.\n\nNär det är klart, kan du [$2 gå in på din wiki]", "config-install-done-path": "Grattis!\nDu har installerat MediaWiki.\n\nInstallationsprogrammet har genererat filen LocalSettings.php.\nDet innehåller alla dina konfigurationer.\n\nDu kommer att behöva ladda ner den och placera den i $4. Nedladdningen borde ha startats automatiskt.\n\nOm ingen nedladdning erbjöds, eller om du har avbrutit det kan du starta om nedladdningen genom att klicka på länken nedan:\n\n$3\n\nOBS: Om du inte gör detta nu, kommer denna genererade konfigurationsfil inte vara tillgänglig för dig senare om du avslutar installationen utan att ladda ned den.\n\nNär det är klart, kan du [$2 gå in på din wiki]", + "config-install-success": "MediaWiki har installerats. Du kan nu besöka <$1$2> för att se din wiki.\nOm du undrar någonting, kolla in vår lista över vanliga ställda frågor:\n eller använda något supportforum som länkas på sidan.", "config-download-localsettings": "Ladda ner LocalSettings.php", "config-help": "hjälp", "config-help-tooltip": "klicka för att expandera", diff --git a/includes/installer/i18n/zh-hans.json b/includes/installer/i18n/zh-hans.json index fab5eef95a..f5fa9f2470 100644 --- a/includes/installer/i18n/zh-hans.json +++ b/includes/installer/i18n/zh-hans.json @@ -324,6 +324,7 @@ "config-install-mainpage-failed": "无法插入首页:$1", "config-install-done": "恭喜!\n您已经安装了MediaWiki。\n\n安装程序已经生成了LocalSettings.php文件,其中包含了您所有的配置。\n\n您需要下载该文件,并将其放在您wiki的根目录(index.php的同级目录)中。稍后下载将自动开始。\n\n如果浏览器没有提示您下载,或者您取消了下载,您可以点击下面的链接重新开始下载:\n\n$3\n\n注意:如果您现在不完成本步骤,而是没有下载便退出了安装过程,此后您将无法获得自动生成的配置文件。\n\n当本步骤完成后,您可以[$2 进入您的wiki]。", "config-install-done-path": "祝贺!您已经安装了MediaWiki。\n\n安装程序已经生成了LocalSettings.php文件。它包含您所有的配置。\n\n您需要下载该文件,并将其放在$4。下载应已自动开始。\n\n如果没有提供下载,或者您取消了下载,您可以点击下面的链接重新开始下载:\n\n$3\n\n注意:如果您现在不完成本步骤,而是没有下载便退出了安装过程,此后您将无法获得自动生成的配置文件。\n\n当本步骤完成后,您可以[$2 进入您的wiki]。", + "config-install-success": "MediaWiki已成功安装。您现在可以访问<$1$2>以查看您的wiki。如果您有问题,请阅览我们的常见问题列表:或使用在该页面上链接的支持论坛之一。", "config-download-localsettings": "下载LocalSettings.php", "config-help": "帮助", "config-help-tooltip": "单击展开", diff --git a/includes/jobqueue/JobSpecification.php b/includes/jobqueue/JobSpecification.php index d844795143..b62b83c666 100644 --- a/includes/jobqueue/JobSpecification.php +++ b/includes/jobqueue/JobSpecification.php @@ -18,7 +18,6 @@ * http://www.gnu.org/copyleft/gpl.html * * @file - * @ingroup JobQueue */ /** diff --git a/includes/jobqueue/aggregator/JobQueueAggregator.php b/includes/jobqueue/aggregator/JobQueueAggregator.php index f26beee4bd..433de93af7 100644 --- a/includes/jobqueue/aggregator/JobQueueAggregator.php +++ b/includes/jobqueue/aggregator/JobQueueAggregator.php @@ -158,6 +158,9 @@ abstract class JobQueueAggregator { } } +/** + * @ingroup JobQueue + */ class JobQueueAggregatorNull extends JobQueueAggregator { protected function doNotifyQueueEmpty( $wiki, $type ) { return true; diff --git a/includes/jobqueue/jobs/ClearUserWatchlistJob.php b/includes/jobqueue/jobs/ClearUserWatchlistJob.php new file mode 100644 index 0000000000..3e8b2ad3e4 --- /dev/null +++ b/includes/jobqueue/jobs/ClearUserWatchlistJob.php @@ -0,0 +1,118 @@ + $user->getId(), 'maxWatchlistId' => $maxWatchlistId ] + ); + } + + /** + * @param Title|null $title Not used by this job. + * @param array $params + * - userId, The ID for the user whose watchlist is being cleared. + * - maxWatchlistId, The maximum wl_id at the time the job was first created, + */ + public function __construct( Title $title = null, array $params ) { + parent::__construct( + 'clearUserWatchlist', + SpecialPage::getTitleFor( 'EditWatchlist', 'clear' ), + $params + ); + + $this->removeDuplicates = true; + } + + public function run() { + global $wgUpdateRowsPerQuery; + $userId = $this->params['userId']; + $maxWatchlistId = $this->params['maxWatchlistId']; + $batchSize = $wgUpdateRowsPerQuery; + + $loadBalancer = MediaWikiServices::getInstance()->getDBLoadBalancer(); + $dbw = $loadBalancer->getConnection( DB_MASTER ); + $dbr = $loadBalancer->getConnection( DB_REPLICA, [ 'watchlist' ] ); + + // Wait before lock to try to reduce time waiting in the lock. + if ( !$loadBalancer->safeWaitForMasterPos( $dbr ) ) { + $this->setLastError( 'Timed out while waiting for slave to catch up before lock' ); + return false; + } + + // Use a named lock so that jobs for this user see each others' changes + $lockKey = "ClearUserWatchlistJob:$userId"; + $scopedLock = $dbw->getScopedLockAndFlush( $lockKey, __METHOD__, 10 ); + if ( !$scopedLock ) { + $this->setLastError( "Could not acquire lock '$lockKey'" ); + return false; + } + + if ( !$loadBalancer->safeWaitForMasterPos( $dbr ) ) { + $this->setLastError( 'Timed out while waiting for slave to catch up within lock' ); + return false; + } + + // Clear any stale REPEATABLE-READ snapshot + $dbr->flushSnapshot( __METHOD__ ); + + $watchlistIds = $dbr->selectFieldValues( + 'watchlist', + 'wl_id', + [ + 'wl_user' => $userId, + 'wl_id <= ' . $maxWatchlistId + ], + __METHOD__, + [ + 'ORDER BY' => 'wl_id ASC', + 'LIMIT' => $batchSize, + ] + ); + + if ( count( $watchlistIds ) == 0 ) { + return true; + } + + $dbw->delete( 'watchlist', [ 'wl_id' => $watchlistIds ], __METHOD__ ); + + // Commit changes and remove lock before inserting next job. + $lbf = MediaWikiServices::getInstance()->getDBLoadBalancerFactory(); + $lbf->commitMasterChanges( __METHOD__ ); + unset( $scopedLock ); + + if ( count( $watchlistIds ) === (int)$batchSize ) { + // Until we get less results than the limit, recursively push + // the same job again. + JobQueueGroup::singleton()->push( new self( $this->getTitle(), $this->getParams() ) ); + } + + return true; + } + + public function getDeduplicationInfo() { + $info = parent::getDeduplicationInfo(); + // This job never has a namespace or title so we can't use it for deduplication + unset( $info['namespace'] ); + unset( $info['title'] ); + return $info; + } + +} diff --git a/includes/libs/filebackend/SwiftFileBackend.php b/includes/libs/filebackend/SwiftFileBackend.php index 373ad93a34..27ce212bad 100644 --- a/includes/libs/filebackend/SwiftFileBackend.php +++ b/includes/libs/filebackend/SwiftFileBackend.php @@ -181,6 +181,29 @@ class SwiftFileBackend extends FileBackendStore { * @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. + * + * 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) + * + * @param array $params + * @return array Sanitized value of 'headers' field in $params + */ protected function sanitizeHdrs( array $params ) { return isset( $params['headers'] ) ? $this->getCustomHeaders( $params['headers'] ) @@ -197,7 +220,7 @@ class SwiftFileBackend extends FileBackendStore { // Normalize casing, and strip out illegal headers foreach ( $rawHeaders as $name => $value ) { $name = strtolower( $name ); - if ( preg_match( '/^content-(type|length)$/', $name ) ) { + if ( preg_match( '/^content-length$/', $name ) ) { continue; // blacklisted } elseif ( preg_match( '/^(x-)?content-/', $name ) ) { $headers[$name] = $value; // allowed @@ -276,7 +299,7 @@ class SwiftFileBackend extends FileBackendStore { 'etag' => md5( $params['content'] ), 'content-type' => $contentType, 'x-object-meta-sha1base36' => $sha1Hash - ] + $this->sanitizeHdrs( $params ), + ] + $this->sanitizeHdrsStrict( $params ), 'body' => $params['content'] ] ]; @@ -340,7 +363,7 @@ class SwiftFileBackend extends FileBackendStore { 'etag' => md5_file( $params['src'] ), 'content-type' => $contentType, 'x-object-meta-sha1base36' => $sha1Hash - ] + $this->sanitizeHdrs( $params ), + ] + $this->sanitizeHdrsStrict( $params ), 'body' => $handle // resource ] ]; @@ -391,7 +414,7 @@ class SwiftFileBackend extends FileBackendStore { 'headers' => [ 'x-copy-from' => '/' . rawurlencode( $srcCont ) . '/' . str_replace( "%2F", "/", rawurlencode( $srcRel ) ) - ] + $this->sanitizeHdrs( $params ), // extra headers merged into object + ] + $this->sanitizeHdrsStrict( $params ), // extra headers merged into object ] ]; $method = __METHOD__; @@ -440,7 +463,7 @@ class SwiftFileBackend extends FileBackendStore { 'headers' => [ 'x-copy-from' => '/' . rawurlencode( $srcCont ) . '/' . str_replace( "%2F", "/", rawurlencode( $srcRel ) ) - ] + $this->sanitizeHdrs( $params ) // extra headers merged into object + ] + $this->sanitizeHdrsStrict( $params ) // extra headers merged into object ] ]; if ( "{$srcCont}/{$srcRel}" !== "{$dstCont}/{$dstRel}" ) { diff --git a/includes/libs/mime/mime.info b/includes/libs/mime/mime.info index d8b8be7701..3670243935 100644 --- a/includes/libs/mime/mime.info +++ b/includes/libs/mime/mime.info @@ -87,6 +87,7 @@ application/x-tcsh [EXECUTABLE] application/x-tcl [EXECUTABLE] application/x-perl [EXECUTABLE] application/x-python [EXECUTABLE] +application/wasm [EXECUTABLE] application/pdf application/acrobat [OFFICE] application/msword [OFFICE] diff --git a/includes/libs/mime/mime.types b/includes/libs/mime/mime.types index f1cd59d18f..ef6854ce40 100644 --- a/includes/libs/mime/mime.types +++ b/includes/libs/mime/mime.types @@ -187,3 +187,4 @@ chemical/x-mdl-rdfile rd chemical/x-mdl-rgfile rg application/x-amf amf application/sla stl +application/wasm wasm diff --git a/includes/libs/objectcache/HashBagOStuff.php b/includes/libs/objectcache/HashBagOStuff.php index 6d583da07c..f8e3b17a8c 100644 --- a/includes/libs/objectcache/HashBagOStuff.php +++ b/includes/libs/objectcache/HashBagOStuff.php @@ -52,7 +52,7 @@ class HashBagOStuff extends BagOStuff { protected function expire( $key ) { $et = $this->bag[$key][self::KEY_EXP]; - if ( $et == self::TTL_INDEFINITE || $et > time() ) { + if ( $et == self::TTL_INDEFINITE || $et > $this->getCurrentTime() ) { return false; } @@ -115,4 +115,8 @@ class HashBagOStuff extends BagOStuff { public function clear() { $this->bag = []; } + + protected function getCurrentTime() { + return time(); + } } diff --git a/includes/libs/objectcache/RedisBagOStuff.php b/includes/libs/objectcache/RedisBagOStuff.php index 583ec37755..f720010f41 100644 --- a/includes/libs/objectcache/RedisBagOStuff.php +++ b/includes/libs/objectcache/RedisBagOStuff.php @@ -23,7 +23,10 @@ /** * Redis-based caching module for redis server >= 2.6.12 * - * @note: avoid use of Redis::MULTI transactions for twemproxy support + * @note Avoid use of Redis::MULTI transactions for twemproxy support + * + * @ingroup Cache + * @ingroup Redis */ class RedisBagOStuff extends BagOStuff { /** @var RedisConnectionPool */ diff --git a/includes/libs/objectcache/WANObjectCache.php b/includes/libs/objectcache/WANObjectCache.php index 51c466996b..8f2c72a141 100644 --- a/includes/libs/objectcache/WANObjectCache.php +++ b/includes/libs/objectcache/WANObjectCache.php @@ -91,6 +91,10 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface { protected $logger; /** @var StatsdDataFactoryInterface */ protected $stats; + /** @var bool Whether to use "interim" caching while keys are tombstoned */ + protected $useInterimHoldOffCaching = true; + /** @var callable|null Function that takes a WAN cache callback and runs it later */ + protected $asyncHandler; /** @var int ERR_* constant for the "last error" registry */ protected $lastRelayError = self::ERR_NONE; @@ -111,6 +115,9 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface { /** Seconds to keep dependency purge keys around */ const CHECK_KEY_TTL = self::TTL_YEAR; + /** Seconds to keep interim value keys for tombstoned keys around */ + const INTERIM_KEY_TTL = 1; + /** Seconds to keep lock keys around */ const LOCK_TTL = 10; /** Default remaining TTL at which to consider pre-emptive regeneration */ @@ -135,6 +142,11 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface { const TTL_LAGGED = 30; /** Idiom for delete() for "no hold-off" */ const HOLDOFF_NONE = 0; + /** Idiom for set()/getWithSetCallback() for "do not augment the storage medium TTL" */ + const STALE_TTL_NONE = 0; + /** Idiom for set()/getWithSetCallback() for "no post-expired grace period" */ + const GRACE_TTL_NONE = 0; + /** Idiom for getWithSetCallback() for "no minimum required as-of timestamp" */ const MIN_TIMESTAMP_NONE = 0.0; @@ -181,6 +193,13 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface { * - relayers : Map of (action => EventRelayer object). Actions include "purge". * - logger : LoggerInterface object * - stats : LoggerInterface object + * - asyncHandler : A function that takes a callback and runs it later. If supplied, + * whenever a preemptive refresh would be triggered in getWithSetCallback(), the + * current cache value is still used instead. However, the async-handler function + * receives a WAN cache callback that, when run, will execute the value generation + * callback supplied by the getWithSetCallback() caller. The result will be saved + * as normal. The handler is expected to call the WAN cache callback at an opportune + * time (e.g. HTTP post-send), though generally within a few 100ms. [optional] */ public function __construct( array $params ) { $this->cache = $params['cache']; @@ -192,6 +211,7 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface { : new EventRelayerNull( [] ); $this->setLogger( isset( $params['logger'] ) ? $params['logger'] : new NullLogger() ); $this->stats = isset( $params['stats'] ) ? $params['stats'] : new NullStatsdDataFactory(); + $this->asyncHandler = isset( $params['asyncHandler'] ) ? $params['asyncHandler'] : null; } public function setLogger( LoggerInterface $logger ) { @@ -204,7 +224,7 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface { * @return WANObjectCache */ public static function newEmpty() { - return new self( [ + return new static( [ 'cache' => new EmptyBagOStuff(), 'pool' => 'empty' ] ); @@ -311,7 +331,7 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface { $wrappedValues += $this->cache->getMulti( $keysGet ); } // Time used to compare/init "check" keys (derived after getMulti() to be pessimistic) - $now = microtime( true ); + $now = $this->getCurrentTime(); // Collect timestamps from all "check" keys $purgeValuesForAll = $this->processCheckKeys( $checkKeysForAll, $wrappedValues, $now ); @@ -367,13 +387,13 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface { $purgeValues = []; foreach ( $timeKeys as $timeKey ) { $purge = isset( $wrappedValues[$timeKey] ) - ? self::parsePurgeValue( $wrappedValues[$timeKey] ) + ? $this->parsePurgeValue( $wrappedValues[$timeKey] ) : false; if ( $purge === false ) { // Key is not set or invalid; regenerate $newVal = $this->makePurgeValue( $now, self::HOLDOFF_TTL ); $this->cache->add( $timeKey, $newVal, self::CHECK_KEY_TTL ); - $purge = self::parsePurgeValue( $newVal ); + $purge = $this->parsePurgeValue( $newVal ); } $purgeValues[] = $purge; } @@ -426,24 +446,25 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface { * they certainly should not see ones that ended up getting rolled back. * Default: false * - lockTSE : if excessive replication/snapshot lag is detected, then store the value - * with this TTL and flag it as stale. This is only useful if the reads for - * this key use getWithSetCallback() with "lockTSE" set. + * with this TTL and flag it as stale. This is only useful if the reads for this key + * use getWithSetCallback() with "lockTSE" set. Note that if "staleTTL" is set + * then it will still add on to this TTL in the excessive lag scenario. * Default: WANObjectCache::TSE_NONE * - staleTTL : Seconds to keep the key around if it is stale. The get()/getMulti() * methods return such stale values with a $curTTL of 0, and getWithSetCallback() * will call the regeneration callback in such cases, passing in the old value * and its as-of time to the callback. This is useful if adaptiveTTL() is used * on the old value's as-of time when it is verified as still being correct. - * Default: 0. + * Default: WANObjectCache::STALE_TTL_NONE. * @note Options added in 1.28: staleTTL * @return bool Success */ final public function set( $key, $value, $ttl = 0, array $opts = [] ) { - $now = microtime( true ); + $now = $this->getCurrentTime(); $lockTSE = isset( $opts['lockTSE'] ) ? $opts['lockTSE'] : self::TSE_NONE; + $staleTTL = isset( $opts['staleTTL'] ) ? $opts['staleTTL'] : self::STALE_TTL_NONE; $age = isset( $opts['since'] ) ? max( 0, $now - $opts['since'] ) : 0; $lag = isset( $opts['lag'] ) ? $opts['lag'] : 0; - $staleTTL = isset( $opts['staleTTL'] ) ? $opts['staleTTL'] : 0; // Do not cache potentially uncommitted data as it might get rolled back if ( !empty( $opts['pending'] ) ) { @@ -580,25 +601,102 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface { * Note that "check" keys won't collide with other regular keys. * * @param string $key - * @return float UNIX timestamp of the check key + * @return float UNIX timestamp */ final public function getCheckKeyTime( $key ) { - $key = self::TIME_KEY_PREFIX . $key; + return $this->getMultiCheckKeyTime( [ $key ] )[$key]; + } - $purge = self::parsePurgeValue( $this->cache->get( $key ) ); - if ( $purge !== false ) { - $time = $purge[self::FLD_TIME]; - } else { - // Casting assures identical floats for the next getCheckKeyTime() calls - $now = (string)microtime( true ); - $this->cache->add( $key, - $this->makePurgeValue( $now, self::HOLDOFF_TTL ), - self::CHECK_KEY_TTL - ); - $time = (float)$now; + /** + * Fetch the values of each timestamp "check" key + * + * This works like getCheckKeyTime() except it takes a list of keys + * and returns a map of timestamps instead of just that of one key + * + * This might be useful if both: + * - a) a class of entities each depend on hundreds of other entities + * - b) these other entities are depended upon by millions of entities + * + * The later entities can each use a "check" key to invalidate their dependee entities. + * However, it is expensive for the former entities to verify against all of the relevant + * "check" keys during each getWithSetCallback() call. A less expensive approach is to do + * these verifications only after a "time-till-verify" (TTV) has passed. This is a middle + * ground between using blind TTLs and using constant verification. The adaptiveTTL() method + * can be used to dynamically adjust the TTV. Also, the initial TTV can make use of the + * last-modified times of the dependant entities (either from the DB or the "check" keys). + * + * Example usage: + * @code + * $value = $cache->getWithSetCallback( + * $cache->makeGlobalKey( 'wikibase-item', $id ), + * self::INITIAL_TTV, // initial time-till-verify + * function ( $oldValue, &$ttv, &$setOpts, $oldAsOf ) use ( $checkKeys, $cache ) { + * $now = microtime( true ); + * // Use $oldValue if it passes max ultimate age and "check" key comparisons + * if ( $oldValue && + * $oldAsOf > max( $cache->getMultiCheckKeyTime( $checkKeys ) ) && + * ( $now - $oldValue['ctime'] ) <= self::MAX_CACHE_AGE + * ) { + * // Increase time-till-verify by 50% of last time to reduce overhead + * $ttv = $cache->adaptiveTTL( $oldAsOf, self::MAX_TTV, self::MIN_TTV, 1.5 ); + * // Unlike $oldAsOf, "ctime" is the ultimate age of the cached data + * return $oldValue; + * } + * + * $mtimes = []; // dependency last-modified times; passed by reference + * $value = [ 'data' => $this->fetchEntityData( $mtimes ), 'ctime' => $now ]; + * // Guess time-till-change among the dependencies, e.g. 1/(total change rate) + * $ttc = 1 / array_sum( array_map( + * function ( $mtime ) use ( $now ) { + * return 1 / ( $mtime ? ( $now - $mtime ) : 900 ); + * }, + * $mtimes + * ) ); + * // The time-to-verify should not be overly pessimistic nor optimistic + * $ttv = min( max( $ttc, self::MIN_TTV ), self::MAX_TTV ); + * + * return $value; + * }, + * [ 'staleTTL' => $cache::TTL_DAY ] // keep around to verify and re-save + * ); + * @endcode + * + * @see WANObjectCache::getCheckKeyTime() + * @see WANObjectCache::getWithSetCallback() + * + * @param array $keys + * @return float[] Map of (key => UNIX timestamp) + * @since 1.31 + */ + final public function getMultiCheckKeyTime( array $keys ) { + $rawKeys = []; + foreach ( $keys as $key ) { + $rawKeys[$key] = self::TIME_KEY_PREFIX . $key; } - return $time; + $rawValues = $this->cache->getMulti( $rawKeys ); + $rawValues += array_fill_keys( $rawKeys, false ); + + $times = []; + foreach ( $rawKeys as $key => $rawKey ) { + $purge = $this->parsePurgeValue( $rawValues[$rawKey] ); + if ( $purge !== false ) { + $time = $purge[self::FLD_TIME]; + } else { + // Casting assures identical floats for the next getCheckKeyTime() calls + $now = (string)$this->getCurrentTime(); + $this->cache->add( + $rawKey, + $this->makePurgeValue( $now, self::HOLDOFF_TTL ), + self::CHECK_KEY_TTL + ); + $time = (float)$now; + } + + $times[$key] = $time; + } + + return $times; } /** @@ -609,20 +707,21 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface { * on all keys that should be changed. When get() is called on those * keys, the relevant "check" keys must be supplied for this to work. * - * The "check" key essentially represents a last-modified field. - * When touched, the field will be updated on all cache servers. - * Keys using it via get(), getMulti(), or getWithSetCallback() will - * be invalidated. It is treated as being HOLDOFF_TTL seconds in the future - * by those methods to avoid race conditions where dependent keys get updated - * with stale values (e.g. from a DB replica DB). - * - * This is typically useful for keys with hardcoded names or in some cases - * dynamically generated names where a low number of combinations exist. - * When a few important keys get a large number of hits, a high cache - * time is usually desired as well as "lockTSE" logic. The resetCheckKey() - * method is less appropriate in such cases since the "time since expiry" - * cannot be inferred, causing any get() after the reset to treat the key - * as being "hot", resulting in more stale value usage. + * The "check" key essentially represents a last-modified time of an entity. + * When the key is touched, the timestamp will be updated to the current time. + * Keys using the "check" key via get(), getMulti(), or getWithSetCallback() will + * be invalidated. This approach is useful if many keys depend on a single entity. + * + * The timestamp of the "check" key is treated as being HOLDOFF_TTL seconds in the + * future by get*() methods in order to avoid race conditions where keys are updated + * with stale values (e.g. from a lagged replica DB). A high TTL is set on the "check" + * key, making it possible to know the timestamp of the last change to the corresponding + * entities in most cases. This might use more cache space than resetCheckKey(). + * + * When a few important keys get a large number of hits, a high cache time is usually + * desired as well as "lockTSE" logic. The resetCheckKey() method is less appropriate + * in such cases since the "time since expiry" cannot be inferred, causing any get() + * after the reset to treat the key as being "hot", resulting in more stale value usage. * * Note that "check" keys won't collide with other regular keys. * @@ -653,12 +752,9 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface { * to, any temporary ejection of that server will cause the value to be * seen as purged as a new server will initialize the "check" key. * - * The advantage is that this does not place high TTL keys on every cache - * server, making it better for code that will cache many different keys - * and either does not use lockTSE or uses a low enough TTL anyway. - * - * This is typically useful for keys with dynamically generated names - * where a high number of combinations exist. + * The advantage here is that the "check" keys, which have high TTLs, will only + * be created when a get*() method actually uses that key. This is better when + * a large number of "check" keys are invalided in a short period of time. * * Note that "check" keys won't collide with other regular keys. * @@ -810,13 +906,21 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface { * * @param string $key Cache key made from makeKey() or makeGlobalKey() * @param int $ttl Seconds to live for key updates. Special values are: - * - WANObjectCache::TTL_INDEFINITE: Cache forever - * - WANObjectCache::TTL_UNCACHEABLE: Do not cache at all + * - WANObjectCache::TTL_INDEFINITE: Cache forever (subject to LRU-style evictions) + * - WANObjectCache::TTL_UNCACHEABLE: Do not cache (if the key exists, it is not deleted) * @param callable $callback Value generation function * @param array $opts Options map: * - checkKeys: List of "check" keys. The key at $key will be seen as invalid when either - * touchCheckKey() or resetCheckKey() is called on any of these keys. + * touchCheckKey() or resetCheckKey() is called on any of the keys in this list. This + * is useful if thousands or millions of keys depend on the same entity. The entity can + * simply have its "check" key updated whenever the entity is modified. * Default: []. + * - graceTTL: Consider reusing expired values instead of refreshing them if they expired + * less than this many seconds ago. The odds of a refresh becomes more likely over time, + * becoming certain once the grace period is reached. This can reduce traffic spikes + * when millions of keys are compared to the same "check" key and touchCheckKey() + * or resetCheckKey() is called on that "check" key. + * Default: WANObjectCache::GRACE_TTL_NONE. * - lockTSE: If the key is tombstoned or expired (by checkKeys) less than this many seconds * ago, then try to have a single thread handle cache regeneration at any given time. * Other threads will try to use stale values if possible. If, on miss, the time since @@ -851,21 +955,29 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface { * This is useful if the source of a key is suspected of having possibly changed * recently, and the caller wants any such changes to be reflected. * Default: WANObjectCache::MIN_TIMESTAMP_NONE. - * - hotTTR: Expected time-till-refresh (TTR) for keys that average ~1 hit/second (1 Hz). - * Keys with a hit rate higher than 1Hz will refresh sooner than this TTR and vise versa. - * Such refreshes won't happen until keys are "ageNew" seconds old. The TTR is useful at + * - hotTTR: Expected time-till-refresh (TTR) in seconds for keys that average ~1 hit per + * second (e.g. 1Hz). Keys with a hit rate higher than 1Hz will refresh sooner than this + * TTR and vise versa. Such refreshes won't happen until keys are "ageNew" seconds old. + * This uses randomization to avoid triggering cache stampedes. The TTR is useful at * reducing the impact of missed cache purges, since the effect of a heavily referenced * key being stale is worse than that of a rarely referenced key. Unlike simply lowering - * $ttl, seldomly used keys are largely unaffected by this option, which makes it possible - * to have a high hit rate for the "long-tail" of less-used keys. + * $ttl, seldomly used keys are largely unaffected by this option, which makes it + * possible to have a high hit rate for the "long-tail" of less-used keys. * Default: WANObjectCache::HOT_TTR. * - lowTTL: Consider pre-emptive updates when the current TTL (seconds) of the key is less * than this. It becomes more likely over time, becoming certain once the key is expired. + * This helps avoid cache stampedes that might be triggered due to the key expiring. * Default: WANObjectCache::LOW_TTL. * - ageNew: Consider popularity refreshes only once a key reaches this age in seconds. * Default: WANObjectCache::AGE_NEW. + * - staleTTL: Seconds to keep the key around if it is stale. This means that on cache + * miss the callback may get $oldValue/$oldAsOf values for keys that have already been + * expired for this specified time. This is useful if adaptiveTTL() is used on the old + * value's as-of time when it is verified as still being correct. + * Default: WANObjectCache::STALE_TTL_NONE * @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 * @note Callable type hints are not used to avoid class-autoloading */ final public function getWithSetCallback( $key, $ttl, $callback, array $opts = [] ) { @@ -895,11 +1007,14 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface { use ( $callback, $version ) { if ( is_array( $oldValue ) && array_key_exists( self::VFLD_DATA, $oldValue ) + && array_key_exists( self::VFLD_VERSION, $oldValue ) + && $oldValue[self::VFLD_VERSION] === $version ) { $oldData = $oldValue[self::VFLD_DATA]; } else { // VFLD_DATA is not set if an old, unversioned, key is present $oldData = false; + $oldAsOf = null; } return [ @@ -953,6 +1068,8 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface { protected function doGetWithSetCallback( $key, $ttl, $callback, array $opts, &$asOf = null ) { $lowTTL = isset( $opts['lowTTL'] ) ? $opts['lowTTL'] : min( self::LOW_TTL, $ttl ); $lockTSE = isset( $opts['lockTSE'] ) ? $opts['lockTSE'] : self::TSE_NONE; + $staleTTL = isset( $opts['staleTTL'] ) ? $opts['staleTTL'] : self::STALE_TTL_NONE; + $graceTTL = isset( $opts['graceTTL'] ) ? $opts['graceTTL'] : self::GRACE_TTL_NONE; $checkKeys = isset( $opts['checkKeys'] ) ? $opts['checkKeys'] : []; $busyValue = isset( $opts['busyValue'] ) ? $opts['busyValue'] : null; $popWindow = isset( $opts['hotTTR'] ) ? $opts['hotTTR'] : self::HOT_TTR; @@ -968,24 +1085,39 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface { $cValue = $this->get( $key, $curTTL, $checkKeys, $asOf ); // current value $value = $cValue; // return value - $preCallbackTime = microtime( true ); + $preCallbackTime = $this->getCurrentTime(); // Determine if a cached value regeneration is needed or desired if ( $value !== false - && $curTTL > 0 + && $this->isAliveOrInGracePeriod( $curTTL, $graceTTL ) && $this->isValid( $value, $versioned, $asOf, $minTime ) - && !$this->worthRefreshExpiring( $curTTL, $lowTTL ) - && !$this->worthRefreshPopular( $asOf, $ageNew, $popWindow, $preCallbackTime ) ) { - $this->stats->increment( "wanobjectcache.$kClass.hit.good" ); + $preemptiveRefresh = ( + $this->worthRefreshExpiring( $curTTL, $lowTTL ) || + $this->worthRefreshPopular( $asOf, $ageNew, $popWindow, $preCallbackTime ) + ); - return $value; + if ( !$preemptiveRefresh ) { + $this->stats->increment( "wanobjectcache.$kClass.hit.good" ); + + return $value; + } elseif ( $this->asyncHandler ) { + // Update the cache value later, such during post-send of an HTTP request + $func = $this->asyncHandler; + $func( function () use ( $key, $ttl, $callback, $opts, $asOf ) { + $opts['minAsOf'] = INF; // force a refresh + $this->doGetWithSetCallback( $key, $ttl, $callback, $opts, $asOf ); + } ); + $this->stats->increment( "wanobjectcache.$kClass.hit.refresh" ); + + return $value; + } } // A deleted key with a negative TTL left must be tombstoned $isTombstone = ( $curTTL !== null && $value === false ); if ( $isTombstone && $lockTSE <= 0 ) { // Use the INTERIM value for tombstoned keys to reduce regeneration load - $lockTSE = 1; + $lockTSE = self::INTERIM_KEY_TTL; } // Assume a key is hot if requested soon after invalidation $isHot = ( $curTTL !== null && $curTTL <= 0 && abs( $curTTL ) <= $lockTSE ); @@ -1044,7 +1176,7 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface { // so use a special INTERIM key to pass the new value around threads. if ( ( $isTombstone && $lockTSE > 0 ) && $valueIsCacheable ) { $tempTTL = max( 1, (int)$lockTSE ); // set() expects seconds - $newAsOf = microtime( true ); + $newAsOf = $this->getCurrentTime(); $wrapped = $this->wrap( $value, $tempTTL, $newAsOf ); // Avoid using set() to avoid pointless mcrouter broadcasting $this->setInterimValue( $key, $wrapped, $tempTTL ); @@ -1052,6 +1184,7 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface { if ( $valueIsCacheable ) { $setOpts['lockTSE'] = $lockTSE; + $setOpts['staleTTL'] = $staleTTL; // Use best known "since" timestamp if not provided $setOpts += [ 'since' => $preCallbackTime ]; // Update the cache; this will fail if the key is tombstoned @@ -1076,8 +1209,12 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface { * @return mixed */ protected function getInterimValue( $key, $versioned, $minTime, &$asOf ) { + if ( !$this->useInterimHoldOffCaching ) { + return false; // disabled + } + $wrapped = $this->cache->get( self::INTERIM_KEY_PREFIX . $key ); - list( $value ) = $this->unwrap( $wrapped, microtime( true ) ); + list( $value ) = $this->unwrap( $wrapped, $this->getCurrentTime() ); if ( $value !== false && $this->isValid( $value, $versioned, $asOf, $minTime ) ) { $asOf = $wrapped[self::FLD_TIME]; @@ -1339,7 +1476,7 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface { * @return bool Success * @since 1.28 */ - public function reap( $key, $purgeTimestamp, &$isStale = false ) { + final public function reap( $key, $purgeTimestamp, &$isStale = false ) { $minAsOf = $purgeTimestamp + self::HOLDOFF_TTL; $wrapped = $this->cache->get( self::VALUE_KEY_PREFIX . $key ); if ( is_array( $wrapped ) && $wrapped[self::FLD_TIME] < $minAsOf ) { @@ -1368,7 +1505,7 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface { * @return bool Success * @since 1.28 */ - public function reapCheckKey( $key, $purgeTimestamp, &$isStale = false ) { + final public function reapCheckKey( $key, $purgeTimestamp, &$isStale = false ) { $purge = $this->parsePurgeValue( $this->cache->get( self::TIME_KEY_PREFIX . $key ) ); if ( $purge && $purge[self::FLD_TIME] < $purgeTimestamp ) { $isStale = true; @@ -1414,7 +1551,7 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface { * @return ArrayIterator Iterator yielding (cache key => entity ID) in $entities order * @since 1.28 */ - public function makeMultiKeys( array $entities, callable $keyFunc ) { + final public function makeMultiKeys( array $entities, callable $keyFunc ) { $map = []; foreach ( $entities as $entity ) { $map[$keyFunc( $entity, $this )] = $entity; @@ -1467,6 +1604,30 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface { $this->processCaches = []; } + /** + * Enable or disable the use of brief caching for tombstoned keys + * + * When a key is purged via delete(), there normally is a period where caching + * is hold-off limited to an extremely short time. This method will disable that + * caching, forcing the callback to run for any of: + * - WANObjectCache::getWithSetCallback() + * - WANObjectCache::getMultiWithSetCallback() + * - WANObjectCache::getMultiWithUnionSetCallback() + * + * This is useful when both: + * - a) the database used by the callback is known to be up-to-date enough + * for some particular purpose (e.g. replica DB has applied transaction X) + * - b) the caller needs to exploit that fact, and therefore needs to avoid the + * use of inherently volatile and possibly stale interim keys + * + * @see WANObjectCache::delete() + * @param bool $enabled Whether to enable interim caching + * @since 1.31 + */ + final public function useInterimHoldOffCaching( $enabled ) { + $this->useInterimHoldOffCaching = $enabled; + } + /** * @param int $flag ATTR_* class constant * @return int QOS_* class constant @@ -1492,6 +1653,46 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface { * $ttl = $cache->adaptiveTTL( $mtime, $cache::TTL_DAY ); * @endcode * + * Another use case is when there are no applicable "last modified" fields in the DB, + * and there are too many dependencies for explicit purges to be viable, and the rate of + * change to relevant content is unstable, and it is highly valued to have the cached value + * be as up-to-date as possible. + * + * Example usage: + * @code + * $query = ""; + * $idListFromComplexQuery = $cache->getWithSetCallback( + * $cache->makeKey( 'complex-graph-query', $hashOfQuery ), + * GraphQueryClass::STARTING_TTL, + * function ( $oldValue, &$ttl, array &$setOpts, $oldAsOf ) use ( $query, $cache ) { + * $gdb = $this->getReplicaGraphDbConnection(); + * // Account for any snapshot/replica DB lag + * $setOpts += GraphDatabase::getCacheSetOptions( $gdb ); + * + * $newList = iterator_to_array( $gdb->query( $query ) ); + * sort( $newList, SORT_NUMERIC ); // normalize + * + * $minTTL = GraphQueryClass::MIN_TTL; + * $maxTTL = GraphQueryClass::MAX_TTL; + * if ( $oldValue !== false ) { + * // Note that $oldAsOf is the last time this callback ran + * $ttl = ( $newList === $oldValue ) + * // No change: cache for 150% of the age of $oldValue + * ? $cache->adaptiveTTL( $oldAsOf, $maxTTL, $minTTL, 1.5 ) + * // Changed: cache for %50 of the age of $oldValue + * : $cache->adaptiveTTL( $oldAsOf, $maxTTL, $minTTL, .5 ); + * } + * + * return $newList; + * }, + * [ + * // Keep stale values around for doing comparisons for TTL calculations. + * // High values improve long-tail keys hit-rates, though might waste space. + * 'staleTTL' => GraphQueryClass::GRACE_TTL + * ] + * ); + * @endcode + * * @param int|float $mtime UNIX timestamp * @param int $maxTTL Maximum TTL (seconds) * @param int $minTTL Minimum TTL (seconds); Default: 30 @@ -1508,7 +1709,7 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface { return $minTTL; // no last-modified time provided } - $age = time() - $mtime; + $age = $this->getCurrentTime() - $mtime; return (int)min( $maxTTL, max( $minTTL, $factor * $age ) ); } @@ -1517,7 +1718,7 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface { * @return int Number of warmup key cache misses last round * @since 1.30 */ - public function getWarmupKeyMisses() { + final public function getWarmupKeyMisses() { return $this->warmupKeyMisses; } @@ -1535,7 +1736,7 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface { if ( $this->purgeRelayer instanceof EventRelayerNull ) { // This handles the mcrouter and the single-DC case $ok = $this->cache->set( $key, - $this->makePurgeValue( microtime( true ), self::HOLDOFF_NONE ), + $this->makePurgeValue( $this->getCurrentTime(), self::HOLDOFF_NONE ), $ttl ); } else { @@ -1581,23 +1782,56 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface { return $ok; } + /** + * Check if a key is fresh or in the grace window and thus due for randomized reuse + * + * If $curTTL > 0 (e.g. not expired) this returns true. Otherwise, the chance of returning + * true decrease steadily from 100% to 0% as the |$curTTL| moves from 0 to $graceTTL seconds. + * This handles widely varying levels of cache access traffic. + * + * If $curTTL <= -$graceTTL (e.g. already expired), then this returns false. + * + * @param float $curTTL Approximate TTL left on the key if present + * @param int $graceTTL Consider using stale values if $curTTL is greater than this + * @return bool + */ + protected function isAliveOrInGracePeriod( $curTTL, $graceTTL ) { + if ( $curTTL > 0 ) { + return true; + } elseif ( $graceTTL <= 0 ) { + return false; + } + + $ageStale = abs( $curTTL ); // seconds of staleness + $curGTTL = ( $graceTTL - $ageStale ); // current grace-time-to-live + if ( $curGTTL <= 0 ) { + return false; // already out of grace period + } + + // Chance of using a stale value is the complement of the chance of refreshing it + return !$this->worthRefreshExpiring( $curGTTL, $graceTTL ); + } + /** * Check if a key is nearing expiration and thus due for randomized regeneration * - * This returns false if $curTTL >= $lowTTL. Otherwise, the chance - * of returning true increases steadily from 0% to 100% as the $curTTL - * moves from $lowTTL to 0 seconds. This handles widely varying - * levels of cache access traffic. + * This returns false if $curTTL >= $lowTTL. Otherwise, the chance of returning true + * increases steadily from 0% to 100% as the $curTTL moves from $lowTTL to 0 seconds. + * This handles widely varying levels of cache access traffic. + * + * If $curTTL <= 0 (e.g. already expired), then this returns false. * * @param float $curTTL Approximate TTL left on the key if present * @param float $lowTTL Consider a refresh when $curTTL is less than this * @return bool */ protected function worthRefreshExpiring( $curTTL, $lowTTL ) { - if ( $curTTL >= $lowTTL ) { + if ( $lowTTL <= 0 ) { + return false; + } elseif ( $curTTL >= $lowTTL ) { return false; } elseif ( $curTTL <= 0 ) { - return true; + return false; } $chance = ( 1 - $curTTL / $lowTTL ); @@ -1621,6 +1855,10 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface { * @return bool */ protected function worthRefreshPopular( $asOf, $ageNew, $timeTillRefresh, $now ) { + if ( $ageNew < 0 || $timeTillRefresh <= 0 ) { + return false; + } + $age = $now - $asOf; $timeOld = $age - $ageNew; if ( $timeOld <= 0 ) { @@ -1687,7 +1925,7 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface { */ protected function unwrap( $wrapped, $now ) { // Check if the value is a tombstone - $purge = self::parsePurgeValue( $wrapped ); + $purge = $this->parsePurgeValue( $wrapped ); if ( $purge !== false ) { // Purged values should always have a negative current $ttl $curTTL = min( $purge[self::FLD_TIME] - $now, self::TINY_NEGATIVE ); @@ -1742,12 +1980,20 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface { return isset( $parts[1] ) ? $parts[1] : $parts[0]; // sanity } + /** + * @return float UNIX timestamp + * @codeCoverageIgnore + */ + protected function getCurrentTime() { + return microtime( true ); + } + /** * @param string $value Wrapped value like "PURGED::" * @return array|bool Array containing a UNIX timestamp (float) and holdoff period (integer), * or false if value isn't a valid purge value */ - protected static function parsePurgeValue( $value ) { + protected function parsePurgeValue( $value ) { if ( !is_string( $value ) ) { return false; } diff --git a/includes/libs/rdbms/database/Database.php b/includes/libs/rdbms/database/Database.php index e04566eb49..7f0718c952 100644 --- a/includes/libs/rdbms/database/Database.php +++ b/includes/libs/rdbms/database/Database.php @@ -461,9 +461,12 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware protected function ignoreErrors( $ignoreErrors = null ) { $res = $this->getFlag( self::DBO_IGNORE ); if ( $ignoreErrors !== null ) { - $ignoreErrors - ? $this->setFlag( self::DBO_IGNORE ) - : $this->clearFlag( self::DBO_IGNORE ); + // setFlag()/clearFlag() do not allow DBO_IGNORE changes for sanity + if ( $ignoreErrors ) { + $this->mFlags |= self::DBO_IGNORE; + } else { + $this->mFlags &= ~self::DBO_IGNORE; + } } return $res; @@ -621,6 +624,10 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware } public function setFlag( $flag, $remember = self::REMEMBER_NOTHING ) { + if ( ( $flag & self::DBO_IGNORE ) ) { + throw new \UnexpectedValueException( "Modifying DBO_IGNORE is not allowed." ); + } + if ( $remember === self::REMEMBER_PRIOR ) { array_push( $this->priorFlags, $this->mFlags ); } @@ -628,6 +635,10 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware } public function clearFlag( $flag, $remember = self::REMEMBER_NOTHING ) { + if ( ( $flag & self::DBO_IGNORE ) ) { + throw new \UnexpectedValueException( "Modifying DBO_IGNORE is not allowed." ); + } + if ( $remember === self::REMEMBER_PRIOR ) { array_push( $this->priorFlags, $this->mFlags ); } @@ -2019,9 +2030,19 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware if ( is_array( $table ) ) { // A parenthesized group - $joinedTable = '(' - . $this->tableNamesWithIndexClauseOrJOIN( $table, $use_index, $ignore_index, $join_conds ) - . ')'; + if ( count( $table ) > 1 ) { + $joinedTable = '(' + . $this->tableNamesWithIndexClauseOrJOIN( $table, $use_index, $ignore_index, $join_conds ) + . ')'; + } else { + // Degenerate case + $innerTable = reset( $table ); + $innerAlias = key( $table ); + $joinedTable = $this->tableNameWithAlias( + $innerTable, + is_string( $innerAlias ) ? $innerAlias : $innerTable + ); + } } else { $joinedTable = $this->tableNameWithAlias( $table, $alias ); } @@ -3273,14 +3294,15 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware * @see WANObjectCache::getWithSetCallback() * * @param IDatabase $db1 - * @param IDatabase $dbs,... + * @param IDatabase $db2 [optional] * @return array Map of values: * - lag: highest lag of any of the DBs or false on error (e.g. replication stopped) * - since: oldest UNIX timestamp of any of the DB lag estimates * - pending: whether any of the DBs have uncommitted changes + * @throws DBError * @since 1.27 */ - public static function getCacheSetOptions( IDatabase $db1 ) { + public static function getCacheSetOptions( IDatabase $db1, IDatabase $db2 = null ) { $res = [ 'lag' => 0, 'since' => INF, 'pending' => false ]; foreach ( func_get_args() as $db ) { /** @var IDatabase $db */ diff --git a/includes/libs/rdbms/database/DatabaseMysqlBase.php b/includes/libs/rdbms/database/DatabaseMysqlBase.php index e0ff475959..305a056900 100644 --- a/includes/libs/rdbms/database/DatabaseMysqlBase.php +++ b/includes/libs/rdbms/database/DatabaseMysqlBase.php @@ -1044,7 +1044,7 @@ abstract class DatabaseMysqlBase extends Database { return true; } - $this->queryLogger->warning( __METHOD__ . " failed to acquire lock '{lockname}'", + $this->queryLogger->info( __METHOD__ . " failed to acquire lock '{lockname}'", [ 'lockname' => $lockName ] ); return false; diff --git a/includes/libs/rdbms/database/IDatabase.php b/includes/libs/rdbms/database/IDatabase.php index 868c2d4b00..85b3481fe3 100644 --- a/includes/libs/rdbms/database/IDatabase.php +++ b/includes/libs/rdbms/database/IDatabase.php @@ -1,10 +1,5 @@ [ 'LEFT JOIN', 'page_latest=rev_id' ] ] * - * @return IResultWrapper|bool If the query returned no rows, a IResultWrapper - * with no rows in it will be returned. If there was a query error, a - * DBQueryError exception will be thrown, except if the "ignore errors" - * option was set, in which case false will be returned. + * @return IResultWrapper Resulting rows + * @throws DBError */ public function select( $table, $vars, $conds = '', $fname = __METHOD__, @@ -799,6 +797,7 @@ interface IDatabase { * @param array|string $join_conds Join conditions * * @return stdClass|bool + * @throws DBError */ public function selectRow( $table, $vars, $conds, $fname = __METHOD__, $options = [], $join_conds = [] @@ -823,6 +822,7 @@ interface IDatabase { * @param string $fname Function name for profiling * @param array $options Options for select * @return int Row count + * @throws DBError */ public function estimateRowCount( $table, $vars = '*', $conds = '', $fname = __METHOD__, $options = [] @@ -844,6 +844,7 @@ interface IDatabase { * @param array $options Options for select * @param array $join_conds Join conditions (since 1.27) * @return int Row count + * @throws DBError */ public function selectRowCount( $tables, $vars = '*', $conds = '', $fname = __METHOD__, $options = [], $join_conds = [] @@ -856,6 +857,7 @@ 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 */ public function fieldExists( $table, $field, $fname = __METHOD__ ); @@ -868,6 +870,7 @@ interface IDatabase { * @param string $index * @param string $fname * @return bool|null + * @throws DBError */ public function indexExists( $table, $index, $fname = __METHOD__ ); @@ -877,6 +880,7 @@ interface IDatabase { * @param string $table * @param string $fname * @return bool + * @throws DBError */ public function tableExists( $table, $fname = __METHOD__ ); @@ -922,6 +926,7 @@ interface IDatabase { * @param array $options Array of options * * @return bool + * @throws DBError */ public function insert( $table, $a, $fname = __METHOD__, $options = [] ); @@ -944,6 +949,7 @@ interface IDatabase { * - IGNORE: Ignore unique key conflicts * - LOW_PRIORITY: MySQL-specific, see MySQL manual. * @return bool + * @throws DBError */ public function update( $table, $values, $conds, $fname = __METHOD__, $options = [] ); @@ -1164,6 +1170,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 */ public function replace( $table, $uniqueIndexes, $rows, $fname = __METHOD__ ); @@ -1200,7 +1207,7 @@ 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 Exception + * @throws DBError * @return bool */ public function upsert( @@ -1225,7 +1232,7 @@ interface IDatabase { * @param array $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 DBUnexpectedError + * @throws DBError */ public function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname = __METHOD__ @@ -1240,6 +1247,7 @@ interface IDatabase { * @param string $fname Name of the calling function * @throws DBUnexpectedError * @return bool|IResultWrapper + * @throws DBError */ public function delete( $table, $conds, $fname = __METHOD__ ); @@ -1270,6 +1278,7 @@ interface IDatabase { * IDatabase::select() for details. * * @return bool + * @throws DBError */ public function insertSelect( $destTable, $srcTable, $varMap, $conds, $fname = __METHOD__, @@ -1351,6 +1360,7 @@ interface IDatabase { * Determines how long the server has been up * * @return int + * @throws DBError */ public function getServerUptime(); @@ -1391,13 +1401,15 @@ 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 */ public function masterPosWait( DBMasterPos $pos, $timeout ); /** * Get the replication position of this replica DB * - * @return DBMasterPos|bool False if this is not a replica DB. + * @return DBMasterPos|bool False if this is not a replica DB + * @throws DBError */ public function getReplicaPos(); @@ -1405,6 +1417,7 @@ interface IDatabase { * Get the position of this master * * @return DBMasterPos|bool False if this is not a master + * @throws DBError */ public function getMasterPos(); @@ -1593,7 +1606,7 @@ interface IDatabase { * Only set the flush flag if you are sure that these warnings are not applicable, * and no explicit transactions are open. * - * @throws DBUnexpectedError + * @throws DBError */ public function commit( $fname = __METHOD__, $flush = '' ); @@ -1614,7 +1627,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 DBUnexpectedError + * @throws DBError * @since 1.23 Added $flush parameter */ public function rollback( $fname = __METHOD__, $flush = '' ); @@ -1628,7 +1641,7 @@ interface IDatabase { * useful to call on a replica DB after waiting on replication to catch up to the master. * * @param string $fname Calling function name - * @throws DBUnexpectedError + * @throws DBError * @since 1.28 */ public function flushSnapshot( $fname = __METHOD__ ); @@ -1687,6 +1700,7 @@ interface IDatabase { * instead. * * @return int|bool Database replication lag in seconds or false on error + * @throws DBError */ public function getLag(); @@ -1701,6 +1715,7 @@ interface IDatabase { * indication of the staleness of subsequent reads. * * @return array ('lag': seconds or false on error, 'since': UNIX timestamp of BEGIN) + * @throws DBError * @since 1.27 */ public function getSessionLagStatus(); @@ -1742,6 +1757,7 @@ interface IDatabase { * * @param array $options * @return void + * @throws DBError */ public function setSessionOptions( array $options ); @@ -1760,6 +1776,7 @@ interface IDatabase { * @param string $lockName Name of lock to poll * @param string $method Name of method calling us * @return bool + * @throws DBError * @since 1.20 */ public function lockIsFree( $lockName, $method ); @@ -1773,6 +1790,7 @@ interface IDatabase { * @param string $method Name of the calling method * @param int $timeout Acquisition timeout in seconds * @return bool + * @throws DBError */ public function lock( $lockName, $method, $timeout = 5 ); @@ -1785,8 +1803,10 @@ interface IDatabase { * @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 + * by this thread (in which case the lock is not released), and NULL if the named lock + * did not exist + * + * @throws DBError */ public function unlock( $lockName, $method ); @@ -1808,7 +1828,7 @@ interface IDatabase { * @param string $fname Name of the calling method * @param int $timeout Acquisition timeout in seconds * @return ScopedCallback|null - * @throws DBUnexpectedError + * @throws DBError * @since 1.27 */ public function getScopedLockAndFlush( $lockKey, $fname, $timeout ); diff --git a/includes/libs/rdbms/database/IMaintainableDatabase.php b/includes/libs/rdbms/database/IMaintainableDatabase.php index fbc2774b05..66012dae43 100644 --- a/includes/libs/rdbms/database/IMaintainableDatabase.php +++ b/includes/libs/rdbms/database/IMaintainableDatabase.php @@ -20,7 +20,6 @@ * http://www.gnu.org/copyleft/gpl.html * * @file - * @ingroup Database */ namespace Wikimedia\Rdbms; diff --git a/includes/libs/rdbms/exception/DBExpectedError.php b/includes/libs/rdbms/exception/DBExpectedError.php index cae7f3e1a6..406d82c139 100644 --- a/includes/libs/rdbms/exception/DBExpectedError.php +++ b/includes/libs/rdbms/exception/DBExpectedError.php @@ -16,7 +16,6 @@ * http://www.gnu.org/copyleft/gpl.html * * @file - * @ingroup Database */ namespace Wikimedia\Rdbms; diff --git a/includes/libs/rdbms/lbfactory/ILBFactory.php b/includes/libs/rdbms/lbfactory/ILBFactory.php index f6d080e4f0..697af0ed76 100644 --- a/includes/libs/rdbms/lbfactory/ILBFactory.php +++ b/includes/libs/rdbms/lbfactory/ILBFactory.php @@ -319,6 +319,7 @@ interface ILBFactory { * - IPAddress : IP address * - UserAgent : User-Agent HTTP header * - ChronologyProtection : cookie/header value specifying ChronologyProtector usage + * - ChronologyPositionTime: timestamp used to get up-to-date DB positions for the agent */ public function setRequestInfo( array $info ); } diff --git a/includes/libs/rdbms/lbfactory/LBFactory.php b/includes/libs/rdbms/lbfactory/LBFactory.php index c891fb6ba7..ef716b68a6 100644 --- a/includes/libs/rdbms/lbfactory/LBFactory.php +++ b/includes/libs/rdbms/lbfactory/LBFactory.php @@ -115,7 +115,8 @@ abstract class LBFactory implements ILBFactory { $this->requestInfo = [ 'IPAddress' => isset( $_SERVER[ 'REMOTE_ADDR' ] ) ? $_SERVER[ 'REMOTE_ADDR' ] : '', 'UserAgent' => isset( $_SERVER['HTTP_USER_AGENT'] ) ? $_SERVER['HTTP_USER_AGENT'] : '', - 'ChronologyProtection' => 'true' + 'ChronologyProtection' => 'true', + 'ChronologyPositionTime' => isset( $_GET['cpPosTime'] ) ? $_GET['cpPosTime'] : null ]; $this->cliMode = isset( $conf['cliMode'] ) ? $conf['cliMode'] : PHP_SAPI === 'cli'; @@ -440,7 +441,7 @@ abstract class LBFactory implements ILBFactory { 'ip' => $this->requestInfo['IPAddress'], 'agent' => $this->requestInfo['UserAgent'], ], - isset( $_GET['cpPosTime'] ) ? $_GET['cpPosTime'] : null + $this->requestInfo['ChronologyPositionTime'] ); $this->chronProt->setLogger( $this->replLogger ); diff --git a/includes/libs/rdbms/lbfactory/LBFactoryMulti.php b/includes/libs/rdbms/lbfactory/LBFactoryMulti.php index 0384588ddc..cfa26479bb 100644 --- a/includes/libs/rdbms/lbfactory/LBFactoryMulti.php +++ b/includes/libs/rdbms/lbfactory/LBFactoryMulti.php @@ -107,6 +107,12 @@ class LBFactoryMulti extends LBFactory { /** @var string */ private $lastSection; + /** @var int */ + private $maxLag = self::MAX_LAG_DEFAULT; + + /** @var int Default 'maxLag' when unspecified */ + const MAX_LAG_DEFAULT = 10; + /** * @see LBFactory::__construct() * @@ -160,6 +166,7 @@ class LBFactoryMulti extends LBFactory { * storage cluster. * - masterTemplateOverrides Server configuration map overrides for all master servers. * - loadMonitorClass Name of the LoadMonitor class to always use. + * - maxLag Avoid replica DBs with more lag than this many seconds. * - readOnlyBySection A map of section name to read-only message. * Missing or false for read/write. */ @@ -171,7 +178,7 @@ class LBFactoryMulti extends LBFactory { $optional = [ 'groupLoadsBySection', 'groupLoadsByDB', 'hostsByName', 'externalLoads', 'externalTemplateOverrides', 'templateOverridesByServer', 'templateOverridesByCluster', 'templateOverridesBySection', 'masterTemplateOverrides', - 'readOnlyBySection', 'loadMonitorClass' ]; + 'readOnlyBySection', 'maxLag', 'loadMonitorClass' ]; foreach ( $required as $key ) { if ( !isset( $conf[$key] ) ) { @@ -319,6 +326,7 @@ class LBFactoryMulti extends LBFactory { $this->baseLoadBalancerParams(), [ 'servers' => $this->makeServerArray( $template, $loads, $groupLoads ), + 'maxLag' => $this->maxLag, 'loadMonitor' => [ 'class' => $this->loadMonitorClass ], 'readOnlyReason' => $readOnlyReason ] diff --git a/includes/libs/rdbms/lbfactory/LBFactorySimple.php b/includes/libs/rdbms/lbfactory/LBFactorySimple.php index df0a806b41..9a6aa3a74d 100644 --- a/includes/libs/rdbms/lbfactory/LBFactorySimple.php +++ b/includes/libs/rdbms/lbfactory/LBFactorySimple.php @@ -41,6 +41,11 @@ class LBFactorySimple extends LBFactory { /** @var string */ private $loadMonitorClass; + /** @var int */ + private $maxLag; + + /** @var int Default 'maxLag' when unspecified */ + const MAX_LAG_DEFAULT = 10; /** * @see LBFactory::__construct() @@ -72,6 +77,7 @@ class LBFactorySimple extends LBFactory { $this->loadMonitorClass = isset( $conf['loadMonitorClass'] ) ? $conf['loadMonitorClass'] : 'LoadMonitor'; + $this->maxLag = isset( $conf['maxLag'] ) ? $conf['maxLag'] : self::MAX_LAG_DEFAULT; } /** @@ -128,6 +134,7 @@ class LBFactorySimple extends LBFactory { $this->baseLoadBalancerParams(), [ 'servers' => $servers, + 'maxLag' => $this->maxLag, 'loadMonitor' => [ 'class' => $this->loadMonitorClass ], ] ) ); diff --git a/includes/libs/rdbms/loadbalancer/ILoadBalancer.php b/includes/libs/rdbms/loadbalancer/ILoadBalancer.php index 22a58055d7..86c43350a0 100644 --- a/includes/libs/rdbms/loadbalancer/ILoadBalancer.php +++ b/includes/libs/rdbms/loadbalancer/ILoadBalancer.php @@ -96,6 +96,7 @@ interface ILoadBalancer { * - loadMonitor : Name of a class used to fetch server lag and load. * - readOnlyReason : Reason the master DB is read-only if so [optional] * - waitTimeout : Maximum time to wait for replicas for consistency [optional] + * - maxLag: Avoid replica DB servers with more lag than this [optional] * - srvCache : BagOStuff object for server cache [optional] * - wanCache : WANObjectCache object [optional] * - chronologyProtector: ChronologyProtector object [optional] diff --git a/includes/libs/rdbms/loadbalancer/LoadBalancer.php b/includes/libs/rdbms/loadbalancer/LoadBalancer.php index 6bb894547c..a9eaa9974e 100644 --- a/includes/libs/rdbms/loadbalancer/LoadBalancer.php +++ b/includes/libs/rdbms/loadbalancer/LoadBalancer.php @@ -18,7 +18,6 @@ * http://www.gnu.org/copyleft/gpl.html * * @file - * @ingroup Database */ namespace Wikimedia\Rdbms; @@ -115,11 +114,13 @@ class LoadBalancer implements ILoadBalancer { private $disabled = false; /** @var bool */ private $chronProtInitialized = false; + /** @var int */ + private $maxLag = self::MAX_LAG_DEFAULT; /** @var int Warn when this many connection are held */ const CONN_HELD_WARN_THRESHOLD = 10; - /** @var int Default 'max lag' when unspecified */ + /** @var int Default 'maxLag' when unspecified */ const MAX_LAG_DEFAULT = 10; /** @var int Seconds to cache master server read-only status */ const TTL_CACHE_READONLY = 5; @@ -178,11 +179,16 @@ class LoadBalancer implements ILoadBalancer { $this->readOnlyReason = $params['readOnlyReason']; } + if ( isset( $params['maxLag'] ) ) { + $this->maxLag = $params['maxLag']; + } + if ( isset( $params['loadMonitor'] ) ) { $this->loadMonitorConfig = $params['loadMonitor']; } else { $this->loadMonitorConfig = [ 'class' => 'LoadMonitorNull' ]; } + $this->loadMonitorConfig += [ 'lagWarnThreshold' => $this->maxLag ]; foreach ( $params['servers'] as $i => $server ) { $this->mLoads[$i] = $server['load']; @@ -275,7 +281,7 @@ class LoadBalancer implements ILoadBalancer { # How much lag this server nominally is allowed to have $maxServerLag = isset( $this->mServers[$i]['max lag'] ) ? $this->mServers[$i]['max lag'] - : self::MAX_LAG_DEFAULT; // default + : $this->maxLag; // default # Constrain that futher by $maxLag argument $maxServerLag = min( $maxServerLag, $maxLag ); @@ -285,7 +291,7 @@ class LoadBalancer implements ILoadBalancer { "Server {host} is not replicating?", [ 'host' => $host ] ); unset( $loads[$i] ); } elseif ( $lag > $maxServerLag ) { - $this->replLogger->warning( + $this->replLogger->info( "Server {host} has {lag} seconds of lag (>= {maxlag})", [ 'host' => $host, 'lag' => $lag, 'maxlag' => $maxServerLag ] ); diff --git a/includes/libs/rdbms/loadmonitor/LoadMonitor.php b/includes/libs/rdbms/loadmonitor/LoadMonitor.php index 8292c0369b..74c7765844 100644 --- a/includes/libs/rdbms/loadmonitor/LoadMonitor.php +++ b/includes/libs/rdbms/loadmonitor/LoadMonitor.php @@ -45,9 +45,22 @@ class LoadMonitor implements ILoadMonitor { /** @var float Moving average ratio (e.g. 0.1 for 10% weight to new weight) */ private $movingAveRatio; + /** @var int Amount of replication lag in seconds before warnings are logged */ + private $lagWarnThreshold; - const VERSION = 1; // cache key version + /** @var int cache key version */ + const VERSION = 1; + /** @var int Default 'max lag' in seconds when unspecified */ + const LAG_WARN_THRESHOLD = 10; + /** + * @param ILoadBalancer $lb + * @param BagOStuff $srvCache + * @param WANObjectCache $wCache + * @param array $options + * - movingAveRatio: moving average constant for server weight updates based on lag + * - lagWarnThreshold: how many seconds of lag trigger warnings + */ public function __construct( ILoadBalancer $lb, BagOStuff $srvCache, WANObjectCache $wCache, array $options = [] ) { @@ -59,6 +72,9 @@ class LoadMonitor implements ILoadMonitor { $this->movingAveRatio = isset( $options['movingAveRatio'] ) ? $options['movingAveRatio'] : 0.1; + $this->lagWarnThreshold = isset( $options['lagWarnThreshold'] ) + ? $options['lagWarnThreshold'] + : self::LAG_WARN_THRESHOLD; } public function setLogger( LoggerInterface $logger ) { @@ -159,9 +175,10 @@ class LoadMonitor implements ILoadMonitor { // Scale from 10% to 100% of nominal weight $weightScales[$i] = max( $newWeight, 0.10 ); + $host = $this->parent->getServerName( $i ); + if ( !$conn ) { $lagTimes[$i] = false; - $host = $this->parent->getServerName( $i ); $this->replLogger->error( __METHOD__ . ": host {db_server} is unreachable", [ 'db_server' => $host ] @@ -174,11 +191,19 @@ class LoadMonitor implements ILoadMonitor { } else { $lagTimes[$i] = $conn->getLag(); if ( $lagTimes[$i] === false ) { - $host = $this->parent->getServerName( $i ); $this->replLogger->error( __METHOD__ . ": host {db_server} is not replicating?", [ 'db_server' => $host ] ); + } elseif ( $lagTimes[$i] > $this->lagWarnThreshold ) { + $this->replLogger->error( + "Server {host} has {lag} seconds of lag (>= {maxlag})", + [ + 'host' => $host, + 'lag' => $lagTimes[$i], + 'maxlag' => $this->lagWarnThreshold + ] + ); } } diff --git a/includes/libs/rdbms/loadmonitor/LoadMonitorMySQL.php b/includes/libs/rdbms/loadmonitor/LoadMonitorMySQL.php index f8ad329bb0..98cff6d77a 100644 --- a/includes/libs/rdbms/loadmonitor/LoadMonitorMySQL.php +++ b/includes/libs/rdbms/loadmonitor/LoadMonitorMySQL.php @@ -46,7 +46,7 @@ class LoadMonitorMySQL extends LoadMonitor { protected function getWeightScale( $index, IDatabase $conn = null ) { if ( !$conn ) { - return 0.0; + return parent::getWeightScale( $index, $conn ); } $weight = 1.0; diff --git a/includes/libs/xmp/XMP.php b/includes/libs/xmp/XMP.php index 0cc14669a7..be823a8bc5 100644 --- a/includes/libs/xmp/XMP.php +++ b/includes/libs/xmp/XMP.php @@ -370,7 +370,7 @@ class XMPReader implements LoggerAwareInterface { $col = xml_get_current_column_number( $this->xmlParser ); $offset = xml_get_current_byte_index( $this->xmlParser ); - $this->logger->warning( + $this->logger->info( '{method} : Error reading XMP content: {error} ' . '(line: {line} column: {column} byte offset: {offset})', [ diff --git a/includes/logging/LogPager.php b/includes/logging/LogPager.php index df432e1517..5404f35fce 100644 --- a/includes/logging/LogPager.php +++ b/includes/logging/LogPager.php @@ -45,6 +45,12 @@ class LogPager extends ReverseChronologicalPager { /** @var string */ private $action = ''; + /** @var bool */ + private $performerRestrictionsEnforced = false; + + /** @var bool */ + private $actionRestrictionsEnforced = false; + /** @var LogEventsList */ public $mLogEventsList; @@ -177,14 +183,7 @@ class LogPager extends ReverseChronologicalPager { } else { $this->mConds['log_user'] = $userid; } - // Paranoia: avoid brute force searches (T19342) - $user = $this->getUser(); - if ( !$user->isAllowed( 'deletedhistory' ) ) { - $this->mConds[] = $this->mDb->bitAnd( 'log_deleted', LogPage::DELETED_USER ) . ' = 0'; - } elseif ( !$user->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) { - $this->mConds[] = $this->mDb->bitAnd( 'log_deleted', LogPage::SUPPRESSED_USER ) . - ' != ' . LogPage::SUPPRESSED_USER; - } + $this->enforcePerformerRestrictions(); $this->performer = $name; } @@ -252,14 +251,7 @@ class LogPager extends ReverseChronologicalPager { } else { $this->mConds['log_title'] = $title->getDBkey(); } - // Paranoia: avoid brute force searches (T19342) - $user = $this->getUser(); - if ( !$user->isAllowed( 'deletedhistory' ) ) { - $this->mConds[] = $db->bitAnd( 'log_deleted', LogPage::DELETED_ACTION ) . ' = 0'; - } elseif ( !$user->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) { - $this->mConds[] = $db->bitAnd( 'log_deleted', LogPage::SUPPRESSED_ACTION ) . - ' != ' . LogPage::SUPPRESSED_ACTION; - } + $this->enforceActionRestrictions(); } /** @@ -420,4 +412,39 @@ class LogPager extends ReverseChronologicalPager { parent::doQuery(); $this->mDb->setBigSelects( 'default' ); } + + /** + * Paranoia: avoid brute force searches (T19342) + */ + private function enforceActionRestrictions() { + if ( $this->actionRestrictionsEnforced ) { + return; + } + $this->actionRestrictionsEnforced = true; + $user = $this->getUser(); + if ( !$user->isAllowed( 'deletedhistory' ) ) { + $this->mConds[] = $this->mDb->bitAnd( 'log_deleted', LogPage::DELETED_USER ) . ' = 0'; + } elseif ( !$user->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) { + $this->mConds[] = $this->mDb->bitAnd( 'log_deleted', LogPage::SUPPRESSED_USER ) . + ' != ' . LogPage::SUPPRESSED_USER; + } + } + + /** + * Paranoia: avoid brute force searches (T19342) + */ + private function enforcePerformerRestrictions() { + // Same as enforceActionRestrictions(), except for _USER instead of _ACTION bits. + if ( $this->performerRestrictionsEnforced ) { + return; + } + $this->performerRestrictionsEnforced = true; + $user = $this->getUser(); + if ( !$user->isAllowed( 'deletedhistory' ) ) { + $this->mConds[] = $this->mDb->bitAnd( 'log_deleted', LogPage::DELETED_ACTION ) . ' = 0'; + } elseif ( !$user->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) { + $this->mConds[] = $this->mDb->bitAnd( 'log_deleted', LogPage::SUPPRESSED_ACTION ) . + ' != ' . LogPage::SUPPRESSED_ACTION; + } + } } diff --git a/includes/media/DjVu.php b/includes/media/DjVu.php index aae66d37e0..eb7b6ba08e 100644 --- a/includes/media/DjVu.php +++ b/includes/media/DjVu.php @@ -357,7 +357,7 @@ class DjVuHandler extends ImageHandler { global $wgDjvuOutputExtension; static $mime; if ( !isset( $mime ) ) { - $magic = MimeMagic::singleton(); + $magic = MediaWiki\MediaWikiServices::getInstance()->getMimeAnalyzer(); $mime = $magic->guessTypesForExtension( $wgDjvuOutputExtension ); } diff --git a/includes/media/FormatMetadata.php b/includes/media/FormatMetadata.php index 666196585a..b008a22688 100644 --- a/includes/media/FormatMetadata.php +++ b/includes/media/FormatMetadata.php @@ -740,8 +740,13 @@ class FormatMetadata extends ContextSource { case 'Software': if ( is_array( $val ) ) { - // if its a software, version array. - $val = $this->msg( 'exif-software-version-value', $val[0], $val[1] )->text(); + if ( count( $val ) > 1 ) { + // if its a software, version array. + $val = $this->msg( 'exif-software-version-value', $val[0], $val[1] )->text(); + } else { + // https://phabricator.wikimedia.org/T178130 + $val = $this->exifMsg( $tag, '', $val[0] ); + } } else { $val = $this->exifMsg( $tag, '', $val ); } diff --git a/includes/media/MediaHandler.php b/includes/media/MediaHandler.php index 481e880cfe..8551a120a5 100644 --- a/includes/media/MediaHandler.php +++ b/includes/media/MediaHandler.php @@ -288,7 +288,7 @@ abstract class MediaHandler { * @return array Thumbnail extension and MIME type */ function getThumbType( $ext, $mime, $params = null ) { - $magic = MimeMagic::singleton(); + $magic = MediaWiki\MediaWikiServices::getInstance()->getMimeAnalyzer(); if ( !$ext || $magic->isMatchingExtension( $ext, $mime ) === false ) { // The extension is not valid for this MIME type and we do // recognize the MIME type diff --git a/includes/media/SVG.php b/includes/media/SVG.php index 2b138930d1..10be97a5cd 100644 --- a/includes/media/SVG.php +++ b/includes/media/SVG.php @@ -511,8 +511,6 @@ class SvgHandler extends ImageHandler { } elseif ( $name == 'lang' ) { // Validate $code if ( $value === '' || !Language::isValidCode( $value ) ) { - wfDebug( "Invalid user language code\n" ); - return false; } diff --git a/includes/objectcache/ObjectCache.php b/includes/objectcache/ObjectCache.php index 6d6d3459d9..67d2346013 100644 --- a/includes/objectcache/ObjectCache.php +++ b/includes/objectcache/ObjectCache.php @@ -332,6 +332,8 @@ class ObjectCache { * @throws UnexpectedValueException */ public static function newWANCacheFromParams( array $params ) { + global $wgCommandLineMode; + $services = MediaWikiServices::getInstance(); $erGroup = $services->getEventRelayerGroup(); @@ -340,12 +342,17 @@ class ObjectCache { $params['channels'][$action] = $channel; } $params['cache'] = self::newFromParams( $params['store'] ); - $params['stats'] = $services->getStatsdDataFactory(); if ( isset( $params['loggroup'] ) ) { $params['logger'] = LoggerFactory::getInstance( $params['loggroup'] ); } else { $params['logger'] = LoggerFactory::getInstance( 'objectcache' ); } + if ( !$wgCommandLineMode ) { + // Send the statsd data post-send on HTTP requests; avoid in CLI mode (T181385) + $params['stats'] = $services->getStatsdDataFactory(); + // Let pre-emptive refreshes happen post-send on HTTP requests + $params['asyncHandler'] = [ DeferredUpdates::class, 'addCallableUpdate' ]; + } $class = $params['class']; return new $class( $params ); diff --git a/includes/page/Article.php b/includes/page/Article.php index c9dc273b47..dadf311da0 100644 --- a/includes/page/Article.php +++ b/includes/page/Article.php @@ -75,6 +75,13 @@ class Article implements Page { /** @var ParserOutput */ public $mParserOutput; + /** + * @var bool Whether render() was called. With the way subclasses work + * here, there doesn't seem to be any other way to stop calling + * OutputPage::enableSectionEditLinks() and still have it work as it did before. + */ + private $disableSectionEditForRender = false; + /** * Constructor and clear the article * @param Title $title Reference to a Title object. @@ -469,12 +476,17 @@ class Article implements Page { $parserCache = MediaWikiServices::getInstance()->getParserCache(); $parserOptions = $this->getParserOptions(); + $poOptions = []; # Render printable version, use printable version cache if ( $outputPage->isPrintable() ) { $parserOptions->setIsPrintable( true ); $parserOptions->setEditSection( false ); - } elseif ( !$this->isCurrent() || !$this->getTitle()->quickUserCan( 'edit', $user ) ) { + $poOptions['enableSectionEditLinks'] = false; + } elseif ( $this->disableSectionEditForRender + || !$this->isCurrent() || !$this->getTitle()->quickUserCan( 'edit', $user ) + ) { $parserOptions->setEditSection( false ); + $poOptions['enableSectionEditLinks'] = false; } # Try client and file cache @@ -533,7 +545,7 @@ class Article implements Page { } else { wfDebug( __METHOD__ . ": showing parser cache contents\n" ); } - $outputPage->addParserOutput( $this->mParserOutput ); + $outputPage->addParserOutput( $this->mParserOutput, $poOptions ); # Ensure that UI elements requiring revision ID have # the correct version information. $outputPage->setRevisionId( $this->mPage->getLatest() ); @@ -597,7 +609,7 @@ class Article implements Page { } $this->mParserOutput = $poolArticleView->getParserOutput(); - $outputPage->addParserOutput( $this->mParserOutput ); + $outputPage->addParserOutput( $this->mParserOutput, $poOptions ); if ( $content->getRedirectTarget() ) { $outputPage->addSubtitle( "" . $this->getContext()->msg( 'redirectpagesub' )->parse() . "" ); @@ -1515,6 +1527,7 @@ class Article implements Page { $this->getContext()->getRequest()->response()->header( 'X-Robots-Tag: noindex' ); $this->getContext()->getOutput()->setArticleBodyOnly( true ); $this->getContext()->getOutput()->enableSectionEditLinks( false ); + $this->disableSectionEditForRender = true; $this->view(); } diff --git a/includes/page/ImagePage.php b/includes/page/ImagePage.php index c4baae445e..1dcdc65ff2 100644 --- a/includes/page/ImagePage.php +++ b/includes/page/ImagePage.php @@ -251,13 +251,14 @@ class ImagePage extends Article { protected function makeMetadataTable( $metadata ) { $r = "