diff options
Diffstat (limited to 'includes')
625 files changed, 14821 insertions, 5801 deletions
diff --git a/includes/AjaxDispatcher.php b/includes/AjaxDispatcher.php index d444a2791f36..2adbc80f3b4d 100644 --- a/includes/AjaxDispatcher.php +++ b/includes/AjaxDispatcher.php @@ -21,6 +21,8 @@ * @ingroup Ajax */ +use MediaWiki\MediaWikiServices; + /** * @defgroup Ajax Ajax */ @@ -135,7 +137,8 @@ class AjaxDispatcher { } // Make sure DB commit succeeds before sending a response - wfGetLBFactory()->commitMasterChanges( __METHOD__ ); + $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory(); + $lbFactory->commitMasterChanges( __METHOD__ ); $result->sendHeaders(); $result->printText(); diff --git a/includes/Block.php b/includes/Block.php index 9d3a2f935c69..b6b3ae05f1c8 100644 --- a/includes/Block.php +++ b/includes/Block.php @@ -20,6 +20,7 @@ * @file */ +use Wikimedia\Rdbms\IDatabase; use MediaWiki\MediaWikiServices; class Block { @@ -265,7 +266,7 @@ class Block { } # Be aware that the != '' check is explicit, since empty values will be - # passed by some callers (bug 29116) + # passed by some callers (T31116) if ( $vagueTarget != '' ) { list( $target, $type ) = self::parseTarget( $vagueTarget ); switch ( $type ) { @@ -358,7 +359,7 @@ class Block { if ( $end === null ) { $end = $start; } - # Per bug 14634, we want to include relevant active rangeblocks; for + # Per T16634, we want to include relevant active rangeblocks; for # rangeblocks, we want to include larger ranges which enclose the given # range. We know that all blocks must be smaller than $wgBlockCIDRLimit, # so we can improve performance by filtering on a LIKE clause @@ -553,7 +554,7 @@ class Block { $affected = $dbw->affectedRows(); if ( $this->isAutoblocking() ) { - // update corresponding autoblock(s) (bug 48813) + // update corresponding autoblock(s) (T50813) $dbw->update( 'ipblocks', $this->getAutoblockUpdateArray(), @@ -1117,7 +1118,7 @@ class Block { } elseif ( $target === null && $vagueTarget == '' ) { # We're not going to find anything useful here # Be aware that the == '' check is explicit, since empty values will be - # passed by some callers (bug 29116) + # passed by some callers (T31116) return null; } elseif ( in_array( @@ -1142,7 +1143,7 @@ class Block { * Get all blocks that match any IP from an array of IP addresses * * @param array $ipChain List of IPs (strings), usually retrieved from the - * X-Forwarded-For header of the request + * X-Forwarded-For header of the request * @param bool $isAnon Exclude anonymous-only blocks if false * @param bool $fromMaster Whether to query the master or replica DB * @return array Array of Blocks @@ -1223,9 +1224,9 @@ class Block { * * @param array $blocks Array of Block objects * @param array $ipChain List of IPs (strings). This is used to determine how "close" - * a block is to the server, and if a block matches exactly, or is in a range. - * The order is furthest from the server to nearest e.g., (Browser, proxy1, proxy2, - * local-squid, ...) + * a block is to the server, and if a block matches exactly, or is in a range. + * The order is furthest from the server to nearest e.g., (Browser, proxy1, proxy2, + * local-squid, ...) * @throws MWException * @return Block|null The "best" block from the list */ @@ -1450,13 +1451,9 @@ class Block { * Set the 'BlockID' cookie to this block's ID and expiry time. The cookie's expiry will be * the same as the block's, to a maximum of 24 hours. * - * An empty value can also be set, in order to retain the cookie but remove the block ID - * (e.g. as used in User::getBlockedStatus). - * * @param WebResponse $response The response on which to set the cookie. - * @param boolean $setEmpty Whether to set the cookie's value to the empty string. */ - public function setCookie( WebResponse $response, $setEmpty = false ) { + public function setCookie( WebResponse $response ) { // Calculate the default expiry time. $maxExpiryTime = wfTimestamp( TS_MW, wfTimestamp() + ( 24 * 60 * 60 ) ); @@ -1467,9 +1464,64 @@ class Block { } // Set the cookie. Reformat the MediaWiki datetime as a Unix timestamp for the cookie. - $cookieValue = $setEmpty ? '' : $this->getId(); - $expiryValue = DateTime::createFromFormat( "YmdHis", $expiryTime ); - $response->setCookie( 'BlockID', $cookieValue, $expiryValue->format( "U" ) ); + $expiryValue = DateTime::createFromFormat( 'YmdHis', $expiryTime )->format( 'U' ); + $cookieOptions = [ 'httpOnly' => false ]; + $cookieValue = $this->getCookieValue(); + $response->setCookie( 'BlockID', $cookieValue, $expiryValue, $cookieOptions ); + } + + /** + * Unset the 'BlockID' cookie. + * + * @param WebResponse $response The response on which to unset the cookie. + */ + public static function clearCookie( WebResponse $response ) { + $response->clearCookie( 'BlockID', [ 'httpOnly' => false ] ); + } + + /** + * Get the BlockID cookie's value for this block. This is usually the block ID concatenated + * with an HMAC in order to avoid spoofing (T152951), but if wgSecretKey is not set will just + * be the block ID. + * @return string The block ID, probably concatenated with "!" and the HMAC. + */ + public function getCookieValue() { + $config = RequestContext::getMain()->getConfig(); + $id = $this->getId(); + $secretKey = $config->get( 'SecretKey' ); + if ( !$secretKey ) { + // If there's no secret key, don't append a HMAC. + return $id; + } + $hmac = MWCryptHash::hmac( $id, $secretKey, false ); + $cookieValue = $id . '!' . $hmac; + return $cookieValue; + } + + /** + * Get the stored ID from the 'BlockID' cookie. The cookie's value is usually a combination of + * the ID and a HMAC (see Block::setCookie), but will sometimes only be the ID. + * @param string $cookieValue The string in which to find the ID. + * @return integer|null The block ID, or null if the HMAC is present and invalid. + */ + public static function getIdFromCookieValue( $cookieValue ) { + // Extract the ID prefix from the cookie value (may be the whole value, if no bang found). + $bangPos = strpos( $cookieValue, '!' ); + $id = ( $bangPos === false ) ? $cookieValue : substr( $cookieValue, 0, $bangPos ); + // Get the site-wide secret key. + $config = RequestContext::getMain()->getConfig(); + $secretKey = $config->get( 'SecretKey' ); + if ( !$secretKey ) { + // If there's no secret key, just use the ID as given. + return $id; + } + $storedHmac = substr( $cookieValue, $bangPos + 1 ); + $calculatedHmac = MWCryptHash::hmac( $id, $secretKey, false ); + if ( $calculatedHmac === $storedHmac ) { + return $id; + } else { + return null; + } } /** diff --git a/includes/Category.php b/includes/Category.php index d558dbc44dbc..ece32ea1059b 100644 --- a/includes/Category.php +++ b/includes/Category.php @@ -96,7 +96,7 @@ class Category { $this->mSubcats = $row->cat_subcats; $this->mFiles = $row->cat_files; - # (bug 13683) If the count is negative, then 1) it's obviously wrong + # (T15683) If the count is negative, then 1) it's obviously wrong # and should not be kept, and 2) we *probably* don't have to scan many # rows to obtain the correct figure, so let's risk a one-time recount. if ( $this->mPages < 0 || $this->mSubcats < 0 || $this->mFiles < 0 ) { diff --git a/includes/CategoryFinder.php b/includes/CategoryFinder.php index 504b35f885d9..595cf9510447 100644 --- a/includes/CategoryFinder.php +++ b/includes/CategoryFinder.php @@ -20,6 +20,8 @@ * @file */ +use Wikimedia\Rdbms\IDatabase; + /** * The "CategoryFinder" class takes a list of articles, creates an internal * representation of all their parent categories (as well as parents of diff --git a/includes/CategoryViewer.php b/includes/CategoryViewer.php index facf847892a1..0205d708cab4 100644 --- a/includes/CategoryViewer.php +++ b/includes/CategoryViewer.php @@ -632,11 +632,12 @@ class CategoryViewer extends ContextSource { private function pagingLinks( $first, $last, $type = '' ) { $prevLink = $this->msg( 'prev-page' )->text(); + $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer(); if ( $first != '' ) { $prevQuery = $this->query; $prevQuery["{$type}until"] = $first; unset( $prevQuery["{$type}from"] ); - $prevLink = Linker::linkKnown( + $prevLink = $linkRenderer->makeKnownLink( $this->addFragmentToTitle( $this->title, $type ), $prevLink, [], @@ -650,7 +651,7 @@ class CategoryViewer extends ContextSource { $lastQuery = $this->query; $lastQuery["{$type}from"] = $last; unset( $lastQuery["{$type}until"] ); - $nextLink = Linker::linkKnown( + $nextLink = $linkRenderer->makeKnownLink( $this->addFragmentToTitle( $this->title, $type ), $nextLink, [], @@ -741,7 +742,13 @@ class CategoryViewer extends ContextSource { $totalcnt = $rescnt; $category = $this->cat; DeferredUpdates::addCallableUpdate( function () use ( $category ) { - $category->refreshCounts(); + # Avoid excess contention on the same category (T162121) + $dbw = wfGetDB( DB_MASTER ); + $name = __METHOD__ . ':' . md5( $this->mName ); + $scopedLock = $dbw->getScopedLockAndFlush( $name, __METHOD__, 1 ); + if ( $scopedLock ) { + $category->refreshCounts(); + } } ); } else { // Case 3: hopeless. Don't give a total count at all. diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php index 566d4aa9ba30..53a147b5249f 100644 --- a/includes/DefaultSettings.php +++ b/includes/DefaultSettings.php @@ -1337,7 +1337,7 @@ $wgXMLMimeTypes = [ * to reduce disk usage, limits can only be selected from a list. * The user preference is saved as an array offset in the database, by default * the offset is set with $wgDefaultUserOptions['imagesize']. Make sure you - * change it if you alter the array (see bug 8858). + * change it if you alter the array (see T10858). * This is the list of settings the user can choose from: */ $wgImageLimits = [ @@ -1442,14 +1442,19 @@ $wgUseTinyRGBForJPGThumbnails = false; * Default parameters for the "<gallery>" tag */ $wgGalleryOptions = [ - 'imagesPerRow' => 0, // Default number of images per-row in the gallery. 0 -> Adapt to screensize - 'imageWidth' => 120, // Width of the cells containing images in galleries (in "px") - 'imageHeight' => 120, // Height of the cells containing images in galleries (in "px") - 'captionLength' => true, // Deprecated @since 1.28 - // Length to truncate filename to in caption when using "showfilename". - // A value of 'true' will truncate the filename to one line using CSS - // and will be the behaviour after deprecation. - 'showBytes' => true, // Show the filesize in bytes in categories + // Default number of images per-row in the gallery. 0 -> Adapt to screensize + 'imagesPerRow' => 0, + // Width of the cells containing images in galleries (in "px") + 'imageWidth' => 120, + // Height of the cells containing images in galleries (in "px") + 'imageHeight' => 120, + // Length to truncate filename to in caption when using "showfilename". + // A value of 'true' will truncate the filename to one line using CSS + // and will be the behaviour after deprecation. + // @deprecated since 1.28 + 'captionLength' => true, + // Show the filesize in bytes in categories + 'showBytes' => true, 'mode' => 'traditional', ]; @@ -2343,6 +2348,19 @@ $wgWANObjectCaches = [ ]; /** + * Verify and enforce WAN cache purges using reliable DB sources as streams. + * + * These secondary cache purges are de-duplicated via simple cache mutexes. + * This improves consistency when cache purges are lost, which becomes more likely + * as more cache servers are added or if there are multiple datacenters. Only keys + * related to important mutable content will be checked. + * + * @var bool + * @since 1.29 + */ +$wgEnableWANCacheReaper = false; + +/** * Main object stash type. This should be a fast storage system for storing * lightweight data like hit counters and user activity. Sites with multiple * data-centers should have this use a store that replicates all writes. The @@ -2807,8 +2825,9 @@ $wgUsePrivateIPs = false; * MediaWiki out of the box. Not all languages listed there have translations, * see languages/messages/ for the list of languages with some localisation. * - * Warning: Don't use language codes listed in $wgDummyLanguageCodes like "no" - * for Norwegian (use "nb" instead), or things will break unexpectedly. + * Warning: Don't use any of MediaWiki's deprecated language codes listed in + * LanguageCode::getDeprecatedCodeMapping or $wgDummyLanguageCodes, like "no" + * for Norwegian (use "nb" instead). If you do, things will break unexpectedly. * * This defines the default interface language for all users, but users can * change it in their preferences. @@ -2867,28 +2886,33 @@ $wgExtraInterlanguageLinkPrefixes = []; $wgExtraLanguageNames = []; /** - * List of language codes that don't correspond to an actual language. - * These codes are mostly left-offs from renames, or other legacy things. - * This array makes them not appear as a selectable language on the installer, - * and excludes them when running the transstat.php script. - */ -$wgDummyLanguageCodes = [ - 'als' => 'gsw', - 'bat-smg' => 'sgs', - 'be-x-old' => 'be-tarask', - 'bh' => 'bho', - 'fiu-vro' => 'vro', - 'no' => 'nb', - 'qqq' => 'qqq', # Used for message documentation. - 'qqx' => 'qqx', # Used for viewing message keys. - 'roa-rup' => 'rup', - 'simple' => 'en', - 'zh-classical' => 'lzh', - 'zh-min-nan' => 'nan', - 'zh-yue' => 'yue', + * List of mappings from one language code to another. + * This array makes the codes not appear as a selectable language on the + * installer, and excludes them when running the transstat.php script. + * + * In Setup.php, the variable $wgDummyLanguageCodes is created by combining + * these codes with a list of "deprecated" codes, which are mostly leftovers + * from renames or other legacy things, and the internal codes 'qqq' and 'qqx'. + * If a mapping in $wgExtraLanguageCodes collide with a built-in mapping, the + * value in $wgExtraLanguageCodes will be used. + * + * @since 1.29 + */ +$wgExtraLanguageCodes = [ + 'bh' => 'bho', // Bihari language family + 'no' => 'nb', // Norwegian language family + 'simple' => 'en', // Simple English ]; /** + * Functionally the same as $wgExtraLanguageCodes, but deprecated. Instead of + * appending values to this array, append them to $wgExtraLanguageCodes. + * + * @deprecated since 1.29 + */ +$wgDummyLanguageCodes = []; + +/** * Set this to true to replace Arabic presentation forms with their standard * forms in the U+0600-U+06FF block. This only works if $wgLanguageCode is * set to "ar". @@ -3344,7 +3368,7 @@ $wgDisableOutputCompression = false; * * Currently this appears to work fine in all browsers, but it's disabled by * default because it normalizes id's a bit too aggressively, breaking preexisting - * content (particularly Cite). See bug 27733, bug 27694, bug 27474. + * content (particularly Cite). See T29733, T29694, T29474. */ $wgExperimentalHtmlIds = false; @@ -4068,7 +4092,7 @@ $wgMaxRedirects = 1; * Attempting to create a redirect to any of the pages in this array * will make the redirect fail. * Userlogout is hard-coded, so it does not need to be listed here. - * (bug 10569) Disallow Mypage and Mytalk as well. + * (T12569) Disallow Mypage and Mytalk as well. * * As of now, this only checks special pages. Redirects to pages in * other namespaces cannot be invalidated by this variable. @@ -4149,7 +4173,7 @@ $wgMaxPPExpandDepth = 40; * * WARNING: Do not add 'file:' to this or internal file links will be broken. * Instead, if you want to support file links, add 'file://'. The same applies - * to any other protocols with the same name as a namespace. See bug #44011 for + * to any other protocols with the same name as a namespace. See task T46011 for * more information. * * @see wfParseUrl @@ -4222,7 +4246,8 @@ $wgAllowImageTag = false; * - RaggettInternalPHP: Use the PECL extension * - RaggettExternal: Shell out to an external binary (tidyBin) * - Html5Depurate: Use external Depurate service - * - Html5Internal: Use the built-in HTML5 balancer + * - Html5Internal: Use the Balancer library in PHP + * - RemexHtml: Use the RemexHtml library in PHP * * - tidyConfigFile: Path to configuration file for any of the Raggett drivers * - debugComment: True to add a comment to the output with warning messages @@ -4552,8 +4577,8 @@ $wgAuthManagerAutoConfig = [ ], // Linking during login is experimental, enable at your own risk - T134952 // MediaWiki\Auth\ConfirmLinkSecondaryAuthenticationProvider::class => [ - // 'class' => MediaWiki\Auth\ConfirmLinkSecondaryAuthenticationProvider::class, - // 'sort' => 100, + // 'class' => MediaWiki\Auth\ConfirmLinkSecondaryAuthenticationProvider::class, + // 'sort' => 100, // ], MediaWiki\Auth\EmailNotificationSecondaryAuthenticationProvider::class => [ 'class' => MediaWiki\Auth\EmailNotificationSecondaryAuthenticationProvider::class, @@ -4773,6 +4798,7 @@ $wgReservedUsernames = [ 'Maintenance script', // Maintenance scripts which perform editing, image import script 'Template namespace initialisation script', // Used in 1.2->1.3 upgrade 'ScriptImporter', // Default user name used by maintenance/importSiteScripts.php + 'Unknown user', // Used in WikiImporter when importing revisions with no author '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) @@ -4851,9 +4877,7 @@ $wgDefaultUserOptions = [ /** * An array of preferences to not show for the user */ -$wgHiddenPrefs = [ - 'rcenhancedfilters', -]; +$wgHiddenPrefs = []; /** * Characters to prevent during new account creations. @@ -5668,7 +5692,7 @@ $wgRateLimits = [ ]; /** - * Array of IPs which should be excluded from rate limits. + * Array of IPs / CIDR ranges which should be excluded from rate limits. * This may be useful for whitelisting NAT gateways for conferences, etc. */ $wgRateLimitsExcludedIPs = []; @@ -5870,6 +5894,15 @@ $wgBotPasswordsCluster = false; */ $wgBotPasswordsDatabase = false; +/** + * Whether to disable user group expiry. This is a transitional feature flag + * in accordance with WMF schema change policy, and will be removed later + * (hopefully before MW 1.29 release). + * + * @since 1.29 + */ +$wgDisableUserGroupExpiry = false; + /** @} */ # end of user rights settings /************************************************************************//** @@ -5967,7 +6000,10 @@ $wgSessionName = false; /** * Whether to set a cookie when a user is autoblocked. Doing so means that a blocked user, even - * after logging out and moving to a new IP address, will still be blocked. + * after logging out and moving to a new IP address, will still be blocked. This cookie will contain + * an authentication code if $wgSecretKey is set, or otherwise will just be the block ID (in + * which case there is a possibility of an attacker discovering the names of revdeleted users, so + * it is best to use this in conjunction with $wgSecretKey being set). */ $wgCookieSetOnAutoblock = false; @@ -6634,51 +6670,64 @@ $wgRCLinkLimits = [ 50, 100, 250, 500 ]; $wgRCLinkDays = [ 1, 3, 7, 14, 30 ]; /** - * Destinations to which notifications about recent changes - * should be sent. + * Configuration for feeds to which notifications about recent changes will be sent. * - * As of MediaWiki 1.22, there are 2 supported 'engine' parameter option in core: - * * 'UDPRCFeedEngine', which is used to send recent changes over UDP to the - * specified server. - * * 'RedisPubSubFeedEngine', which is used to send recent changes to Redis. + * The following feed classes are available by default: + * - 'UDPRCFeedEngine' - sends recent changes over UDP to the specified server. + * - 'RedisPubSubFeedEngine' - send recent changes to Redis. * - * The common options are: - * * 'uri' -- the address to which the notices are to be sent. - * * 'formatter' -- the class name (implementing RCFeedFormatter) which will - * produce the text to send. This can also be an object of the class. - * * 'omit_bots' -- whether the bot edits should be in the feed - * * 'omit_anon' -- whether anonymous edits should be in the feed - * * 'omit_user' -- whether edits by registered users should be in the feed - * * 'omit_minor' -- whether minor edits should be in the feed - * * 'omit_patrolled' -- whether patrolled edits should be in the feed + * Only 'class' or 'uri' is required. If 'uri' is set instead of 'class', then + * RecentChange::getEngine() is used to determine the class. All options are + * passed to the constructor. * - * The IRC-specific options are: - * * 'add_interwiki_prefix' -- whether the titles should be prefixed with - * the first entry in the $wgLocalInterwikis array (or the value of - * $wgLocalInterwiki, if set) + * Common options: + * - 'class' -- The class to use for this feed (must implement RCFeed). + * - 'omit_bots' -- Exclude bot edits from the feed. (default: false) + * - 'omit_anon' -- Exclude anonymous edits from the feed. (default: false) + * - 'omit_user' -- Exclude edits by registered users from the feed. (default: false) + * - 'omit_minor' -- Exclude minor edits from the feed. (default: false) + * - 'omit_patrolled' -- Exclude patrolled edits from the feed. (default: false) * - * The JSON-specific options are: - * * 'channel' -- if set, the 'channel' parameter is also set in JSON values. + * FormattedRCFeed-specific options: + * - 'uri' -- [required] The address to which the messages are sent. + * The uri scheme of this string will be looked up in $wgRCEngines + * to determine which RCFeedEngine class to use. + * - 'formatter' -- [required] The class (implementing RCFeedFormatter) which will + * produce the text to send. This can also be an object of the class. + * Formatters available by default: JSONRCFeedFormatter, XMLRCFeedFormatter, + * IRCColourfulRCFeedFormatter. + * + * IRCColourfulRCFeedFormatter-specific options: + * - 'add_interwiki_prefix' -- whether the titles should be prefixed with + * the first entry in the $wgLocalInterwikis array (or the value of + * $wgLocalInterwiki, if set) + * + * JSONRCFeedFormatter-specific options: + * - 'channel' -- if set, the 'channel' parameter is also set in JSON values. * * @example $wgRCFeeds['example'] = [ + * 'uri' => 'udp://localhost:1336', * 'formatter' => 'JSONRCFeedFormatter', - * 'uri' => "udp://localhost:1336", * 'add_interwiki_prefix' => false, * 'omit_bots' => true, * ]; - * @example $wgRCFeeds['exampleirc'] = [ + * @example $wgRCFeeds['example'] = [ + * 'uri' => 'udp://localhost:1338', * 'formatter' => 'IRCColourfulRCFeedFormatter', - * 'uri' => "udp://localhost:1338", * 'add_interwiki_prefix' => false, * 'omit_bots' => true, * ]; + * @example $wgRCFeeds['example'] = [ + * 'class' => 'ExampleRCFeed', + * ]; * @since 1.22 */ $wgRCFeeds = []; /** - * Used by RecentChange::getEngine to find the correct engine to use for a given URI scheme. - * Keys are scheme names, values are names of engine classes. + * Used by RecentChange::getEngine to find the correct engine for a given URI scheme. + * Keys are scheme names, values are names of FormattedRCFeed sub classes. + * @since 1.22 */ $wgRCEngines = [ 'redis' => 'RedisPubSubFeedEngine', @@ -7346,6 +7395,19 @@ $wgJobQueueAggregator = [ ]; /** + * Whether to include the number of jobs that are queued + * for the API's maxlag parameter. + * The total number of jobs will be divided by this to get an + * estimated second of maxlag. Typically bots backoff at maxlag=5, + * so setting this to the max number of jobs that should be in your + * queue divided by 5 should have the effect of stopping bots once + * that limit is hit. + * + * @since 1.29 + */ +$wgJobQueueIncludeInMaxLagFactor = false; + +/** * Additional functions to be performed with updateSpecialPages. * Expensive Querypages are already updated. */ @@ -8231,7 +8293,19 @@ $wgRedirectOnLogin = null; * The remaining elements are passed through to the class as constructor * parameters. * - * @par Example: + * @par Example using local redis instance: + * @code + * $wgPoolCounterConf = [ 'ArticleView' => [ + * 'class' => 'PoolCounterRedis', + * 'timeout' => 15, // wait timeout in seconds + * 'workers' => 1, // maximum number of active threads in each pool + * 'maxqueue' => 5, // maximum number of total threads in each pool + * 'servers' => [ '127.0.0.1' ], + * 'redisConfig' => [] + * ] ]; + * @endcode + * + * @par Example using C daemon from https://www.mediawiki.org/wiki/Extension:PoolCounter: * @code * $wgPoolCounterConf = [ 'ArticleView' => [ * 'class' => 'PoolCounter_Client', @@ -8239,7 +8313,7 @@ $wgRedirectOnLogin = null; * 'workers' => 5, // maximum number of active threads in each pool * 'maxqueue' => 50, // maximum number of total threads in each pool * ... any extension-specific options... - * ]; + * ] ]; * @endcode */ $wgPoolCounterConf = null; @@ -8321,7 +8395,7 @@ $wgPagePropsHaveSortkey = true; /** * Port where you have HTTPS running * Supports HTTPS on non-standard ports - * @see bug 65184 + * @see T67184 * @since 1.24 */ $wgHttpsPort = 443; @@ -8509,6 +8583,7 @@ $wgCSPFalsePositiveUrls = [ 'https://atpixel.alephd.com' => true, 'https://rtb.metrigo.com' => true, 'https://d5p.de17a.com' => true, + 'https://ad.lkqd.net/vpaid/vpaid.js' => true, ]; /** @@ -8529,6 +8604,25 @@ $wgExperiencedUserEdits = 500; $wgExperiencedUserMemberSince = 30; # days /** + * Mapping of interwiki index prefixes to descriptors that + * can be used to change the display of interwiki search results. + * + * Descriptors are appended to CSS classes of interwiki results + * which using InterwikiSearchResultWidget. + * + * Predefined descriptors include the following words: + * definition, textbook, news, quotation, book, travel, course + * + * @par Example: + * @code + * $wgInterwikiPrefixDisplayTypes = [ + * 'iwprefix' => 'definition' + *]; + * @endcode + */ +$wgInterwikiPrefixDisplayTypes = []; + +/** * For really cool vim folding this needs to be at the end: * vim: foldmarker=@{,@} foldmethod=marker * @} diff --git a/includes/Defines.php b/includes/Defines.php index 35c2a2d8adea..6bc70edbc500 100644 --- a/includes/Defines.php +++ b/includes/Defines.php @@ -21,10 +21,11 @@ */ require_once __DIR__ . '/libs/mime/defines.php'; -require_once __DIR__ . '/libs/time/defines.php'; require_once __DIR__ . '/libs/rdbms/defines.php'; require_once __DIR__ . '/compat/normal/UtfNormalDefines.php'; +use Wikimedia\Rdbms\IDatabase; + /** * @defgroup Constants MediaWiki constants */ diff --git a/includes/EditPage.php b/includes/EditPage.php index 05fa366ebafb..7bdc3bc940cc 100644 --- a/includes/EditPage.php +++ b/includes/EditPage.php @@ -363,8 +363,8 @@ class EditPage { /** @var bool */ public $bot = true; - /** @var null|string */ - public $contentModel = null; + /** @var string */ + public $contentModel; /** @var null|string */ public $contentFormat = null; @@ -498,7 +498,10 @@ class EditPage { $this->enableApiEditOverride = $enableOverride; } - function submit() { + /** + * @deprecated since 1.29, call edit directly + */ + public function submit() { $this->edit(); } @@ -513,7 +516,7 @@ class EditPage { * is made and all is well do we actually save and redirect to * the newly-edited page. */ - function edit() { + public function edit() { global $wgOut, $wgRequest, $wgUser; // Allow extensions to modify/prevent this form or submission if ( !Hooks::run( 'AlternateEdit', [ $this ] ) ) { @@ -837,7 +840,7 @@ class EditPage { * @param WebRequest $request * @throws ErrorPageError */ - function importFormData( &$request ) { + public function importFormData( &$request ) { global $wgContLang, $wgUser; # Section edit can come from either the form or a link @@ -990,7 +993,7 @@ class EditPage { $this->recreate = false; // When creating a new section, we can preload a section title by passing it as the - // preloadtitle parameter in the URL (Bug 13100) + // preloadtitle parameter in the URL (T15100) if ( $this->section == 'new' && $request->getVal( 'preloadtitle' ) ) { $this->sectiontitle = $request->getVal( 'preloadtitle' ); // Once wpSummary isn't being use for setting section titles, we should delete this. @@ -1024,7 +1027,7 @@ class EditPage { throw new ErrorPageError( 'editpage-invalidcontentmodel-title', 'editpage-invalidcontentmodel-text', - [ $this->contentModel ] + [ wfEscapeWikiText( $this->contentModel ) ] ); } @@ -1032,7 +1035,10 @@ class EditPage { throw new ErrorPageError( 'editpage-notsupportedcontentformat-title', 'editpage-notsupportedcontentformat-text', - [ $this->contentFormat, ContentHandler::getLocalizedName( $this->contentModel ) ] + [ + wfEscapeWikiText( $this->contentFormat ), + wfEscapeWikiText( ContentHandler::getLocalizedName( $this->contentModel ) ) + ] ); } @@ -1068,7 +1074,7 @@ class EditPage { * Called on the first invocation, e.g. when a user clicks an edit link * @return bool If the requested section is valid */ - function initialiseForm() { + public function initialiseForm() { global $wgUser; $this->edittime = $this->page->getTimestamp(); $this->editRevId = $this->page->getLatest(); @@ -1252,11 +1258,7 @@ class EditPage { } $revision = $this->mArticle->getRevisionFetched(); if ( $revision === null ) { - if ( !$this->contentModel ) { - $this->contentModel = $this->getTitle()->getContentModel(); - } $handler = ContentHandler::getForModelID( $this->contentModel ); - return $handler->makeEmptyContent(); } $content = $revision->getContent( Revision::FOR_THIS_USER, $user ); @@ -1296,11 +1298,7 @@ class EditPage { $content = $rev ? $rev->getContent( Revision::RAW ) : null; if ( $content === false || $content === null ) { - if ( !$this->contentModel ) { - $this->contentModel = $this->getTitle()->getContentModel(); - } $handler = ContentHandler::getForModelID( $this->contentModel ); - return $handler->makeEmptyContent(); } elseif ( !$this->undidRev ) { // Content models should always be the same since we error @@ -1420,7 +1418,7 @@ class EditPage { * @return bool * @private */ - function tokenOk( &$request ) { + public function tokenOk( &$request ) { global $wgUser; $token = $request->getVal( 'wpEditToken' ); $this->mTokenOk = $wgUser->matchEditToken( $token ); @@ -1621,15 +1619,7 @@ class EditPage { */ protected function runPostMergeFilters( Content $content, Status $status, User $user ) { // Run old style post-section-merge edit filter - if ( !ContentHandler::runLegacyHooks( 'EditFilterMerged', - [ $this, $content, &$this->hookError, $this->summary ], - '1.21' - ) ) { - # Error messages etc. could be handled within the hook... - $status->fatal( 'hookaborted' ); - $status->value = self::AS_HOOK_ERROR; - return false; - } elseif ( $this->hookError != '' ) { + if ( $this->hookError != '' ) { # ...or the hook could be expecting us to produce an error $status->fatal( 'hookaborted' ); $status->value = self::AS_HOOK_ERROR_EXPECTED; @@ -1722,7 +1712,7 @@ class EditPage { * AS_BLOCKED_PAGE_FOR_USER. All that stuff needs to be cleaned up some * time. */ - function internalAttemptSave( &$result, $bot = false ) { + public function internalAttemptSave( &$result, $bot = false ) { global $wgUser, $wgRequest, $wgParser, $wgMaxArticleSize; global $wgContentHandlerUseDB; @@ -1916,7 +1906,7 @@ class EditPage { // Don't save a new page if it's blank or if it's a MediaWiki: // message with content equivalent to default (allow empty pages - // in this case to disable messages, see bug 50124) + // in this case to disable messages, see T52124) $defaultMessageText = $this->mTitle->getDefaultMessageText(); if ( $this->mTitle->getNamespace() === NS_MEDIAWIKI && $defaultMessageText !== false ) { $defaultText = $defaultMessageText; @@ -2278,7 +2268,7 @@ class EditPage { * one might think of X as the "base revision", which is NOT what this returns. * @return Revision Current version when the edit was started */ - function getBaseRevision() { + public function getBaseRevision() { if ( !$this->mBaseRevision ) { $db = wfGetDB( DB_MASTER ); $this->mBaseRevision = $this->editRevId @@ -2330,13 +2320,10 @@ class EditPage { return false; } - function setHeaders() { - global $wgOut, $wgUser, $wgAjaxEditStash, $wgCookieSetOnAutoblock; + public function setHeaders() { + global $wgOut, $wgUser, $wgAjaxEditStash; $wgOut->addModules( 'mediawiki.action.edit' ); - if ( $wgCookieSetOnAutoblock === true ) { - $wgOut->addModules( 'mediawiki.user.blockcookie' ); - } $wgOut->addModuleStyles( 'mediawiki.action.edit.styles' ); if ( $wgUser->getOption( 'showtoolbar' ) ) { @@ -2437,7 +2424,7 @@ class EditPage { # Show log extract when the user is currently blocked if ( $namespace == NS_USER || $namespace == NS_USER_TALK ) { $username = explode( '/', $this->mTitle->getText(), 2 )[0]; - $user = User::newFromName( $username, false /* allow IP users*/ ); + $user = User::newFromName( $username, false /* allow IP users */ ); $ip = User::isIP( $username ); $block = Block::newFromTarget( $user, $user ); if ( !( $user && $user->isLoggedIn() ) && !$ip ) { # User does not exist @@ -2488,11 +2475,13 @@ class EditPage { } # Give a notice if the user is editing a deleted/moved page... if ( !$this->mTitle->exists() ) { + $dbr = wfGetDB( DB_REPLICA ); + LogEventsList::showLogExtract( $wgOut, [ 'delete', 'move' ], $this->mTitle, '', [ 'lim' => 10, - 'conds' => [ "log_action != 'revision'" ], + 'conds' => [ 'log_action != ' . $dbr->addQuotes( 'revision' ) ], 'showIfEmpty' => false, 'msgKey' => [ 'recreate-moveddeleted-warn' ] ] @@ -2590,7 +2579,7 @@ class EditPage { * The $formCallback parameter is deprecated since MediaWiki 1.25. Please * use the EditPage::showEditForm:fields hook instead. */ - function showEditForm( $formCallback = null ) { + public function showEditForm( $formCallback = null ) { global $wgOut, $wgUser; # need to parse the preview early so that we know which templates are used, @@ -2621,7 +2610,7 @@ class EditPage { return; } - $this->showHeader(); + $this->showHeader(); $wgOut->addHTML( $this->editFormPageTop ); @@ -2644,7 +2633,7 @@ class EditPage { } // @todo add EditForm plugin interface and use it here! - // search for textarea1 and textares2, and allow EditForm to override all uses. + // search for textarea1 and textarea2, and allow EditForm to override all uses. $wgOut->addHTML( Html::openElement( 'form', [ @@ -2734,7 +2723,7 @@ class EditPage { if ( $this->hasPresetSummary ) { // If a summary has been preset using &summary= we don't want to prompt for // a different summary. Only prompt for a summary if the summary is blanked. - // (Bug 17416) + // (T19416) $this->autoSumm = md5( '' ); } @@ -3032,7 +3021,7 @@ class EditPage { * * @return array An array in the format [ $label, $input ] */ - function getSummaryInput( $summary = "", $labelText = null, + public function getSummaryInput( $summary = "", $labelText = null, $inputAttrs = null, $spanLabelAttrs = null ) { // Note: the maxlength is overridden in JS to 255 and to make it use UTF-8 bytes, not characters. @@ -3072,6 +3061,7 @@ class EditPage { */ protected function showSummaryInput( $isSubjectPreview, $summary = "" ) { global $wgOut; + # Add a class if 'missingsummary' is triggered to allow styling of the summary line $summaryClass = $this->missingSummary ? 'mw-summarymissed' : 'mw-summary'; if ( $isSubjectPreview ) { @@ -3287,14 +3277,14 @@ HTML */ protected function showPreview( $text ) { global $wgOut; - if ( $this->mTitle->getNamespace() == NS_CATEGORY ) { + if ( $this->mArticle instanceof CategoryPage ) { $this->mArticle->openShowCategory(); } # This hook seems slightly odd here, but makes things more # consistent for extensions. Hooks::run( 'OutputPageBeforeHTML', [ &$wgOut, &$text ] ); $wgOut->addHTML( $text ); - if ( $this->mTitle->getNamespace() == NS_CATEGORY ) { + if ( $this->mArticle instanceof CategoryPage ) { $this->mArticle->closeShowCategory(); } } @@ -3306,7 +3296,7 @@ HTML * If this is a section edit, we'll replace the section as for final * save and then make a comparison. */ - function showDiff() { + public function showDiff() { global $wgUser, $wgContLang, $wgOut; $oldtitlemsg = 'currentrev'; @@ -3520,12 +3510,12 @@ HTML $message = $this->context->msg( 'edithelppage' )->inContentLanguage()->text(); $edithelpurl = Skin::makeInternalOrExternalUrl( $message ); - $attrs = [ - 'target' => 'helpwindow', - 'href' => $edithelpurl, - ]; - $edithelp = Html::linkButton( $this->context->msg( 'edithelp' )->text(), - $attrs, [ 'mw-ui-quiet' ] ) . + $edithelp = + Html::linkButton( + $this->context->msg( 'edithelp' )->text(), + [ 'target' => 'helpwindow', 'href' => $edithelpurl ], + [ 'mw-ui-quiet' ] + ) . $this->context->msg( 'word-separator' )->escaped() . $this->context->msg( 'newwindow' )->parse(); @@ -3548,15 +3538,7 @@ HTML // Avoid PHP 7.1 warning of passing $this by reference $editPage = $this; if ( Hooks::run( 'EditPageBeforeConflictDiff', [ &$editPage, &$wgOut ] ) ) { - $stats = MediaWikiServices::getInstance()->getStatsdDataFactory(); - $stats->increment( 'edit.failures.conflict' ); - // Only include 'standard' namespaces to avoid creating unknown numbers of statsd metrics - if ( - $this->mTitle->getNamespace() >= NS_MAIN && - $this->mTitle->getNamespace() <= NS_CATEGORY_TALK - ) { - $stats->increment( 'edit.failures.conflict.byNamespaceId.' . $this->mTitle->getNamespace() ); - } + $this->incrementConflictStats(); $wgOut->wrapWikiMsg( '<h2>$1</h2>', "yourdiff" ); @@ -3576,6 +3558,18 @@ HTML } } + protected function incrementConflictStats() { + $stats = MediaWikiServices::getInstance()->getStatsdDataFactory(); + $stats->increment( 'edit.failures.conflict' ); + // Only include 'standard' namespaces to avoid creating unknown numbers of statsd metrics + if ( + $this->mTitle->getNamespace() >= NS_MAIN && + $this->mTitle->getNamespace() <= NS_CATEGORY_TALK + ) { + $stats->increment( 'edit.failures.conflict.byNamespaceId.' . $this->mTitle->getNamespace() ); + } + } + /** * @return string */ @@ -3586,12 +3580,11 @@ HTML } elseif ( $this->getContextTitle()->isRedirect() ) { $cancelParams['redirect'] = 'no'; } - $attrs = [ 'id' => 'mw-editform-cancel' ]; - return Linker::linkKnown( + return MediaWikiServices::getInstance()->getLinkRenderer()->makeKnownLink( $this->getContextTitle(), - $this->context->msg( 'cancel' )->parse(), - Html::buttonAttributes( $attrs, [ 'mw-ui-quiet' ] ), + new HtmlArmor( $this->context->msg( 'cancel' )->parse() ), + Html::buttonAttributes( [ 'id' => 'mw-editform-cancel' ], [ 'mw-ui-quiet' ] ), $cancelParams ); } @@ -3683,12 +3676,10 @@ HTML * @throws MWException * @return string */ - function getPreviewText() { + public function getPreviewText() { global $wgOut, $wgRawHtml, $wgLang; global $wgAllowUserCss, $wgAllowUserJs; - $stats = MediaWikiServices::getInstance()->getStatsdDataFactory(); - if ( $wgRawHtml && !$this->mTokenOk ) { // Could be an offsite preview attempt. This is very unsafe if // HTML is enabled, as it could be an attack. @@ -3701,7 +3692,7 @@ HTML $this->context->msg( 'session_fail_preview_html' )->text() . "</div>", true, /* interface */true ); } - $stats->increment( 'edit.failures.session_loss' ); + $this->incrementEditFailureStats( 'session_loss' ); return $parsedNote; } @@ -3725,15 +3716,15 @@ HTML if ( $this->mTriedSave && !$this->mTokenOk ) { if ( $this->mTokenOkExceptSuffix ) { $note = $this->context->msg( 'token_suffix_mismatch' )->plain(); - $stats->increment( 'edit.failures.bad_token' ); + $this->incrementEditFailureStats( 'bad_token' ); } else { $note = $this->context->msg( 'session_fail_preview' )->plain(); - $stats->increment( 'edit.failures.session_loss' ); + $this->incrementEditFailureStats( 'session_loss' ); } } elseif ( $this->incompleteForm ) { $note = $this->context->msg( 'edit_form_incomplete' )->plain(); if ( $this->mTriedSave ) { - $stats->increment( 'edit.failures.incomplete_form' ); + $this->incrementEditFailureStats( 'incomplete_form' ); } } else { $note = $this->context->msg( 'previewnote' )->plain() . ' ' . $continueEditing; @@ -3779,7 +3770,6 @@ HTML } $hook_args = [ $this, &$content ]; - ContentHandler::runLegacyHooks( 'EditPageGetPreviewText', $hook_args, '1.25' ); Hooks::run( 'EditPageGetPreviewContent', $hook_args ); $parserResult = $this->doPreviewParse( $content ); @@ -3822,6 +3812,11 @@ HTML return $previewhead . $previewHTML . $this->previewTextAfterContent; } + private function incrementEditFailureStats( $failureType ) { + $stats = MediaWikiServices::getInstance()->getStatsdDataFactory(); + $stats->increment( 'edit.failures.' . $failureType ); + } + /** * Get parser options for a preview * @return ParserOptions @@ -3860,7 +3855,7 @@ HTML /** * @return array */ - function getTemplates() { + public function getTemplates() { if ( $this->preview || $this->section != '' ) { $templates = []; if ( !isset( $this->mParserOutput ) ) { @@ -3884,7 +3879,7 @@ HTML * @param Title $title Title object for the page being edited (optional) * @return string */ - static function getEditToolbar( $title = null ) { + public static function getEditToolbar( $title = null ) { global $wgContLang, $wgOut; global $wgEnableUploads, $wgForeignFileRepos; @@ -4014,71 +4009,113 @@ HTML } /** + * Return an array of checkbox definitions. + * + * Array keys correspond to the `<input>` 'name' attribute to use for each checkbox. + * + * Array values are associative arrays with the following keys: + * - 'label-message' (required): message for label text + * - 'id' (required): 'id' attribute for the `<input>` + * - 'default' (required): default checkedness (true or false) + * - 'title-message' (optional): used to generate 'title' attribute for the `<label>` + * - 'tooltip' (optional): used to generate 'title' and 'accesskey' attributes + * from messages like 'tooltip-foo', 'accesskey-foo' + * - 'label-id' (optional): 'id' attribute for the `<label>` + * - 'legacy-name' (optional): short name for backwards-compatibility + * @param array $checked Array of checkbox name (matching the 'legacy-name') => bool, + * where bool indicates the checked status of the checkbox + * @return array + */ + protected function getCheckboxesDefinition( $checked ) { + global $wgUser; + $checkboxes = []; + + // don't show the minor edit checkbox if it's a new page or section + if ( !$this->isNew && $wgUser->isAllowed( 'minoredit' ) ) { + $checkboxes['wpMinoredit'] = [ + 'id' => 'wpMinoredit', + 'label-message' => 'minoredit', + // Uses messages: tooltip-minoredit, accesskey-minoredit + 'tooltip' => 'minoredit', + 'label-id' => 'mw-editpage-minoredit', + 'legacy-name' => 'minor', + 'default' => $checked['minor'], + ]; + } + + if ( $wgUser->isLoggedIn() ) { + $checkboxes['wpWatchthis'] = [ + 'id' => 'wpWatchthis', + 'label-message' => 'watchthis', + // Uses messages: tooltip-watch, accesskey-watch + 'tooltip' => 'watch', + 'label-id' => 'mw-editpage-watch', + 'legacy-name' => 'watch', + 'default' => $checked['watch'], + ]; + } + + $editPage = $this; + Hooks::run( 'EditPageGetCheckboxesDefinition', [ $editPage, &$checkboxes ] ); + + return $checkboxes; + } + + /** * Returns an array of html code of the following checkboxes: * minor and watch * * @param int $tabindex Current tabindex - * @param array $checked Array of checkbox => bool, where bool indicates the checked - * status of the checkbox - * + * @param array $checked See getCheckboxesDefinition() * @return array */ public function getCheckboxes( &$tabindex, $checked ) { - global $wgUser, $wgUseMediaWikiUIEverywhere; + global $wgUseMediaWikiUIEverywhere; $checkboxes = []; + $checkboxesDef = $this->getCheckboxesDefinition( $checked ); - // don't show the minor edit checkbox if it's a new page or section + // Backwards-compatibility for the EditPageBeforeEditChecks hook if ( !$this->isNew ) { $checkboxes['minor'] = ''; - $minorLabel = $this->context->msg( 'minoredit' )->parse(); - if ( $wgUser->isAllowed( 'minoredit' ) ) { - $attribs = [ - 'tabindex' => ++$tabindex, - 'accesskey' => $this->context->msg( 'accesskey-minoredit' )->text(), - 'id' => 'wpMinoredit', - ]; - $minorEditHtml = - Xml::check( 'wpMinoredit', $checked['minor'], $attribs ) . - " <label for='wpMinoredit' id='mw-editpage-minoredit'" . - Xml::expandAttributes( [ 'title' => Linker::titleAttrib( 'minoredit', 'withaccess' ) ] ) . - ">{$minorLabel}</label>"; - - if ( $wgUseMediaWikiUIEverywhere ) { - $checkboxes['minor'] = Html::openElement( 'div', [ 'class' => 'mw-ui-checkbox' ] ) . - $minorEditHtml . - Html::closeElement( 'div' ); - } else { - $checkboxes['minor'] = $minorEditHtml; - } - } } - - $watchLabel = $this->context->msg( 'watchthis' )->parse(); $checkboxes['watch'] = ''; - if ( $wgUser->isLoggedIn() ) { + + foreach ( $checkboxesDef as $name => $options ) { + $legacyName = isset( $options['legacy-name'] ) ? $options['legacy-name'] : $name; + $label = $this->context->msg( $options['label-message'] )->parse(); $attribs = [ 'tabindex' => ++$tabindex, - 'accesskey' => $this->context->msg( 'accesskey-watch' )->text(), - 'id' => 'wpWatchthis', + 'id' => $options['id'], ]; - $watchThisHtml = - Xml::check( 'wpWatchthis', $checked['watch'], $attribs ) . - " <label for='wpWatchthis' id='mw-editpage-watch'" . - Xml::expandAttributes( [ 'title' => Linker::titleAttrib( 'watch', 'withaccess' ) ] ) . - ">{$watchLabel}</label>"; + $labelAttribs = [ + 'for' => $options['id'], + ]; + if ( isset( $options['tooltip'] ) ) { + $attribs['accesskey'] = $this->context->msg( "accesskey-{$options['tooltip']}" )->text(); + $labelAttribs['title'] = Linker::titleAttrib( $options['tooltip'], 'withaccess' ); + } + if ( isset( $options['title-message'] ) ) { + $labelAttribs['title'] = $this->context->msg( $options['title-message'] )->text(); + } + if ( isset( $options['label-id'] ) ) { + $labelAttribs['id'] = $options['label-id']; + } + $checkboxHtml = + Xml::check( $name, $options['default'], $attribs ) . + ' ' . + Xml::tags( 'label', $labelAttribs, $label ); + if ( $wgUseMediaWikiUIEverywhere ) { - $checkboxes['watch'] = Html::openElement( 'div', [ 'class' => 'mw-ui-checkbox' ] ) . - $watchThisHtml . - Html::closeElement( 'div' ); - } else { - $checkboxes['watch'] = $watchThisHtml; + $checkboxHtml = Html::rawElement( 'div', [ 'class' => 'mw-ui-checkbox' ], $checkboxHtml ); } + + $checkboxes[ $legacyName ] = $checkboxHtml; } // Avoid PHP 7.1 warning of passing $this by reference $editPage = $this; - Hooks::run( 'EditPageBeforeEditChecks', [ &$editPage, &$checkboxes, &$tabindex ] ); + Hooks::run( 'EditPageBeforeEditChecks', [ &$editPage, &$checkboxes, &$tabindex ], '1.29' ); return $checkboxes; } @@ -4102,34 +4139,41 @@ HTML } else { $buttonLabelKey = !$this->mTitle->exists() ? 'savearticle' : 'savechanges'; } - $buttonLabel = $this->context->msg( $buttonLabelKey )->text(); $attribs = [ 'id' => 'wpSave', 'name' => 'wpSave', 'tabindex' => ++$tabindex, ] + Linker::tooltipAndAccesskeyAttribs( 'save' ); - $buttons['save'] = Html::submitButton( $buttonLabel, $attribs, [ 'mw-ui-progressive' ] ); + $buttons['save'] = Html::submitButton( + $this->context->msg( $buttonLabelKey )->text(), + $attribs, + [ 'mw-ui-progressive' ] + ); - ++$tabindex; // use the same for preview and live preview $attribs = [ 'id' => 'wpPreview', 'name' => 'wpPreview', - 'tabindex' => $tabindex, + 'tabindex' => ++$tabindex, ] + Linker::tooltipAndAccesskeyAttribs( 'preview' ); - $buttons['preview'] = Html::submitButton( $this->context->msg( 'showpreview' )->text(), - $attribs ); + $buttons['preview'] = Html::submitButton( + $this->context->msg( 'showpreview' )->text(), + $attribs + ); $attribs = [ 'id' => 'wpDiff', 'name' => 'wpDiff', 'tabindex' => ++$tabindex, ] + Linker::tooltipAndAccesskeyAttribs( 'diff' ); - $buttons['diff'] = Html::submitButton( $this->context->msg( 'showdiff' )->text(), - $attribs ); + $buttons['diff'] = Html::submitButton( + $this->context->msg( 'showdiff' )->text(), + $attribs + ); // Avoid PHP 7.1 warning of passing $this by reference $editPage = $this; Hooks::run( 'EditPageBeforeEditButtons', [ &$editPage, &$buttons, &$tabindex ] ); + return $buttons; } @@ -4137,7 +4181,7 @@ HTML * Creates a basic error page which informs the user that * they have attempted to edit a nonexistent section. */ - function noSuchSectionPage() { + public function noSuchSectionPage() { global $wgOut; $wgOut->prepareErrorPage( $this->context->msg( 'nosuchsectiontitle' ) ); diff --git a/includes/FeedUtils.php b/includes/FeedUtils.php index 071a3db98fa4..3268291b51fc 100644 --- a/includes/FeedUtils.php +++ b/includes/FeedUtils.php @@ -129,13 +129,6 @@ class FeedUtils { } if ( $oldid ) { - - # $diffText = $de->getDiff( wfMessage( 'revisionasof', - # $wgLang->timeanddate( $timestamp ), - # $wgLang->date( $timestamp ), - # $wgLang->time( $timestamp ) )->text(), - # wfMessage( 'currentrev' )->text() ); - $diffText = ''; // Don't bother generating the diff if we won't be able to show it if ( $wgFeedDiffCutoff > 0 ) { @@ -196,7 +189,7 @@ class FeedUtils { if ( $html === null ) { - // Omit large new page diffs, bug 29110 + // Omit large new page diffs, T31110 // Also use diff link for non-textual content $diffText = self::getDiffLink( $title, $newid ); } else { diff --git a/includes/FileDeleteForm.php b/includes/FileDeleteForm.php index f850152050c0..f284d924a06e 100644 --- a/includes/FileDeleteForm.php +++ b/includes/FileDeleteForm.php @@ -21,6 +21,7 @@ * @author Rob Church <robchur@gmail.com> * @ingroup Media */ +use MediaWiki\MediaWikiServices; /** * File deletion user interface @@ -205,7 +206,8 @@ class FileDeleteForm { $dbw->endAtomic( __METHOD__ ); } else { // Page deleted but file still there? rollback page delete - wfGetLBFactory()->rollbackMasterChanges( __METHOD__ ); + $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory(); + $lbFactory->rollbackMasterChanges( __METHOD__ ); } } else { // Done; nothing changed @@ -301,9 +303,10 @@ class FileDeleteForm { if ( $wgUser->isAllowed( 'editinterface' ) ) { $title = wfMessage( 'filedelete-reason-dropdown' )->inContentLanguage()->getTitle(); - $link = Linker::linkKnown( + $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer(); + $link = $linkRenderer->makeKnownLink( $title, - wfMessage( 'filedelete-edit-reasonlist' )->escaped(), + wfMessage( 'filedelete-edit-reasonlist' )->text(), [], [ 'action' => 'edit' ] ); diff --git a/includes/GlobalFunctions.php b/includes/GlobalFunctions.php index 5343248a9c46..3747c23865fd 100644 --- a/includes/GlobalFunctions.php +++ b/includes/GlobalFunctions.php @@ -222,18 +222,18 @@ function wfAppendToArrayIfNotDefault( $key, $value, $default, &$changed ) { /** * Merge arrays in the style of getUserPermissionsErrors, with duplicate removal * e.g. - * wfMergeErrorArrays( - * [ [ 'x' ] ], - * [ [ 'x', '2' ] ], - * [ [ 'x' ] ], - * [ [ 'y' ] ] - * ); + * wfMergeErrorArrays( + * [ [ 'x' ] ], + * [ [ 'x', '2' ] ], + * [ [ 'x' ] ], + * [ [ 'y' ] ] + * ); * returns: - * [ - * [ 'x', '2' ], - * [ 'x' ], - * [ 'y' ] - * ] + * [ + * [ 'x', '2' ], + * [ 'x' ], + * [ 'y' ] + * ] * * @param array $array1,... * @return array @@ -1787,6 +1787,7 @@ function wfHttpError( $code, $label, $desc ) { $wgOut->sendCacheControl(); } + MediaWiki\HeaderCallback::warnIfHeadersSent(); header( 'Content-type: text/html; charset=utf-8' ); print '<!DOCTYPE html>' . '<html><head><title>' . @@ -2572,8 +2573,8 @@ function wfInitShellLocale() { * @param string $script MediaWiki cli script path * @param array $parameters Arguments and options to the script * @param array $options Associative array of options: - * 'php': The path to the php executable - * 'wrapper': Path to a PHP wrapper to handle the maintenance script + * 'php': The path to the php executable + * 'wrapper': Path to a PHP wrapper to handle the maintenance script * @return string */ function wfShellWikiCmd( $script, array $parameters = [], array $options = [] ) { @@ -3084,7 +3085,7 @@ function wfGetDB( $db, $groups = [], $wiki = false ) { * or MediaWikiServices::getDBLoadBalancerFactory() instead. * * @param string|bool $wiki Wiki ID, or false for the current wiki - * @return LoadBalancer + * @return \Wikimedia\Rdbms\LoadBalancer */ function wfGetLB( $wiki = false ) { if ( $wiki === false ) { @@ -3100,7 +3101,7 @@ function wfGetLB( $wiki = false ) { * * @deprecated since 1.27, use MediaWikiServices::getDBLoadBalancerFactory() instead. * - * @return LBFactory + * @return \Wikimedia\Rdbms\LBFactory */ function wfGetLBFactory() { return \MediaWiki\MediaWikiServices::getInstance()->getDBLoadBalancerFactory(); diff --git a/includes/HeaderCallback.php b/includes/HeaderCallback.php new file mode 100644 index 000000000000..b2ca6733f6cd --- /dev/null +++ b/includes/HeaderCallback.php @@ -0,0 +1,69 @@ +<?php + +namespace MediaWiki; + +class HeaderCallback { + private static $headersSentException; + private static $messageSent = false; + + /** + * Register a callback to be called when headers are sent. There can only + * be one of these handlers active, so all relevant actions have to be in + * here. + */ + public static function register() { + header_register_callback( [ __CLASS__, 'callback' ] ); + } + + /** + * The callback, which is called by the transport + */ + public static function callback() { + // Prevent caching of responses with cookies (T127993) + $headers = []; + foreach ( headers_list() as $header ) { + list( $name, $value ) = explode( ':', $header, 2 ); + $headers[strtolower( trim( $name ) )][] = trim( $value ); + } + + if ( isset( $headers['set-cookie'] ) ) { + $cacheControl = isset( $headers['cache-control'] ) + ? implode( ', ', $headers['cache-control'] ) + : ''; + + if ( !preg_match( '/(?:^|,)\s*(?:private|no-cache|no-store)\s*(?:$|,)/i', + $cacheControl ) + ) { + header( 'Expires: Thu, 01 Jan 1970 00:00:00 GMT' ); + header( 'Cache-Control: private, max-age=0, s-maxage=0' ); + \MediaWiki\Logger\LoggerFactory::getInstance( 'cache-cookies' )->warning( + 'Cookies set on {url} with Cache-Control "{cache-control}"', [ + 'url' => \WebRequest::getGlobalRequestURL(), + 'cookies' => $headers['set-cookie'], + 'cache-control' => $cacheControl ?: '<not set>', + ] + ); + } + } + + // Save a backtrace for logging in case it turns out that headers were sent prematurely + self::$headersSentException = new \Exception( 'Headers already sent from this point' ); + } + + /** + * Log a warning message if headers have already been sent. This can be + * called before flushing the output. + */ + public static function warnIfHeadersSent() { + if ( headers_sent() && !self::$messageSent ) { + self::$messageSent = true; + \MWDebug::warning( 'Headers already sent, should send headers earlier than ' . + wfGetCaller( 3 ) ); + $logger = \MediaWiki\Logger\LoggerFactory::getInstance( 'headers-sent' ); + $logger->error( 'Warning: headers were already sent from the location below', [ + 'exception' => self::$headersSentException, + 'detection-trace' => new \Exception( 'Detected here' ), + ] ); + } + } +} diff --git a/includes/HistoryBlob.php b/includes/HistoryBlob.php index 3d86201c9465..56cf815e457f 100644 --- a/includes/HistoryBlob.php +++ b/includes/HistoryBlob.php @@ -590,7 +590,7 @@ class DiffHistoryBlob implements HistoryBlob { /** * Compute a binary "Adler-32" checksum as defined by LibXDiff, i.e. with - * the bytes backwards and initialised with 0 instead of 1. See bug 34428. + * the bytes backwards and initialised with 0 instead of 1. See T36428. * * @param string $s * @return string|bool False if the hash extension is not available diff --git a/includes/Html.php b/includes/Html.php index b46ea81c5e6b..8fe4dbe513cb 100644 --- a/includes/Html.php +++ b/includes/Html.php @@ -220,8 +220,10 @@ class Html { * Identical to rawElement(), but HTML-escapes $contents (like * Xml::element()). * - * @param string $element - * @param array $attribs + * @param string $element Name of the element, e.g., 'a' + * @param array $attribs Associative array of attributes, e.g., [ + * 'href' => 'https://www.mediawiki.org/' ]. See expandAttributes() for + * further documentation. * @param string $contents * * @return string @@ -239,8 +241,10 @@ class Html { * Identical to rawElement(), but has no third parameter and omits the end * tag (and the self-closing '/' in XML mode for empty elements). * - * @param string $element - * @param array $attribs + * @param string $element Name of the element, e.g., 'a' + * @param array $attribs Associative array of attributes, e.g., [ + * 'href' => 'https://www.mediawiki.org/' ]. See expandAttributes() for + * further documentation. * * @return string */ @@ -459,7 +463,7 @@ class Html { * * @param array $attribs Associative array of attributes, e.g., [ * 'href' => 'https://www.mediawiki.org/' ]. Values will be HTML-escaped. - * A value of false means to omit the attribute. For boolean attributes, + * A value of false or null means to omit the attribute. For boolean attributes, * you can omit the key, e.g., [ 'checked' ] instead of * [ 'checked' => 'checked' ] or such. * @@ -759,7 +763,7 @@ class Html { $attribs['name'] = $name; if ( substr( $value, 0, 1 ) == "\n" ) { - // Workaround for bug 12130: browsers eat the initial newline + // Workaround for T14130: browsers eat the initial newline // assuming that it's just for show, but they do keep the later // newlines, which we may want to preserve during editing. // Prepending a single newline diff --git a/includes/LinkFilter.php b/includes/LinkFilter.php index 7b3d72b336d3..2f5055871acf 100644 --- a/includes/LinkFilter.php +++ b/includes/LinkFilter.php @@ -19,6 +19,7 @@ * * @file */ +use Wikimedia\Rdbms\LikeMatch; /** * Some functions to help implement an external link filter for spam control. diff --git a/includes/Linker.php b/includes/Linker.php index 794e2e91711f..bed9957f00cb 100644 --- a/includes/Linker.php +++ b/includes/Linker.php @@ -164,10 +164,10 @@ class Linker { } /** - * Make appropriate markup for a link to the current article. This is - * currently rendered as the bold link text. The calling sequence is the - * same as the other make*LinkObj static functions, despite $query not - * being used. + * Make appropriate markup for a link to the current article. This is since + * MediaWiki 1.29.0 rendered as an <a> tag without an href and with a class + * showing the link text. The calling sequence is the same as for the other + * make*LinkObj static functions, but $query is not used. * * @since 1.16.3 * @param Title $nt @@ -179,7 +179,7 @@ class Linker { * @return string */ public static function makeSelfLinkObj( $nt, $html = '', $query = '', $trail = '', $prefix = '' ) { - $ret = "<strong class=\"selflink\">{$prefix}{$html}</strong>{$trail}"; + $ret = "<a class=\"mw-selflink selflink\">{$prefix}{$html}</a>{$trail}"; if ( !Hooks::run( 'SelfLinkBegin', [ $nt, &$html, &$trail, &$prefix, &$ret ] ) ) { return $ret; } @@ -188,7 +188,7 @@ class Linker { $html = htmlspecialchars( $nt->getPrefixedText() ); } list( $inside, $trail ) = self::splitTrail( $trail ); - return "<strong class=\"selflink\">{$prefix}{$html}{$inside}</strong>{$trail}"; + return "<a class=\"mw-selflink selflink\">{$prefix}{$html}{$inside}</a>{$trail}"; } /** @@ -590,7 +590,7 @@ class Linker { # ThumbnailImage::toHtml() already adds page= onto the end of DjVu URLs # So we don't need to pass it here in $query. However, the URL for the - # zoom icon still needs it, so we make a unique query for it. See bug 14771 + # zoom icon still needs it, so we make a unique query for it. See T16771 $url = $title->getLocalURL( $query ); if ( $page ) { $url = wfAppendQuery( $url, [ 'page' => $page ] ); @@ -892,7 +892,7 @@ class Linker { if ( $altUserName === false ) { $altUserName = IP::prettifyIP( $userName ); } - $classes .= ' mw-anonuserlink'; // Separate link class for anons (bug 43179) + $classes .= ' mw-anonuserlink'; // Separate link class for anons (T45179) } else { $page = Title::makeTitle( NS_USER, $userName ); } @@ -933,13 +933,14 @@ class Linker { if ( $userId ) { // check if the user has an edit $attribs = []; + $attribs['class'] = 'mw-usertoollinks-contribs'; if ( $redContribsWhenNoEdits ) { if ( intval( $edits ) === 0 && $edits !== 0 ) { $user = User::newFromId( $userId ); $edits = $user->getEditCount(); } if ( $edits === 0 ) { - $attribs['class'] = 'new'; + $attribs['class'] .= ' new'; } } $contribsPage = SpecialPage::getTitleFor( 'Contributions', $userText ); @@ -986,7 +987,10 @@ class Linker { */ public static function userTalkLink( $userId, $userText ) { $userTalkPage = Title::makeTitle( NS_USER_TALK, $userText ); - $userTalkLink = self::link( $userTalkPage, wfMessage( 'talkpagelinktext' )->escaped() ); + $moreLinkAttribs['class'] = 'mw-usertoollinks-talk'; + $userTalkLink = self::link( $userTalkPage, + wfMessage( 'talkpagelinktext' )->escaped(), + $moreLinkAttribs ); return $userTalkLink; } @@ -998,7 +1002,10 @@ class Linker { */ public static function blockLink( $userId, $userText ) { $blockPage = SpecialPage::getTitleFor( 'Block', $userText ); - $blockLink = self::link( $blockPage, wfMessage( 'blocklink' )->escaped() ); + $moreLinkAttribs['class'] = 'mw-usertoollinks-block'; + $blockLink = self::link( $blockPage, + wfMessage( 'blocklink' )->escaped(), + $moreLinkAttribs ); return $blockLink; } @@ -1009,7 +1016,10 @@ class Linker { */ public static function emailLink( $userId, $userText ) { $emailPage = SpecialPage::getTitleFor( 'Emailuser', $userText ); - $emailLink = self::link( $emailPage, wfMessage( 'emaillink' )->escaped() ); + $moreLinkAttribs['class'] = 'mw-usertoollinks-mail'; + $emailLink = self::link( $emailPage, + wfMessage( 'emaillink' )->escaped(), + $moreLinkAttribs ); return $emailLink; } @@ -1086,7 +1096,7 @@ class Linker { ) { # Sanitize text a bit: $comment = str_replace( "\n", " ", $comment ); - # Allow HTML entities (for bug 13815) + # Allow HTML entities (for T15815) $comment = Sanitizer::escapeHtmlAllowEntities( $comment ); # Render autocomments and make links: @@ -1155,7 +1165,7 @@ class Linker { $section = str_replace( '[[', '', $section ); $section = str_replace( ']]', '', $section ); - $section = Sanitizer::normalizeSectionNameWhitespace( $section ); # bug 22784 + $section = Sanitizer::normalizeSectionNameWhitespace( $section ); # T24784 if ( $local ) { $sectionTitle = Title::newFromText( '#' . $section ); } else { @@ -1364,7 +1374,7 @@ class Linker { } else { $suffix = ''; } - # bug 7425 + # T9425 $target = trim( $target ); # Look at the first character if ( $target != '' && $target[0] === '/' ) { @@ -1551,7 +1561,7 @@ class Linker { $title = wfMessage( 'toc' )->inLanguage( $lang )->escaped(); return '<div id="toc" class="toc">' - . '<div id="toctitle"><h2>' . $title . "</h2></div>\n" + . '<div id="toctitle" class="toctitle"><h2>' . $title . "</h2></div>\n" . $toc . "</ul>\n</div>\n"; } @@ -1787,7 +1797,7 @@ class Linker { if ( $context->getRequest()->getBool( 'bot' ) ) { $query['bot'] = '1'; - $query['hidediff'] = '1'; // bug 15999 + $query['hidediff'] = '1'; // T17999 } $disableRollbackEditCount = false; @@ -2121,4 +2131,3 @@ class Linker { } } - diff --git a/includes/MWGrants.php b/includes/MWGrants.php index 58efdc727864..c7c54fd5037f 100644 --- a/includes/MWGrants.php +++ b/includes/MWGrants.php @@ -17,6 +17,7 @@ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * http://www.gnu.org/copyleft/gpl.html */ +use MediaWiki\MediaWikiServices; /** * A collection of public static functions to deal with grants. @@ -178,9 +179,10 @@ class MWGrants { * @return string (proto-relative) HTML link */ public static function getGrantsLink( $grant, $lang = null ) { - return \Linker::linkKnown( + $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer(); + return $linkRenderer->makeKnownLink( \SpecialPage::getTitleFor( 'Listgrants', false, $grant ), - htmlspecialchars( self::grantName( $grant, $lang ) ) + self::grantName( $grant, $lang ) ); } diff --git a/includes/MWTimestamp.php b/includes/MWTimestamp.php index c1e5cc410bcd..7f3649e39c06 100644 --- a/includes/MWTimestamp.php +++ b/includes/MWTimestamp.php @@ -21,6 +21,7 @@ * @since 1.20 * @author Tyler Romeo, 2012 */ +use Wikimedia\Timestamp\ConvertibleTimestamp; /** * Library for creating and parsing MW-style timestamps. Based on the JS diff --git a/includes/MagicWord.php b/includes/MagicWord.php index 5968e879038e..ee9591870014 100644 --- a/includes/MagicWord.php +++ b/includes/MagicWord.php @@ -46,8 +46,8 @@ * $magicWords = []; * * $magicWords['en'] = [ - * 'magicwordkey' => [ 0, 'case_insensitive_magic_word' ], - * 'magicwordkey2' => [ 1, 'CASE_sensitive_magic_word2' ], + * 'magicwordkey' => [ 0, 'case_insensitive_magic_word' ], + * 'magicwordkey2' => [ 1, 'CASE_sensitive_magic_word2' ], * ]; * @endcode * @@ -502,7 +502,7 @@ class MagicWord { # multiple matched parts (variable match); some will be empty because of # synonyms. The variable will be the second non-empty one so remove any # blank elements and re-sort the indices. - # See also bug 6526 + # See also T8526 $matches = array_values( array_filter( $matches ) ); @@ -526,7 +526,7 @@ class MagicWord { $this->mFound = false; $text = preg_replace_callback( $this->getRegex(), - [ &$this, 'pregRemoveAndRecord' ], + [ $this, 'pregRemoveAndRecord' ], $text ); @@ -541,7 +541,7 @@ class MagicWord { $this->mFound = false; $text = preg_replace_callback( $this->getRegexStart(), - [ &$this, 'pregRemoveAndRecord' ], + [ $this, 'pregRemoveAndRecord' ], $text ); diff --git a/includes/MediaWiki.php b/includes/MediaWiki.php index faca533618eb..ef0563e5502d 100644 --- a/includes/MediaWiki.php +++ b/includes/MediaWiki.php @@ -21,7 +21,10 @@ */ use MediaWiki\Logger\LoggerFactory; +use Psr\Log\LoggerInterface; use MediaWiki\MediaWikiServices; +use Wikimedia\Rdbms\ChronologyProtector; +use Wikimedia\Rdbms\LBFactory; /** * The MediaWiki class is the helper class for the index.php entry point. @@ -71,7 +74,7 @@ class MediaWiki { if ( $request->getCheck( 'search' ) ) { // Compatibility with old search URLs which didn't use Special:Search // Just check for presence here, so blank requests still - // show the search page when using ugly URLs (bug 8054). + // show the search page when using ugly URLs (T10054). $ret = SpecialPage::getTitleFor( 'Search' ); } elseif ( $curid ) { // URLs like this are generated by RC, because rc_title isn't always accurate @@ -181,7 +184,7 @@ class MediaWiki { $unused = null; // To pass it by reference Hooks::run( 'BeforeInitialize', [ &$title, &$unused, &$output, &$user, $request, $this ] ); - // Invalid titles. Bug 21776: The interwikis must redirect even if the page name is empty. + // Invalid titles. T23776: The interwikis must redirect even if the page name is empty. if ( is_null( $title ) || ( $title->getDBkey() == '' && !$title->isExternal() ) || $title->isSpecial( 'Badtitle' ) ) { @@ -201,7 +204,7 @@ class MediaWiki { ? [] // relies on HMAC key signature alone : $title->getUserPermissionsErrors( 'read', $user ); if ( count( $permErrors ) ) { - // Bug 32276: allowing the skin to generate output with $wgTitle or + // T34276: allowing the skin to generate output with $wgTitle or // $this->context->title set to the input title would allow anonymous users to // determine whether a page exists, potentially leaking private data. In fact, the // curid and oldid request parameters would allow page titles to be enumerated even @@ -518,7 +521,7 @@ class MediaWiki { try { $this->main(); } catch ( ErrorPageError $e ) { - // Bug 62091: while exceptions are convenient to bubble up GUI errors, + // T64091: while exceptions are convenient to bubble up GUI errors, // they are not internal application faults. As with normal requests, this // should commit, print the output, do deferred updates, jobs, and profiling. $this->doPreOutputCommit(); @@ -940,24 +943,45 @@ class MediaWiki { $n = intval( $jobRunRate ); } - $runJobsLogger = LoggerFactory::getInstance( 'runJobs' ); + $logger = LoggerFactory::getInstance( 'runJobs' ); - // Fall back to running the job(s) while the user waits if needed - if ( !$this->config->get( 'RunJobsAsync' ) ) { - $runner = new JobRunner( $runJobsLogger ); - $runner->run( [ 'maxJobs' => $n ] ); - return; - } - - // Do not send request if there are probably no jobs try { - $group = JobQueueGroup::singleton(); - if ( !$group->queuesHaveJobs( JobQueueGroup::TYPE_DEFAULT ) ) { - return; + if ( $this->config->get( 'RunJobsAsync' ) ) { + // Send an HTTP request to the job RPC entry point if possible + $invokedWithSuccess = $this->triggerAsyncJobs( $n, $logger ); + if ( !$invokedWithSuccess ) { + // Fall back to blocking on running the job(s) + $logger->warning( "Jobs switched to blocking; Special:RunJobs disabled" ); + $this->triggerSyncJobs( $n, $logger ); + } + } else { + $this->triggerSyncJobs( $n, $logger ); } } catch ( JobQueueError $e ) { + // Do not make the site unavailable (T88312) MWExceptionHandler::logException( $e ); - return; // do not make the site unavailable + } + } + + /** + * @param integer $n Number of jobs to try to run + * @param LoggerInterface $runJobsLogger + */ + private function triggerSyncJobs( $n, LoggerInterface $runJobsLogger ) { + $runner = new JobRunner( $runJobsLogger ); + $runner->run( [ 'maxJobs' => $n ] ); + } + + /** + * @param integer $n Number of jobs to try to run + * @param LoggerInterface $runJobsLogger + * @return bool Success + */ + private function triggerAsyncJobs( $n, LoggerInterface $runJobsLogger ) { + // Do not send request if there are probably no jobs + $group = JobQueueGroup::singleton(); + if ( !$group->queuesHaveJobs( JobQueueGroup::TYPE_DEFAULT ) ) { + return true; } $query = [ 'title' => 'Special:RunJobs', @@ -1024,12 +1048,6 @@ class MediaWiki { $runJobsLogger->error( "Failed to start cron API (socket error $errno): $errstr" ); } - // Fall back to running the job(s) while the user waits if needed - if ( !$invokedWithSuccess ) { - $runJobsLogger->warning( "Jobs switched to blocking; Special:RunJobs disabled" ); - - $runner = new JobRunner( $runJobsLogger ); - $runner->run( [ 'maxJobs' => $n ] ); - } + return $invokedWithSuccess; } } diff --git a/includes/MediaWikiServices.php b/includes/MediaWikiServices.php index 7c9363ca19a8..e44fefe9b531 100644 --- a/includes/MediaWikiServices.php +++ b/includes/MediaWikiServices.php @@ -9,10 +9,10 @@ use EventRelayerGroup; use GenderCache; use GlobalVarConfig; use Hooks; -use LBFactory; +use Wikimedia\Rdbms\LBFactory; use LinkCache; use Liuggio\StatsdClient\Factory\StatsdDataFactory; -use LoadBalancer; +use Wikimedia\Rdbms\LoadBalancer; use MediaHandlerFactory; use MediaWiki\Linker\LinkRenderer; use MediaWiki\Linker\LinkRendererFactory; diff --git a/includes/MergeHistory.php b/includes/MergeHistory.php index e57f88099ac6..cc589c981124 100644 --- a/includes/MergeHistory.php +++ b/includes/MergeHistory.php @@ -24,6 +24,8 @@ * * @file */ +use Wikimedia\Timestamp\TimestampException; +use Wikimedia\Rdbms\IDatabase; /** * Handles the backend logic of merging the histories of two diff --git a/includes/MovePage.php b/includes/MovePage.php index ae12ba5e7d84..9a83d35705ea 100644 --- a/includes/MovePage.php +++ b/includes/MovePage.php @@ -501,7 +501,7 @@ class MovePage { $defaultContentModelChanging = ( $oldDefault !== $newDefault && $oldDefault === $contentModel ); - // bug 57084: log_page should be the ID of the *moved* page + // T59084: log_page should be the ID of the *moved* page $oldid = $this->oldTitle->getArticleID(); $logTitle = clone $this->oldTitle; @@ -550,13 +550,13 @@ class MovePage { ); if ( !$redirectContent ) { - // Clean up the old title *before* reset article id - bug 45348 + // Clean up the old title *before* reset article id - T47348 WikiPage::onArticleDelete( $this->oldTitle ); } $this->oldTitle->resetArticleID( 0 ); // 0 == non existing $nt->resetArticleID( $oldid ); - $newpage->loadPageData( WikiPage::READ_LOCKING ); // bug 46397 + $newpage->loadPageData( WikiPage::READ_LOCKING ); // T48397 $newpage->updateRevisionOn( $dbw, $nullRevision ); @@ -581,7 +581,7 @@ class MovePage { # Recreate the redirect, this time in the other direction. if ( $redirectContent ) { $redirectArticle = WikiPage::factory( $this->oldTitle ); - $redirectArticle->loadFromRow( false, WikiPage::READ_LOCKING ); // bug 46397 + $redirectArticle->loadFromRow( false, WikiPage::READ_LOCKING ); // T48397 $newid = $redirectArticle->insertOn( $dbw ); if ( $newid ) { // sanity $this->oldTitle->resetArticleID( $newid ); diff --git a/includes/NoLocalSettings.php b/includes/NoLocalSettings.php index b0a6b7bd0499..50950ef30031 100644 --- a/includes/NoLocalSettings.php +++ b/includes/NoLocalSettings.php @@ -20,7 +20,7 @@ * @file */ -# bug 30219 : can not use pathinfo() on URLs since slashes do not match +# T32219 : can not use pathinfo() on URLs since slashes do not match $matches = []; $ext = 'php'; $path = '/'; diff --git a/includes/OutputPage.php b/includes/OutputPage.php index 211f44bf5c34..551141ec031d 100644 --- a/includes/OutputPage.php +++ b/includes/OutputPage.php @@ -303,6 +303,11 @@ class OutputPage extends ContextSource { private $limitReportJSData = []; /** + * Link: header contents + */ + private $mLinkHeader = []; + + /** * Constructor for OutputPage. This should not be called directly. * Instead a new RequestContext should be created and it will implicitly create * a OutputPage tied to that context. @@ -534,14 +539,32 @@ class OutputPage extends ContextSource { if ( $module instanceof ResourceLoaderModule && $module->getOrigin() <= $this->getAllowedModules( $type ) && ( is_null( $position ) || $module->getPosition() == $position ) - && ( !$this->mTarget || in_array( $this->mTarget, $module->getTargets() ) ) ) { + if ( $this->mTarget && !in_array( $this->mTarget, $module->getTargets() ) ) { + $this->warnModuleTargetFilter( $module->getName() ); + continue; + } $filteredModules[] = $val; } } return $filteredModules; } + private function warnModuleTargetFilter( $moduleName ) { + static $warnings = []; + if ( isset( $warnings[$this->mTarget][$moduleName] ) ) { + return; + } + $warnings[$this->mTarget][$moduleName] = true; + $this->getResourceLoader()->getLogger()->debug( + 'Module "{module}" not loadable on target "{target}".', + [ + 'module' => $moduleName, + 'target' => $this->mTarget, + ] + ); + } + /** * Get the list of modules to include on this page * @@ -763,7 +786,7 @@ class OutputPage extends ContextSource { 'epoch' => $config->get( 'CacheEpoch' ) ]; if ( $config->get( 'UseSquid' ) ) { - // bug 44570: the core page itself may not change, but resources might + // T46570: the core page itself may not change, but resources might $modifiedTimes['sepoch'] = wfTimestamp( TS_MW, time() - $config->get( 'SquidMaxage' ) ); } Hooks::run( 'OutputPageCheckLastModified', [ &$modifiedTimes, $this ] ); @@ -1448,7 +1471,7 @@ class OutputPage extends ContextSource { ResourceLoaderModule::ORIGIN_CORE_INDIVIDUAL ); - // Site-wide styles are controlled by a config setting, see bug 71621 + // Site-wide styles are controlled by a config setting, see T73621 // for background on why. User styles are never allowed. if ( $this->getConfig()->get( 'AllowSiteCSSOnRestrictedPages' ) ) { $styleOrigin = ResourceLoaderModule::ORIGIN_USER_SITEWIDE; @@ -1550,6 +1573,7 @@ class OutputPage extends ContextSource { // been changed somehow, and keep it if so. $anonPO = ParserOptions::newFromAnon(); $anonPO->setEditSection( false ); + $anonPO->setAllowUnsafeRawHtml( false ); if ( !$options->matches( $anonPO ) ) { wfLogWarning( __METHOD__ . ': Setting a changed bogus ParserOptions: ' . wfGetAllCallers( 5 ) ); $options->isBogus = false; @@ -1563,6 +1587,7 @@ class OutputPage extends ContextSource { // either. $po = ParserOptions::newFromAnon(); $po->setEditSection( false ); + $po->setAllowUnsafeRawHtml( false ); $po->isBogus = true; if ( $options !== null ) { $this->mParserOptions = empty( $options->isBogus ) ? $options : null; @@ -1572,6 +1597,7 @@ class OutputPage extends ContextSource { $this->mParserOptions = ParserOptions::newFromContext( $this->getContext() ); $this->mParserOptions->setEditSection( false ); + $this->mParserOptions->setAllowUnsafeRawHtml( false ); } if ( $options !== null && !empty( $options->isBogus ) ) { @@ -2085,6 +2111,28 @@ class OutputPage extends ContextSource { } /** + * Add an HTTP Link: header + * + * @param string $header Header value + */ + public function addLinkHeader( $header ) { + $this->mLinkHeader[] = $header; + } + + /** + * Return a Link: header. Based on the values of $mLinkHeader. + * + * @return string + */ + public function getLinkHeader() { + if ( !$this->mLinkHeader ) { + return false; + } + + return 'Link: ' . implode( ',', $this->mLinkHeader ); + } + + /** * Get a complete Key header * * @return string @@ -2340,6 +2388,12 @@ class OutputPage extends ContextSource { // jQuery etc. can work correctly. $response->header( 'X-UA-Compatible: IE=Edge' ); + $this->addLogoPreloadLinkHeaders(); + $linkHeader = $this->getLinkHeader(); + if ( $linkHeader ) { + $response->header( $linkHeader ); + } + // Prevent framing, if requested $frameOptions = $this->getFrameOptions(); if ( $frameOptions ) { @@ -2349,6 +2403,11 @@ class OutputPage extends ContextSource { if ( $this->mArticleBodyOnly ) { echo $this->mBodytext; } else { + // Enable safe mode if requested + if ( $this->getRequest()->getBool( 'safemode' ) ) { + $this->disallowUserJs(); + } + $sk = $this->getSkin(); // add skin specific modules $modules = $sk->getDefaultModules(); @@ -2484,7 +2543,7 @@ class OutputPage extends ContextSource { ) { $displayReturnto = null; - # Due to bug 32276, if a user does not have read permissions, + # Due to T34276, if a user does not have read permissions, # $this->getTitle() will just give Special:Badtitle, which is # not especially useful as a returnto parameter. Use the title # from the request instead, if there was one. @@ -2515,9 +2574,10 @@ class OutputPage extends ContextSource { $query['returntoquery'] = wfArrayToCgi( $returntoquery ); } } - $loginLink = Linker::linkKnown( + $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer(); + $loginLink = $linkRenderer->makeKnownLink( SpecialPage::getTitleFor( 'Userlogin' ), - $this->msg( 'loginreqlink' )->escaped(), + $this->msg( 'loginreqlink' )->text(), [], $query ); @@ -2705,7 +2765,9 @@ class OutputPage extends ContextSource { } else { $titleObj = Title::newFromText( $returnto ); } - if ( !is_object( $titleObj ) ) { + // We don't want people to return to external interwiki. That + // might potentially be used as part of a phishing scheme + if ( !is_object( $titleObj ) || $titleObj->isExternal() ) { $titleObj = Title::newMainPage(); } @@ -3065,7 +3127,7 @@ class OutputPage extends ContextSource { $curRevisionId = 0; $articleId = 0; - $canonicalSpecialPageName = false; # bug 21115 + $canonicalSpecialPageName = false; # T23115 $title = $this->getTitle(); $ns = $title->getNamespace(); @@ -3075,7 +3137,7 @@ class OutputPage extends ContextSource { $sk = $this->getSkin(); // Get the relevant title so that AJAX features can use the correct page name - // when making API requests from certain special pages (bug 34972). + // when making API requests from certain special pages (T36972). $relevantTitle = $sk->getRelevantTitle(); $relevantUser = $sk->getRelevantUser(); @@ -3258,9 +3320,11 @@ class OutputPage extends ContextSource { } foreach ( $this->mMetatags as $tag ) { - if ( 0 == strcasecmp( 'http:', substr( $tag[0], 0, 5 ) ) ) { + if ( strncasecmp( $tag[0], 'http:', 5 ) === 0 ) { $a = 'http-equiv'; $tag[0] = substr( $tag[0], 5 ); + } elseif ( strncasecmp( $tag[0], 'og:', 3 ) === 0 ) { + $a = 'property'; } else { $a = 'name'; } @@ -3562,7 +3626,6 @@ class OutputPage extends ContextSource { protected function buildExemptModules() { global $wgContLang; - $resourceLoader = $this->getResourceLoader(); $chunks = []; // Things that go after the ResourceLoaderDynamicStyles marker $append = []; @@ -3695,6 +3758,8 @@ class OutputPage extends ContextSource { */ public static function transformResourcePath( Config $config, $path ) { global $IP; + + $localDir = $IP; $remotePathPrefix = $config->get( 'ResourceBasePath' ); if ( $remotePathPrefix === '' ) { // The configured base path is required to be empty string for @@ -3703,12 +3768,23 @@ class OutputPage extends ContextSource { } else { $remotePath = $remotePathPrefix; } - if ( strpos( $path, $remotePath ) !== 0 ) { - // Path is outside wgResourceBasePath, ignore. + if ( strpos( $path, $remotePath ) !== 0 || substr( $path, 0, 2 ) === '//' ) { + // - Path is outside wgResourceBasePath, ignore. + // - Path is protocol-relative. Fixes T155310. Not supported by RelPath lib. return $path; } + // For files in resources, extensions/ or skins/, ResourceBasePath is preferred here. + // For other misc files in $IP, we'll fallback to that as well. There is, however, a fourth + // supported dir/path pair in the configuration (wgUploadDirectory, wgUploadPath) + // which is not expected to be in wgResourceBasePath on CDNs. (T155146) + $uploadPath = $config->get( 'UploadPath' ); + if ( strpos( $path, $uploadPath ) === 0 ) { + $localDir = $config->get( 'UploadDirectory' ); + $remotePathPrefix = $remotePath = $uploadPath; + } + $path = RelPath\getRelativePath( $path, $remotePath ); - return self::transformFilePath( $remotePathPrefix, $IP, $path ); + return self::transformFilePath( $remotePathPrefix, $localDir, $path ); } /** @@ -3822,7 +3898,7 @@ class OutputPage extends ContextSource { * $wgOut->addWikiText( "<div class='error'>\n" * . wfMessage( 'some-error' )->plain() . "\n</div>" ); * - * The newline after the opening div is needed in some wikitext. See bug 19226. + * The newline after the opening div is needed in some wikitext. See T21226. * * @param string $wrap */ @@ -3922,4 +3998,82 @@ class OutputPage extends ContextSource { 'mediawiki.widgets.styles', ] ); } + + /** + * Add Link headers for preloading the wiki's logo. + * + * @since 1.26 + */ + protected function addLogoPreloadLinkHeaders() { + $logo = $this->getConfig()->get( 'Logo' ); // wgLogo + $logoHD = $this->getConfig()->get( 'LogoHD' ); // wgLogoHD + + $tags = []; + $logosPerDppx = []; + $logos = []; + + $logosPerDppx['1.0'] = $logo; + + if ( !$logoHD ) { + // No media queries required if we only have one variant + $this->addLinkHeader( '<' . $logo . '>;rel=preload;as=image' ); + return; + } + + foreach ( $logoHD as $dppx => $src ) { + // Only 1.5x and 2x are supported + // Note: Keep in sync with ResourceLoaderSkinModule + if ( in_array( $dppx, [ '1.5x', '2x' ] ) ) { + // LogoHD uses a string in this format: "1.5x" + $dppx = substr( $dppx, 0, -1 ); + $logosPerDppx[$dppx] = $src; + } + } + + // Because PHP can't have floats as array keys + uksort( $logosPerDppx, function ( $a , $b ) { + $a = floatval( $a ); + $b = floatval( $b ); + + if ( $a == $b ) { + return 0; + } + // Sort from smallest to largest (e.g. 1x, 1.5x, 2x) + return ( $a < $b ) ? -1 : 1; + } ); + + foreach ( $logosPerDppx as $dppx => $src ) { + $logos[] = [ 'dppx' => $dppx, 'src' => $src ]; + } + + $logosCount = count( $logos ); + // Logic must match ResourceLoaderSkinModule: + // - 1x applies to resolution < 1.5dppx + // - 1.5x applies to resolution >= 1.5dppx && < 2dppx + // - 2x applies to resolution >= 2dppx + // Note that min-resolution and max-resolution are both inclusive. + for ( $i = 0; $i < $logosCount; $i++ ) { + if ( $i === 0 ) { + // Smallest dppx + // min-resolution is ">=" (larger than or equal to) + // "not min-resolution" is essentially "<" + $media_query = 'not all and (min-resolution: ' . $logos[ 1 ]['dppx'] . 'dppx)'; + } elseif ( $i !== $logosCount - 1 ) { + // In between + // Media query expressions can only apply "not" to the entire expression + // (e.g. can't express ">= 1.5 and not >= 2). + // Workaround: Use <= 1.9999 in place of < 2. + $upper_bound = floatval( $logos[ $i + 1 ]['dppx'] ) - 0.000001; + $media_query = '(min-resolution: ' . $logos[ $i ]['dppx'] . + 'dppx) and (max-resolution: ' . $upper_bound . 'dppx)'; + } else { + // Largest dppx + $media_query = '(min-resolution: ' . $logos[ $i ]['dppx'] . 'dppx)'; + } + + $this->addLinkHeader( + '<' . $logos[$i]['src'] . '>;rel=preload;as=image;media=' . $media_query + ); + } + } } diff --git a/includes/PHPVersionCheck.php b/includes/PHPVersionCheck.php index e6e96c7ede89..c56931e484c9 100644 --- a/includes/PHPVersionCheck.php +++ b/includes/PHPVersionCheck.php @@ -75,16 +75,17 @@ class PHPVersionCheck { * @return $this */ function checkRequiredPHPVersion() { - if ( !function_exists( 'version_compare' ) - || version_compare( $this->getPHPImplVersion(), $this->minimumVersionPHP ) < 0 + if ( + !function_exists( 'version_compare' ) + || version_compare( $this->getPHPImplVersion(), $this->minimumVersionPHP ) < 0 ) { $shortText = "MediaWiki $this->mwVersion requires at least PHP version" - . " $this->minimumVersionPHP, you are using PHP {$this->getPHPImplVersion()}."; + . " $this->minimumVersionPHP, you are using PHP {$this->getPHPImplVersion()}."; $longText = "Error: You might be using on older PHP version. \n" - . "MediaWiki $this->mwVersion needs PHP $this->minimumVersionPHP or higher.\n\n" - . "Check if you have a newer php executable with a different name, " - . "such as php5.\n\n"; + . "MediaWiki $this->mwVersion needs PHP $this->minimumVersionPHP or higher.\n\n" + . "Check if you have a newer php executable with a different name, " + . "such as php5.\n\n"; $longHtml = <<<HTML Please consider <a href="http://www.php.net/downloads.php">upgrading your copy of PHP</a>. @@ -112,10 +113,10 @@ HTML; $shortText = "Installing some external dependencies (e.g. via composer) is required."; $longText = "Error: You are missing some external dependencies. \n" - . "MediaWiki now also has some external dependencies that need to be installed\n" - . "via composer or from a separate git repo. Please see\n" - . "https://www.mediawiki.org/wiki/Download_from_Git#Fetch_external_libraries\n" - . "for help on installing the required components."; + . "MediaWiki now also has some external dependencies that need to be installed\n" + . "via composer or from a separate git repo. Please see\n" + . "https://www.mediawiki.org/wiki/Download_from_Git#Fetch_external_libraries\n" + . "for help on installing the required components."; $longHtml = <<<HTML MediaWiki now also has some external dependencies that need to be installed via @@ -150,12 +151,12 @@ HTML; foreach ( $missingExtensions as $ext ) { $missingExtText .= " * $ext <$baseUrl/$ext>\n"; $missingExtHtml .= "<li><b>$ext</b> " - . "(<a href=\"$baseUrl/$ext\">more information</a>)</li>"; + . "(<a href=\"$baseUrl/$ext\">more information</a>)</li>"; } $cliText = "Error: Missing one or more required components of PHP.\n" - . "You are missing a required extension to PHP that MediaWiki needs.\n" - . "Please install:\n" . $missingExtText; + . "You are missing a required extension to PHP that MediaWiki needs.\n" + . "Please install:\n" . $missingExtText; $longHtml = <<<HTML You are missing a required extension to PHP that MediaWiki @@ -198,7 +199,7 @@ HTML; } $encLogo = htmlspecialchars( str_replace( '//', '/', $dirname . '/' ) . - 'resources/assets/mediawiki.png' ); + 'resources/assets/mediawiki.png' ); $shortHtml = htmlspecialchars( $shortText ); header( 'Content-type: text/html; charset=UTF-8' ); diff --git a/includes/Preferences.php b/includes/Preferences.php index 89982a61e8d1..b428e87be4d4 100644 --- a/includes/Preferences.php +++ b/includes/Preferences.php @@ -120,7 +120,7 @@ class Preferences { } } - # # Make sure that form fields have their parent set. See bug 41337. + # # Make sure that form fields have their parent set. See T43337. $dummyForm = new HTMLForm( [], $context ); $disable = !$user->isAllowed( 'editmyoptions' ); @@ -222,24 +222,48 @@ class Preferences { 'section' => 'personal/info', ]; + $lang = $context->getLanguage(); + # Get groups to which the user belongs $userEffectiveGroups = $user->getEffectiveGroups(); - $userGroups = $userMembers = []; + $userGroupMemberships = $user->getGroupMemberships(); + $userGroups = $userMembers = $userTempGroups = $userTempMembers = []; foreach ( $userEffectiveGroups as $ueg ) { if ( $ueg == '*' ) { // Skip the default * group, seems useless here continue; } - $groupName = User::getGroupName( $ueg ); - $userGroups[] = User::makeGroupLinkHTML( $ueg, $groupName ); - $memberName = User::getGroupMember( $ueg, $userName ); - $userMembers[] = User::makeGroupLinkHTML( $ueg, $memberName ); - } - asort( $userGroups ); - asort( $userMembers ); + if ( isset( $userGroupMemberships[$ueg] ) ) { + $groupStringOrObject = $userGroupMemberships[$ueg]; + } else { + $groupStringOrObject = $ueg; + } - $lang = $context->getLanguage(); + $userG = UserGroupMembership::getLink( $groupStringOrObject, $context, 'html' ); + $userM = UserGroupMembership::getLink( $groupStringOrObject, $context, 'html', + $userName ); + + // Store expiring groups separately, so we can place them before non-expiring + // groups in the list. This is to avoid the ambiguity of something like + // "administrator, bureaucrat (until X date)" -- users might wonder whether the + // expiry date applies to both groups, or just the last one + if ( $groupStringOrObject instanceof UserGroupMembership && + $groupStringOrObject->getExpiry() + ) { + $userTempGroups[] = $userG; + $userTempMembers[] = $userM; + } else { + $userGroups[] = $userG; + $userMembers[] = $userM; + } + } + sort( $userGroups ); + sort( $userMembers ); + sort( $userTempGroups ); + sort( $userTempMembers ); + $userGroups = array_merge( $userTempGroups, $userGroups ); + $userMembers = array_merge( $userTempMembers, $userMembers ); $defaultPreferences['usergroups'] = [ 'type' => 'info', @@ -493,9 +517,9 @@ class Preferences { } else { $disableEmailPrefs = true; $emailauthenticated = $context->msg( 'emailnotauthenticated' )->parse() . '<br />' . - Linker::linkKnown( + $linkRenderer->makeKnownLink( SpecialPage::getTitleFor( 'Confirmemail' ), - $context->msg( 'emailconfirmlink' )->escaped() + $context->msg( 'emailconfirmlink' )->text() ) . '<br />'; $emailauthenticationclass = "mw-email-not-authenticated"; } @@ -942,11 +966,12 @@ class Preferences { 'raw' => [ 'EditWatchlist', 'raw' ], 'clear' => [ 'EditWatchlist', 'clear' ], ]; + $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer(); foreach ( $editWatchlistModes as $editWatchlistMode => $mode ) { // Messages: prefs-editwatchlist-edit, prefs-editwatchlist-raw, prefs-editwatchlist-clear - $editWatchlistLinks[] = Linker::linkKnown( + $editWatchlistLinks[] = $linkRenderer->makeKnownLink( SpecialPage::getTitleFor( $mode[0], $mode[1] ), - $context->msg( "prefs-editwatchlist-{$editWatchlistMode}" )->parse() + new HtmlArmor( $context->msg( "prefs-editwatchlist-{$editWatchlistMode}" )->parse() ) ); } @@ -1176,8 +1201,7 @@ class Preferences { if ( $dateopts ) { if ( !in_array( 'default', $dateopts ) ) { - $dateopts[] = 'default'; // Make sure default is always valid - // Bug 19237 + $dateopts[] = 'default'; // Make sure default is always valid T21237 } // FIXME KLUGE: site default might not be valid for user language diff --git a/includes/PrefixSearch.php b/includes/PrefixSearch.php index 48b1d72e1b8f..62ee5c650d12 100644 --- a/includes/PrefixSearch.php +++ b/includes/PrefixSearch.php @@ -231,7 +231,7 @@ abstract class PrefixSearch { } } - # normalize searchKey, so aliases with spaces can be found - bug 25675 + # normalize searchKey, so aliases with spaces can be found - T27675 $searchKey = str_replace( ' ', '_', $searchKey ); $searchKey = $wgContLang->caseFold( $searchKey ); @@ -243,7 +243,7 @@ abstract class PrefixSearch { } foreach ( $wgContLang->getSpecialPageAliases() as $page => $aliases ) { - if ( !in_array( $page, SpecialPageFactory::getNames() ) ) {# bug 20885 + if ( !in_array( $page, SpecialPageFactory::getNames() ) ) {# T22885 continue; } @@ -256,7 +256,7 @@ abstract class PrefixSearch { $matches = []; foreach ( $keys as $pageKey => $page ) { if ( $searchKey === '' || strpos( $pageKey, $searchKey ) === 0 ) { - // bug 27671: Don't use SpecialPage::getTitleFor() here because it + // T29671: Don't use SpecialPage::getTitleFor() here because it // localizes its input leading to searches for e.g. Special:All // returning Spezial:MediaWiki-Systemnachrichten and returning // Spezial:Alle_Seiten twice when $wgLanguageCode == 'de' diff --git a/includes/ProtectionForm.php b/includes/ProtectionForm.php index bcf4dda98ade..a68c36fb6bab 100644 --- a/includes/ProtectionForm.php +++ b/includes/ProtectionForm.php @@ -22,6 +22,7 @@ * * @file */ +use MediaWiki\MediaWikiServices; /** * Handles the page protection UI and backend @@ -182,21 +183,10 @@ class ProtectionForm { throw new ErrorPageError( 'protect-badnamespace-title', 'protect-badnamespace-text' ); } - $out = $this->mContext->getOutput(); - if ( !wfMessage( 'protect-dropdown' )->inContentLanguage()->isDisabled() ) { - $reasonsList = Xml::getArrayFromWikiTextList( - wfMessage( 'protect-dropdown' )->inContentLanguage()->text() - ); - $out->addModules( 'mediawiki.reasonSuggest' ); - $out->addJsConfigVars( [ - 'reasons' => $reasonsList - ] ); - } - if ( $this->mContext->getRequest()->wasPosted() ) { if ( $this->save() ) { $q = $this->mArticle->isRedirect() ? 'redirect=no' : ''; - $out->redirect( $this->mTitle->getFullURL( $q ) ); + $this->mContext->getOutput()->redirect( $this->mTitle->getFullURL( $q ) ); } } else { $this->show(); @@ -554,9 +544,10 @@ class ProtectionForm { $out .= Xml::closeElement( 'fieldset' ); if ( $user->isAllowed( 'editinterface' ) ) { - $link = Linker::linkKnown( + $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer(); + $link = $linkRenderer->makeKnownLink( $context->msg( 'protect-dropdown' )->inContentLanguage()->getTitle(), - $context->msg( 'protect-edit-reasonlist' )->escaped(), + $context->msg( 'protect-edit-reasonlist' )->text(), [], [ 'action' => 'edit' ] ); diff --git a/includes/Revision.php b/includes/Revision.php index 8721ef9a56fb..bae974f15c5f 100644 --- a/includes/Revision.php +++ b/includes/Revision.php @@ -19,8 +19,12 @@ * * @file */ + +use Wikimedia\Rdbms\IDatabase; use MediaWiki\Linker\LinkTarget; use MediaWiki\MediaWikiServices; +use Wikimedia\Rdbms\ResultWrapper; +use Wikimedia\Rdbms\FakeResultWrapper; /** * @todo document @@ -216,7 +220,7 @@ class Revision implements IDBAccessObject { // Pre-1.5 ar_text row $attribs['text'] = self::getRevisionText( $row, 'ar_' ); if ( $attribs['text'] === false ) { - throw new MWException( 'Unable to load text from archive row (possibly bug 22624)' ); + throw new MWException( 'Unable to load text from archive row (possibly T24624)' ); } } return new self( $attribs ); @@ -1036,28 +1040,6 @@ class Revision implements IDBAccessObject { } /** - * Fetch revision text if it's available to the specified audience. - * If the specified audience does not have the ability to view this - * revision, an empty string will be returned. - * - * @param int $audience One of: - * Revision::FOR_PUBLIC to be displayed to all users - * Revision::FOR_THIS_USER to be displayed to the given user - * Revision::RAW get the text regardless of permissions - * @param User $user User object to check for, only if FOR_THIS_USER is passed - * to the $audience parameter - * - * @deprecated since 1.21, use getContent() instead - * @return string - */ - public function getText( $audience = self::FOR_PUBLIC, User $user = null ) { - wfDeprecated( __METHOD__, '1.21' ); - - $content = $this->getContent( $audience, $user ); - return ContentHandler::getContentText( $content ); # returns the raw content text, if applicable - } - - /** * Fetch revision content if it's available to the specified audience. * If the specified audience does not have the ability to view this * revision, null will be returned. @@ -1260,8 +1242,9 @@ class Revision implements IDBAccessObject { /** * Get revision text associated with an old or archive row - * $row is usually an object from wfFetchRow(), both the flags and the text - * field must be included. + * + * Both the flags and the text field must be included. Including the old_id + * field will activate cache usage as long as the $wiki parameter is not set. * * @param stdClass $row The text data * @param string $prefix Table prefix (default 'old_') @@ -1272,8 +1255,6 @@ class Revision implements IDBAccessObject { * @return string|false Text the text requested or false on failure */ public static function getRevisionText( $row, $prefix = 'old_', $wiki = false ) { - - # Get data $textField = $prefix . 'text'; $flagsField = $prefix . 'flags'; @@ -1289,21 +1270,35 @@ class Revision implements IDBAccessObject { return false; } - # Use external methods for external objects, text in table is URL-only then + // Use external methods for external objects, text in table is URL-only then if ( in_array( 'external', $flags ) ) { $url = $text; $parts = explode( '://', $url, 2 ); if ( count( $parts ) == 1 || $parts[1] == '' ) { return false; } - $text = ExternalStore::fetchFromURL( $url, [ 'wiki' => $wiki ] ); - } - // If the text was fetched without an error, convert it - if ( $text !== false ) { - $text = self::decompressRevisionText( $text, $flags ); + if ( isset( $row->old_id ) && $wiki === false ) { + // Make use of the wiki-local revision text cache + $cache = MediaWikiServices::getInstance()->getMainWANObjectCache(); + // The cached value should be decompressed, so handle that and return here + return $cache->getWithSetCallback( + $cache->makeKey( 'revisiontext', 'textid', $row->old_id ), + self::getCacheTTL( $cache ), + function () use ( $url, $wiki, $flags ) { + // No negative caching per Revision::loadText() + $text = ExternalStore::fetchFromURL( $url, [ 'wiki' => $wiki ] ); + + return self::decompressRevisionText( $text, $flags ); + }, + [ 'pcGroup' => self::TEXT_CACHE_GROUP, 'pcTTL' => $cache::TTL_PROC_LONG ] + ); + } else { + $text = ExternalStore::fetchFromURL( $url, [ 'wiki' => $wiki ] ); + } } - return $text; + + return self::decompressRevisionText( $text, $flags ); } /** @@ -1349,6 +1344,13 @@ class Revision implements IDBAccessObject { * @return string|bool Decompressed text, or false on failure */ public static function decompressRevisionText( $text, $flags ) { + global $wgLegacyEncoding, $wgContLang; + + if ( $text === false ) { + // Text failed to be fetched; nothing to do + return false; + } + if ( in_array( 'gzip', $flags ) ) { # Deal with optional compression of archived pages. # This can be done periodically via maintenance/compressOld.php, and @@ -1371,7 +1373,6 @@ class Revision implements IDBAccessObject { $text = $obj->getText(); } - global $wgLegacyEncoding; if ( $text !== false && $wgLegacyEncoding && !in_array( 'utf-8', $flags ) && !in_array( 'utf8', $flags ) ) { @@ -1379,7 +1380,6 @@ class Revision implements IDBAccessObject { # Upconvert on demand. # ("utf8" checked for compatibility with some broken # conversion scripts 2008-12-30) - global $wgContLang; $text = $wgContLang->iconv( $wgLegacyEncoding, 'UTF-8', $text ); } @@ -1579,15 +1579,14 @@ class Revision implements IDBAccessObject { } /** - * Lazy-load the revision's text. - * Currently hardcoded to the 'text' table storage engine. + * Get the text cache TTL * - * @return string|bool The revision's text, or false on failure + * @param WANObjectCache $cache + * @return integer */ - private function loadText() { + private static function getCacheTTL( WANObjectCache $cache ) { global $wgRevisionCacheExpiry; - $cache = ObjectCache::getMainWANInstance(); if ( $cache->getQoS( $cache::ATTR_EMULATION ) <= $cache::QOS_EMULATION_SQL ) { // Do not cache RDBMs blobs in...the RDBMs store $ttl = $cache::TTL_UNCACHEABLE; @@ -1595,10 +1594,22 @@ class Revision implements IDBAccessObject { $ttl = $wgRevisionCacheExpiry ?: $cache::TTL_UNCACHEABLE; } + return $ttl; + } + + /** + * Lazy-load the revision's text. + * Currently hardcoded to the 'text' table storage engine. + * + * @return string|bool The revision's text, or false on failure + */ + private function loadText() { + $cache = ObjectCache::getMainWANInstance(); + // No negative caching; negative hits on text rows may be due to corrupted replica DBs return $cache->getWithSetCallback( $cache->makeKey( 'revisiontext', 'textid', $this->getTextId() ), - $ttl, + self::getCacheTTL( $cache ), function () { return $this->fetchText(); }, @@ -1803,6 +1814,7 @@ class Revision implements IDBAccessObject { * * @param Title $title * @param int $id + * @param int $flags * @return string|bool False if not found */ static function getTimestampFromId( $title, $id, $flags = 0 ) { diff --git a/includes/RevisionList.php b/includes/RevisionList.php index 052fd16fe2f8..ccdedb8e6165 100644 --- a/includes/RevisionList.php +++ b/includes/RevisionList.php @@ -21,6 +21,8 @@ */ use MediaWiki\MediaWikiServices; +use Wikimedia\Rdbms\ResultWrapper; +use Wikimedia\Rdbms\IDatabase; /** * List for revision table items for a single page @@ -35,7 +37,7 @@ abstract class RevisionListBase extends ContextSource implements Iterator { /** @var ResultWrapper|bool */ protected $res; - /** @var bool|object */ + /** @var bool|Revision */ protected $current; /** @@ -363,13 +365,14 @@ class RevisionItem extends RevisionItemBase { * @return string */ protected function getRevisionLink() { - $date = htmlspecialchars( $this->list->getLanguage()->userTimeAndDate( - $this->revision->getTimestamp(), $this->list->getUser() ) ); + $date = $this->list->getLanguage()->userTimeAndDate( + $this->revision->getTimestamp(), $this->list->getUser() ); if ( $this->isDeleted() && !$this->canViewContent() ) { - return $date; + return htmlspecialchars( $date ); } - return Linker::linkKnown( + $linkRenderer = $this->getLinkRenderer(); + return $linkRenderer->makeKnownLink( $this->list->title, $date, [], @@ -391,9 +394,10 @@ class RevisionItem extends RevisionItemBase { if ( $this->isDeleted() && !$this->canViewContent() ) { return $this->context->msg( 'diff' )->escaped(); } else { - return Linker::linkKnown( + $linkRenderer = $this->getLinkRenderer(); + return $linkRenderer->makeKnownLink( $this->list->title, - $this->list->msg( 'diff' )->escaped(), + $this->list->msg( 'diff' )->text(), [], [ 'diff' => $this->revision->getId(), diff --git a/includes/Sanitizer.php b/includes/Sanitizer.php index 6779189486ff..5f6abee48535 100644 --- a/includes/Sanitizer.php +++ b/includes/Sanitizer.php @@ -344,12 +344,12 @@ class Sanitizer { $space = '[\x09\x0a\x0c\x0d\x20]'; self::$attribsRegex = "/(?:^|$space)({$attribFirst}{$attrib}*) - ($space*=$space* + ($space*=$space* (?: - # The attribute value: quoted or alone - \"([^\"]*)(?:\"|\$) - | '([^']*)(?:'|\$) - | (((?!$space|>).)*) + # The attribute value: quoted or alone + \"([^\"]*)(?:\"|\$) + | '([^']*)(?:'|\$) + | (((?!$space|>).)*) ) )?(?=$space|\$)/sx"; } @@ -545,7 +545,7 @@ class Sanitizer { $badtag = true; } elseif ( in_array( $t, $tagstack ) && !isset( $htmlnest[$t] ) ) { $badtag = true; - # Is it a self closed htmlpair ? (bug 5487) + # Is it a self closed htmlpair ? (T7487) } elseif ( $brace == '/>' && isset( $htmlpairs[$t] ) ) { // Eventually we'll just remove the self-closing // slash, in order to be consistent with HTML5 @@ -922,7 +922,7 @@ class Sanitizer { // Normalize Halfwidth and Fullwidth Unicode block that IE6 might treat as ascii $value = preg_replace_callback( - '/[!-[]-z]/u', // U+FF01 to U+FF5A, excluding U+FF3C (bug 58088) + '/[!-[]-z]/u', // U+FF01 to U+FF5A, excluding U+FF3C (T60088) function ( $matches ) { $cp = UtfNormal\Utils::utf8ToCodepoint( $matches[0] ); if ( $cp === false ) { @@ -1119,6 +1119,7 @@ class Sanitizer { '>' => '>', // we've received invalid input '"' => '"', // which should have been escaped. '{' => '{', + '}' => '}', // prevent unpaired language conversion syntax '[' => '[', "''" => '''', 'ISBN' => 'ISBN', @@ -1507,7 +1508,7 @@ class Sanitizer { /** * Decode any character references, numeric or named entities, - * in the next and normalize the resulting string. (bug 14952) + * in the next and normalize the resulting string. (T16952) * * This is useful for page titles, not for text to be displayed, * MediaWiki allows HTML entities to escape normalization as a feature. @@ -1925,7 +1926,7 @@ class Sanitizer { * 3.5. * * This function is an implementation of the specification as requested in - * bug 22449. + * T24449. * * Client-side forms will use the same standard validation rules via JS or * HTML 5 validation; additional restrictions can be enforced server-side @@ -1948,7 +1949,7 @@ class Sanitizer { // Please note strings below are enclosed in brackets [], this make the // hyphen "-" a range indicator. Hence it is double backslashed below. - // See bug 26948 + // See T28948 $rfc5322_atext = "a-z0-9!#$%&'*+\\-\/=?^_`{|}~"; $rfc1034_ldh_str = "a-z0-9\\-"; diff --git a/includes/Setup.php b/includes/Setup.php index ecd164d3bc94..5ea96dd0404d 100644 --- a/includes/Setup.php +++ b/includes/Setup.php @@ -244,7 +244,7 @@ if ( $wgUseInstantCommons ) { 'transformVia404' => true, 'fetchDescription' => true, 'descriptionCacheExpiry' => 43200, - 'apiThumbCacheExpiry' => 86400, + 'apiThumbCacheExpiry' => 0, ]; } /* @@ -329,7 +329,7 @@ if ( $wgEnableEmail ) { $wgUseEnotif = $wgEnotifUserTalk || $wgEnotifWatchlist; } else { // Disable all other email settings automatically if $wgEnableEmail - // is set to false. - bug 63678 + // is set to false. - T65678 $wgAllowHTMLEmail = false; $wgEmailAuthentication = false; // do not require auth if you're not sending email anyway $wgEnableUserEmail = false; @@ -403,6 +403,12 @@ if ( is_array( $wgExtraNamespaces ) ) { $wgCanonicalNamespaceNames = $wgCanonicalNamespaceNames + $wgExtraNamespaces; } +// Merge in the legacy language codes, incorporating overrides from the config +$wgDummyLanguageCodes += [ + 'qqq' => 'qqq', // Used for message documentation + 'qqx' => 'qqx', // Used for viewing message keys +] + $wgExtraLanguageCodes + LanguageCode::getDeprecatedCodeMapping(); + // These are now the same, always // To determine the user language, use $wgLang->getCode() $wgContLanguageCode = $wgLanguageCode; @@ -521,35 +527,6 @@ if ( $wgSharedDB && $wgSharedTables ) { // is complete. define( 'MW_SERVICE_BOOTSTRAP_COMPLETE', 1 ); -// Install a header callback to prevent caching of responses with cookies (T127993) -if ( !$wgCommandLineMode ) { - header_register_callback( function () { - $headers = []; - foreach ( headers_list() as $header ) { - list( $name, $value ) = explode( ':', $header, 2 ); - $headers[strtolower( trim( $name ) )][] = trim( $value ); - } - - if ( isset( $headers['set-cookie'] ) ) { - $cacheControl = isset( $headers['cache-control'] ) - ? implode( ', ', $headers['cache-control'] ) - : ''; - - if ( !preg_match( '/(?:^|,)\s*(?:private|no-cache|no-store)\s*(?:$|,)/i', $cacheControl ) ) { - header( 'Expires: Thu, 01 Jan 1970 00:00:00 GMT' ); - header( 'Cache-Control: private, max-age=0, s-maxage=0' ); - MediaWiki\Logger\LoggerFactory::getInstance( 'cache-cookies' )->warning( - 'Cookies set on {url} with Cache-Control "{cache-control}"', [ - 'url' => WebRequest::getGlobalRequestURL(), - 'cookies' => $headers['set-cookie'], - 'cache-control' => $cacheControl ?: '<not set>', - ] - ); - } - } - } ); -} - MWExceptionHandler::installHandler(); require_once "$IP/includes/compat/normal/UtfNormalUtil.php"; diff --git a/includes/SiteStats.php b/includes/SiteStats.php index ff7875c9480b..bc6b84248ca2 100644 --- a/includes/SiteStats.php +++ b/includes/SiteStats.php @@ -20,6 +20,8 @@ * @file */ +use Wikimedia\Rdbms\IDatabase; + /** * Static accessor class for site_stats and related things */ @@ -186,6 +188,7 @@ class SiteStats { wfMemcKey( 'SiteStats', 'groupcounts', $group ), $cache::TTL_HOUR, function ( $oldValue, &$ttl, array &$setOpts ) use ( $group ) { + global $wgDisableUserGroupExpiry; $dbr = wfGetDB( DB_REPLICA ); $setOpts += Database::getCacheSetOptions( $dbr ); @@ -193,7 +196,12 @@ class SiteStats { return $dbr->selectField( 'user_groups', 'COUNT(*)', - [ 'ug_group' => $group ], + [ + 'ug_group' => $group, + $wgDisableUserGroupExpiry ? + '1' : + 'ug_expiry IS NULL OR ug_expiry >= ' . $dbr->addQuotes( $dbr->timestamp() ) + ], __METHOD__ ); }, diff --git a/includes/TemplateParser.php b/includes/TemplateParser.php index 470a75c4b982..924c347aa53f 100644 --- a/includes/TemplateParser.php +++ b/includes/TemplateParser.php @@ -54,18 +54,11 @@ class TemplateParser { * @throws UnexpectedValueException If $templateName attempts upwards directory traversal */ protected function getTemplateFilename( $templateName ) { - // Prevent upwards directory traversal using same methods as Title::secureAndSplit + // Prevent path traversal. Based on Language::isValidCode(). + // This is for paranoia. The $templateName should never come from + // untrusted input. if ( - strpos( $templateName, '.' ) !== false && - ( - $templateName === '.' || $templateName === '..' || - strpos( $templateName, './' ) === 0 || - strpos( $templateName, '../' ) === 0 || - strpos( $templateName, '/./' ) !== false || - strpos( $templateName, '/../' ) !== false || - substr( $templateName, -2 ) === '/.' || - substr( $templateName, -3 ) === '/..' - ) + strcspn( $templateName, ":/\\\000&<>'\"%" ) !== strlen( $templateName ) ) { throw new UnexpectedValueException( "Malformed \$templateName: $templateName" ); } diff --git a/includes/Title.php b/includes/Title.php index 65b69a29a60d..28db064f329d 100644 --- a/includes/Title.php +++ b/includes/Title.php @@ -21,6 +21,8 @@ * * @file */ + +use Wikimedia\Rdbms\IDatabase; use MediaWiki\Linker\LinkTarget; use MediaWiki\Interwiki\InterwikiLookup; use MediaWiki\MediaWikiServices; @@ -134,7 +136,7 @@ class Title implements LinkTarget { /** * @var int Namespace index when there is no namespace. Don't change the - * following default, NS_MAIN is hardcoded in several places. See bug 696. + * following default, NS_MAIN is hardcoded in several places. See T2696. * Zero except in {{transclusion}} tags. */ public $mDefaultNamespace = NS_MAIN; @@ -307,7 +309,7 @@ class Title implements LinkTarget { } } - // Convert things like é ā or 〗 into normalized (bug 14952) text + // Convert things like é ā or 〗 into normalized (T16952) text $filteredText = Sanitizer::decodeCharReferencesAndNormalize( $text ); $t = new Title(); @@ -1233,12 +1235,6 @@ class Title implements LinkTarget { && ( $this->hasContentModel( CONTENT_MODEL_CSS ) || $this->hasContentModel( CONTENT_MODEL_JAVASCRIPT ) ); - # @note This hook is also called in ContentHandler::getDefaultModel. - # It's called here again to make sure hook functions can force this - # method to return true even outside the MediaWiki namespace. - - Hooks::run( 'TitleIsCssOrJsPage', [ $this, &$isCssOrJsPage ], '1.25' ); - return $isCssOrJsPage; } @@ -1688,6 +1684,33 @@ class Title implements LinkTarget { } /** + * Get a url appropriate for making redirects based on an untrusted url arg + * + * This is basically the same as getFullUrl(), but in the case of external + * interwikis, we send the user to a landing page, to prevent possible + * phishing attacks and the like. + * + * @note Uses current protocol by default, since technically relative urls + * aren't allowed in redirects per HTTP spec, so this is not suitable for + * places where the url gets cached, as might pollute between + * https and non-https users. + * @see self::getLocalURL for the arguments. + * @param array|string $query + * @param string $proto Protocol type to use in URL + * @return String. A url suitable to use in an HTTP location header. + */ + public function getFullUrlForRedirect( $query = '', $proto = PROTO_CURRENT ) { + $target = $this; + if ( $this->isExternal() ) { + $target = SpecialPage::getTitleFor( + 'GoToInterwiki', + $this->getPrefixedDBKey() + ); + } + return $target->getFullUrl( $query, false, $proto ); + } + + /** * Get a URL with no fragment or server name (relative URL) from a Title object. * If this page is generated with action=render, however, * $wgServer is prepended to make an absolute URL. @@ -2128,8 +2151,7 @@ class Title implements LinkTarget { private function checkCSSandJSPermissions( $action, $user, $errors, $rigor, $short ) { # Protect css/js subpages of user pages # XXX: this might be better using restrictions - # XXX: right 'editusercssjs' is deprecated, for backward compatibility only - if ( $action != 'patrol' && !$user->isAllowed( 'editusercssjs' ) ) { + if ( $action != 'patrol' ) { if ( preg_match( '/^' . preg_quote( $user->getName(), '/' ) . '\//', $this->mTextform ) ) { if ( $this->isCssSubpage() && !$user->isAllowedAny( 'editmyusercss', 'editusercss' ) ) { $errors[] = [ 'mycustomcssprotected', $action ]; @@ -2293,6 +2315,17 @@ class Title implements LinkTarget { ) { $errors[] = [ 'delete-toobig', $wgLang->formatNum( $wgDeleteRevisionsLimit ) ]; } + } elseif ( $action === 'undelete' ) { + if ( count( $this->getUserPermissionsErrorsInternal( 'edit', $user, $rigor, true ) ) ) { + // Undeleting implies editing + $errors[] = [ 'undelete-cantedit' ]; + } + if ( !$this->exists() + && count( $this->getUserPermissionsErrorsInternal( 'create', $user, $rigor, true ) ) + ) { + // Undeleting where nothing currently exists implies creating + $errors[] = [ 'undelete-cantcreate' ]; + } } return $errors; } @@ -2423,7 +2456,7 @@ class Title implements LinkTarget { * * @param string $action The action to check * @param bool $short Short circuit on first error - * @return array List of errors + * @return array Array containing an error message key and any parameters */ private function missingPermissionError( $action, $short ) { // We avoid expensive display logic for quickUserCan's and such @@ -2431,19 +2464,7 @@ class Title implements LinkTarget { return [ 'badaccess-group0' ]; } - $groups = array_map( [ 'User', 'makeGroupLinkWiki' ], - User::getGroupsWithPermission( $action ) ); - - if ( count( $groups ) ) { - global $wgLang; - return [ - 'badaccess-groups', - $wgLang->commaList( $groups ), - count( $groups ) - ]; - } else { - return [ 'badaccess-group0' ]; - } + return User::newFatalPermissionDeniedStatus( $action )->getErrorsArray()[0]; } /** @@ -2567,6 +2588,29 @@ class Title implements LinkTarget { * protection, or false if there's none. */ public function getTitleProtection() { + $protection = $this->getTitleProtectionInternal(); + if ( $protection ) { + if ( $protection['permission'] == 'sysop' ) { + $protection['permission'] = 'editprotected'; // B/C + } + if ( $protection['permission'] == 'autoconfirmed' ) { + $protection['permission'] = 'editsemiprotected'; // B/C + } + } + return $protection; + } + + /** + * Fetch title protection settings + * + * To work correctly, $this->loadRestrictions() needs to have access to the + * actual protections in the database without munging 'sysop' => + * 'editprotected' and 'autoconfirmed' => 'editsemiprotected'. Other + * callers probably want $this->getTitleProtection() instead. + * + * @return array|bool + */ + protected function getTitleProtectionInternal() { // Can't protect pages in special namespaces if ( $this->getNamespace() < 0 ) { return false; @@ -2594,12 +2638,6 @@ class Title implements LinkTarget { // fetchRow returns false if there are no rows. $row = $dbr->fetchRow( $res ); if ( $row ) { - if ( $row['permission'] == 'sysop' ) { - $row['permission'] = 'editprotected'; // B/C - } - if ( $row['permission'] == 'autoconfirmed' ) { - $row['permission'] = 'editsemiprotected'; // B/C - } $row['expiry'] = $dbr->decodeExpiry( $row['expiry'] ); } $this->mTitleProtection = $row; @@ -2944,8 +2982,6 @@ class Title implements LinkTarget { continue; } - // This code should be refactored, now that it's being used more generally, - // But I don't really see any harm in leaving it in Block for now -werdna $expiry = $dbr->decodeExpiry( $row->pr_expiry ); // Only apply the restrictions if they haven't expired! @@ -2997,7 +3033,7 @@ class Title implements LinkTarget { $this->loadRestrictionsFromRows( $rows, $oldFashionedRestrictions ); } else { - $title_protection = $this->getTitleProtection(); + $title_protection = $this->getTitleProtectionInternal(); if ( $title_protection ) { $now = wfTimestampNow(); @@ -3759,14 +3795,14 @@ class Title implements LinkTarget { } $newPageName = preg_replace( '#^' . preg_quote( $this->getDBkey(), '#' ) . '#', - StringUtils::escapeRegexReplacement( $nt->getDBkey() ), # bug 21234 + StringUtils::escapeRegexReplacement( $nt->getDBkey() ), # T23234 $oldSubpage->getDBkey() ); if ( $oldSubpage->isTalkPage() ) { $newNs = $nt->getTalkPage()->getNamespace(); } else { $newNs = $nt->getSubjectPage()->getNamespace(); } - # Bug 14385: we need makeTitleSafe because the new page names may + # T16385: we need makeTitleSafe because the new page names may # be longer than 255 characters. $newSubpage = Title::makeTitleSafe( $newNs, $newPageName ); @@ -3883,7 +3919,7 @@ class Title implements LinkTarget { * categories' names. * * @return array Array of parents in the form: - * $parent => $currentarticle + * $parent => $currentarticle */ public function getParentCategories() { global $wgContLang; @@ -3964,14 +4000,22 @@ class Title implements LinkTarget { * @return int|bool Old revision ID, or false if none exists */ public function getPreviousRevisionID( $revId, $flags = 0 ) { - $db = ( $flags & self::GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_REPLICA ); + /* This function and getNextRevisionID have bad performance when + used on a page with many revisions on mysql. An explicit extended + primary key may help in some cases, if the PRIMARY KEY is banned: + T159319 */ + if ( $flags & self::GAID_FOR_UPDATE ) { + $db = wfGetDB( DB_MASTER ); + } else { + $db = wfGetDB( DB_REPLICA, 'contributions' ); + } $revId = $db->selectField( 'revision', 'rev_id', [ 'rev_page' => $this->getArticleID( $flags ), 'rev_id < ' . intval( $revId ) ], __METHOD__, - [ 'ORDER BY' => 'rev_id DESC' ] + [ 'ORDER BY' => 'rev_id DESC', 'IGNORE INDEX' => 'PRIMARY' ] ); if ( $revId === false ) { @@ -3989,14 +4033,18 @@ class Title implements LinkTarget { * @return int|bool Next revision ID, or false if none exists */ public function getNextRevisionID( $revId, $flags = 0 ) { - $db = ( $flags & self::GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_REPLICA ); + if ( $flags & self::GAID_FOR_UPDATE ) { + $db = wfGetDB( DB_MASTER ); + } else { + $db = wfGetDB( DB_REPLICA, 'contributions' ); + } $revId = $db->selectField( 'revision', 'rev_id', [ 'rev_page' => $this->getArticleID( $flags ), 'rev_id > ' . intval( $revId ) ], __METHOD__, - [ 'ORDER BY' => 'rev_id' ] + [ 'ORDER BY' => 'rev_id', 'IGNORE INDEX' => 'PRIMARY' ] ); if ( $revId === false ) { @@ -4019,7 +4067,10 @@ class Title implements LinkTarget { $row = $db->selectRow( 'revision', Revision::selectFields(), [ 'rev_page' => $pageId ], __METHOD__, - [ 'ORDER BY' => 'rev_timestamp ASC', 'LIMIT' => 1 ] + [ + 'ORDER BY' => 'rev_timestamp ASC', + 'IGNORE INDEX' => 'rev_timestamp' + ] ); if ( $row ) { return new Revision( $row ); diff --git a/includes/TitleArray.php b/includes/TitleArray.php index 5a28b8503570..bf2344bbb771 100644 --- a/includes/TitleArray.php +++ b/includes/TitleArray.php @@ -24,6 +24,8 @@ * @file */ +use Wikimedia\Rdbms\ResultWrapper; + /** * The TitleArray class only exists to provide the newFromResult method at pre- * sent. diff --git a/includes/TitleArrayFromResult.php b/includes/TitleArrayFromResult.php index 668ea54b919b..189fb4054980 100644 --- a/includes/TitleArrayFromResult.php +++ b/includes/TitleArrayFromResult.php @@ -24,6 +24,8 @@ * @file */ +use Wikimedia\Rdbms\ResultWrapper; + class TitleArrayFromResult extends TitleArray implements Countable { /** @var ResultWrapper */ public $res; diff --git a/includes/TrackingCategories.php b/includes/TrackingCategories.php new file mode 100644 index 000000000000..a9ebd762db4e --- /dev/null +++ b/includes/TrackingCategories.php @@ -0,0 +1,131 @@ +<?php +/** + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @ingroup Categories + */ + +/** + * This class performs some operations related to tracking categories, such as creating + * a list of all such categories. + */ +class TrackingCategories { + /** @var Config */ + private $config; + + /** + * Tracking categories that exist in core + * + * @var array + */ + private static $coreTrackingCategories = [ + 'index-category', + 'noindex-category', + 'duplicate-args-category', + 'expensive-parserfunction-category', + 'post-expand-template-argument-category', + 'post-expand-template-inclusion-category', + 'hidden-category-category', + 'broken-file-category', + 'node-count-exceeded-category', + 'expansion-depth-exceeded-category', + 'restricted-displaytitle-ignored', + 'deprecated-self-close-category', + 'template-loop-category', + ]; + + /** + * @param Config $config + */ + public function __construct( Config $config ) { + $this->config = $config; + } + + /** + * Read the global and extract title objects from the corresponding messages + * @return array Array( 'msg' => Title, 'cats' => Title[] ) + */ + public function getTrackingCategories() { + $categories = array_merge( + self::$coreTrackingCategories, + ExtensionRegistry::getInstance()->getAttribute( 'TrackingCategories' ), + $this->config->get( 'TrackingCategories' ) // deprecated + ); + + // Only show magic link tracking categories if they are enabled + $enableMagicLinks = $this->config->get( 'EnableMagicLinks' ); + if ( $enableMagicLinks['ISBN'] ) { + $categories[] = 'magiclink-tracking-isbn'; + } + if ( $enableMagicLinks['RFC'] ) { + $categories[] = 'magiclink-tracking-rfc'; + } + if ( $enableMagicLinks['PMID'] ) { + $categories[] = 'magiclink-tracking-pmid'; + } + + $trackingCategories = []; + foreach ( $categories as $catMsg ) { + /* + * Check if the tracking category varies by namespace + * Otherwise only pages in the current namespace will be displayed + * If it does vary, show pages considering all namespaces + */ + $msgObj = wfMessage( $catMsg )->inContentLanguage(); + $allCats = []; + $catMsgTitle = Title::makeTitleSafe( NS_MEDIAWIKI, $catMsg ); + if ( !$catMsgTitle ) { + continue; + } + + // Match things like {{NAMESPACE}} and {{NAMESPACENUMBER}}. + // False positives are ok, this is just an efficiency shortcut + if ( strpos( $msgObj->plain(), '{{' ) !== false ) { + $ns = MWNamespace::getValidNamespaces(); + foreach ( $ns as $namesp ) { + $tempTitle = Title::makeTitleSafe( $namesp, $catMsg ); + if ( !$tempTitle ) { + continue; + } + $catName = $msgObj->title( $tempTitle )->text(); + # Allow tracking categories to be disabled by setting them to "-" + if ( $catName !== '-' ) { + $catTitle = Title::makeTitleSafe( NS_CATEGORY, $catName ); + if ( $catTitle ) { + $allCats[] = $catTitle; + } + } + } + } else { + $catName = $msgObj->text(); + # Allow tracking categories to be disabled by setting them to "-" + if ( $catName !== '-' ) { + $catTitle = Title::makeTitleSafe( NS_CATEGORY, $catName ); + if ( $catTitle ) { + $allCats[] = $catTitle; + } + } + } + $trackingCategories[$catMsg] = [ + 'cats' => $allCats, + 'msg' => $catMsgTitle, + ]; + } + + return $trackingCategories; + } +} diff --git a/includes/WatchedItemQueryService.php b/includes/WatchedItemQueryService.php index c80e4a53198d..ba7707411a87 100644 --- a/includes/WatchedItemQueryService.php +++ b/includes/WatchedItemQueryService.php @@ -1,7 +1,9 @@ <?php +use Wikimedia\Rdbms\IDatabase; use MediaWiki\Linker\LinkTarget; use Wikimedia\Assert\Assert; +use Wikimedia\Rdbms\LoadBalancer; /** * Class performing complex database queries related to WatchedItems. @@ -401,7 +403,7 @@ class WatchedItemQueryService { if ( !isset( $options['start'] ) && !isset( $options['end'] ) ) { if ( $db->getType() === 'mysql' ) { // This is an index optimization for mysql - $conds[] = "rc_timestamp > ''"; + $conds[] = 'rc_timestamp > ' . $db->addQuotes( '' ); } } @@ -504,7 +506,7 @@ class WatchedItemQueryService { $conds[] = 'rc_user_text != ' . $db->addQuotes( $options['notByUser'] ); } - // Avoid brute force searches (bug 17342) + // Avoid brute force searches (T19342) $bitmask = 0; if ( !$user->isAllowed( 'deletedhistory' ) ) { $bitmask = Revision::DELETED_USER; diff --git a/includes/WatchedItemQueryServiceExtension.php b/includes/WatchedItemQueryServiceExtension.php index 8fcf1311dfd6..93d50330899c 100644 --- a/includes/WatchedItemQueryServiceExtension.php +++ b/includes/WatchedItemQueryServiceExtension.php @@ -1,5 +1,8 @@ <?php +use Wikimedia\Rdbms\ResultWrapper; +use Wikimedia\Rdbms\IDatabase; + /** * Extension mechanism for WatchedItemQueryService * diff --git a/includes/WatchedItemStore.php b/includes/WatchedItemStore.php index 3cdc59cda0eb..70fdbf13af97 100644 --- a/includes/WatchedItemStore.php +++ b/includes/WatchedItemStore.php @@ -1,16 +1,22 @@ <?php +use Wikimedia\Rdbms\IDatabase; use Liuggio\StatsdClient\Factory\StatsdDataFactoryInterface; use MediaWiki\Linker\LinkTarget; +use MediaWiki\MediaWikiServices; use Wikimedia\Assert\Assert; use Wikimedia\ScopedCallback; +use Wikimedia\Rdbms\LoadBalancer; /** * Storage layer class for WatchedItems. * Database interaction. * - * @author Addshore + * Uses database because this uses User::isAnon + * + * @group Database * + * @author Addshore * @since 1.27 */ class WatchedItemStore implements StatsdAwareInterface { @@ -734,7 +740,7 @@ class WatchedItemStore implements StatsdAwareInterface { global $wgUpdateRowsPerQuery; $dbw = $this->getConnectionRef( DB_MASTER ); - $factory = wfGetLBFactory(); + $factory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory(); $ticket = $factory->getEmptyTransactionTicket( __METHOD__ ); $watchersChunks = array_chunk( $watchers, $wgUpdateRowsPerQuery ); diff --git a/includes/WebRequest.php b/includes/WebRequest.php index 3bbdc3f312c4..3d5e372c5a66 100644 --- a/includes/WebRequest.php +++ b/includes/WebRequest.php @@ -216,7 +216,7 @@ class WebRequest { $host = $parts[0]; if ( $wgAssumeProxiesUseDefaultProtocolPorts && isset( $_SERVER['HTTP_X_FORWARDED_PROTO'] ) ) { - // Bug 70021: Assume that upstream proxy is running on the default + // T72021: Assume that upstream proxy is running on the default // port based on the protocol. We have no reliable way to determine // the actual port in use upstream. $port = $stdPort; @@ -308,7 +308,7 @@ class WebRequest { * available variant URLs. */ public function interpolateTitle() { - // bug 16019: title interpolation on API queries is useless and sometimes harmful + // T18019: title interpolation on API queries is useless and sometimes harmful if ( defined( 'MW_API' ) ) { return; } @@ -408,7 +408,7 @@ class WebRequest { * @since 1.28 * @param string $name * @param string|null $default Optional default - * @return string + * @return string|null */ public function getRawVal( $name, $default = null ) { $name = strtr( $name, '.', '_' ); // See comment in self::getGPCVal() @@ -432,7 +432,7 @@ class WebRequest { * * @param string $name * @param string $default Optional default (or null) - * @return string + * @return string|null */ public function getVal( $name, $default = null ) { $val = $this->getGPCVal( $this->data, $name, $default ); @@ -482,7 +482,7 @@ class WebRequest { * * @param string $name * @param array $default Optional default (or null) - * @return array + * @return array|null */ public function getArray( $name, $default = null ) { $val = $this->getGPCVal( $this->data, $name, $default ); @@ -1229,7 +1229,7 @@ HTML; if ( IP::isPublic( $ipchain[$i + 1] ) || $wgUsePrivateIPs || - $proxyLookup->isConfiguredProxy( $curIP ) // bug 48919; treat IP as sane + $proxyLookup->isConfiguredProxy( $curIP ) // T50919; treat IP as sane ) { // Follow the next IP according to the proxy $nextIP = IP::canonicalize( $ipchain[$i + 1] ); diff --git a/includes/WebResponse.php b/includes/WebResponse.php index 339b2e3ff0ac..0208a72ab962 100644 --- a/includes/WebResponse.php +++ b/includes/WebResponse.php @@ -39,6 +39,7 @@ class WebResponse { * @param null|int $http_response_code Forces the HTTP response code to the specified value. */ public function header( $string, $replace = true, $http_response_code = null ) { + \MediaWiki\HeaderCallback::warnIfHeadersSent(); if ( $http_response_code ) { header( $string, $replace, $http_response_code ); } else { diff --git a/includes/WebStart.php b/includes/WebStart.php index 6e4fb09de021..15804c7bd416 100644 --- a/includes/WebStart.php +++ b/includes/WebStart.php @@ -30,7 +30,7 @@ if ( ini_get( 'mbstring.func_overload' ) ) { die( 'MediaWiki does not support installations where mbstring.func_overload is non-zero.' ); } -# bug 15461: Make IE8 turn off content sniffing. Everybody else should ignore this +# T17461: Make IE8 turn off content sniffing. Everybody else should ignore this # We're adding it here so that it's *always* set, even for alternate entry # points and when $wgOut gets disabled or overridden. header( 'X-Content-Type-Options: nosniff' ); @@ -104,6 +104,9 @@ if ( !interface_exists( 'Psr\Log\LoggerInterface' ) ) { die( 1 ); } +# Install a header callback +MediaWiki\HeaderCallback::register(); + if ( defined( 'MW_CONFIG_CALLBACK' ) ) { # Use a callback function to configure MediaWiki call_user_func( MW_CONFIG_CALLBACK ); diff --git a/includes/Xml.php b/includes/Xml.php index 8f18046f5fc4..4e8796744c44 100644 --- a/includes/Xml.php +++ b/includes/Xml.php @@ -59,8 +59,8 @@ class Xml { * Given an array of ('attributename' => 'value'), it generates the code * to set the XML attributes : attributename="value". * The values are passed to Sanitizer::encodeAttribute. - * Return null if no attributes given. - * @param array $attribs Array of attributes for an XML element + * Returns null or empty string if no attributes given. + * @param array|null $attribs Array of attributes for an XML element * @throws MWException * @return null|string */ @@ -564,36 +564,6 @@ class Xml { } /** - * Converts textual drop-down list to array - * - * @param string $list Correctly formatted text (newline delimited) to be - * used to generate the options. - * @return array - */ - public static function getArrayFromWikiTextList( $list = '' ) { - $options = []; - - foreach ( explode( "\n", $list ) as $option ) { - $value = trim( $option ); - if ( $value == '' ) { - continue; - } elseif ( substr( $value, 0, 1 ) == '*' && substr( $value, 1, 1 ) != '*' ) { - // A new group is starting ... - $value = trim( substr( $value, 1 ) ); - $options[] = $value; - } elseif ( substr( $value, 0, 2 ) == '**' ) { - // groupmember - $value = trim( substr( $value, 2 ) ); - $options[] = $value; - } else { - // groupless reason list - $options[] = $value; - } - } - return $options; - } - - /** * Shortcut for creating fieldsets. * * @param string|bool $legend Legend of the fieldset. If evaluates to false, diff --git a/includes/actions/HistoryAction.php b/includes/actions/HistoryAction.php index e8aec1cf6eac..d1be7d4b1ba4 100644 --- a/includes/actions/HistoryAction.php +++ b/includes/actions/HistoryAction.php @@ -24,6 +24,8 @@ */ use MediaWiki\MediaWikiServices; +use Wikimedia\Rdbms\ResultWrapper; +use Wikimedia\Rdbms\FakeResultWrapper; /** * This class handles printing the history page for an article. In order to @@ -146,6 +148,9 @@ class HistoryAction extends FormlessAction { $out->setStatusCode( 404 ); } $out->addWikiMsg( 'nohistory' ); + + $dbr = wfGetDB( DB_REPLICA ); + # show deletion/move log if there is an entry LogEventsList::showLogExtract( $out, @@ -153,7 +158,7 @@ class HistoryAction extends FormlessAction { $this->getTitle(), '', [ 'lim' => 10, - 'conds' => [ "log_action != 'revision'" ], + 'conds' => [ 'log_action != ' . $dbr->addQuotes( 'revision' ) ], 'showIfEmpty' => false, 'msgKey' => [ 'moveddeleted-notice' ] ] diff --git a/includes/actions/InfoAction.php b/includes/actions/InfoAction.php index 5fb83b3d184f..167b7098bdad 100644 --- a/includes/actions/InfoAction.php +++ b/includes/actions/InfoAction.php @@ -835,7 +835,7 @@ class InfoAction extends FormlessAction { $real_names = []; $user_names = []; $anon_ips = []; - $linkRenderer = MediaWikiServices::getLinkRenderer(); + $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer(); # Sift for real versus user names /** @var $user User */ diff --git a/includes/actions/PurgeAction.php b/includes/actions/PurgeAction.php index 942b73163482..b2002ffae9ac 100644 --- a/includes/actions/PurgeAction.php +++ b/includes/actions/PurgeAction.php @@ -42,7 +42,7 @@ class PurgeAction extends FormAction { } public function onSubmit( $data ) { - return $this->page->doPurge( WikiPage::PURGE_ALL ); + return $this->page->doPurge(); } public function show() { diff --git a/includes/actions/RollbackAction.php b/includes/actions/RollbackAction.php index aa2858d8e3c6..9d336e46138f 100644 --- a/includes/actions/RollbackAction.php +++ b/includes/actions/RollbackAction.php @@ -41,6 +41,7 @@ class RollbackAction extends FormlessAction { * - confirm-rollback-button * - rollbackfailed * - rollback-missingparam + * - rollback-success-notify */ /** @@ -123,8 +124,13 @@ class RollbackAction extends FormlessAction { $old = Linker::revUserTools( $current ); $new = Linker::revUserTools( $target ); - $this->getOutput()->addHTML( $this->msg( 'rollback-success' )->rawParams( $old, $new ) - ->parseAsBlock() ); + $this->getOutput()->addHTML( + $this->msg( 'rollback-success' ) + ->rawParams( $old, $new ) + ->params( $current->getUserText( Revision::FOR_THIS_USER, $user ) ) + ->params( $target->getUserText( Revision::FOR_THIS_USER, $user ) ) + ->parseAsBlock() + ); if ( $user->getBoolOption( 'watchrollback' ) ) { $user->addWatch( $this->page->getTitle(), User::IGNORE_USER_RIGHTS ); diff --git a/includes/actions/ViewAction.php b/includes/actions/ViewAction.php index 0ba964f9b1ec..134b8a45b11a 100644 --- a/includes/actions/ViewAction.php +++ b/includes/actions/ViewAction.php @@ -58,9 +58,6 @@ class ViewAction extends FormlessAction { $touched = null; } - // If a page was purged on HTTP GET, relect that timestamp to avoid sending 304s - $touched = max( $touched, $this->page->getLastPurgeTimestamp() ); - // Send HTTP 304 if the IMS matches or otherwise set expiry/last-modified headers if ( $touched && $this->getOutput()->checkLastModified( $touched ) ) { wfDebug( __METHOD__ . ": done 304\n" ); diff --git a/includes/api/ApiAMCreateAccount.php b/includes/api/ApiAMCreateAccount.php index 5d12590fdf77..b8bd511bc0f4 100644 --- a/includes/api/ApiAMCreateAccount.php +++ b/includes/api/ApiAMCreateAccount.php @@ -132,6 +132,6 @@ class ApiAMCreateAccount extends ApiBase { } public function getHelpUrls() { - return 'https://www.mediawiki.org/wiki/API:Account_creation'; + return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Account_creation'; } } diff --git a/includes/api/ApiAuthManagerHelper.php b/includes/api/ApiAuthManagerHelper.php index 5327d7a99bca..8862cc7f9f04 100644 --- a/includes/api/ApiAuthManagerHelper.php +++ b/includes/api/ApiAuthManagerHelper.php @@ -169,6 +169,7 @@ class ApiAuthManagerHelper { $this->module->getMain()->markParamsUsed( array_keys( $data ) ); if ( $sensitive ) { + $this->module->getMain()->markParamsSensitive( array_keys( $sensitive ) ); $this->module->requirePostedParameters( array_keys( $sensitive ), 'noprefix' ); } @@ -208,6 +209,7 @@ class ApiAuthManagerHelper { $res->status === AuthenticationResponse::RESTART ) { $this->formatMessage( $ret, 'message', $res->message ); + $ret['messagecode'] = ApiMessage::create( $res->message )->getApiCode(); } if ( $res->status === AuthenticationResponse::FAIL || diff --git a/includes/api/ApiBase.php b/includes/api/ApiBase.php index b8dd4641d900..b698ceffbc68 100644 --- a/includes/api/ApiBase.php +++ b/includes/api/ApiBase.php @@ -24,6 +24,8 @@ * @file */ +use Wikimedia\Rdbms\IDatabase; + /** * This abstract class implements many basic API functions, and is the base of * all API classes. @@ -186,6 +188,13 @@ abstract class ApiBase extends ContextSource { */ const PARAM_EXTRA_NAMESPACES = 18; + /* + * (boolean) Is the parameter sensitive? Note 'password'-type fields are + * always sensitive regardless of the value of this field. + * @since 1.29 + */ + const PARAM_SENSITIVE = 19; + /**@}*/ const ALL_DEFAULT_STRING = '*'; @@ -700,7 +709,7 @@ abstract class ApiBase extends ContextSource { * @return array */ public function extractRequestParams( $parseLimit = true ) { - // Cache parameters, for performance and to avoid bug 24564. + // Cache parameters, for performance and to avoid T26564. if ( !isset( $this->mParamCache[$parseLimit] ) ) { $params = $this->getFinalParams(); $results = []; @@ -1023,6 +1032,10 @@ abstract class ApiBase extends ContextSource { } else { $type = 'NULL'; // allow everything } + + if ( $type == 'password' || !empty( $paramSettings[self::PARAM_SENSITIVE] ) ) { + $this->getMain()->markParamsSensitive( $encParamName ); + } } if ( $type == 'boolean' ) { @@ -1326,7 +1339,7 @@ abstract class ApiBase extends ContextSource { } if ( !$allowMultiple && count( $valuesList ) != 1 ) { - // Bug 33482 - Allow entries with | in them for non-multiple values + // T35482 - Allow entries with | in them for non-multiple values if ( in_array( $value, $allowedValues, true ) ) { return $value; } @@ -1718,6 +1731,18 @@ abstract class ApiBase extends ContextSource { $this->logFeatureUsage( $feature ); } $this->addWarning( $msg, 'deprecation', $data ); + + // No real need to deduplicate here, ApiErrorFormatter does that for + // us (assuming the hook is deterministic). + $msgs = [ $this->msg( 'api-usage-mailinglist-ref' ) ]; + Hooks::run( 'ApiDeprecationHelp', [ &$msgs ] ); + if ( count( $msgs ) > 1 ) { + $key = '$' . join( ' $', range( 1, count( $msgs ) ) ); + $msg = ( new RawMessage( $key ) )->params( $msgs ); + } else { + $msg = reset( $msgs ); + } + $this->getMain()->addWarning( $msg, 'deprecation-help' ); } /** @@ -2016,6 +2041,7 @@ abstract class ApiBase extends ContextSource { $params['token'] = [ ApiBase::PARAM_TYPE => 'string', ApiBase::PARAM_REQUIRED => true, + ApiBase::PARAM_SENSITIVE => true, ApiBase::PARAM_HELP_MSG => [ 'api-help-param-token', $this->needsToken(), diff --git a/includes/api/ApiBlock.php b/includes/api/ApiBlock.php index 58e3d1c58d2e..4d37af316278 100644 --- a/includes/api/ApiBlock.php +++ b/includes/api/ApiBlock.php @@ -46,7 +46,7 @@ class ApiBlock extends ApiBase { $this->requireOnlyOneParameter( $params, 'user', 'userid' ); - # bug 15810: blocked admins should have limited access here + # T17810: blocked admins should have limited access here if ( $user->isBlocked() ) { $status = SpecialBlock::checkUnblockSelf( $params['user'], $user ); if ( $status !== true ) { @@ -69,7 +69,7 @@ class ApiBlock extends ApiBase { } else { $target = User::newFromName( $params['user'] ); - // Bug 38633 - if the target is a user (not an IP address), but it + // T40633 - if the target is a user (not an IP address), but it // doesn't exist or is unusable, error. if ( $target instanceof User && ( $target->isAnon() /* doesn't exist */ || !User::isUsableName( $target->getName() ) ) @@ -193,6 +193,6 @@ class ApiBlock extends ApiBase { } public function getHelpUrls() { - return 'https://www.mediawiki.org/wiki/API:Block'; + return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Block'; } } diff --git a/includes/api/ApiChangeAuthenticationData.php b/includes/api/ApiChangeAuthenticationData.php index c25920e72859..35c4e568c613 100644 --- a/includes/api/ApiChangeAuthenticationData.php +++ b/includes/api/ApiChangeAuthenticationData.php @@ -93,6 +93,6 @@ class ApiChangeAuthenticationData extends ApiBase { } public function getHelpUrls() { - return 'https://www.mediawiki.org/wiki/API:Manage_authentication_data'; + return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Manage_authentication_data'; } } diff --git a/includes/api/ApiCheckToken.php b/includes/api/ApiCheckToken.php index 3cc7a8a058df..480915e60cf8 100644 --- a/includes/api/ApiCheckToken.php +++ b/includes/api/ApiCheckToken.php @@ -73,6 +73,7 @@ class ApiCheckToken extends ApiBase { 'token' => [ ApiBase::PARAM_TYPE => 'string', ApiBase::PARAM_REQUIRED => true, + ApiBase::PARAM_SENSITIVE => true, ], 'maxtokenage' => [ ApiBase::PARAM_TYPE => 'integer', diff --git a/includes/api/ApiClearHasMsg.php b/includes/api/ApiClearHasMsg.php index a5474b5823a0..3b2463098e5b 100644 --- a/includes/api/ApiClearHasMsg.php +++ b/includes/api/ApiClearHasMsg.php @@ -50,6 +50,6 @@ class ApiClearHasMsg extends ApiBase { } public function getHelpUrls() { - return 'https://www.mediawiki.org/wiki/API:ClearHasMsg'; + return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:ClearHasMsg'; } } diff --git a/includes/api/ApiClientLogin.php b/includes/api/ApiClientLogin.php index 3f5bc0c0c899..0d512b387fa0 100644 --- a/includes/api/ApiClientLogin.php +++ b/includes/api/ApiClientLogin.php @@ -132,6 +132,6 @@ class ApiClientLogin extends ApiBase { } public function getHelpUrls() { - return 'https://www.mediawiki.org/wiki/API:Login'; + return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Login'; } } diff --git a/includes/api/ApiDelete.php b/includes/api/ApiDelete.php index 50c24aeca8b5..99065c4fe855 100644 --- a/includes/api/ApiDelete.php +++ b/includes/api/ApiDelete.php @@ -218,6 +218,6 @@ class ApiDelete extends ApiBase { } public function getHelpUrls() { - return 'https://www.mediawiki.org/wiki/API:Delete'; + return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Delete'; } } diff --git a/includes/api/ApiEditPage.php b/includes/api/ApiEditPage.php index b45be31ffea6..0b8156b0f8ea 100644 --- a/includes/api/ApiEditPage.php +++ b/includes/api/ApiEditPage.php @@ -611,6 +611,6 @@ class ApiEditPage extends ApiBase { } public function getHelpUrls() { - return 'https://www.mediawiki.org/wiki/API:Edit'; + return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Edit'; } } diff --git a/includes/api/ApiEmailUser.php b/includes/api/ApiEmailUser.php index 8aff6f8afd46..72c7c358d2ea 100644 --- a/includes/api/ApiEmailUser.php +++ b/includes/api/ApiEmailUser.php @@ -114,6 +114,6 @@ class ApiEmailUser extends ApiBase { } public function getHelpUrls() { - return 'https://www.mediawiki.org/wiki/API:Email'; + return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Email'; } } diff --git a/includes/api/ApiErrorFormatter.php b/includes/api/ApiErrorFormatter.php index 814004a67362..c52b731bbdb3 100644 --- a/includes/api/ApiErrorFormatter.php +++ b/includes/api/ApiErrorFormatter.php @@ -414,12 +414,17 @@ class ApiErrorFormatter_BackCompat extends ApiErrorFormatter { if ( $tag === 'error' ) { // In BC mode, only one error - $value = [ - 'code' => $msg->getApiCode(), - 'info' => $value, - ] + $msg->getApiData(); - $this->result->addValue( null, 'error', $value, - ApiResult::OVERRIDE | ApiResult::ADD_ON_TOP | ApiResult::NO_SIZE_CHECK ); + $existingError = $this->result->getResultData( [ 'error' ] ); + if ( !is_array( $existingError ) || + !isset( $existingError['code'] ) || !isset( $existingError['info'] ) + ) { + $value = [ + 'code' => $msg->getApiCode(), + 'info' => $value, + ] + $msg->getApiData(); + $this->result->addValue( null, 'error', $value, + ApiResult::OVERRIDE | ApiResult::ADD_ON_TOP | ApiResult::NO_SIZE_CHECK ); + } } else { if ( $modulePath === null ) { $moduleName = 'unknown'; diff --git a/includes/api/ApiExpandTemplates.php b/includes/api/ApiExpandTemplates.php index 6f7cf652c3f4..e15d7da1e588 100644 --- a/includes/api/ApiExpandTemplates.php +++ b/includes/api/ApiExpandTemplates.php @@ -210,6 +210,6 @@ class ApiExpandTemplates extends ApiBase { } public function getHelpUrls() { - return 'https://www.mediawiki.org/wiki/API:Parsing_wikitext#expandtemplates'; + return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Parsing_wikitext#expandtemplates'; } } diff --git a/includes/api/ApiFeedRecentChanges.php b/includes/api/ApiFeedRecentChanges.php index e0e50edd9c68..0b04c8cc928e 100644 --- a/includes/api/ApiFeedRecentChanges.php +++ b/includes/api/ApiFeedRecentChanges.php @@ -57,7 +57,7 @@ class ApiFeedRecentChanges extends ApiBase { $this->getMain()->setCacheMode( 'public' ); if ( !$this->getMain()->getParameter( 'smaxage' ) ) { - // bug 63249: This page gets hit a lot, cache at least 15 seconds. + // T65249: This page gets hit a lot, cache at least 15 seconds. $this->getMain()->setCacheMaxAge( 15 ); } diff --git a/includes/api/ApiFeedWatchlist.php b/includes/api/ApiFeedWatchlist.php index b9bb761b3c3d..b7c5ccc269c5 100644 --- a/includes/api/ApiFeedWatchlist.php +++ b/includes/api/ApiFeedWatchlist.php @@ -52,6 +52,7 @@ class ApiFeedWatchlist extends ApiBase { public function execute() { $config = $this->getConfig(); $feedClasses = $config->get( 'FeedClasses' ); + $params = []; try { $params = $this->extractRequestParams(); @@ -306,6 +307,6 @@ class ApiFeedWatchlist extends ApiBase { } public function getHelpUrls() { - return 'https://www.mediawiki.org/wiki/API:Watchlist_feed'; + return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Watchlist_feed'; } } diff --git a/includes/api/ApiFormatBase.php b/includes/api/ApiFormatBase.php index 67f54a8068ea..eb23bd63ace7 100644 --- a/includes/api/ApiFormatBase.php +++ b/includes/api/ApiFormatBase.php @@ -186,7 +186,7 @@ abstract class ApiFormatBase extends ApiBase { $this->getMain()->getRequest()->response()->header( "Content-Type: $mime; charset=utf-8" ); - // Set X-Frame-Options API results (bug 39180) + // Set X-Frame-Options API results (T41180) $apiFrameOptions = $this->getConfig()->get( 'ApiFrameOptions' ); if ( $apiFrameOptions ) { $this->getMain()->getRequest()->response()->header( "X-Frame-Options: $apiFrameOptions" ); @@ -269,7 +269,7 @@ abstract class ApiFormatBase extends ApiBase { false, FormatJson::ALL_OK ); - // Bug 66776: wfMangleFlashPolicy() is needed to avoid a nasty bug in + // T68776: wfMangleFlashPolicy() is needed to avoid a nasty bug in // Flash, but what it does isn't friendly for the API, so we need to // work around it. if ( preg_match( '/\<\s*cross-domain-policy\s*\>/i', $json ) ) { @@ -330,7 +330,7 @@ abstract class ApiFormatBase extends ApiBase { } public function getHelpUrls() { - return 'https://www.mediawiki.org/wiki/API:Data_formats'; + return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Data_formats'; } } diff --git a/includes/api/ApiFormatJson.php b/includes/api/ApiFormatJson.php index 8ebfe48cf88a..e5dafae60208 100644 --- a/includes/api/ApiFormatJson.php +++ b/includes/api/ApiFormatJson.php @@ -91,7 +91,7 @@ class ApiFormatJson extends ApiFormatBase { $data = $this->getResult()->getResultData( null, $transform ); $json = FormatJson::encode( $data, $this->getIsHtml(), $opt ); - // Bug 66776: wfMangleFlashPolicy() is needed to avoid a nasty bug in + // T68776: wfMangleFlashPolicy() is needed to avoid a nasty bug in // Flash, but what it does isn't friendly for the API, so we need to // work around it. if ( preg_match( '/\<\s*cross-domain-policy(?=\s|\>)/i', $json ) ) { @@ -103,7 +103,7 @@ class ApiFormatJson extends ApiFormatBase { if ( isset( $params['callback'] ) ) { $callback = preg_replace( "/[^][.\\'\\\"_A-Za-z0-9]/", '', $params['callback'] ); # Prepend a comment to try to avoid attacks against content - # sniffers, such as bug 68187. + # sniffers, such as T70187. $this->printText( "/**/$callback($json)" ); } else { $this->printText( $json ); diff --git a/includes/api/ApiFormatPhp.php b/includes/api/ApiFormatPhp.php index a744f57becf1..671f356194c0 100644 --- a/includes/api/ApiFormatPhp.php +++ b/includes/api/ApiFormatPhp.php @@ -60,7 +60,7 @@ class ApiFormatPhp extends ApiFormatBase { } $text = serialize( $this->getResult()->getResultData( null, $transforms ) ); - // Bug 66776: wfMangleFlashPolicy() is needed to avoid a nasty bug in + // T68776: wfMangleFlashPolicy() is needed to avoid a nasty bug in // Flash, but what it does isn't friendly for the API. There's nothing // we can do here that isn't actively broken in some manner, so let's // just be broken in a useful manner. diff --git a/includes/api/ApiHelp.php b/includes/api/ApiHelp.php index e347a9f2c526..df9ca981ef06 100644 --- a/includes/api/ApiHelp.php +++ b/includes/api/ApiHelp.php @@ -844,9 +844,9 @@ class ApiHelp extends ApiBase { public function getHelpUrls() { return [ - 'https://www.mediawiki.org/wiki/API:Main_page', - 'https://www.mediawiki.org/wiki/API:FAQ', - 'https://www.mediawiki.org/wiki/API:Quick_start_guide', + 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Main_page', + 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:FAQ', + 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Quick_start_guide', ]; } } diff --git a/includes/api/ApiImport.php b/includes/api/ApiImport.php index bf5e4ce3b288..b46f0b1e5105 100644 --- a/includes/api/ApiImport.php +++ b/includes/api/ApiImport.php @@ -171,7 +171,7 @@ class ApiImport extends ApiBase { } public function getHelpUrls() { - return 'https://www.mediawiki.org/wiki/API:Import'; + return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Import'; } } diff --git a/includes/api/ApiLinkAccount.php b/includes/api/ApiLinkAccount.php index 9a21e7620cf5..f5c5deeb74da 100644 --- a/includes/api/ApiLinkAccount.php +++ b/includes/api/ApiLinkAccount.php @@ -124,6 +124,6 @@ class ApiLinkAccount extends ApiBase { } public function getHelpUrls() { - return 'https://www.mediawiki.org/wiki/API:Linkaccount'; + return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Linkaccount'; } } diff --git a/includes/api/ApiLogin.php b/includes/api/ApiLogin.php index 6cf1fad30cd8..41bec355a7bc 100644 --- a/includes/api/ApiLogin.php +++ b/includes/api/ApiLogin.php @@ -70,14 +70,7 @@ class ApiLogin extends ApiBase { return; } - try { - $this->requirePostedParameters( [ 'password', 'token' ] ); - } catch ( ApiUsageException $ex ) { - // Make this a warning for now, upgrade to an error in 1.29. - foreach ( $ex->getStatusValue()->getErrors() as $error ) { - $this->addDeprecation( $error, 'login-params-in-query-string' ); - } - } + $this->requirePostedParameters( [ 'password', 'token' ] ); $params = $this->extractRequestParams(); @@ -209,7 +202,7 @@ class ApiLogin extends ApiBase { case 'Aborted': $result['reason'] = 'Authentication requires user interaction, ' . - 'which is not supported by action=login.'; + 'which is not supported by action=login.'; if ( $this->getConfig()->get( 'EnableBotPasswords' ) ) { $result['reason'] .= ' To be able to login with action=login, see [[Special:BotPasswords]].'; $result['reason'] .= ' To continue using main-account login, see action=clientlogin.'; @@ -257,6 +250,7 @@ class ApiLogin extends ApiBase { 'token' => [ ApiBase::PARAM_TYPE => 'string', ApiBase::PARAM_REQUIRED => false, // for BC + ApiBase::PARAM_SENSITIVE => true, ApiBase::PARAM_HELP_MSG => [ 'api-help-param-token', 'login' ], ], ]; @@ -272,7 +266,7 @@ class ApiLogin extends ApiBase { } public function getHelpUrls() { - return 'https://www.mediawiki.org/wiki/API:Login'; + return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Login'; } /** diff --git a/includes/api/ApiLogout.php b/includes/api/ApiLogout.php index d5c28f1d6a05..d56c096c7b36 100644 --- a/includes/api/ApiLogout.php +++ b/includes/api/ApiLogout.php @@ -75,6 +75,6 @@ class ApiLogout extends ApiBase { } public function getHelpUrls() { - return 'https://www.mediawiki.org/wiki/API:Logout'; + return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Logout'; } } diff --git a/includes/api/ApiMain.php b/includes/api/ApiMain.php index 52f1d95830ff..4068a50bb681 100644 --- a/includes/api/ApiMain.php +++ b/includes/api/ApiMain.php @@ -26,6 +26,8 @@ */ use MediaWiki\Logger\LoggerFactory; +use MediaWiki\MediaWikiServices; +use Wikimedia\Timestamp\TimestampException; /** * This is the main API class, used for both external and internal processing. @@ -159,6 +161,7 @@ class ApiMain extends ApiBase { private $mCacheMode = 'private'; private $mCacheControl = []; private $mParamsUsed = []; + private $mParamsSensitive = []; /** @var bool|null Cached return value from self::lacksSameOriginSecurity() */ private $lacksSameOriginSecurity = null; @@ -543,7 +546,7 @@ class ApiMain extends ApiBase { $runTime = microtime( true ) - $t; $this->logRequest( $runTime ); if ( $this->mModule->isWriteMode() && $this->getRequest()->wasPosted() ) { - $this->getStats()->timing( + MediaWikiServices::getInstance()->getStatsdDataFactory()->timing( 'api.' . $this->mModule->getModuleName() . '.executeTiming', 1000 * $runTime ); } @@ -574,7 +577,7 @@ class ApiMain extends ApiBase { * @param Exception $e */ protected function handleException( Exception $e ) { - // Bug 63145: Rollback any open database transactions + // T65145: Rollback any open database transactions if ( !( $e instanceof ApiUsageException || $e instanceof UsageException ) ) { // UsageExceptions are intentional, so don't rollback if that's the case try { @@ -1109,10 +1112,16 @@ class ApiMain extends ApiBase { $result->addContentValue( $path, 'docref', - $this->msg( 'api-usage-docref', $link )->inLanguage( $formatter->getLanguage() )->text() + trim( + $this->msg( 'api-usage-docref', $link )->inLanguage( $formatter->getLanguage() )->text() + . ' ' + . $this->msg( 'api-usage-mailinglist-ref' )->inLanguage( $formatter->getLanguage() )->text() + ) ); } else { - if ( $config->get( 'ShowExceptionDetails' ) ) { + if ( $config->get( 'ShowExceptionDetails' ) && + ( !$e instanceof DBError || $config->get( 'ShowDBErrorBacktrace' ) ) + ) { $result->addContentValue( $path, 'trace', @@ -1222,6 +1231,35 @@ class ApiMain extends ApiBase { } /** + * @return array + */ + private function getMaxLag() { + $dbLag = MediaWikiServices::getInstance()->getDBLoadBalancer()->getMaxLag(); + $lagInfo = [ + 'host' => $dbLag[0], + 'lag' => $dbLag[1], + 'type' => 'db' + ]; + + $jobQueueLagFactor = $this->getConfig()->get( 'JobQueueIncludeInMaxLagFactor' ); + if ( $jobQueueLagFactor ) { + // Turn total number of jobs into seconds by using the configured value + $totalJobs = array_sum( JobQueueGroup::singleton()->getQueueSizes() ); + $jobQueueLag = $totalJobs / (float)$jobQueueLagFactor; + if ( $jobQueueLag > $lagInfo['lag'] ) { + $lagInfo = [ + 'host' => wfHostname(), // XXX: Is there a better value that could be used? + 'lag' => $jobQueueLag, + 'type' => 'jobqueue', + 'jobs' => $totalJobs, + ]; + } + } + + return $lagInfo; + } + + /** * Check the max lag if necessary * @param ApiBase $module Api module being used * @param array $params Array an array containing the request parameters. @@ -1230,18 +1268,22 @@ class ApiMain extends ApiBase { protected function checkMaxLag( $module, $params ) { if ( $module->shouldCheckMaxlag() && isset( $params['maxlag'] ) ) { $maxLag = $params['maxlag']; - list( $host, $lag ) = wfGetLB()->getMaxLag(); - if ( $lag > $maxLag ) { + $lagInfo = $this->getMaxLag(); + if ( $lagInfo['lag'] > $maxLag ) { $response = $this->getRequest()->response(); $response->header( 'Retry-After: ' . max( intval( $maxLag ), 5 ) ); - $response->header( 'X-Database-Lag: ' . intval( $lag ) ); + $response->header( 'X-Database-Lag: ' . intval( $lagInfo['lag'] ) ); if ( $this->getConfig()->get( 'ShowHostnames' ) ) { - $this->dieWithError( [ 'apierror-maxlag', $lag, $host ] ); + $this->dieWithError( + [ 'apierror-maxlag', $lagInfo['lag'], $lagInfo['host'] ], + 'maxlag', + $lagInfo + ); } - $this->dieWithError( [ 'apierror-maxlag-generic', $lag ], 'maxlag' ); + $this->dieWithError( [ 'apierror-maxlag-generic', $lagInfo['lag'] ], 'maxlag', $lagInfo ); } } @@ -1594,13 +1636,17 @@ class ApiMain extends ApiBase { " {$logCtx['ip']} " . "T={$logCtx['timeSpentBackend']}ms"; + $sensitive = array_flip( $this->getSensitiveParams() ); foreach ( $this->getParamsUsed() as $name ) { $value = $request->getVal( $name ); if ( $value === null ) { continue; } - if ( strlen( $value ) > 256 ) { + if ( isset( $sensitive[$name] ) ) { + $value = '[redacted]'; + $encValue = '[redacted]'; + } elseif ( strlen( $value ) > 256 ) { $value = substr( $value, 0, 256 ); $encValue = $this->encodeRequestLogValue( $value ) . '[...]'; } else { @@ -1651,6 +1697,24 @@ class ApiMain extends ApiBase { } /** + * Get the request parameters that should be considered sensitive + * @since 1.29 + * @return array + */ + protected function getSensitiveParams() { + return array_keys( $this->mParamsSensitive ); + } + + /** + * Mark parameters as sensitive + * @since 1.29 + * @param string|string[] $params + */ + public function markParamsSensitive( $params ) { + $this->mParamsSensitive += array_fill_keys( (array)$params, true ); + } + + /** * Get a request value, and register the fact that it was used, for logging. * @param string $name * @param mixed $default @@ -1662,7 +1726,7 @@ class ApiMain extends ApiBase { $ret = $this->getRequest()->getVal( $name ); if ( $ret === null ) { if ( $this->getRequest()->getArray( $name ) !== null ) { - // See bug 10262 for why we don't just implode( '|', ... ) the + // See T12262 for why we don't just implode( '|', ... ) the // array. $this->addWarning( [ 'apiwarn-unsupportedarray', $name ] ); } diff --git a/includes/api/ApiManageTags.php b/includes/api/ApiManageTags.php index 3c080939c0ca..42de16101856 100644 --- a/includes/api/ApiManageTags.php +++ b/includes/api/ApiManageTags.php @@ -125,6 +125,6 @@ class ApiManageTags extends ApiBase { } public function getHelpUrls() { - return 'https://www.mediawiki.org/wiki/API:Tag_management'; + return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Tag_management'; } } diff --git a/includes/api/ApiMergeHistory.php b/includes/api/ApiMergeHistory.php index 357698e13c40..79e99095675d 100644 --- a/includes/api/ApiMergeHistory.php +++ b/includes/api/ApiMergeHistory.php @@ -137,6 +137,6 @@ class ApiMergeHistory extends ApiBase { } public function getHelpUrls() { - return 'https://www.mediawiki.org/wiki/API:Mergehistory'; + return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Mergehistory'; } } diff --git a/includes/api/ApiMove.php b/includes/api/ApiMove.php index ab7199f6a50b..1fb034f85fb5 100644 --- a/includes/api/ApiMove.php +++ b/includes/api/ApiMove.php @@ -103,7 +103,7 @@ class ApiMove extends ApiBase { // a redirect to the new title. This is not safe, but what we did before was // even worse: we just determined whether a redirect should have been created, // and reported that it was created if it should have, without any checks. - // Also note that isRedirect() is unreliable because of bug 37209. + // Also note that isRedirect() is unreliable because of T39209. $r['redirectcreated'] = $fromTitle->exists(); $r['moveoverredirect'] = $toTitleExists; @@ -292,6 +292,6 @@ class ApiMove extends ApiBase { } public function getHelpUrls() { - return 'https://www.mediawiki.org/wiki/API:Move'; + return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Move'; } } diff --git a/includes/api/ApiOpenSearch.php b/includes/api/ApiOpenSearch.php index e6fe27ca2a34..ff65d0e29d77 100644 --- a/includes/api/ApiOpenSearch.php +++ b/includes/api/ApiOpenSearch.php @@ -309,7 +309,7 @@ class ApiOpenSearch extends ApiBase { } public function getHelpUrls() { - return 'https://www.mediawiki.org/wiki/API:Opensearch'; + return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Opensearch'; } /** diff --git a/includes/api/ApiOptions.php b/includes/api/ApiOptions.php index 466d1865d68c..5b0d86a7f647 100644 --- a/includes/api/ApiOptions.php +++ b/includes/api/ApiOptions.php @@ -169,7 +169,7 @@ class ApiOptions extends ApiBase { } public function getHelpUrls() { - return 'https://www.mediawiki.org/wiki/API:Options'; + return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Options'; } protected function getExamplesMessages() { diff --git a/includes/api/ApiPageSet.php b/includes/api/ApiPageSet.php index d42e306efa61..a235532cf31d 100644 --- a/includes/api/ApiPageSet.php +++ b/includes/api/ApiPageSet.php @@ -24,6 +24,8 @@ * @file */ use MediaWiki\MediaWikiServices; +use Wikimedia\Rdbms\ResultWrapper; +use Wikimedia\Rdbms\IDatabase; /** * This class contains a list of pages that the client has requested. @@ -64,10 +66,12 @@ class ApiPageSet extends ApiBase { private $mMissingPageIDs = []; private $mRedirectTitles = []; private $mSpecialTitles = []; + private $mAllSpecials = []; // separate from mAllPages to avoid breaking getAllTitlesByNamespace() private $mNormalizedTitles = []; private $mInterwikiTitles = []; /** @var Title[] */ private $mPendingRedirectIDs = []; + private $mPendingRedirectSpecialPages = []; // [dbkey] => [ Title $from, Title $to ] private $mResolvedRedirectTitles = []; private $mConvertedTitles = []; private $mGoodRevIDs = []; @@ -810,6 +814,8 @@ class ApiPageSet extends ApiBase { // Get validated and normalized title objects $linkBatch = $this->processTitlesArray( $titles ); if ( $linkBatch->isEmpty() ) { + // There might be special-page redirects + $this->resolvePendingRedirects(); return; } @@ -1030,7 +1036,7 @@ class ApiPageSet extends ApiBase { // Repeat until all redirects have been resolved // The infinite loop is prevented by keeping all known pages in $this->mAllPages - while ( $this->mPendingRedirectIDs ) { + while ( $this->mPendingRedirectIDs || $this->mPendingRedirectSpecialPages ) { // Resolve redirects by querying the pagelinks table, and repeat the process // Create a new linkBatch object for the next pass $linkBatch = $this->getRedirectTargets(); @@ -1061,58 +1067,82 @@ class ApiPageSet extends ApiBase { * @return LinkBatch */ private function getRedirectTargets() { - $lb = new LinkBatch(); + $titlesToResolve = []; $db = $this->getDB(); - $res = $db->select( - 'redirect', - [ - 'rd_from', - 'rd_namespace', - 'rd_fragment', - 'rd_interwiki', - 'rd_title' - ], [ 'rd_from' => array_keys( $this->mPendingRedirectIDs ) ], - __METHOD__ - ); - foreach ( $res as $row ) { - $rdfrom = intval( $row->rd_from ); - $from = $this->mPendingRedirectIDs[$rdfrom]->getPrefixedText(); - $to = Title::makeTitle( - $row->rd_namespace, - $row->rd_title, - $row->rd_fragment, - $row->rd_interwiki - ); - $this->mResolvedRedirectTitles[$from] = $this->mPendingRedirectIDs[$rdfrom]; - unset( $this->mPendingRedirectIDs[$rdfrom] ); - if ( $to->isExternal() ) { - $this->mInterwikiTitles[$to->getPrefixedText()] = $to->getInterwiki(); - } elseif ( !isset( $this->mAllPages[$row->rd_namespace][$row->rd_title] ) ) { - $lb->add( $row->rd_namespace, $row->rd_title ); + if ( $this->mPendingRedirectIDs ) { + $res = $db->select( + 'redirect', + [ + 'rd_from', + 'rd_namespace', + 'rd_fragment', + 'rd_interwiki', + 'rd_title' + ], [ 'rd_from' => array_keys( $this->mPendingRedirectIDs ) ], + __METHOD__ + ); + foreach ( $res as $row ) { + $rdfrom = intval( $row->rd_from ); + $from = $this->mPendingRedirectIDs[$rdfrom]->getPrefixedText(); + $to = Title::makeTitle( + $row->rd_namespace, + $row->rd_title, + $row->rd_fragment, + $row->rd_interwiki + ); + $this->mResolvedRedirectTitles[$from] = $this->mPendingRedirectIDs[$rdfrom]; + unset( $this->mPendingRedirectIDs[$rdfrom] ); + if ( $to->isExternal() ) { + $this->mInterwikiTitles[$to->getPrefixedText()] = $to->getInterwiki(); + } elseif ( !isset( $this->mAllPages[$to->getNamespace()][$to->getDBkey()] ) ) { + $titlesToResolve[] = $to; + } + $this->mRedirectTitles[$from] = $to; + } + + if ( $this->mPendingRedirectIDs ) { + // We found pages that aren't in the redirect table + // Add them + foreach ( $this->mPendingRedirectIDs as $id => $title ) { + $page = WikiPage::factory( $title ); + $rt = $page->insertRedirect(); + if ( !$rt ) { + // What the hell. Let's just ignore this + continue; + } + if ( $rt->isExternal() ) { + $this->mInterwikiTitles[$rt->getPrefixedText()] = $rt->getInterwiki(); + } elseif ( !isset( $this->mAllPages[$rt->getNamespace()][$rt->getDBkey()] ) ) { + $titlesToResolve[] = $rt; + } + $from = $title->getPrefixedText(); + $this->mResolvedRedirectTitles[$from] = $title; + $this->mRedirectTitles[$from] = $rt; + unset( $this->mPendingRedirectIDs[$id] ); + } } - $this->mRedirectTitles[$from] = $to; } - if ( $this->mPendingRedirectIDs ) { - // We found pages that aren't in the redirect table - // Add them - foreach ( $this->mPendingRedirectIDs as $id => $title ) { - $page = WikiPage::factory( $title ); - $rt = $page->insertRedirect(); - if ( !$rt ) { - // What the hell. Let's just ignore this - continue; + if ( $this->mPendingRedirectSpecialPages ) { + foreach ( $this->mPendingRedirectSpecialPages as $key => list( $from, $to ) ) { + $fromKey = $from->getPrefixedText(); + $this->mResolvedRedirectTitles[$fromKey] = $from; + $this->mRedirectTitles[$fromKey] = $to; + if ( $to->isExternal() ) { + $this->mInterwikiTitles[$to->getPrefixedText()] = $to->getInterwiki(); + } elseif ( !isset( $this->mAllPages[$to->getNamespace()][$to->getDBkey()] ) ) { + $titlesToResolve[] = $to; } - $lb->addObj( $rt ); - $from = $title->getPrefixedText(); - $this->mResolvedRedirectTitles[$from] = $title; - $this->mRedirectTitles[$from] = $rt; - unset( $this->mPendingRedirectIDs[$id] ); } + $this->mPendingRedirectSpecialPages = []; + + // Set private caching since we don't know what criteria the + // special pages used to decide on these redirects. + $this->mCacheMode = 'private'; } - return $lb; + return $this->processTitlesArray( $titlesToResolve ); } /** @@ -1151,12 +1181,14 @@ class ApiPageSet extends ApiBase { $titleObj = Title::newFromTextThrow( $title, $this->mDefaultNamespace ); } catch ( MalformedTitleException $ex ) { // Handle invalid titles gracefully - $this->mAllPages[0][$title] = $this->mFakePageId; - $this->mInvalidTitles[$this->mFakePageId] = [ - 'title' => $title, - 'invalidreason' => $this->getErrorFormatter()->formatException( $ex, [ 'bc' => true ] ), - ]; - $this->mFakePageId--; + if ( !isset( $this->mAllPages[0][$title] ) ) { + $this->mAllPages[0][$title] = $this->mFakePageId; + $this->mInvalidTitles[$this->mFakePageId] = [ + 'title' => $title, + 'invalidreason' => $this->getErrorFormatter()->formatException( $ex, [ 'bc' => true ] ), + ]; + $this->mFakePageId--; + } continue; // There's nothing else we can do } } else { @@ -1184,8 +1216,31 @@ class ApiPageSet extends ApiBase { if ( $titleObj->getNamespace() < 0 ) { // Handle Special and Media pages $titleObj = $titleObj->fixSpecialName(); - $this->mSpecialTitles[$this->mFakePageId] = $titleObj; - $this->mFakePageId--; + $ns = $titleObj->getNamespace(); + $dbkey = $titleObj->getDBkey(); + if ( !isset( $this->mAllSpecials[$ns][$dbkey] ) ) { + $this->mAllSpecials[$ns][$dbkey] = $this->mFakePageId; + $target = null; + if ( $ns === NS_SPECIAL && $this->mResolveRedirects ) { + $special = SpecialPageFactory::getPage( $dbkey ); + if ( $special instanceof RedirectSpecialArticle ) { + // Only RedirectSpecialArticle is intended to redirect to an article, other kinds of + // RedirectSpecialPage are probably applying weird URL parameters we don't want to handle. + $context = new DerivativeContext( $this ); + $context->setTitle( $titleObj ); + $context->setRequest( new FauxRequest ); + $special->setContext( $context ); + list( /* $alias */, $subpage ) = SpecialPageFactory::resolveAlias( $dbkey ); + $target = $special->getRedirect( $subpage ); + } + } + if ( $target ) { + $this->mPendingRedirectSpecialPages[$dbkey] = [ $titleObj, $target ]; + } else { + $this->mSpecialTitles[$this->mFakePageId] = $titleObj; + $this->mFakePageId--; + } + } } else { // Regular page $linkBatch->addObj( $titleObj ); @@ -1384,7 +1439,7 @@ class ApiPageSet extends ApiBase { * @return array */ private static function getPositiveIntegers( $array ) { - // bug 25734 API: possible issue with revids validation + // T27734 API: possible issue with revids validation // It seems with a load of revision rows, MySQL gets upset // Remove any < 0 integers, as they can't be valid foreach ( $array as $i => $int ) { diff --git a/includes/api/ApiParamInfo.php b/includes/api/ApiParamInfo.php index 67983e7a3aa0..39b589783231 100644 --- a/includes/api/ApiParamInfo.php +++ b/includes/api/ApiParamInfo.php @@ -543,6 +543,6 @@ class ApiParamInfo extends ApiBase { } public function getHelpUrls() { - return 'https://www.mediawiki.org/wiki/API:Parameter_information'; + return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Parameter_information'; } } diff --git a/includes/api/ApiParse.php b/includes/api/ApiParse.php index 287ffb7c0ebf..d6489688e6ab 100644 --- a/includes/api/ApiParse.php +++ b/includes/api/ApiParse.php @@ -340,6 +340,9 @@ class ApiParse extends ApiBase { if ( isset( $prop['sections'] ) ) { $result_array['sections'] = $p_result->getSections(); } + if ( isset( $prop['parsewarnings'] ) ) { + $result_array['parsewarnings'] = $p_result->getWarnings(); + } if ( isset( $prop['displaytitle'] ) ) { $result_array['displaytitle'] = $p_result->getDisplayTitle() ?: @@ -452,6 +455,7 @@ class ApiParse extends ApiBase { 'modulestyles' => 'm', 'properties' => 'pp', 'limitreportdata' => 'lr', + 'parsewarnings' => 'pw' ]; $this->setIndexedTagNames( $result_array, $result_mapping ); $result->addValue( null, $this->getModuleName(), $result_array ); @@ -751,7 +755,8 @@ class ApiParse extends ApiBase { ], 'prop' => [ ApiBase::PARAM_DFLT => 'text|langlinks|categories|links|templates|' . - 'images|externallinks|sections|revid|displaytitle|iwlinks|properties', + 'images|externallinks|sections|revid|displaytitle|iwlinks|' . + 'properties|parsewarnings', ApiBase::PARAM_ISMULTI => true, ApiBase::PARAM_TYPE => [ 'text', @@ -777,6 +782,7 @@ class ApiParse extends ApiBase { 'limitreportdata', 'limitreporthtml', 'parsetree', + 'parsewarnings' ], ApiBase::PARAM_HELP_MSG_PER_VALUE => [ 'parsetree' => [ 'apihelp-parse-paramvalue-prop-parsetree', CONTENT_MODEL_WIKITEXT ], @@ -829,6 +835,6 @@ class ApiParse extends ApiBase { } public function getHelpUrls() { - return 'https://www.mediawiki.org/wiki/API:Parsing_wikitext#parse'; + return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Parsing_wikitext#parse'; } } diff --git a/includes/api/ApiPatrol.php b/includes/api/ApiPatrol.php index c33542f1c7aa..06e8ae28c226 100644 --- a/includes/api/ApiPatrol.php +++ b/includes/api/ApiPatrol.php @@ -112,6 +112,6 @@ class ApiPatrol extends ApiBase { } public function getHelpUrls() { - return 'https://www.mediawiki.org/wiki/API:Patrol'; + return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Patrol'; } } diff --git a/includes/api/ApiProtect.php b/includes/api/ApiProtect.php index c74f890a57c5..1be4b10382fc 100644 --- a/includes/api/ApiProtect.php +++ b/includes/api/ApiProtect.php @@ -199,6 +199,6 @@ class ApiProtect extends ApiBase { } public function getHelpUrls() { - return 'https://www.mediawiki.org/wiki/API:Protect'; + return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Protect'; } } diff --git a/includes/api/ApiPurge.php b/includes/api/ApiPurge.php index 324d030fdbf4..83227a2fc2d8 100644 --- a/includes/api/ApiPurge.php +++ b/includes/api/ApiPurge.php @@ -37,11 +37,6 @@ class ApiPurge extends ApiBase { * Purges the cache of a page */ public function execute() { - $main = $this->getMain(); - if ( !$main->isInternalMode() && !$main->getRequest()->wasPosted() ) { - $this->addDeprecation( 'apiwarn-deprecation-purge-get', 'purge-via-GET' ); - } - $params = $this->extractRequestParams(); $continuationManager = new ApiContinuationManager( $this, [], [] ); @@ -60,12 +55,8 @@ class ApiPurge extends ApiBase { ApiQueryBase::addTitleInfo( $r, $title ); $page = WikiPage::factory( $title ); if ( !$user->pingLimiter( 'purge' ) ) { - $flags = WikiPage::PURGE_ALL; - if ( !$this->getRequest()->wasPosted() ) { - $flags ^= WikiPage::PURGE_GLOBAL_PCACHE; // skip DB_MASTER write - } // Directly purge and skip the UI part of purge() - $page->doPurge( $flags ); + $page->doPurge(); $r['purged'] = true; } else { $this->addWarning( 'apierror-ratelimited' ); @@ -157,20 +148,7 @@ class ApiPurge extends ApiBase { } public function mustBePosted() { - // Anonymous users are not allowed a non-POST request - return !$this->getUser()->isAllowed( 'purge' ); - } - - protected function getHelpFlags() { - $flags = parent::getHelpFlags(); - - // Claim that we must be posted for the purposes of help and paraminfo. - // @todo Remove this when self::mustBePosted() is updated for T145649 - if ( !in_array( 'mustbeposted', $flags, true ) ) { - $flags[] = 'mustbeposted'; - } - - return $flags; + return true; } public function getAllowedParams( $flags = 0 ) { @@ -198,6 +176,6 @@ class ApiPurge extends ApiBase { } public function getHelpUrls() { - return 'https://www.mediawiki.org/wiki/API:Purge'; + return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Purge'; } } diff --git a/includes/api/ApiQuery.php b/includes/api/ApiQuery.php index 8196cfa2bbfd..5395bf049118 100644 --- a/includes/api/ApiQuery.php +++ b/includes/api/ApiQuery.php @@ -536,10 +536,10 @@ class ApiQuery extends ApiBase { public function getHelpUrls() { return [ - 'https://www.mediawiki.org/wiki/API:Query', - 'https://www.mediawiki.org/wiki/API:Meta', - 'https://www.mediawiki.org/wiki/API:Properties', - 'https://www.mediawiki.org/wiki/API:Lists', + 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Query', + 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Meta', + 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Properties', + 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Lists', ]; } } diff --git a/includes/api/ApiQueryAllCategories.php b/includes/api/ApiQueryAllCategories.php index 614b06c0b53b..aa89158f9027 100644 --- a/includes/api/ApiQueryAllCategories.php +++ b/includes/api/ApiQueryAllCategories.php @@ -200,6 +200,6 @@ class ApiQueryAllCategories extends ApiQueryGeneratorBase { } public function getHelpUrls() { - return 'https://www.mediawiki.org/wiki/API:Allcategories'; + return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Allcategories'; } } diff --git a/includes/api/ApiQueryAllDeletedRevisions.php b/includes/api/ApiQueryAllDeletedRevisions.php index b09b97702db5..5682cc20340f 100644 --- a/includes/api/ApiQueryAllDeletedRevisions.php +++ b/includes/api/ApiQueryAllDeletedRevisions.php @@ -230,7 +230,7 @@ class ApiQueryAllDeletedRevisions extends ApiQueryRevisionsBase { } if ( !is_null( $params['user'] ) || !is_null( $params['excludeuser'] ) ) { - // Paranoia: avoid brute force searches (bug 17342) + // Paranoia: avoid brute force searches (T19342) // (shouldn't be able to get here without 'deletedhistory', but // check it again just in case) if ( !$user->isAllowed( 'deletedhistory' ) ) { @@ -455,6 +455,6 @@ class ApiQueryAllDeletedRevisions extends ApiQueryRevisionsBase { } public function getHelpUrls() { - return 'https://www.mediawiki.org/wiki/API:Alldeletedrevisions'; + return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Alldeletedrevisions'; } } diff --git a/includes/api/ApiQueryAllImages.php b/includes/api/ApiQueryAllImages.php index e3e5ed6c9f48..daeedbef6a1e 100644 --- a/includes/api/ApiQueryAllImages.php +++ b/includes/api/ApiQueryAllImages.php @@ -26,6 +26,8 @@ * @file */ +use Wikimedia\Rdbms\IDatabase; + /** * Query module to enumerate all available pages. * @@ -85,6 +87,7 @@ class ApiQueryAllImages extends ApiQueryGeneratorBase { $db = $this->getDB(); $params = $this->extractRequestParams(); + $userId = !is_null( $params['user'] ) ? User::idFromName( $params['user'] ) : null; // Table and return fields $this->addTables( 'image' ); @@ -189,7 +192,11 @@ class ApiQueryAllImages extends ApiQueryGeneratorBase { // Image filters if ( !is_null( $params['user'] ) ) { - $this->addWhereFld( 'img_user_text', $params['user'] ); + if ( $userId ) { + $this->addWhereFld( 'img_user', $userId ); + } else { + $this->addWhereFld( 'img_user_text', $params['user'] ); + } } if ( $params['filterbots'] != 'all' ) { $this->addTables( 'user_groups' ); @@ -197,7 +204,10 @@ class ApiQueryAllImages extends ApiQueryGeneratorBase { 'LEFT JOIN', [ 'ug_group' => User::getGroupsWithPermission( 'bot' ), - 'ug_user = img_user' + 'ug_user = img_user', + $this->getConfig()->get( 'DisableUserGroupExpiry' ) ? + '1' : + 'ug_expiry IS NULL OR ug_expiry >= ' . $db->addQuotes( $db->timestamp() ) ] ] ] ); $groupCond = ( $params['filterbots'] == 'nobots' ? 'NULL' : 'NOT NULL' ); @@ -266,7 +276,11 @@ class ApiQueryAllImages extends ApiQueryGeneratorBase { if ( $params['sort'] == 'timestamp' ) { $this->addOption( 'ORDER BY', 'img_timestamp' . $sortFlag ); if ( !is_null( $params['user'] ) ) { - $this->addOption( 'USE INDEX', [ 'image' => 'img_usertext_timestamp' ] ); + if ( $userId ) { + $this->addOption( 'USE INDEX', [ 'image' => 'img_user_timestamp' ] ); + } else { + $this->addOption( 'USE INDEX', [ 'image' => 'img_usertext_timestamp' ] ); + } } else { $this->addOption( 'USE INDEX', [ 'image' => 'img_timestamp' ] ); } @@ -414,6 +428,6 @@ class ApiQueryAllImages extends ApiQueryGeneratorBase { } public function getHelpUrls() { - return 'https://www.mediawiki.org/wiki/API:Allimages'; + return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Allimages'; } } diff --git a/includes/api/ApiQueryAllLinks.php b/includes/api/ApiQueryAllLinks.php index 3b24e37409a5..9d6bf4632530 100644 --- a/includes/api/ApiQueryAllLinks.php +++ b/includes/api/ApiQueryAllLinks.php @@ -308,6 +308,6 @@ class ApiQueryAllLinks extends ApiQueryGeneratorBase { public function getHelpUrls() { $name = ucfirst( $this->getModuleName() ); - return "https://www.mediawiki.org/wiki/API:{$name}"; + return "https://www.mediawiki.org/wiki/Special:MyLanguage/API:{$name}"; } } diff --git a/includes/api/ApiQueryAllMessages.php b/includes/api/ApiQueryAllMessages.php index 244effc5238a..271d28112422 100644 --- a/includes/api/ApiQueryAllMessages.php +++ b/includes/api/ApiQueryAllMessages.php @@ -256,6 +256,6 @@ class ApiQueryAllMessages extends ApiQueryBase { } public function getHelpUrls() { - return 'https://www.mediawiki.org/wiki/API:Allmessages'; + return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Allmessages'; } } diff --git a/includes/api/ApiQueryAllPages.php b/includes/api/ApiQueryAllPages.php index 7460bd537759..315def049ba3 100644 --- a/includes/api/ApiQueryAllPages.php +++ b/includes/api/ApiQueryAllPages.php @@ -76,10 +76,13 @@ class ApiQueryAllPages extends ApiQueryGeneratorBase { $this->addWhere( "page_title $op= $cont_from" ); } - if ( $params['filterredir'] == 'redirects' ) { - $this->addWhereFld( 'page_is_redirect', 1 ); - } elseif ( $params['filterredir'] == 'nonredirects' ) { - $this->addWhereFld( 'page_is_redirect', 0 ); + $miserMode = $this->getConfig()->get( 'MiserMode' ); + if ( !$miserMode ) { + if ( $params['filterredir'] == 'redirects' ) { + $this->addWhereFld( 'page_is_redirect', 1 ); + } elseif ( $params['filterredir'] == 'nonredirects' ) { + $this->addWhereFld( 'page_is_redirect', 0 ); + } } $this->addWhereFld( 'page_namespace', $params['namespace'] ); @@ -108,6 +111,18 @@ class ApiQueryAllPages extends ApiQueryGeneratorBase { $selectFields = $resultPageSet->getPageTableFields(); } + $miserModeFilterRedirValue = null; + $miserModeFilterRedir = $miserMode && $params['filterredir'] !== 'all'; + if ( $miserModeFilterRedir ) { + $selectFields[] = 'page_is_redirect'; + + if ( $params['filterredir'] == 'redirects' ) { + $miserModeFilterRedirValue = 1; + } elseif ( $params['filterredir'] == 'nonredirects' ) { + $miserModeFilterRedirValue = 0; + } + } + $this->addFields( $selectFields ); $forceNameTitleIndex = true; if ( isset( $params['minsize'] ) ) { @@ -219,6 +234,11 @@ class ApiQueryAllPages extends ApiQueryGeneratorBase { break; } + if ( $miserModeFilterRedir && (int)$row->page_is_redirect !== $miserModeFilterRedirValue ) { + // Filter implemented in PHP due to being in Miser Mode + continue; + } + if ( is_null( $resultPageSet ) ) { $title = Title::makeTitle( $row->page_namespace, $row->page_title ); $vals = [ @@ -242,7 +262,7 @@ class ApiQueryAllPages extends ApiQueryGeneratorBase { } public function getAllowedParams() { - return [ + $ret = [ 'from' => null, 'continue' => [ ApiBase::PARAM_HELP_MSG => 'api-help-param-continue', @@ -314,6 +334,12 @@ class ApiQueryAllPages extends ApiQueryGeneratorBase { ApiBase::PARAM_DFLT => 'all' ], ]; + + if ( $this->getConfig()->get( 'MiserMode' ) ) { + $ret['filterredir'][ApiBase::PARAM_HELP_MSG_APPEND] = [ 'api-help-param-limited-in-miser-mode' ]; + } + + return $ret; } protected function getExamplesMessages() { @@ -329,6 +355,6 @@ class ApiQueryAllPages extends ApiQueryGeneratorBase { } public function getHelpUrls() { - return 'https://www.mediawiki.org/wiki/API:Allpages'; + return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Allpages'; } } diff --git a/includes/api/ApiQueryAllRevisions.php b/includes/api/ApiQueryAllRevisions.php index b64b2c8401b1..20746c9a8c44 100644 --- a/includes/api/ApiQueryAllRevisions.php +++ b/includes/api/ApiQueryAllRevisions.php @@ -131,7 +131,7 @@ class ApiQueryAllRevisions extends ApiQueryRevisionsBase { } if ( $params['user'] !== null || $params['excludeuser'] !== null ) { - // Paranoia: avoid brute force searches (bug 17342) + // Paranoia: avoid brute force searches (T19342) if ( !$this->getUser()->isAllowed( 'deletedhistory' ) ) { $bitmask = Revision::DELETED_USER; } elseif ( !$this->getUser()->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) { @@ -290,6 +290,6 @@ class ApiQueryAllRevisions extends ApiQueryRevisionsBase { } public function getHelpUrls() { - return 'https://www.mediawiki.org/wiki/API:Allrevisions'; + return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Allrevisions'; } } diff --git a/includes/api/ApiQueryAllUsers.php b/includes/api/ApiQueryAllUsers.php index 2e2ac320fd58..0f0b2afaaa78 100644 --- a/includes/api/ApiQueryAllUsers.php +++ b/includes/api/ApiQueryAllUsers.php @@ -116,8 +116,18 @@ class ApiQueryAllUsers extends ApiQueryBase { // Filter only users that belong to a given group. This might // produce as many rows-per-user as there are groups being checked. $this->addTables( 'user_groups', 'ug1' ); - $this->addJoinConds( [ 'ug1' => [ 'INNER JOIN', [ 'ug1.ug_user=user_id', - 'ug1.ug_group' => $params['group'] ] ] ] ); + $this->addJoinConds( [ + 'ug1' => [ + 'INNER JOIN', + [ + 'ug1.ug_user=user_id', + 'ug1.ug_group' => $params['group'], + $this->getConfig()->get( 'DisableUserGroupExpiry' ) ? + '1' : + 'ug1.ug_expiry IS NULL OR ug1.ug_expiry >= ' . $db->addQuotes( $db->timestamp() ) + ] + ] + ] ); $maxDuplicateRows *= count( $params['group'] ); } @@ -135,7 +145,12 @@ class ApiQueryAllUsers extends ApiQueryBase { ) ]; } $this->addJoinConds( [ 'ug1' => [ 'LEFT OUTER JOIN', - array_merge( [ 'ug1.ug_user=user_id' ], $exclude ) + array_merge( [ + 'ug1.ug_user=user_id', + $this->getConfig()->get( 'DisableUserGroupExpiry' ) ? + '1' : + 'ug1.ug_expiry IS NULL OR ug1.ug_expiry >= ' . $db->addQuotes( $db->timestamp() ) + ], $exclude ) ] ] ); $this->addWhere( 'ug1.ug_user IS NULL' ); } @@ -148,7 +163,12 @@ class ApiQueryAllUsers extends ApiQueryBase { if ( $fld_groups || $fld_rights ) { $this->addFields( [ 'groups' => - $db->buildGroupConcatField( '|', 'user_groups', 'ug_group', 'ug_user=user_id' ) + $db->buildGroupConcatField( '|', 'user_groups', 'ug_group', [ + 'ug_user=user_id', + $this->getConfig()->get( 'DisableUserGroupExpiry' ) ? + '1' : + 'ug_expiry IS NULL OR ug_expiry >= ' . $db->addQuotes( $db->timestamp() ) + ] ) ] ); } @@ -166,7 +186,7 @@ class ApiQueryAllUsers extends ApiQueryBase { ], ] ] ); - // Actually count the actions using a subquery (bug 64505 and bug 64507) + // Actually count the actions using a subquery (T66505 and T66507) $timestamp = $db->timestamp( wfTimestamp( TS_UNIX ) - $activeUserSeconds ); $this->addFields( [ 'recentactions' => '(' . $db->selectSQLText( @@ -375,6 +395,6 @@ class ApiQueryAllUsers extends ApiQueryBase { } public function getHelpUrls() { - return 'https://www.mediawiki.org/wiki/API:Allusers'; + return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Allusers'; } } diff --git a/includes/api/ApiQueryAuthManagerInfo.php b/includes/api/ApiQueryAuthManagerInfo.php index 661ec5ab647c..c775942e76ed 100644 --- a/includes/api/ApiQueryAuthManagerInfo.php +++ b/includes/api/ApiQueryAuthManagerInfo.php @@ -127,6 +127,6 @@ class ApiQueryAuthManagerInfo extends ApiQueryBase { } public function getHelpUrls() { - return 'https://www.mediawiki.org/wiki/API:Authmanagerinfo'; + return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Authmanagerinfo'; } } diff --git a/includes/api/ApiQueryBacklinks.php b/includes/api/ApiQueryBacklinks.php index 613589e4d8aa..56cbaac3c189 100644 --- a/includes/api/ApiQueryBacklinks.php +++ b/includes/api/ApiQueryBacklinks.php @@ -59,19 +59,19 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase { 'code' => 'bl', 'prefix' => 'pl', 'linktbl' => 'pagelinks', - 'helpurl' => 'https://www.mediawiki.org/wiki/API:Backlinks', + 'helpurl' => 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Backlinks', ], 'embeddedin' => [ 'code' => 'ei', 'prefix' => 'tl', 'linktbl' => 'templatelinks', - 'helpurl' => 'https://www.mediawiki.org/wiki/API:Embeddedin', + 'helpurl' => 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Embeddedin', ], 'imageusage' => [ 'code' => 'iu', 'prefix' => 'il', 'linktbl' => 'imagelinks', - 'helpurl' => 'https://www.mediawiki.org/wiki/API:Imageusage', + 'helpurl' => 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Imageusage', ] ]; @@ -152,7 +152,7 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase { if ( $this->params['filterredir'] == 'redirects' ) { $this->addWhereFld( 'page_is_redirect', 1 ); } elseif ( $this->params['filterredir'] == 'nonredirects' && !$this->redirect ) { - // bug 22245 - Check for !redirect, as filtering nonredirects, when + // T24245 - Check for !redirect, as filtering nonredirects, when // getting what links to them is contradictory $this->addWhereFld( 'page_is_redirect', 0 ); } diff --git a/includes/api/ApiQueryBacklinksprop.php b/includes/api/ApiQueryBacklinksprop.php index 4ed7f52b26f5..00cbcd9fe43b 100644 --- a/includes/api/ApiQueryBacklinksprop.php +++ b/includes/api/ApiQueryBacklinksprop.php @@ -432,6 +432,6 @@ class ApiQueryBacklinksprop extends ApiQueryGeneratorBase { public function getHelpUrls() { $name = ucfirst( $this->getModuleName() ); - return "https://www.mediawiki.org/wiki/API:{$name}"; + return "https://www.mediawiki.org/wiki/Special:MyLanguage/API:{$name}"; } } diff --git a/includes/api/ApiQueryBase.php b/includes/api/ApiQueryBase.php index 281fb61eb724..87bb6a79d00e 100644 --- a/includes/api/ApiQueryBase.php +++ b/includes/api/ApiQueryBase.php @@ -24,6 +24,8 @@ * @file */ +use Wikimedia\Rdbms\ResultWrapper; + /** * This is a base class for all Query modules. * It provides some common functionality such as constructing various SQL @@ -258,7 +260,7 @@ abstract class ApiQueryBase extends ApiBase { /** * Equivalent to addWhere(array($field => $value)) * @param string $field Field name - * @param string $value Value; ignored if null or empty array; + * @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 @@ -325,7 +327,7 @@ abstract class ApiQueryBase extends ApiBase { * Add an option such as LIMIT or USE INDEX. If an option was set * before, the old value will be overwritten * @param string $name Option name - * @param string $value Option value + * @param string|string[] $value Option value */ protected function addOption( $name, $value = null ) { if ( is_null( $value ) ) { diff --git a/includes/api/ApiQueryBlocks.php b/includes/api/ApiQueryBlocks.php index 004086059c33..076a09efdf13 100644 --- a/includes/api/ApiQueryBlocks.php +++ b/includes/api/ApiQueryBlocks.php @@ -335,6 +335,6 @@ class ApiQueryBlocks extends ApiQueryBase { } public function getHelpUrls() { - return 'https://www.mediawiki.org/wiki/API:Blocks'; + return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Blocks'; } } diff --git a/includes/api/ApiQueryCategories.php b/includes/api/ApiQueryCategories.php index f2498cae20cf..c4428d575a1a 100644 --- a/includes/api/ApiQueryCategories.php +++ b/includes/api/ApiQueryCategories.php @@ -227,6 +227,6 @@ class ApiQueryCategories extends ApiQueryGeneratorBase { } public function getHelpUrls() { - return 'https://www.mediawiki.org/wiki/API:Categories'; + return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Categories'; } } diff --git a/includes/api/ApiQueryCategoryInfo.php b/includes/api/ApiQueryCategoryInfo.php index 34162407801e..2a3bf3871837 100644 --- a/includes/api/ApiQueryCategoryInfo.php +++ b/includes/api/ApiQueryCategoryInfo.php @@ -115,6 +115,6 @@ class ApiQueryCategoryInfo extends ApiQueryBase { } public function getHelpUrls() { - return 'https://www.mediawiki.org/wiki/API:Categoryinfo'; + return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Categoryinfo'; } } diff --git a/includes/api/ApiQueryCategoryMembers.php b/includes/api/ApiQueryCategoryMembers.php index 3a8847c33964..c570ec997e59 100644 --- a/includes/api/ApiQueryCategoryMembers.php +++ b/includes/api/ApiQueryCategoryMembers.php @@ -391,6 +391,6 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase { } public function getHelpUrls() { - return 'https://www.mediawiki.org/wiki/API:Categorymembers'; + return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Categorymembers'; } } diff --git a/includes/api/ApiQueryContributors.php b/includes/api/ApiQueryContributors.php index ac5ccca84c2e..183409d24f24 100644 --- a/includes/api/ApiQueryContributors.php +++ b/includes/api/ApiQueryContributors.php @@ -160,7 +160,13 @@ class ApiQueryContributors extends ApiQueryBase { $this->addTables( 'user_groups' ); $this->addJoinConds( [ 'user_groups' => [ $excludeGroups ? 'LEFT OUTER JOIN' : 'INNER JOIN', - [ 'ug_user=rev_user', 'ug_group' => $limitGroups ] + [ + 'ug_user=rev_user', + 'ug_group' => $limitGroups, + $this->getConfig()->get( 'DisableUserGroupExpiry' ) ? + '1' : + 'ug_expiry IS NULL OR ug_expiry >= ' . $db->addQuotes( $db->timestamp() ) + ] ] ] ); $this->addWhereIf( 'ug_user IS NULL', $excludeGroups ); } @@ -250,6 +256,6 @@ class ApiQueryContributors extends ApiQueryBase { } public function getHelpUrls() { - return 'https://www.mediawiki.org/wiki/API:Contributors'; + return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Contributors'; } } diff --git a/includes/api/ApiQueryDeletedRevisions.php b/includes/api/ApiQueryDeletedRevisions.php index d0b82144690b..90fd6953d061 100644 --- a/includes/api/ApiQueryDeletedRevisions.php +++ b/includes/api/ApiQueryDeletedRevisions.php @@ -123,7 +123,7 @@ class ApiQueryDeletedRevisions extends ApiQueryRevisionsBase { } if ( !is_null( $params['user'] ) || !is_null( $params['excludeuser'] ) ) { - // Paranoia: avoid brute force searches (bug 17342) + // Paranoia: avoid brute force searches (T19342) // (shouldn't be able to get here without 'deletedhistory', but // check it again just in case) if ( !$user->isAllowed( 'deletedhistory' ) ) { @@ -288,6 +288,6 @@ class ApiQueryDeletedRevisions extends ApiQueryRevisionsBase { } public function getHelpUrls() { - return 'https://www.mediawiki.org/wiki/API:Deletedrevisions'; + return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Deletedrevisions'; } } diff --git a/includes/api/ApiQueryDeletedrevs.php b/includes/api/ApiQueryDeletedrevs.php index 6a259cd00a80..b68a8682c580 100644 --- a/includes/api/ApiQueryDeletedrevs.php +++ b/includes/api/ApiQueryDeletedrevs.php @@ -203,7 +203,7 @@ class ApiQueryDeletedrevs extends ApiQueryBase { } if ( !is_null( $params['user'] ) || !is_null( $params['excludeuser'] ) ) { - // Paranoia: avoid brute force searches (bug 17342) + // Paranoia: avoid brute force searches (T19342) // (shouldn't be able to get here without 'deletedhistory', but // check it again just in case) if ( !$user->isAllowed( 'deletedhistory' ) ) { @@ -250,7 +250,7 @@ class ApiQueryDeletedrevs extends ApiQueryBase { $this->addOption( 'LIMIT', $limit + 1 ); $this->addOption( 'USE INDEX', - [ 'archive' => ( $mode == 'user' ? 'usertext_timestamp' : 'name_title_timestamp' ) ] + [ 'archive' => ( $mode == 'user' ? 'ar_usertext_timestamp' : 'name_title_timestamp' ) ] ); if ( $mode == 'all' ) { if ( $params['unique'] ) { @@ -505,6 +505,6 @@ class ApiQueryDeletedrevs extends ApiQueryBase { } public function getHelpUrls() { - return 'https://www.mediawiki.org/wiki/API:Deletedrevs'; + return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Deletedrevs'; } } diff --git a/includes/api/ApiQueryDuplicateFiles.php b/includes/api/ApiQueryDuplicateFiles.php index 02b7883d7b4d..2ebd6de7d6a9 100644 --- a/includes/api/ApiQueryDuplicateFiles.php +++ b/includes/api/ApiQueryDuplicateFiles.php @@ -189,6 +189,6 @@ class ApiQueryDuplicateFiles extends ApiQueryGeneratorBase { } public function getHelpUrls() { - return 'https://www.mediawiki.org/wiki/API:Duplicatefiles'; + return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Duplicatefiles'; } } diff --git a/includes/api/ApiQueryExtLinksUsage.php b/includes/api/ApiQueryExtLinksUsage.php index 9b055377ef6b..6c29b6030f11 100644 --- a/includes/api/ApiQueryExtLinksUsage.php +++ b/includes/api/ApiQueryExtLinksUsage.php @@ -230,6 +230,6 @@ class ApiQueryExtLinksUsage extends ApiQueryGeneratorBase { } public function getHelpUrls() { - return 'https://www.mediawiki.org/wiki/API:Exturlusage'; + return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Exturlusage'; } } diff --git a/includes/api/ApiQueryExternalLinks.php b/includes/api/ApiQueryExternalLinks.php index 8c9c887ae50f..71fd6d1b4b77 100644 --- a/includes/api/ApiQueryExternalLinks.php +++ b/includes/api/ApiQueryExternalLinks.php @@ -134,6 +134,6 @@ class ApiQueryExternalLinks extends ApiQueryBase { } public function getHelpUrls() { - return 'https://www.mediawiki.org/wiki/API:Extlinks'; + return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Extlinks'; } } diff --git a/includes/api/ApiQueryFileRepoInfo.php b/includes/api/ApiQueryFileRepoInfo.php index c4912366c47b..4589991191f0 100644 --- a/includes/api/ApiQueryFileRepoInfo.php +++ b/includes/api/ApiQueryFileRepoInfo.php @@ -111,6 +111,6 @@ class ApiQueryFileRepoInfo extends ApiQueryBase { } public function getHelpUrls() { - return 'https://www.mediawiki.org/wiki/API:Filerepoinfo'; + return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Filerepoinfo'; } } diff --git a/includes/api/ApiQueryFilearchive.php b/includes/api/ApiQueryFilearchive.php index 116dbb3d34db..7383cba6cb19 100644 --- a/includes/api/ApiQueryFilearchive.php +++ b/includes/api/ApiQueryFilearchive.php @@ -292,6 +292,6 @@ class ApiQueryFilearchive extends ApiQueryBase { } public function getHelpUrls() { - return 'https://www.mediawiki.org/wiki/API:Filearchive'; + return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Filearchive'; } } diff --git a/includes/api/ApiQueryIWBacklinks.php b/includes/api/ApiQueryIWBacklinks.php index 6e2fb67b8d99..a10ba164a213 100644 --- a/includes/api/ApiQueryIWBacklinks.php +++ b/includes/api/ApiQueryIWBacklinks.php @@ -215,6 +215,6 @@ class ApiQueryIWBacklinks extends ApiQueryGeneratorBase { } public function getHelpUrls() { - return 'https://www.mediawiki.org/wiki/API:Iwbacklinks'; + return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Iwbacklinks'; } } diff --git a/includes/api/ApiQueryIWLinks.php b/includes/api/ApiQueryIWLinks.php index cfd990b21301..9313af30efa8 100644 --- a/includes/api/ApiQueryIWLinks.php +++ b/includes/api/ApiQueryIWLinks.php @@ -194,6 +194,6 @@ class ApiQueryIWLinks extends ApiQueryBase { } public function getHelpUrls() { - return 'https://www.mediawiki.org/wiki/API:Iwlinks'; + return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Iwlinks'; } } diff --git a/includes/api/ApiQueryImageInfo.php b/includes/api/ApiQueryImageInfo.php index c9dae8de92ce..b2664dff1efd 100644 --- a/includes/api/ApiQueryImageInfo.php +++ b/includes/api/ApiQueryImageInfo.php @@ -509,7 +509,7 @@ class ApiQueryImageInfo extends ApiQueryBase { if ( $mto && !$mto->isError() ) { $vals['thumburl'] = wfExpandUrl( $mto->getUrl(), PROTO_CURRENT ); - // bug 23834 - If the URL's are the same, we haven't resized it, so shouldn't give the wanted + // T25834 - If the URLs are the same, we haven't resized it, so shouldn't give the wanted // thumbnail sizes for the thumbnail actual size if ( $mto->getUrl() !== $file->getUrl() ) { $vals['thumbwidth'] = intval( $mto->getWidth() ); @@ -821,6 +821,6 @@ class ApiQueryImageInfo extends ApiQueryBase { } public function getHelpUrls() { - return 'https://www.mediawiki.org/wiki/API:Imageinfo'; + return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Imageinfo'; } } diff --git a/includes/api/ApiQueryImages.php b/includes/api/ApiQueryImages.php index ae6f5bf564aa..0086c58a9301 100644 --- a/includes/api/ApiQueryImages.php +++ b/includes/api/ApiQueryImages.php @@ -172,6 +172,6 @@ class ApiQueryImages extends ApiQueryGeneratorBase { } public function getHelpUrls() { - return 'https://www.mediawiki.org/wiki/API:Images'; + return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Images'; } } diff --git a/includes/api/ApiQueryInfo.php b/includes/api/ApiQueryInfo.php index e789dd4fb583..c2cdfe4adc9b 100644 --- a/includes/api/ApiQueryInfo.php +++ b/includes/api/ApiQueryInfo.php @@ -946,6 +946,6 @@ class ApiQueryInfo extends ApiQueryBase { } public function getHelpUrls() { - return 'https://www.mediawiki.org/wiki/API:Info'; + return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Info'; } } diff --git a/includes/api/ApiQueryLangBacklinks.php b/includes/api/ApiQueryLangBacklinks.php index 8d5b5f3ea6a2..fd67d7c464fc 100644 --- a/includes/api/ApiQueryLangBacklinks.php +++ b/includes/api/ApiQueryLangBacklinks.php @@ -214,6 +214,6 @@ class ApiQueryLangBacklinks extends ApiQueryGeneratorBase { } public function getHelpUrls() { - return 'https://www.mediawiki.org/wiki/API:Langbacklinks'; + return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Langbacklinks'; } } diff --git a/includes/api/ApiQueryLangLinks.php b/includes/api/ApiQueryLangLinks.php index 55e3c85265a5..df33d027245f 100644 --- a/includes/api/ApiQueryLangLinks.php +++ b/includes/api/ApiQueryLangLinks.php @@ -190,6 +190,6 @@ class ApiQueryLangLinks extends ApiQueryBase { } public function getHelpUrls() { - return 'https://www.mediawiki.org/wiki/API:Langlinks'; + return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Langlinks'; } } diff --git a/includes/api/ApiQueryLinks.php b/includes/api/ApiQueryLinks.php index 4556e29062b8..29c0b74c37b8 100644 --- a/includes/api/ApiQueryLinks.php +++ b/includes/api/ApiQueryLinks.php @@ -42,13 +42,13 @@ class ApiQueryLinks extends ApiQueryGeneratorBase { $this->table = 'pagelinks'; $this->prefix = 'pl'; $this->titlesParam = 'titles'; - $this->helpUrl = 'https://www.mediawiki.org/wiki/API:Links'; + $this->helpUrl = 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Links'; break; case self::TEMPLATES: $this->table = 'templatelinks'; $this->prefix = 'tl'; $this->titlesParam = 'templates'; - $this->helpUrl = 'https://www.mediawiki.org/wiki/API:Templates'; + $this->helpUrl = 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Templates'; break; default: ApiBase::dieDebug( __METHOD__, 'Unknown module name' ); diff --git a/includes/api/ApiQueryLogEvents.php b/includes/api/ApiQueryLogEvents.php index 4d84aade2cc8..df8a11ee8bca 100644 --- a/includes/api/ApiQueryLogEvents.php +++ b/includes/api/ApiQueryLogEvents.php @@ -198,7 +198,7 @@ class ApiQueryLogEvents extends ApiQueryBase { $this->addWhere( 'log_title ' . $db->buildLike( $title->getDBkey(), $db->anyString() ) ); } - // Paranoia: avoid brute force searches (bug 17342) + // Paranoia: avoid brute force searches (T19342) if ( $params['namespace'] !== null || !is_null( $title ) || !is_null( $user ) ) { if ( !$this->getUser()->isAllowed( 'deletedhistory' ) ) { $titleBits = LogPage::DELETED_ACTION; @@ -467,6 +467,6 @@ class ApiQueryLogEvents extends ApiQueryBase { } public function getHelpUrls() { - return 'https://www.mediawiki.org/wiki/API:Logevents'; + return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Logevents'; } } diff --git a/includes/api/ApiQueryMyStashedFiles.php b/includes/api/ApiQueryMyStashedFiles.php index 1324f2ff498a..457f6c6e5c9a 100644 --- a/includes/api/ApiQueryMyStashedFiles.php +++ b/includes/api/ApiQueryMyStashedFiles.php @@ -145,6 +145,6 @@ class ApiQueryMyStashedFiles extends ApiQueryBase { } public function getHelpUrls() { - return 'https://www.mediawiki.org/wiki/API:mystashedfiles'; + return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:mystashedfiles'; } } diff --git a/includes/api/ApiQueryPagePropNames.php b/includes/api/ApiQueryPagePropNames.php index fc50b5067e65..4966bcde04ed 100644 --- a/includes/api/ApiQueryPagePropNames.php +++ b/includes/api/ApiQueryPagePropNames.php @@ -104,6 +104,6 @@ class ApiQueryPagePropNames extends ApiQueryBase { } public function getHelpUrls() { - return 'https://www.mediawiki.org/wiki/API:Pagepropnames'; + return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Pagepropnames'; } } diff --git a/includes/api/ApiQueryPageProps.php b/includes/api/ApiQueryPageProps.php index de1df3477013..e49dfbcf1d34 100644 --- a/includes/api/ApiQueryPageProps.php +++ b/includes/api/ApiQueryPageProps.php @@ -120,6 +120,6 @@ class ApiQueryPageProps extends ApiQueryBase { } public function getHelpUrls() { - return 'https://www.mediawiki.org/wiki/API:Pageprops'; + return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Pageprops'; } } diff --git a/includes/api/ApiQueryPagesWithProp.php b/includes/api/ApiQueryPagesWithProp.php index f1f4d9a4530f..e90356d33e0e 100644 --- a/includes/api/ApiQueryPagesWithProp.php +++ b/includes/api/ApiQueryPagesWithProp.php @@ -173,6 +173,6 @@ class ApiQueryPagesWithProp extends ApiQueryGeneratorBase { } public function getHelpUrls() { - return 'https://www.mediawiki.org/wiki/API:Pageswithprop'; + return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Pageswithprop'; } } diff --git a/includes/api/ApiQueryPrefixSearch.php b/includes/api/ApiQueryPrefixSearch.php index 3bf6d3fe4b68..5606f3c922ed 100644 --- a/includes/api/ApiQueryPrefixSearch.php +++ b/includes/api/ApiQueryPrefixSearch.php @@ -127,6 +127,6 @@ class ApiQueryPrefixSearch extends ApiQueryGeneratorBase { } public function getHelpUrls() { - return 'https://www.mediawiki.org/wiki/API:Prefixsearch'; + return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Prefixsearch'; } } diff --git a/includes/api/ApiQueryProtectedTitles.php b/includes/api/ApiQueryProtectedTitles.php index 62b2e42e03a2..5f6510ea281d 100644 --- a/includes/api/ApiQueryProtectedTitles.php +++ b/includes/api/ApiQueryProtectedTitles.php @@ -234,6 +234,6 @@ class ApiQueryProtectedTitles extends ApiQueryGeneratorBase { } public function getHelpUrls() { - return 'https://www.mediawiki.org/wiki/API:Protectedtitles'; + return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Protectedtitles'; } } diff --git a/includes/api/ApiQueryQueryPage.php b/includes/api/ApiQueryQueryPage.php index 908cdee667b9..caa5f05743fd 100644 --- a/includes/api/ApiQueryQueryPage.php +++ b/includes/api/ApiQueryQueryPage.php @@ -166,6 +166,6 @@ class ApiQueryQueryPage extends ApiQueryGeneratorBase { } public function getHelpUrls() { - return 'https://www.mediawiki.org/wiki/API:Querypage'; + return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Querypage'; } } diff --git a/includes/api/ApiQueryRandom.php b/includes/api/ApiQueryRandom.php index 00bd467cab33..cc1fc89f0761 100644 --- a/includes/api/ApiQueryRandom.php +++ b/includes/api/ApiQueryRandom.php @@ -209,6 +209,6 @@ class ApiQueryRandom extends ApiQueryGeneratorBase { } public function getHelpUrls() { - return 'https://www.mediawiki.org/wiki/API:Random'; + return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Random'; } } diff --git a/includes/api/ApiQueryRecentChanges.php b/includes/api/ApiQueryRecentChanges.php index 2c76e971058b..0dc01aabc249 100644 --- a/includes/api/ApiQueryRecentChanges.php +++ b/includes/api/ApiQueryRecentChanges.php @@ -147,7 +147,7 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase { /* Build our basic query. Namely, something along the lines of: * SELECT * FROM recentchanges WHERE rc_timestamp > $start - * AND rc_timestamp < $end AND rc_namespace = $namespace + * AND rc_timestamp < $end AND rc_namespace = $namespace */ $this->addTables( 'recentchanges' ); $this->addTimestampWhereRange( 'rc_timestamp', $params['dir'], $params['start'], $params['end'] ); @@ -320,7 +320,7 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase { $this->addWhereFld( 'ct_tag', $params['tag'] ); } - // Paranoia: avoid brute force searches (bug 17342) + // Paranoia: avoid brute force searches (T19342) if ( !is_null( $params['user'] ) || !is_null( $params['excludeuser'] ) ) { if ( !$user->isAllowed( 'deletedhistory' ) ) { $bitmask = Revision::DELETED_USER; @@ -699,6 +699,6 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase { } public function getHelpUrls() { - return 'https://www.mediawiki.org/wiki/API:Recentchanges'; + return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Recentchanges'; } } diff --git a/includes/api/ApiQueryRevisions.php b/includes/api/ApiQueryRevisions.php index 48f604664fbb..7b8394f7fd04 100644 --- a/includes/api/ApiQueryRevisions.php +++ b/includes/api/ApiQueryRevisions.php @@ -244,7 +244,7 @@ class ApiQueryRevisions extends ApiQueryRevisionsBase { } } if ( $params['user'] !== null || $params['excludeuser'] !== null ) { - // Paranoia: avoid brute force searches (bug 17342) + // Paranoia: avoid brute force searches (T19342) if ( !$this->getUser()->isAllowed( 'deletedhistory' ) ) { $bitmask = Revision::DELETED_USER; } elseif ( !$this->getUser()->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) { @@ -447,6 +447,6 @@ class ApiQueryRevisions extends ApiQueryRevisionsBase { } public function getHelpUrls() { - return 'https://www.mediawiki.org/wiki/API:Revisions'; + return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Revisions'; } } diff --git a/includes/api/ApiQuerySearch.php b/includes/api/ApiQuerySearch.php index 05b693d552f2..72b39b64a9c3 100644 --- a/includes/api/ApiQuerySearch.php +++ b/includes/api/ApiQuerySearch.php @@ -415,6 +415,6 @@ class ApiQuerySearch extends ApiQueryGeneratorBase { } public function getHelpUrls() { - return 'https://www.mediawiki.org/wiki/API:Search'; + return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Search'; } } diff --git a/includes/api/ApiQuerySiteinfo.php b/includes/api/ApiQuerySiteinfo.php index 5093608297d9..6b896c95ad9f 100644 --- a/includes/api/ApiQuerySiteinfo.php +++ b/includes/api/ApiQuerySiteinfo.php @@ -88,6 +88,9 @@ class ApiQuerySiteinfo extends ApiQueryBase { case 'languages': $fit = $this->appendLanguages( $p ); break; + case 'languagevariants': + $fit = $this->appendLanguageVariants( $p ); + break; case 'skins': $fit = $this->appendSkins( $p ); break; @@ -713,6 +716,49 @@ class ApiQuerySiteinfo extends ApiQueryBase { return $this->getResult()->addValue( 'query', $property, $data ); } + // Export information about which page languages will trigger + // language conversion. (T153341) + public function appendLanguageVariants( $property ) { + $langNames = LanguageConverter::$languagesWithVariants; + if ( $this->getConfig()->get( 'DisableLangConversion' ) ) { + // Ensure result is empty if language conversion is disabled. + $langNames = []; + } + sort( $langNames ); + + $data = []; + foreach ( $langNames as $langCode ) { + $lang = Language::factory( $langCode ); + if ( $lang->getConverter() instanceof FakeConverter ) { + // Only languages which do not return instances of + // FakeConverter implement language conversion. + continue; + } + $data[$langCode] = []; + ApiResult::setIndexedTagName( $data[$langCode], 'variant' ); + ApiResult::setArrayType( $data[$langCode], 'kvp', 'code' ); + + $variants = $lang->getVariants(); + sort( $variants ); + foreach ( $variants as $v ) { + $fallbacks = $lang->getConverter()->getVariantFallbacks( $v ); + if ( !is_array( $fallbacks ) ) { + $fallbacks = [ $fallbacks ]; + } + $data[$langCode][$v] = [ + 'fallbacks' => $fallbacks, + ]; + ApiResult::setIndexedTagName( + $data[$langCode][$v]['fallbacks'], 'variant' + ); + } + } + ApiResult::setIndexedTagName( $data, 'lang' ); + ApiResult::setArrayType( $data, 'kvp', 'code' ); + + return $this->getResult()->addValue( 'query', $property, $data ); + } + public function appendSkins( $property ) { $data = []; $allowed = Skin::getAllowedSkins(); @@ -772,7 +818,7 @@ class ApiQuerySiteinfo extends ApiQueryBase { } public function appendProtocols( $property ) { - // Make a copy of the global so we don't try to set the _element key of it - bug 45130 + // Make a copy of the global so we don't try to set the _element key of it - T47130 $protocols = array_values( $this->getConfig()->get( 'UrlProtocols' ) ); ApiResult::setArrayType( $protocols, 'BCarray' ); ApiResult::setIndexedTagName( $protocols, 'p' ); @@ -851,6 +897,7 @@ class ApiQuerySiteinfo extends ApiQueryBase { 'rightsinfo', 'restrictions', 'languages', + 'languagevariants', 'skins', 'extensiontags', 'functionhooks', @@ -886,6 +933,6 @@ class ApiQuerySiteinfo extends ApiQueryBase { } public function getHelpUrls() { - return 'https://www.mediawiki.org/wiki/API:Siteinfo'; + return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Siteinfo'; } } diff --git a/includes/api/ApiQueryStashImageInfo.php b/includes/api/ApiQueryStashImageInfo.php index abb827fe4f45..1924ca0339a8 100644 --- a/includes/api/ApiQueryStashImageInfo.php +++ b/includes/api/ApiQueryStashImageInfo.php @@ -123,6 +123,6 @@ class ApiQueryStashImageInfo extends ApiQueryImageInfo { } public function getHelpUrls() { - return 'https://www.mediawiki.org/wiki/API:Stashimageinfo'; + return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Stashimageinfo'; } } diff --git a/includes/api/ApiQueryTags.php b/includes/api/ApiQueryTags.php index 43eb7e806640..be67dd249b19 100644 --- a/includes/api/ApiQueryTags.php +++ b/includes/api/ApiQueryTags.php @@ -178,6 +178,6 @@ class ApiQueryTags extends ApiQueryBase { } public function getHelpUrls() { - return 'https://www.mediawiki.org/wiki/API:Tags'; + return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Tags'; } } diff --git a/includes/api/ApiQueryTokens.php b/includes/api/ApiQueryTokens.php index 5b700dbc9c0d..85205c8a4163 100644 --- a/includes/api/ApiQueryTokens.php +++ b/includes/api/ApiQueryTokens.php @@ -131,6 +131,6 @@ class ApiQueryTokens extends ApiQueryBase { } public function getHelpUrls() { - return 'https://www.mediawiki.org/wiki/API:Tokens'; + return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Tokens'; } } diff --git a/includes/api/ApiQueryUserContributions.php b/includes/api/ApiQueryUserContributions.php index 31a92380015f..181cddbeda0d 100644 --- a/includes/api/ApiQueryUserContributions.php +++ b/includes/api/ApiQueryUserContributions.php @@ -582,6 +582,6 @@ class ApiQueryContributions extends ApiQueryBase { } public function getHelpUrls() { - return 'https://www.mediawiki.org/wiki/API:Usercontribs'; + return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Usercontribs'; } } diff --git a/includes/api/ApiQueryUserInfo.php b/includes/api/ApiQueryUserInfo.php index 7bc00cb15837..1bb54c12a3ec 100644 --- a/includes/api/ApiQueryUserInfo.php +++ b/includes/api/ApiQueryUserInfo.php @@ -143,6 +143,19 @@ class ApiQueryUserInfo extends ApiQueryBase { ApiResult::setIndexedTagName( $vals['groups'], 'g' ); // even if empty } + if ( isset( $this->prop['groupmemberships'] ) ) { + $ugms = $user->getGroupMemberships(); + $vals['groupmemberships'] = []; + foreach ( $ugms as $group => $ugm ) { + $vals['groupmemberships'][] = [ + 'group' => $group, + 'expiry' => ApiResult::formatExpiry( $ugm->getExpiry() ), + ]; + } + ApiResult::setArrayType( $vals['groupmemberships'], 'array' ); // even if empty + ApiResult::setIndexedTagName( $vals['groupmemberships'], 'groupmembership' ); // even if empty + } + if ( isset( $this->prop['implicitgroups'] ) ) { $vals['implicitgroups'] = $user->getAutomaticGroups(); ApiResult::setArrayType( $vals['implicitgroups'], 'array' ); // even if empty @@ -302,6 +315,7 @@ class ApiQueryUserInfo extends ApiQueryBase { 'blockinfo', 'hasmsg', 'groups', + 'groupmemberships', 'implicitgroups', 'rights', 'changeablegroups', @@ -338,6 +352,6 @@ class ApiQueryUserInfo extends ApiQueryBase { } public function getHelpUrls() { - return 'https://www.mediawiki.org/wiki/API:Userinfo'; + return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Userinfo'; } } diff --git a/includes/api/ApiQueryUsers.php b/includes/api/ApiQueryUsers.php index 2d620a4e9ed6..4515f7f606de 100644 --- a/includes/api/ApiQueryUsers.php +++ b/includes/api/ApiQueryUsers.php @@ -42,6 +42,7 @@ class ApiQueryUsers extends ApiQueryBase { // everything except 'blockinfo' which might show hidden records if the user // making the request has the appropriate permissions 'groups', + 'groupmemberships', 'implicitgroups', 'rights', 'editcount', @@ -97,6 +98,8 @@ class ApiQueryUsers extends ApiQueryBase { } public function execute() { + $db = $this->getDB(); + $params = $this->extractRequestParams(); $this->requireMaxOneParameter( $params, 'userids', 'users' ); @@ -167,11 +170,16 @@ class ApiQueryUsers extends ApiQueryBase { $this->addTables( 'user_groups' ); $this->addJoinConds( [ 'user_groups' => [ 'INNER JOIN', 'ug_user=user_id' ] ] ); - $this->addFields( [ 'user_name', 'ug_group' ] ); + $this->addFields( [ 'user_name' ] ); + $this->addFields( UserGroupMembership::selectFields() ); + if ( !$this->getConfig()->get( 'DisableUserGroupExpiry' ) ) { + $this->addWhere( 'ug_expiry IS NULL OR ug_expiry >= ' . + $db->addQuotes( $db->timestamp() ) ); + } $userGroupsRes = $this->select( __METHOD__ ); foreach ( $userGroupsRes as $row ) { - $userGroups[$row->user_name][] = $row->ug_group; + $userGroups[$row->user_name][] = $row; } } @@ -207,6 +215,15 @@ class ApiQueryUsers extends ApiQueryBase { $data[$key]['groups'] = $user->getEffectiveGroups(); } + if ( isset( $this->prop['groupmemberships'] ) ) { + $data[$key]['groupmemberships'] = array_map( function( $ugm ) { + return [ + 'group' => $ugm->getGroup(), + 'expiry' => ApiResult::formatExpiry( $ugm->getExpiry() ), + ]; + }, $user->getGroupMemberships() ); + } + if ( isset( $this->prop['implicitgroups'] ) ) { $data[$key]['implicitgroups'] = $user->getAutomaticGroups(); } @@ -303,6 +320,10 @@ class ApiQueryUsers extends ApiQueryBase { ApiResult::setArrayType( $data[$u]['groups'], 'array' ); ApiResult::setIndexedTagName( $data[$u]['groups'], 'g' ); } + if ( isset( $this->prop['groupmemberships'] ) && isset( $data[$u]['groupmemberships'] ) ) { + ApiResult::setArrayType( $data[$u]['groupmemberships'], 'array' ); + ApiResult::setIndexedTagName( $data[$u]['groupmemberships'], 'groupmembership' ); + } if ( isset( $this->prop['implicitgroups'] ) && isset( $data[$u]['implicitgroups'] ) ) { ApiResult::setArrayType( $data[$u]['implicitgroups'], 'array' ); ApiResult::setIndexedTagName( $data[$u]['implicitgroups'], 'g' ); @@ -347,6 +368,7 @@ class ApiQueryUsers extends ApiQueryBase { ApiBase::PARAM_TYPE => [ 'blockinfo', 'groups', + 'groupmemberships', 'implicitgroups', 'rights', 'editcount', @@ -384,6 +406,6 @@ class ApiQueryUsers extends ApiQueryBase { } public function getHelpUrls() { - return 'https://www.mediawiki.org/wiki/API:Users'; + return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Users'; } } diff --git a/includes/api/ApiQueryWatchlist.php b/includes/api/ApiQueryWatchlist.php index 3f597511a132..f8f6e7d8a108 100644 --- a/includes/api/ApiQueryWatchlist.php +++ b/includes/api/ApiQueryWatchlist.php @@ -475,7 +475,8 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase { ApiBase::PARAM_TYPE => 'user' ], 'token' => [ - ApiBase::PARAM_TYPE => 'string' + ApiBase::PARAM_TYPE => 'string', + ApiBase::PARAM_SENSITIVE => true, ], 'continue' => [ ApiBase::PARAM_HELP_MSG => 'api-help-param-continue', @@ -501,6 +502,6 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase { } public function getHelpUrls() { - return 'https://www.mediawiki.org/wiki/API:Watchlist'; + return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Watchlist'; } } diff --git a/includes/api/ApiQueryWatchlistRaw.php b/includes/api/ApiQueryWatchlistRaw.php index a1078a5d4824..b0b1cde92b22 100644 --- a/includes/api/ApiQueryWatchlistRaw.php +++ b/includes/api/ApiQueryWatchlistRaw.php @@ -170,7 +170,8 @@ class ApiQueryWatchlistRaw extends ApiQueryGeneratorBase { ApiBase::PARAM_TYPE => 'user' ], 'token' => [ - ApiBase::PARAM_TYPE => 'string' + ApiBase::PARAM_TYPE => 'string', + ApiBase::PARAM_SENSITIVE => true, ], 'dir' => [ ApiBase::PARAM_DFLT => 'ascending', @@ -198,6 +199,6 @@ class ApiQueryWatchlistRaw extends ApiQueryGeneratorBase { } public function getHelpUrls() { - return 'https://www.mediawiki.org/wiki/API:Watchlistraw'; + return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Watchlistraw'; } } diff --git a/includes/api/ApiRemoveAuthenticationData.php b/includes/api/ApiRemoveAuthenticationData.php index 359d045fdd32..661b50c68e61 100644 --- a/includes/api/ApiRemoveAuthenticationData.php +++ b/includes/api/ApiRemoveAuthenticationData.php @@ -106,6 +106,6 @@ class ApiRemoveAuthenticationData extends ApiBase { } public function getHelpUrls() { - return 'https://www.mediawiki.org/wiki/API:Manage_authentication_data'; + return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Manage_authentication_data'; } } diff --git a/includes/api/ApiResetPassword.php b/includes/api/ApiResetPassword.php index b5fa8ed859e0..4f3fc0dc03f7 100644 --- a/includes/api/ApiResetPassword.php +++ b/includes/api/ApiResetPassword.php @@ -134,6 +134,6 @@ class ApiResetPassword extends ApiBase { } public function getHelpUrls() { - return 'https://www.mediawiki.org/wiki/API:Manage_authentication_data'; + return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Manage_authentication_data'; } } diff --git a/includes/api/ApiResult.php b/includes/api/ApiResult.php index e27cf7dd64fc..6734740f77a3 100644 --- a/includes/api/ApiResult.php +++ b/includes/api/ApiResult.php @@ -364,7 +364,7 @@ class ApiResult implements ApiSerializable { } } if ( is_array( $value ) ) { - // Work around PHP bug 45959 by copying to a temporary + // Work around https://bugs.php.net/bug.php?id=45959 by copying to a temporary // (in this case, foreach gets $k === "1" but $tmp[$k] assigns as if $k === 1) $tmp = []; foreach ( $value as $k => $v ) { diff --git a/includes/api/ApiRevisionDelete.php b/includes/api/ApiRevisionDelete.php index 4896e7e527a8..4580aa213e16 100644 --- a/includes/api/ApiRevisionDelete.php +++ b/includes/api/ApiRevisionDelete.php @@ -199,6 +199,6 @@ class ApiRevisionDelete extends ApiBase { } public function getHelpUrls() { - return 'https://www.mediawiki.org/wiki/API:Revisiondelete'; + return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Revisiondelete'; } } diff --git a/includes/api/ApiRollback.php b/includes/api/ApiRollback.php index 9584f09be445..76b6cc672214 100644 --- a/includes/api/ApiRollback.php +++ b/includes/api/ApiRollback.php @@ -202,6 +202,6 @@ class ApiRollback extends ApiBase { } public function getHelpUrls() { - return 'https://www.mediawiki.org/wiki/API:Rollback'; + return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Rollback'; } } diff --git a/includes/api/ApiRsd.php b/includes/api/ApiRsd.php index 4fac37da350a..fdc62a8ea927 100644 --- a/includes/api/ApiRsd.php +++ b/includes/api/ApiRsd.php @@ -89,7 +89,7 @@ class ApiRsd extends ApiBase { 'apiLink' => wfExpandUrl( wfScript( 'api' ), PROTO_CURRENT ), // Docs link is optional, but recommended. - 'docs' => 'https://www.mediawiki.org/wiki/API', + 'docs' => 'https://www.mediawiki.org/wiki/Special:MyLanguage/API', // Some APIs may need a blog ID, but it may be left blank. 'blogID' => '', diff --git a/includes/api/ApiSetNotificationTimestamp.php b/includes/api/ApiSetNotificationTimestamp.php index 5769ff6d3917..1fc8fc25f96f 100644 --- a/includes/api/ApiSetNotificationTimestamp.php +++ b/includes/api/ApiSetNotificationTimestamp.php @@ -248,6 +248,6 @@ class ApiSetNotificationTimestamp extends ApiBase { } public function getHelpUrls() { - return 'https://www.mediawiki.org/wiki/API:SetNotificationTimestamp'; + return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:SetNotificationTimestamp'; } } diff --git a/includes/api/ApiSetPageLanguage.php b/includes/api/ApiSetPageLanguage.php index 3ff99f11c0c8..2d6d9be48a35 100755 --- a/includes/api/ApiSetPageLanguage.php +++ b/includes/api/ApiSetPageLanguage.php @@ -144,6 +144,6 @@ class ApiSetPageLanguage extends ApiBase { } public function getHelpUrls() { - return 'https://www.mediawiki.org/wiki/API:SetPageLanguage'; + return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:SetPageLanguage'; } } diff --git a/includes/api/ApiTag.php b/includes/api/ApiTag.php index 7470ff3507c4..76c676293f13 100644 --- a/includes/api/ApiTag.php +++ b/includes/api/ApiTag.php @@ -187,6 +187,6 @@ class ApiTag extends ApiBase { } public function getHelpUrls() { - return 'https://www.mediawiki.org/wiki/API:Tag'; + return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Tag'; } } diff --git a/includes/api/ApiUnblock.php b/includes/api/ApiUnblock.php index 3eeb7a490eba..887edaae8104 100644 --- a/includes/api/ApiUnblock.php +++ b/includes/api/ApiUnblock.php @@ -44,7 +44,7 @@ class ApiUnblock extends ApiBase { if ( !$user->isAllowed( 'block' ) ) { $this->dieWithError( 'apierror-permissiondenied-unblock', 'permissiondenied' ); } - # bug 15810: blocked admins should have limited access here + # T17810: blocked admins should have limited access here if ( $user->isBlocked() ) { $status = SpecialBlock::checkUnblockSelf( $params['user'], $user ); if ( $status !== true ) { @@ -132,6 +132,6 @@ class ApiUnblock extends ApiBase { } public function getHelpUrls() { - return 'https://www.mediawiki.org/wiki/API:Block'; + return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Block'; } } diff --git a/includes/api/ApiUndelete.php b/includes/api/ApiUndelete.php index 7fda1ea01a40..3aa7b608dca7 100644 --- a/includes/api/ApiUndelete.php +++ b/includes/api/ApiUndelete.php @@ -33,7 +33,6 @@ class ApiUndelete extends ApiBase { $this->useTransactionalTimeLimit(); $params = $this->extractRequestParams(); - $this->checkUserRightsAny( 'undelete' ); $user = $this->getUser(); if ( $user->isBlocked() ) { @@ -45,6 +44,10 @@ class ApiUndelete extends ApiBase { $this->dieWithError( [ 'apierror-invalidtitle', wfEscapeWikiText( $params['title'] ) ] ); } + if ( !$titleObj->userCan( 'undelete', $user, 'secure' ) ) { + $this->dieWithError( 'permdenied-undelete' ); + } + // Check if user can add tags if ( !is_null( $params['tags'] ) ) { $ableToTag = ChangeTags::canAddTagsAccompanyingChange( $params['tags'], $user ); @@ -145,6 +148,6 @@ class ApiUndelete extends ApiBase { } public function getHelpUrls() { - return 'https://www.mediawiki.org/wiki/API:Undelete'; + return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Undelete'; } } diff --git a/includes/api/ApiUpload.php b/includes/api/ApiUpload.php index f8213740644c..a283b5a21534 100644 --- a/includes/api/ApiUpload.php +++ b/includes/api/ApiUpload.php @@ -494,6 +494,13 @@ class ApiUpload extends ApiBase { $this->mParams['filekey'], $this->mParams['filename'], !$this->mParams['async'] ); } elseif ( isset( $this->mParams['file'] ) ) { + // Can't async upload directly from a POSTed file, we'd have to + // stash the file and then queue the publish job. The user should + // just submit the two API queries to perform those two steps. + if ( $this->mParams['async'] ) { + $this->dieWithError( 'apierror-cannot-async-upload-file' ); + } + $this->mUpload = new UploadFromFile(); $this->mUpload->initialize( $this->mParams['filename'], @@ -637,7 +644,8 @@ class ApiUpload extends ApiBase { break; case UploadBase::HOOK_ABORTED: - $this->dieWithError( $params, 'hookaborted', [ 'details' => $verification['error'] ] ); + $msg = $verification['error'] === '' ? 'hookaborted' : $verification['error']; + $this->dieWithError( $msg, 'hookaborted', [ 'details' => $verification['error'] ] ); break; default: $this->dieWithError( 'apierror-unknownerror-nocode', 'unknown-error', @@ -920,6 +928,6 @@ class ApiUpload extends ApiBase { } public function getHelpUrls() { - return 'https://www.mediawiki.org/wiki/API:Upload'; + return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Upload'; } } diff --git a/includes/api/ApiUserrights.php b/includes/api/ApiUserrights.php index 4ef974cfcee6..d857e4afdd3c 100644 --- a/includes/api/ApiUserrights.php +++ b/includes/api/ApiUserrights.php @@ -1,9 +1,7 @@ <?php /** - * - * - * Created on Mar 24, 2009 + * API userrights module * * Copyright © 2009 Roan Kattouw "<Firstname>.<Lastname>@gmail.com" * @@ -59,6 +57,41 @@ class ApiUserrights extends ApiBase { $params = $this->extractRequestParams(); + // Figure out expiry times from the input + // @todo Remove this isset check when removing $wgDisableUserGroupExpiry + if ( isset( $params['expiry'] ) ) { + $expiry = (array)$params['expiry']; + } else { + $expiry = [ 'infinity' ]; + } + if ( count( $expiry ) !== count( $params['add'] ) ) { + if ( count( $expiry ) === 1 ) { + $expiry = array_fill( 0, count( $params['add'] ), $expiry[0] ); + } else { + $this->dieWithError( [ + 'apierror-toofewexpiries', + count( $expiry ), + count( $params['add'] ) + ] ); + } + } + + // Validate the expiries + $groupExpiries = []; + foreach ( $expiry as $index => $expiryValue ) { + $group = $params['add'][$index]; + $groupExpiries[$group] = UserrightsPage::expiryToTimestamp( $expiryValue ); + + if ( $groupExpiries[$group] === false ) { + $this->dieWithError( [ 'apierror-invalidexpiry', wfEscapeWikiText( $expiryValue ) ] ); + } + + // not allowed to have things expiring in the past + if ( $groupExpiries[$group] && $groupExpiries[$group] < wfTimestampNow() ) { + $this->dieWithError( [ 'apierror-pastexpiry', wfEscapeWikiText( $expiryValue ) ] ); + } + } + $user = $this->getUrUser( $params ); $tags = $params['tags']; @@ -76,8 +109,8 @@ 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'], $params['reason'], $tags + $user, (array)$params['add'], (array)$params['remove'], + $params['reason'], $tags, $groupExpiries ); $result = $this->getResult(); @@ -120,7 +153,7 @@ class ApiUserrights extends ApiBase { } public function getAllowedParams() { - return [ + $a = [ 'user' => [ ApiBase::PARAM_TYPE => 'user', ], @@ -131,6 +164,11 @@ class ApiUserrights extends ApiBase { ApiBase::PARAM_TYPE => $this->getAllGroups(), ApiBase::PARAM_ISMULTI => true ], + 'expiry' => [ + ApiBase::PARAM_ISMULTI => true, + ApiBase::PARAM_ALLOW_DUPLICATES => true, + ApiBase::PARAM_DFLT => 'infinite', + ], 'remove' => [ ApiBase::PARAM_TYPE => $this->getAllGroups(), ApiBase::PARAM_ISMULTI => true @@ -147,6 +185,10 @@ class ApiUserrights extends ApiBase { ApiBase::PARAM_ISMULTI => true ], ]; + if ( !$this->getUserRightsPage()->canProcessExpiries() ) { + unset( $a['expiry'] ); + } + return $a; } public function needsToken() { @@ -158,15 +200,20 @@ class ApiUserrights extends ApiBase { } protected function getExamplesMessages() { - return [ + $a = [ 'action=userrights&user=FooBot&add=bot&remove=sysop|bureaucrat&token=123ABC' => 'apihelp-userrights-example-user', 'action=userrights&userid=123&add=bot&remove=sysop|bureaucrat&token=123ABC' => 'apihelp-userrights-example-userid', ]; + if ( $this->getUserRightsPage()->canProcessExpiries() ) { + $a['action=userrights&user=SometimeSysop&add=sysop&expiry=1%20month&token=123ABC'] + = 'apihelp-userrights-example-expiry'; + } + return $a; } public function getHelpUrls() { - return 'https://www.mediawiki.org/wiki/API:User_group_membership'; + return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:User_group_membership'; } } diff --git a/includes/api/ApiValidatePassword.php b/includes/api/ApiValidatePassword.php index 6968523f288e..943149da0f3a 100644 --- a/includes/api/ApiValidatePassword.php +++ b/includes/api/ApiValidatePassword.php @@ -76,6 +76,6 @@ class ApiValidatePassword extends ApiBase { } public function getHelpUrls() { - return 'https://www.mediawiki.org/wiki/API:Validatepassword'; + return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Validatepassword'; } } diff --git a/includes/api/ApiWatch.php b/includes/api/ApiWatch.php index 37d319f15178..efe21f11d602 100644 --- a/includes/api/ApiWatch.php +++ b/includes/api/ApiWatch.php @@ -183,6 +183,6 @@ class ApiWatch extends ApiBase { } public function getHelpUrls() { - return 'https://www.mediawiki.org/wiki/API:Watch'; + return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Watch'; } } diff --git a/includes/api/i18n/ar.json b/includes/api/i18n/ar.json index 86078d49424b..122219589f81 100644 --- a/includes/api/i18n/ar.json +++ b/includes/api/i18n/ar.json @@ -8,7 +8,8 @@ "Hiba Alshawi", "Maroen1990", "محمد أحمد عبد الفتاح", - "ديفيد" + "ديفيد", + "ASHmed" ] }, "apihelp-main-param-action": "أي فعل للعمل.", @@ -311,6 +312,7 @@ "apihelp-protect-example-protect": "حماية صفحة.", "apihelp-protect-example-unprotect": "إلغاء حماية الصفحة من خلال وضع قيود ل<kbd>all</kbd> (أي يُسمَح أي شخص باتخاذ الإجراءات).", "apihelp-protect-example-unprotect2": "إلغاء حماية الصفحة عن طريق عدم وضع أية قيود.", + "apihelp-purge-description": "مسح ذاكرة التخزين المؤقت للعناوين المعطاة", "apihelp-purge-param-forcelinkupdate": "تحديث جداول الروابط.", "apihelp-purge-param-forcerecursivelinkupdate": "تحديث جدول الروابط، وتحديث جداول الروابط لأية صفحة تستخدم هذه الصفحة كقالب.", "apihelp-purge-example-simple": "إفراغ كاش <kbd>Main Page</kbd> وصفحة <kbd>API</kbd>.", diff --git a/includes/api/i18n/cs.json b/includes/api/i18n/cs.json index e8fd1652b985..a6f1a28cc40b 100644 --- a/includes/api/i18n/cs.json +++ b/includes/api/i18n/cs.json @@ -13,10 +13,10 @@ "Dvorapa" ] }, - "apihelp-main-description": "<div class=\"hlist plainlinks api-main-links\">\n* [[mw:API:Main_page|Dokumentace]]\n* [[mw:API:FAQ|FAQ]]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api E-mailová konference]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce Oznámení k API]\n* [https://phabricator.wikimedia.org/maniphest/query/GebfyV4uCaLd/#R Chyby a požadavky]\n</div>\n<strong>Stav:</strong> Všechny funkce uvedené na této stránce by měly fungovat, ale API se stále aktivně vyvíjí a může se kdykoli změnit. Upozornění na změny získáte přihlášením se k [https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ e-mailové konferenci mediawiki-api-announce].\n\n<strong>Chybné požadavky:</strong> Pokud jsou do API zaslány chybné požadavky, bude vrácena HTTP hlavička s klíčem „MediaWiki-API-Error“ a hodnota této hlavičky a chybový kód budou nastaveny na stejnou hodnotu. Více informací najdete [[mw:API:Errors_and_warnings|v dokumentaci]].\n\n<strong>Testování:</strong> Pro jednoduché testování požadavků na API zkuste [[Special:ApiSandbox]].", + "apihelp-main-description": "<div class=\"hlist plainlinks api-main-links\">\n* [[mw:Special:MyLanguage/API:Main_page|Dokumentace]]\n* [[mw:Special:MyLanguage/API:FAQ|FAQ]]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api E-mailová konference]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce Oznámení k API]\n* [https://phabricator.wikimedia.org/maniphest/query/GebfyV4uCaLd/#R Chyby a požadavky]\n</div>\n<strong>Stav:</strong> Všechny funkce uvedené na této stránce by měly fungovat, ale API se stále aktivně vyvíjí a může se kdykoli změnit. Upozornění na změny získáte přihlášením se k [https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ e-mailové konferenci mediawiki-api-announce].\n\n<strong>Chybné požadavky:</strong> Pokud jsou do API zaslány chybné požadavky, bude vrácena HTTP hlavička s klíčem „MediaWiki-API-Error“ a hodnota této hlavičky a chybový kód budou nastaveny na stejnou hodnotu. Více informací najdete [[mw:Special:MyLanguage/API:Errors_and_warnings|v dokumentaci]].\n\n<strong>Testování:</strong> Pro jednoduché testování požadavků na API zkuste [[Special:ApiSandbox]].", "apihelp-main-param-action": "Která akce se má provést.", "apihelp-main-param-format": "Formát výstupu.", - "apihelp-main-param-maxlag": "Maximální zpoždění lze použít, když je MediaWiki nainstalováno na cluster s replikovanou databází. Abyste se vyhnuli zhoršování už tak špatného replikačního zpoždění, můžete tímto parametrem nechat klienta čekat, dokud replikační zpoždění neklesne pod uvedenou hodnotu. V případě příliš vysokého zpoždění se vrátí chybový kód „<samp>maxlag</samp>“ s hlášením typu „<samp>Waiting for $host: $lag seconds lagged</samp>“.<br />Více informací najdete v [[mw:Manual:Maxlag_parameter|příručce]].", + "apihelp-main-param-maxlag": "Maximální zpoždění lze použít, když je MediaWiki nainstalováno na cluster s replikovanou databází. Abyste se vyhnuli zhoršování už tak špatného replikačního zpoždění, můžete tímto parametrem nechat klienta čekat, dokud replikační zpoždění neklesne pod uvedenou hodnotu. V případě příliš vysokého zpoždění se vrátí chybový kód „<samp>maxlag</samp>“ s hlášením typu „<samp>Waiting for $host: $lag seconds lagged</samp>“.<br />Více informací najdete v [[mw:Special:MyLanguage/Manual:Maxlag_parameter|příručce]].", "apihelp-main-param-smaxage": "Nastaví HTTP hlavičku pro řízení kešování <code>s-maxage</code> na uvedený počet sekund. Chyby se nekešují nikdy.", "apihelp-main-param-maxage": "Nastaví HTTP hlavičku pro řízení kešování <code>max-age</code> na uvedený počet sekund. Chyby se nekešují nikdy.", "apihelp-main-param-assert": "Pokud je nastaveno na „<kbd>user</kbd>“, ověří, že je uživatel přihlášen, pokud je nastaveno na „<kbd>bot</kbd>“, ověří, že má oprávnění „bot“.", @@ -32,7 +32,7 @@ "apihelp-block-param-nocreate": "Nedovolit registraci nových uživatelů.", "apihelp-block-param-noemail": "Zakázat uživateli posílat e-maily prostřednictvím wiki. (Vyžaduje oprávnění „<code>blockemail</code>“.)", "apihelp-block-param-hidename": "Skrýt uživatelské jméno v knize zablokování. (Vyžaduje oprávnění <code>hideuser</code>.)", - "apihelp-block-param-allowusertalk": "Povolit uživateli editovat svou vlastní diskusní stránku (závisí na <var>[[mw:Manual:$wgBlockAllowsUTEdit|$wgBlockAllowsUTEdit]]</var>).", + "apihelp-block-param-allowusertalk": "Povolit uživateli editovat svou vlastní diskusní stránku (závisí na <var>[[mw:Special:MyLanguage/Manual:$wgBlockAllowsUTEdit|$wgBlockAllowsUTEdit]]</var>).", "apihelp-block-param-reblock": "Pokud již uživatel blokován je, přepsat současný blok.", "apihelp-block-param-watchuser": "Sledovat stránku uživatele nebo IP adresy a jejich diskuzní stránky.", "apihelp-block-example-ip-simple": "Na tři dny zablokovat IP adresu <kbd>192.0.2.5</kbd> s odůvodněním <kbd>First strike</kbd>.", @@ -152,7 +152,7 @@ "apihelp-opensearch-param-search": "Hledaný řetězec.", "apihelp-opensearch-param-limit": "Maximální počet vrácených výsledků", "apihelp-opensearch-param-namespace": "Jmenné prostory pro vyhledávání.", - "apihelp-opensearch-param-suggest": "Pokud je <var>[[mw:Manual:$wgEnableOpenSearchSuggest|$wgEnableOpenSearchSuggest]]</var> vypnuto, nedělat nic.", + "apihelp-opensearch-param-suggest": "Pokud je <var>[[mw:Special:MyLanguage/Manual:$wgEnableOpenSearchSuggest|$wgEnableOpenSearchSuggest]]</var> vypnuto, nedělat nic.", "apihelp-opensearch-param-format": "Formát výstupu.", "apihelp-opensearch-example-te": "Najít stránky začínající na „<kbd>Te</kbd>“.", "apihelp-options-param-reset": "Vrátit nastavení na výchozí hodnoty.", @@ -227,7 +227,7 @@ "apihelp-query+watchlistraw-description": "Získat všechny stránky, které jsou aktuálním uživatelem sledovány.", "apihelp-query+watchlistraw-example-simple": "Seznam sledovaných stránek uživatele.", "apihelp-stashedit-param-summary": "Změnit shrnutí.", - "apihelp-unblock-param-user": "Uživatel, IP adresa nebo rozsah IP adres k odblokování. Nelze použít dohromady s <var>$1id</var> nebo <var>$luserid</var>.", + "apihelp-unblock-param-user": "Uživatel, IP adresa nebo rozsah IP adres k odblokování. Nelze použít dohromady s <var>$1id</var> nebo <var>$1userid</var>.", "apihelp-watch-example-watch": "Sledovat stránku <kbd>Main Page</kbd>.", "apihelp-watch-example-generator": "Zobrazit prvních několik stránek z hlavního jmenného prostoru.", "apihelp-format-example-generic": "Výsledek dotazu vrátit ve formátu $1.", @@ -244,8 +244,8 @@ "apihelp-xml-param-includexmlnamespace": "Pokud je uvedeno, přidá jmenný prostor XML.", "apihelp-xmlfm-description": "Vypisuje data ve formátu XML (v čitelné HTML podobě).", "api-format-title": "Odpověď z MediaWiki API", - "api-format-prettyprint-header": "Toto je HTML reprezentace formátu $1. HTML se hodí pro ladění, ale pro aplikační použití je nevhodné.\n\nPro změnu výstupního formátu uveďte parametr <var>format</var>. Abyste viděli ne-HTML reprezentaci formátu $1, nastavte <kbd>format=$2</kbd>.\n\nVíce informací najdete v [[mw:API|úplné dokumentaci]] nebo v [[Special:ApiHelp/main|nápovědě k API]].", - "api-format-prettyprint-header-only-html": "Toto je HTML reprezentace určená pro ladění, která není vhodná pro použití v aplikacích.\n\nVíce informací najdete v [[mw:API|úplné dokumentaci]] nebo [[Special:ApiHelp/main|dokumentaci API]].", + "api-format-prettyprint-header": "Toto je HTML reprezentace formátu $1. HTML se hodí pro ladění, ale pro aplikační použití je nevhodné.\n\nPro změnu výstupního formátu uveďte parametr <var>format</var>. Abyste viděli ne-HTML reprezentaci formátu $1, nastavte <kbd>format=$2</kbd>.\n\nVíce informací najdete v [[mw:Special:MyLanguage/API|úplné dokumentaci]] nebo v [[Special:ApiHelp/main|nápovědě k API]].", + "api-format-prettyprint-header-only-html": "Toto je HTML reprezentace určená pro ladění, která není vhodná pro použití v aplikacích.\n\nVíce informací najdete v [[mw:Special:MyLanguage/API|úplné dokumentaci]] nebo [[Special:ApiHelp/main|dokumentaci API]].", "api-help-title": "Nápověda k MediaWiki API", "api-help-lead": "Toto je automaticky generovaná dokumentační stránka k MediaWiki API.\n\nDokumentace a příklady: https://www.mediawiki.org/wiki/API", "api-help-main-header": "Hlavní modul", diff --git a/includes/api/i18n/de.json b/includes/api/i18n/de.json index 596c35e6a739..074d69ebe462 100644 --- a/includes/api/i18n/de.json +++ b/includes/api/i18n/de.json @@ -16,13 +16,15 @@ "Ljonka", "FriedhelmW", "Predatorix", - "Luke081515" + "Luke081515", + "Eddie", + "Zenith" ] }, - "apihelp-main-description": "<div class=\"hlist plainlinks api-main-links\">\n* [[mw:API:Main_page|Dokumentation]]\n* [[mw:API:FAQ|Häufig gestellte Fragen]]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api Mailingliste]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce API-Ankündigungen]\n* [https://phabricator.wikimedia.org/maniphest/query/GebfyV4uCaLd/#R Fehlerberichte und Anfragen]\n</div>\n<strong>Status:</strong> Alle auf dieser Seite gezeigten Funktionen sollten funktionieren, allerdings ist die API in aktiver Entwicklung und kann sich zu jeder Zeit ändern. Abonniere die [https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ MediaWiki-API-Ankündigungs-Mailingliste], um über Aktualisierungen informiert zu werden.\n\n<strong>Fehlerhafte Anfragen:</strong> Wenn fehlerhafte Anfragen an die API gesendet werden, wird ein HTTP-Header mit dem Schlüssel „MediaWiki-API-Error“ gesendet. Der Wert des Headers und der Fehlercode werden auf den gleichen Wert gesetzt. Für weitere Informationen siehe [[mw:API:Errors_and_warnings|API: Fehler und Warnungen]].\n\n<strong>Testen:</strong> Zum einfachen Testen von API-Anfragen, siehe [[Special:ApiSandbox]].", + "apihelp-main-description": "<div class=\"hlist plainlinks api-main-links\">\n* [[mw:Special:MyLanguage/API:Main_page|Dokumentation]]\n* [[mw:Special:MyLanguage/API:FAQ|Häufig gestellte Fragen]]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api Mailingliste]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce API-Ankündigungen]\n* [https://phabricator.wikimedia.org/maniphest/query/GebfyV4uCaLd/#R Fehlerberichte und Anfragen]\n</div>\n<strong>Status:</strong> Alle auf dieser Seite gezeigten Funktionen sollten funktionieren, allerdings ist die API in aktiver Entwicklung und kann sich zu jeder Zeit ändern. Abonniere die [https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ MediaWiki-API-Ankündigungs-Mailingliste], um über Aktualisierungen informiert zu werden.\n\n<strong>Fehlerhafte Anfragen:</strong> Wenn fehlerhafte Anfragen an die API gesendet werden, wird ein HTTP-Header mit dem Schlüssel „MediaWiki-API-Error“ gesendet. Der Wert des Headers und der Fehlercode werden auf den gleichen Wert gesetzt. Für weitere Informationen siehe [[mw:Special:MyLanguage/API:Errors_and_warnings|API: Fehler und Warnungen]].\n\n<strong>Testen:</strong> Zum einfachen Testen von API-Anfragen, siehe [[Special:ApiSandbox]].", "apihelp-main-param-action": "Auszuführende Aktion.", "apihelp-main-param-format": "Format der Ausgabe.", - "apihelp-main-param-maxlag": "maxlag kann verwendet werden, wenn MediaWiki auf einem datenbankreplizierten Cluster installiert ist. Um weitere Replikationsrückstände zu verhindern, lässt dieser Parameter den Client warten, bis der Replikationsrückstand kleiner als der angegebene Wert (in Sekunden) ist. Bei einem größerem Rückstand wird der Fehlercode <samp>maxlag</samp> zurückgegeben mit einer Nachricht wie <samp>Waiting for $host: $lag seconds lagged</samp>.<br />Siehe [[mw:Manual:Maxlag_parameter|Handbuch: Maxlag parameter]] für weitere Informationen.", + "apihelp-main-param-maxlag": "maxlag kann verwendet werden, wenn MediaWiki auf einem datenbankreplizierten Cluster installiert ist. Um weitere Replikationsrückstände zu verhindern, lässt dieser Parameter den Client warten, bis der Replikationsrückstand kleiner als der angegebene Wert (in Sekunden) ist. Bei einem größerem Rückstand wird der Fehlercode <samp>maxlag</samp> zurückgegeben mit einer Nachricht wie <samp>Waiting for $host: $lag seconds lagged</samp>.<br />Siehe [[mw:Special:MyLanguage/Manual:Maxlag_parameter|Handbuch: Maxlag parameter]] für weitere Informationen.", "apihelp-main-param-smaxage": "Den <code>s-maxage</code>-HTTP-Cache-Control-Header auf diese Anzahl Sekunden festlegen. Fehler werden niemals gepuffert.", "apihelp-main-param-maxage": "Den <code>max-age</code>-HTTP-Cache-Control-Header auf diese Anzahl Sekunden festlegen. Fehler werden niemals gecacht.", "apihelp-main-param-assert": "Sicherstellen, dass der Benutzer eingeloggt ist, wenn auf <kbd>user</kbd> gesetzt, oder Bot ist, wenn auf <kbd>bot</kbd> gesetzt.", @@ -33,9 +35,11 @@ "apihelp-main-param-responselanginfo": "Bezieht die für <var>uselang</var> und <var>errorlang</var> verwendeten Sprachen im Ergebnis mit ein.", "apihelp-main-param-origin": "Beim Zugriff auf die API mit einer Kreuz-Domain-AJAX-Anfrage (CORS) muss dies als entstehende Domäne festgelegt werden. Dies muss in jeder Vorfluganfrage mit eingeschlossen werden und deshalb ein Teil der Anfragen-URI sein (nicht des POST-Körpers).\n\nFür authentifizierte Anfragen muss dies exakt einem der Ursprünge im Header <code>Origin</code> entsprechen, so dass es auf etwas wie <kbd>https://de.wikipedia.org</kbd> oder <kbd>https://meta.wikimedia.org</kbd> festgelegt werden muss. Falls dieser Parameter nicht mit dem Header <code>Origin</code> übereinstimmt, wird eine 403-Antwort zurückgegeben. Falls dieser Parameter mit dem Header <code>Origin</code> übereinstimmt und der Ursprung weißgelistet ist, werden die Header <code>Access-Control-Allow-Origin</code> und <code>Access-Control-Allow-Credentials</code> festgelegt.\n\nGib für nicht authentifizierte Anfragen den Wert <kbd>*</kbd> an. Dies verursacht, dass der Header <code>Access-Control-Allow-Origin</code> festgelegt wird, aber <code>Access-Control-Allow-Credentials</code> wird <code>false</code> sein und alle benutzerspezifischen Daten werden beschränkt.", "apihelp-main-param-uselang": "Zu verwendende Sprache für Nachrichtenübersetzungen. <kbd>[[Special:ApiHelp/query+siteinfo|action=query&meta=siteinfo]]</kbd> mit <kbd>siprop=languages</kbd> gibt eine Liste der Sprachcodes zurück. Gib <kbd>user</kbd> zum Verwenden der aktuellen Benutzerspracheinstellung oder <kbd>content</kbd> an, um die Inhaltssprache des Wikis zu verwenden.", + "apihelp-main-param-errorformat": "Zu verwendendes Format zur Ausgabe von Warnungen und Fehlertexten.\n; plaintext: Wikitext mit entfernten HTML-Tags und ersetzten Entitäten.\n; wikitext: Ungeparster Wikitext.\n; html: HTML.\n; raw: Nachrichtenschlüssel und Parameter.\n; none: Keine Textausgabe, nur die Fehlercodes.\n; bc: Vor MediaWiki 1.29 verwendetes Format. <var>errorlang</var> und <var>errorsuselocal</var> werden ignoriert.", "apihelp-main-param-errorsuselocal": "Falls angegeben, verwenden Fehlertexte lokalisierte Nachrichten aus dem {{ns:MediaWiki}}-Namensraum.", "apihelp-block-description": "Einen Benutzer sperren.", "apihelp-block-param-user": "Benutzername, IP-Adresse oder IP-Adressbereich, der gesperrt werden soll. Kann nicht zusammen mit <var>$1userid</var> verwendet werden.", + "apihelp-block-param-userid": "Die zu sperrende Benutzerkennung. Kann nicht zusammen mit <var>$1user</var> verwendet werden.", "apihelp-block-param-expiry": "Sperrdauer. Kann relativ (z. B. <kbd>5 months</kbd> oder <kbd>2 weeks</kbd>) oder absolut (z. B. <kbd>2014-09-18T12:34:56Z</kbd>) sein. Wenn auf <kbd>infinite</kbd>, <kbd>indefinite</kbd> oder <kbd>never</kbd> gesetzt, ist die Sperre unbegrenzt.", "apihelp-block-param-reason": "Sperrbegründung.", "apihelp-block-param-anononly": "Nur anonyme Benutzer sperren (z. B. anonyme Bearbeitungen für diese IP deaktivieren).", @@ -43,12 +47,14 @@ "apihelp-block-param-autoblock": "Die zuletzt verwendete IP-Adresse automatisch sperren und alle darauffolgenden IP-Adressen, die versuchen sich anzumelden.", "apihelp-block-param-noemail": "Benutzer davon abhalten, E-Mails auf dem Wiki zu versenden (erfordert das <code>blockemail</code>-Recht).", "apihelp-block-param-hidename": "Den Benutzernamen im Sperr-Logbuch verstecken (erfordert das <code>hideuser</code>-Recht).", - "apihelp-block-param-allowusertalk": "Dem Benutzer erlauben, seine eigene Diskussionsseite zu bearbeiten (abhängig von <var>[[mw:Manual:$wgBlockAllowsUTEdit|$wgBlockAllowsUTEdit]]</var>).", + "apihelp-block-param-allowusertalk": "Dem Benutzer erlauben, seine eigene Diskussionsseite zu bearbeiten (abhängig von <var>[[mw:Special:MyLanguage/Manual:$wgBlockAllowsUTEdit|$wgBlockAllowsUTEdit]]</var>).", "apihelp-block-param-reblock": "Falls der Benutzer bereits gesperrt ist, die vorhandene Sperre überschreiben.", "apihelp-block-param-watchuser": "Benutzer- und Diskussionsseiten des Benutzers oder der IP-Adresse beobachten.", "apihelp-block-param-tags": "Auf den Eintrag im Sperr-Logbuch anzuwendende Änderungsmarkierungen.", "apihelp-block-example-ip-simple": "IP <kbd>192.0.2.5</kbd> für drei Tage mit der Begründung „First strike“ (erste Verwarnung) sperren", "apihelp-block-example-user-complex": "Benutzer <kbd>Vandal</kbd> unbeschränkt sperren mit der Begründung „Vandalism“ (Vandalismus), Erstellung neuer Benutzerkonten sowie Versand von E-Mails verhindern.", + "apihelp-changeauthenticationdata-description": "Ändert die Authentifizierungsdaten für den aktuellen Benutzer.", + "apihelp-changeauthenticationdata-example-password": "Versucht, das Passwort des aktuellen Benutzers in <kbd>ExamplePassword</kbd> zu ändern.", "apihelp-checktoken-description": "Überprüft die Gültigkeit eines über <kbd>[[Special:ApiHelp/query+tokens|action=query&meta=tokens]]</kbd> erhaltenen Tokens.", "apihelp-checktoken-param-type": "Typ des Tokens, das getestet werden soll.", "apihelp-checktoken-param-token": "Token, das getestet werden soll.", @@ -56,6 +62,7 @@ "apihelp-checktoken-example-simple": "Überprüft die Gültigkeit des <kbd>csrf</kbd>-Tokens.", "apihelp-clearhasmsg-description": "Löschen des <code>hasmsg</code>-Flags („hat Nachrichten“-Flag) für den aktuellen Benutzer.", "apihelp-clearhasmsg-example-1": "<code>hasmsg</code>-Flags für den aktuellen Benutzer löschen", + "apihelp-clientlogin-example-login": "Startet den Prozess der Anmeldung in dem Wiki als Benutzer <kbd>Example</kbd> mit dem Passwort <kbd>ExamplePassword</kbd>.", "apihelp-compare-description": "Abrufen des Unterschieds zwischen zwei Seiten.\n\nDu musst eine Versionsnummer, einen Seitentitel oder eine Seitennummer für „from“ als auch „to“ angeben.", "apihelp-compare-param-fromtitle": "Erster zu vergleichender Titel.", "apihelp-compare-param-fromid": "Erste zu vergleichende Seitennummer.", @@ -217,6 +224,7 @@ "apihelp-import-param-rootpage": "Als Unterseite dieser Seite importieren. Kann nicht zusammen mit <var>$1namespace</var> verwendet werden.", "apihelp-import-param-tags": "Auf den Eintrag im Import-Logbuch und die Nullversion bei den importierten Seiten anzuwendende Änderungsmarkierungen.", "apihelp-import-example-import": "Importiere [[meta:Help:ParserFunctions]] mit der kompletten Versionsgeschichte in den Namensraum 100.", + "apihelp-linkaccount-description": "Verbindet ein Benutzerkonto von einem Drittanbieter mit dem aktuellen Benutzer.", "apihelp-login-description": "Anmelden und Authentifizierungs-Cookies beziehen.\n\nDiese Aktion sollte nur in Kombination mit [[Special:BotPasswords]] verwendet werden. Die Verwendung für die Anmeldung beim Hauptkonto ist veraltet und kann ohne Warnung fehlschlagen. Um sich sicher beim Hauptkonto anzumelden, verwende <kbd>[[Special:ApiHelp/clientlogin|action=clientlogin]]</kbd>.", "apihelp-login-param-name": "Benutzername.", "apihelp-login-param-password": "Passwort.", @@ -237,6 +245,8 @@ "apihelp-managetags-example-activate": "Aktiviert eine Markierung namens <kbd>spam</kbd> mit der Begründung <kbd>For use in edit patrolling</kbd> (für die Eingangskontrolle).", "apihelp-managetags-example-deactivate": "Deaktiviert eine Markierung namens <kbd>spam</kbd> mit der Begründung <kbd>No longer required</kbd> (nicht mehr benötigt).", "apihelp-mergehistory-description": "Führt Versionsgeschichten von Seiten zusammen.", + "apihelp-mergehistory-param-reason": "Grund für die Zusammenführung der Versionsgeschichten", + "apihelp-mergehistory-example-merge": "Fügt alle Versionen von <kbd>Oldpage</kbd> der Versionsgeschichte von <kbd>Newpage</kbd> hinzu.", "apihelp-move-description": "Eine Seite verschieben.", "apihelp-move-param-from": "Titel der zu verschiebenden Seite. Kann nicht zusammen mit <var>$1fromid</var> verwendet werden.", "apihelp-move-param-fromid": "Seitenkennung der zu verschiebenden Seite. Kann nicht zusammen mit <var>$1from</var> verwendet werden.", @@ -255,7 +265,7 @@ "apihelp-opensearch-param-search": "Such-Zeichenfolge.", "apihelp-opensearch-param-limit": "Maximale Anzahl zurückzugebender Ergebnisse.", "apihelp-opensearch-param-namespace": "Zu durchsuchende Namensräume.", - "apihelp-opensearch-param-suggest": "Nichts unternehmen, falls <var>[[mw:Manual:$wgEnableOpenSearchSuggest|$wgEnableOpenSearchSuggest]]</var> falsch ist.", + "apihelp-opensearch-param-suggest": "Nichts unternehmen, falls <var>[[mw:Special:MyLanguage/Manual:$wgEnableOpenSearchSuggest|$wgEnableOpenSearchSuggest]]</var> falsch ist.", "apihelp-opensearch-param-redirects": "Wie mit Weiterleitungen umgegangen werden soll:\n;return:Gibt die Weiterleitung selbst zurück.\n;resolve:Gibt die Zielseite zurück. Kann weniger als $1limit Ergebnisse zurückgeben.\nAus Kompatibilitätsgründen ist für $1format=json die Vorgabe \"return\" und \"resolve\" für alle anderen Formate.", "apihelp-opensearch-param-format": "Das Format der Ausgabe.", "apihelp-opensearch-param-warningsaserror": "Wenn Warnungen mit <kbd>format=json</kbd> auftreten, gib einen API-Fehler zurück, anstatt ihn zu ignorieren.", @@ -294,12 +304,14 @@ "apihelp-parse-paramvalue-prop-sections": "Gibt die Abschnitte im geparsten Wikitext zurück.", "apihelp-parse-paramvalue-prop-revid": "Ergänzt die Versionskennung der geparsten Seite.", "apihelp-parse-paramvalue-prop-displaytitle": "Ergänzt den Titel des geparsten Wikitextes.", + "apihelp-parse-paramvalue-prop-headhtml": "Gibt geparsten <code><head></code> der Seite zurück.", "apihelp-parse-paramvalue-prop-jsconfigvars": "Gibt die JavaScript-Konfigurationsvariablen speziell für die Seite aus. Zur Anwendung verwende <code>mw.config.set()</code>.", "apihelp-parse-paramvalue-prop-encodedjsconfigvars": "Gibt die JavaScript-Konfigurationsvariablen speziell für die Seite als JSON-Zeichenfolge aus.", "apihelp-parse-paramvalue-prop-indicators": "Gibt das HTML der Seitenstatusindikatoren zurück, die auf der Seite verwendet werden.", "apihelp-parse-paramvalue-prop-iwlinks": "Gibt Interwiki-Links des geparsten Wikitextes zurück.", "apihelp-parse-paramvalue-prop-wikitext": "Gibt den originalen Wikitext zurück, der geparst wurde.", "apihelp-parse-paramvalue-prop-properties": "Gibt verschiedene Eigenschaften zurück, die im geparsten Wikitext definiert sind.", + "apihelp-parse-paramvalue-prop-parsewarnings": "Gibt die Warnungen aus, die beim Parsen des Inhalts aufgetreten sind.", "apihelp-parse-param-section": "Parst nur den Inhalt dieser Abschnittsnummer.\n\nFalls <kbd>new</kbd>, parst <var>$1text</var> und <var>$1sectiontitle</var>, als ob ein neuer Abschnitt der Seite hinzugefügt wird.\n\n<kbd>new</kbd> ist nur erlaubt mit der Angabe <var>text</var>.", "apihelp-parse-param-sectiontitle": "Überschrift des neuen Abschnittes, wenn <var>section</var> = <kbd>new</kbd> ist.\n\nAnders als beim Bearbeiten der Seite wird der Parameter nicht durch die <var>summary</var> ersetzt, wenn er weggelassen oder leer ist.", "apihelp-parse-param-disablepp": "Benutze <var>$1disablelimitreport</var> stattdessen.", @@ -332,7 +344,7 @@ "apihelp-protect-example-protect": "Schützt eine Seite", "apihelp-protect-example-unprotect": "Entsperrt eine Seite, indem die Einschränkungen durch den Schutz auf <kbd>all</kbd> gestellt werden (z. B. darf jeder die Aktion ausführen).", "apihelp-protect-example-unprotect2": "Eine Seite entsperren, indem keine Einschränkungen übergeben werden", - "apihelp-purge-description": "Setzt den Cache der angegebenen Seiten zurück.\n\nFalls kein Benutzer angemeldet ist, müssen POST-Anfragen genutzt werden.", + "apihelp-purge-description": "Setzt den Cache der angegebenen Seiten zurück.", "apihelp-purge-param-forcelinkupdate": "Aktualisiert die Linktabellen.", "apihelp-purge-param-forcerecursivelinkupdate": "Aktualisiert die Linktabelle der Seite und alle Linktabellen der Seiten, die sie als Vorlage einbinden.", "apihelp-purge-example-simple": "Purgt die <kbd>Main Page</kbd> und die <kbd>API</kbd>-Seite.", @@ -483,6 +495,7 @@ "apihelp-query+allrevisions-param-generatetitles": "Wenn als Generator verwendet, werden eher Titel als Bearbeitungs-IDs erzeugt.", "apihelp-query+allrevisions-example-user": "Liste die letzten 50 Beiträge, sortiert nach Benutzer <kbd>Beispiel</kbd> auf.", "apihelp-query+allrevisions-example-ns-main": "Liste die ersten 50 Bearbeitungen im Hauptnamensraum auf.", + "apihelp-query+mystashedfiles-param-prop": "Welche Eigenschaften für die Dateien abgerufen werden sollen.", "apihelp-query+mystashedfiles-paramvalue-prop-size": "Ruft die Dateigröße und Bildabmessungen ab.", "apihelp-query+mystashedfiles-param-limit": "Wie viele Dateien zurückgegeben werden sollen.", "apihelp-query+alltransclusions-description": "Liste alle Transklusionen auf (eingebettete Seiten die {{x}} benutzen), einschließlich nicht vorhandener.", @@ -677,6 +690,7 @@ "apihelp-query+imageinfo-paramvalue-prop-userid": "Füge die ID des Benutzers zu jeder hochgeladenen Dateiversion hinzu.", "apihelp-query+imageinfo-paramvalue-prop-comment": "Kommentar zu der Version.", "apihelp-query+imageinfo-paramvalue-prop-parsedcomment": "Analysiere den Kommentar zu dieser Version.", + "apihelp-query+imageinfo-paramvalue-prop-canonicaltitle": "Ergänzt den kanonischen Titel für die Datei.", "apihelp-query+imageinfo-paramvalue-prop-url": "Gibt die URL zur Datei- und Beschreibungsseite zurück.", "apihelp-query+imageinfo-paramvalue-prop-size": "Fügt die Größe der Datei in Bytes und (falls zutreffend) in Höhe, Breite und Seitenzahl hinzu.", "apihelp-query+imageinfo-paramvalue-prop-dimensions": "Alias für die Größe.", @@ -740,7 +754,10 @@ "apihelp-query+langlinks-param-limit": "Wie viele Sprachlinks zurückgegeben werden sollen.", "apihelp-query+langlinks-param-prop": "Zusätzlich zurückzugebende Eigenschaften jedes Interlanguage-Links:", "apihelp-query+langlinks-paramvalue-prop-url": "Ergänzt die vollständige URL.", + "apihelp-query+langlinks-paramvalue-prop-autonym": "Ergänzt den Namen der Muttersprache.", "apihelp-query+langlinks-param-dir": "Die Auflistungsrichtung.", + "apihelp-query+links-description": "Gibt alle Links von den angegebenen Seiten zurück.", + "apihelp-query+links-param-namespace": "Zeigt nur Links in diesen Namensräumen.", "apihelp-query+links-param-limit": "Wie viele Links zurückgegeben werden sollen.", "apihelp-query+links-param-dir": "Die Auflistungsrichtung.", "apihelp-query+links-example-simple": "Links von der <kbd>Hauptseite</kbd> abrufen", @@ -748,22 +765,33 @@ "apihelp-query+linkshere-param-prop": "Zurückzugebende Eigenschaften:", "apihelp-query+linkshere-paramvalue-prop-pageid": "Die Seitenkennung jeder Seite.", "apihelp-query+linkshere-paramvalue-prop-title": "Titel jeder Seite.", + "apihelp-query+linkshere-param-limit": "Wie viel zurückgegeben werden soll.", + "apihelp-query+linkshere-example-simple": "Holt eine Liste von Seiten, die auf [[Main Page]] verlinken.", "apihelp-query+logevents-description": "Ereignisse von den Logbüchern abrufen.", "apihelp-query+logevents-param-prop": "Zurückzugebende Eigenschaften:", + "apihelp-query+logevents-paramvalue-prop-ids": "Ergänzt die Kennung des Logbuchereignisses.", + "apihelp-query+logevents-paramvalue-prop-title": "Ergänzt den Titel der Seite für das Logbuchereignis.", "apihelp-query+logevents-paramvalue-prop-type": "Ergänzt den Typ des Logbuchereignisses.", + "apihelp-query+logevents-paramvalue-prop-user": "Ergänzt den verantwortlichen Benutzer für das Logbuchereignis.", "apihelp-query+logevents-paramvalue-prop-comment": "Ergänzt den Kommentar des Logbuchereignisses.", "apihelp-query+logevents-example-simple": "Listet die letzten Logbuch-Ereignisse auf.", "apihelp-query+pageswithprop-paramvalue-prop-ids": "Fügt die Seitenkennung hinzu.", "apihelp-query+pageswithprop-param-limit": "Die maximale Anzahl zurückzugebender Seiten.", + "apihelp-query+pageswithprop-param-dir": "In welche Richtung sortiert werden soll.", "apihelp-query+prefixsearch-param-search": "Such-Zeichenfolge.", + "apihelp-query+prefixsearch-param-namespace": "Welche Namensräume durchsucht werden sollen.", + "apihelp-query+prefixsearch-param-limit": "Maximale Anzahl zurückzugebender Ergebnisse.", "apihelp-query+prefixsearch-param-offset": "Anzahl der zu überspringenden Ergebnisse.", "apihelp-query+prefixsearch-param-profile": "Zu verwendendes Suchprofil.", + "apihelp-query+protectedtitles-param-limit": "Wie viele Seiten insgesamt zurückgegeben werden sollen.", "apihelp-query+protectedtitles-param-prop": "Zurückzugebende Eigenschaften:", "apihelp-query+querypage-param-limit": "Anzahl der zurückzugebenden Ergebnisse.", "apihelp-query+recentchanges-description": "Listet die letzten Änderungen auf.", "apihelp-query+recentchanges-param-user": "Listet nur Änderungen von diesem Benutzer auf.", "apihelp-query+recentchanges-param-excludeuser": "Listet keine Änderungen von diesem Benutzer auf.", "apihelp-query+recentchanges-param-tag": "Listet nur Änderungen auf, die mit dieser Markierung markiert sind.", + "apihelp-query+recentchanges-param-prop": "Bezieht zusätzliche Informationen mit ein:", + "apihelp-query+recentchanges-paramvalue-prop-comment": "Fügt den Kommentar für die Bearbeitung hinzu.", "apihelp-query+recentchanges-paramvalue-prop-flags": "Ergänzt Markierungen für die Bearbeitung.", "apihelp-query+recentchanges-paramvalue-prop-timestamp": "Ergänzt den Zeitstempel für die Bearbeitung.", "apihelp-query+recentchanges-paramvalue-prop-title": "Ergänzt den Seitentitel der Bearbeitung.", @@ -772,6 +800,7 @@ "apihelp-query+redirects-param-prop": "Zurückzugebende Eigenschaften:", "apihelp-query+redirects-paramvalue-prop-pageid": "Seitenkennung einer jeden Weiterleitung.", "apihelp-query+redirects-paramvalue-prop-title": "Titel einer jeden Weiterleitung.", + "apihelp-query+redirects-param-namespace": "Schließt nur Seiten in diesen Namensräumen ein.", "apihelp-query+redirects-param-limit": "Wie viele Weiterleitungen zurückgegeben werden sollen.", "apihelp-query+revisions-param-tag": "Listet nur Versionen auf, die mit dieser Markierung markiert sind.", "apihelp-query+revisions+base-param-prop": "Zurückzugebende Eigenschaften jeder Version:", @@ -793,14 +822,20 @@ "apihelp-query+search-param-limit": "Wie viele Seiten insgesamt zurückgegeben werden sollen.", "apihelp-query+search-example-simple": "Nach <kbd>meaning</kbd> suchen.", "apihelp-query+search-example-text": "Texte nach <kbd>meaning</kbd> durchsuchen.", + "apihelp-query+siteinfo-paramvalue-prop-languagevariants": "Gibt eine Liste für die Sprachcodes zurück, bei denen der [[mw:Special:MyLanguage/LanguageConverter|Sprachkonverter]] aktiviert ist und die unterstützten Varianten für jede Sprache.", "apihelp-query+siteinfo-example-simple": "Websiteinformationen abrufen", + "apihelp-query+stashimageinfo-param-sessionkey": "Alias für $1filekey, für die Rückwärtskompatibilität.", + "apihelp-query+stashimageinfo-example-simple": "Gibt Informationen für eine gespeicherte Datei zurück.", + "apihelp-query+stashimageinfo-example-params": "Gibt Vorschaubilder für zwei gespeicherte Dateien zurück.", "apihelp-query+tags-description": "Änderungs-Tags auflisten.", "apihelp-query+tags-param-prop": "Zurückzugebende Eigenschaften:", "apihelp-query+tags-paramvalue-prop-name": "Ergänzt den Namen der Markierung.", "apihelp-query+tags-paramvalue-prop-displayname": "Ergänzt die Systemnachricht für die Markierung.", "apihelp-query+tags-paramvalue-prop-description": "Ergänzt die Beschreibung der Markierung.", "apihelp-query+tags-example-simple": "Verfügbare Tags auflisten", + "apihelp-query+templates-param-limit": "Wie viele Vorlagen zurückgegeben werden sollen.", "apihelp-query+templates-param-dir": "Die Auflistungsrichtung.", + "apihelp-query+tokens-param-type": "Typen der Token, die abgerufen werden sollen.", "apihelp-query+transcludedin-param-prop": "Zurückzugebende Eigenschaften:", "apihelp-query+transcludedin-paramvalue-prop-pageid": "Seitenkennung jeder Seite.", "apihelp-query+usercontribs-description": "Alle Bearbeitungen von einem Benutzer abrufen.", @@ -857,7 +892,7 @@ "apihelp-rsd-example-simple": "Das RSD-Schema exportieren", "apihelp-setnotificationtimestamp-param-entirewatchlist": "An allen beobachteten Seiten arbeiten.", "apihelp-setpagelanguage-description": "Ändert die Sprache einer Seite.", - "apihelp-setpagelanguage-description-disabled": "Das Ändern der Sprache von Seiten ist auf diesem Wiki nicht erlaubt.\n\nAktiviere <var>[[mw:Manual:$wgPageLanguageUseDB|$wgPageLanguageUseDB]]</var>, um diese Aktion zu verwenden.", + "apihelp-setpagelanguage-description-disabled": "Das Ändern der Sprache von Seiten ist auf diesem Wiki nicht erlaubt.\n\nAktiviere <var>[[mw:Special:MyLanguage/Manual:$wgPageLanguageUseDB|$wgPageLanguageUseDB]]</var>, um diese Aktion zu verwenden.", "apihelp-setpagelanguage-param-title": "Titel der Seite, deren Sprache du ändern möchtest. Kann nicht zusammen mit <var>$1pageid</var> verwendet werden.", "apihelp-setpagelanguage-param-pageid": "Kennung der Seite, deren Sprache du ändern möchtest. Kann nicht zusammen mit <var>$1title</var> verwendet werden.", "apihelp-setpagelanguage-param-lang": "Code der Sprache, auf den die Seite geändert werden soll. Verwende <kbd>default</kbd>, um die Seite auf die Standardinhaltssprache des Wikis zurückzusetzen.", @@ -898,7 +933,7 @@ "apihelp-userrights-description": "Ändert die Gruppenzugehörigkeit eines Benutzers.", "apihelp-userrights-param-user": "Benutzername.", "apihelp-userrights-param-userid": "Benutzerkennung.", - "apihelp-userrights-param-add": "Fügt den Benutzer zu diesen Gruppen hinzu.", + "apihelp-userrights-param-add": "Fügt den Benutzer zu diesen Gruppen hinzu oder falls er bereits Mitglied ist, aktualisiert den Ablauf seiner Mitgliedschaft in dieser Gruppe.", "apihelp-userrights-param-remove": "Entfernt den Benutzer von diesen Gruppen.", "apihelp-userrights-param-reason": "Grund für die Änderung.", "apihelp-userrights-param-tags": "Auf den Eintrag im Benutzerrechte-Logbuch anzuwendende Änderungsmarkierungen.", @@ -925,7 +960,7 @@ "apihelp-xml-param-includexmlnamespace": "Falls angegeben, ergänzt einen XML-Namensraum.", "apihelp-xmlfm-description": "Daten im XML-Format ausgeben (schöngedruckt in HTML).", "api-format-title": "MediaWiki-API-Ergebnis", - "api-format-prettyprint-header": "Dies ist die HTML-Repräsentation des $1-Formats. HTML ist zur Fehlerbehebung gut, aber unpassend für den Anwendungsgebrauch.\n\nGib den Parameter <var>format</var> an, um das Ausgabeformat zu ändern. Um die Nicht-HTML-Repräsentation des $1-Formats anzusehen, lege <kbd>format=$2</kbd> fest.\n\nSiehe die [[mw:API|vollständige Dokumentation]] oder die [[Special:ApiHelp/main|API-Hilfe]] für weitere Informationen.", + "api-format-prettyprint-header": "Dies ist die Darstellung des $1-Formats in HTML. HTML ist gut zur Fehlerbehebung geeignet, aber unpassend für die Nutzung durch Anwendungen.\n\nGib den Parameter <var>format</var> an, um das Ausgabeformat zu ändern. Lege <kbd>format=$2</kbd> fest, um die von HTML abweichende Darstellung des $1-Formats zu erhalten.\n\nSiehe auch die [[mw:Special:MyLanguage/API|vollständige Dokumentation der API]] oder die [[Special:ApiHelp/main|API-Hilfe]] für weitere Informationen.", "api-format-prettyprint-status": "Diese Antwort wird mit dem HTTP-Status $1 $2 zurückgegeben.", "api-pageset-param-titles": "Eine Liste der Titel, an denen gearbeitet werden soll.", "api-pageset-param-pageids": "Eine Liste der Seitenkennungen, an denen gearbeitet werden soll.", @@ -968,7 +1003,7 @@ "api-help-param-default-empty": "Standard: <span class=\"apihelp-empty\">(leer)</span>", "api-help-param-token": "Ein „$1“-Token abgerufen von [[Special:ApiHelp/query+tokens|action=query&meta=tokens]]", "api-help-param-token-webui": "Aus Kompatibilitätsgründen wird der in der Weboberfläche verwendete Token ebenfalls akzeptiert.", - "api-help-param-disabled-in-miser-mode": "Deaktiviert aufgrund des [[mw:Manual:$wgMiserMode|Miser-Modus]].", + "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": "<span class=\"apihelp-empty\">(keine Beschreibung)</span>", "api-help-examples": "{{PLURAL:$1|Beispiel|Beispiele}}:", @@ -977,20 +1012,40 @@ "api-help-right-apihighlimits": "Höhere Beschränkungen in API-Anfragen verwenden (langsame Anfragen: $1; schnelle Anfragen: $2). Die Beschränkungen für langsame Anfragen werden auch auf Mehrwertparameter angewandt.", "api-help-open-in-apisandbox": "<small>[in Spielwiese öffnen]</small>", "api-help-authmanagerhelper-messageformat": "Zu verwendendes Format zur Rückgabe von Nachrichten.", + "apierror-badgenerator-unknown": "<kbd>generator=$1</kbd> unbekannt.", + "apierror-badip": "Der IP-Parameter ist nicht gültig.", + "apierror-badmd5": "Die angegebene MD5-Prüfsumme war falsch.", + "apierror-badmodule-badsubmodule": "Das Modul <kbd>$1</kbd> hat kein Untermodul namens „$2“.", + "apierror-badmodule-nosubmodules": "Das Modul <kbd>$1</kbd> hat keine Untermodule.", + "apierror-badparameter": "Ungültiger Wert für den Parameter <var>$1</var>.", + "apierror-badquery": "Ungültige Abfrage.", + "apierror-cannot-async-upload-file": "Die Parameter <var>async</var> und <var>file</var> können nicht kombiniert werden. Falls du eine asynchrone Verarbeitung deiner hochgeladenen Datei wünschst, lade sie zuerst mithilfe des Parameters <var>stash</var> auf den Speicher hoch. Veröffentliche anschließend die gespeicherte Datei asynchron mithilfe <var>filekey</var> und <var>async</var>.", + "apierror-invalidsection": "Der Parameter <var>section</var> muss eine gültige Abschnittskennung oder <kbd>new</kbd> sein.", "apierror-invaliduserid": "Die Benutzerkennung <var>$1</var> ist nicht gültig.", "apierror-nosuchuserid": "Es gibt keinen Benutzer mit der Kennung $1.", "apierror-pagelang-disabled": "Das Ändern der Sprache von Seiten ist auf diesem Wiki nicht erlaubt.", + "apierror-protect-invalidaction": "Ungültiger Schutztyp „$1“.", + "apierror-readonly": "Das Wiki ist derzeit im schreibgeschützten Modus.", + "apierror-revwrongpage": "Die Version $1 ist keine Version von $2.", + "apierror-sectionreplacefailed": "Der aktualisierte Abschnitt konnte nicht zusammengeführt werden.", "apierror-stashinvalidfile": "Ungültige gespeicherte Datei.", "apierror-stashnosuchfilekey": "Kein derartiger Dateischlüssel: $1.", "apierror-stashwrongowner": "Falscher Besitzer: $1", "apierror-systemblocked": "Du wurdest von MediaWiki automatisch gesperrt.", "apierror-unknownerror-nocode": "Unbekannter Fehler.", "apierror-unknownerror": "Unbekannter Fehler: „$1“.", + "apierror-unknownformat": "Nicht erkanntes Format „$1“.", "apiwarn-invalidcategory": "„$1“ ist keine Kategorie.", "apiwarn-invalidtitle": "„$1“ ist kein gültiger Titel.", "apiwarn-notfile": "„$1“ ist keine Datei.", + "apiwarn-toomanyvalues": "Es wurden zu viele Werte für den Parameter <var>$1</var> angegeben. Die Obergrenze liegt bei $2.", + "apiwarn-validationfailed-badpref": "Keine gültige Einstellung.", + "apiwarn-validationfailed-cannotset": "Kann nicht von diesem Modul festgelegt werden.", + "apiwarn-validationfailed-keytoolong": "Der Schlüssel ist zu lang. Es sind nicht mehr als $1 Bytes erlaubt.", + "apiwarn-validationfailed": "Validierungsfehler für <kbd>$1</kbd>: $2", "api-feed-error-title": "Fehler ($1)", "api-usage-docref": "Siehe $1 zur Verwendung der API.", + "api-usage-mailinglist-ref": "Abonniere die Mailingliste „mediawiki-api-announce“ auf <https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce> zum Feststellen von API-Veralterungen und „Breaking Changes“.", "api-credits-header": "Danksagungen", "api-credits": "API-Entwickler:\n* Roan Kattouw (Hauptentwickler von September 2007 bis 2009)\n* Victor Vasiliev\n* Bryan Tong Minh\n* Sam Reed\n* Yuri Astrakhan (Autor, Hauptentwickler von September 2006 bis September 2007)\n* Brad Jorsch (Hauptentwickler seit 2013)\n\nBitte sende deine Kommentare, Vorschläge und Fragen an mediawiki-api@lists.wikimedia.org\noder reiche einen Fehlerbericht auf https://phabricator.wikimedia.org/ ein." } diff --git a/includes/api/i18n/en.json b/includes/api/i18n/en.json index c1fefd6c3f79..7a04cafafbda 100644 --- a/includes/api/i18n/en.json +++ b/includes/api/i18n/en.json @@ -6,10 +6,10 @@ ] }, - "apihelp-main-description": "<div class=\"hlist plainlinks api-main-links\">\n* [[mw:API:Main_page|Documentation]]\n* [[mw: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</div>\n<strong>Status:</strong> All features shown on this page should be working, but the API is still in active development, and may change at any time. Subscribe to [https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ the mediawiki-api-announce mailing list] for notice of updates.\n\n<strong>Erroneous requests:</strong> When erroneous requests are sent to the API, an HTTP header will be sent with the key \"MediaWiki-API-Error\" and then both the value of the header and the error code sent back will be set to the same value. For more information see [[mw:API:Errors_and_warnings|API: Errors and warnings]].\n\n<strong>Testing:</strong> For ease of testing API requests, see [[Special:ApiSandbox]].", + "apihelp-main-description": "<div class=\"hlist plainlinks api-main-links\">\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</div>\n<strong>Status:</strong> All features shown on this page should be working, but the API is still in active development, and may change at any time. Subscribe to [https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ the mediawiki-api-announce mailing list] for notice of updates.\n\n<strong>Erroneous requests:</strong> When erroneous requests are sent to the API, an HTTP header will be sent with the key \"MediaWiki-API-Error\" and then both the value of the header and the error code sent back will be set to the same value. For more information see [[mw:Special:MyLanguage/API:Errors_and_warnings|API: Errors and warnings]].\n\n<strong>Testing:</strong> For ease of testing API requests, see [[Special:ApiSandbox]].", "apihelp-main-param-action": "Which action to perform.", "apihelp-main-param-format": "The format of the output.", - "apihelp-main-param-maxlag": "Maximum lag can be used when MediaWiki is installed on a database replicated cluster. To save actions causing any more site replication lag, this parameter can make the client wait until the replication lag is less than the specified value. In case of excessive lag, error code <samp>maxlag</samp> is returned with a message like <samp>Waiting for $host: $lag seconds lagged</samp>.<br />See [[mw:Manual:Maxlag_parameter|Manual: Maxlag parameter]] for more information.", + "apihelp-main-param-maxlag": "Maximum lag can be used when MediaWiki is installed on a database replicated cluster. To save actions causing any more site replication lag, this parameter can make the client wait until the replication lag is less than the specified value. In case of excessive lag, error code <samp>maxlag</samp> is returned with a message like <samp>Waiting for $host: $lag seconds lagged</samp>.<br />See [[mw:Special:MyLanguage/Manual:Maxlag_parameter|Manual: Maxlag parameter]] for more information.", "apihelp-main-param-smaxage": "Set the <code>s-maxage</code> HTTP cache control header to this many seconds. Errors are never cached.", "apihelp-main-param-maxage": "Set the <code>max-age</code> HTTP cache control header to this many seconds. Errors are never cached.", "apihelp-main-param-assert": "Verify the user is logged in if set to <kbd>user</kbd>, or has the bot user right if <kbd>bot</kbd>.", @@ -34,7 +34,7 @@ "apihelp-block-param-autoblock": "Automatically block the last used IP address, and any subsequent IP addresses they try to login from.", "apihelp-block-param-noemail": "Prevent user from sending email through the wiki. (Requires the <code>blockemail</code> right).", "apihelp-block-param-hidename": "Hide the username from the block log. (Requires the <code>hideuser</code> right).", - "apihelp-block-param-allowusertalk": "Allow the user to edit their own talk page (depends on <var>[[mw:Manual:$wgBlockAllowsUTEdit|$wgBlockAllowsUTEdit]]</var>).", + "apihelp-block-param-allowusertalk": "Allow the user to edit their own talk page (depends on <var>[[mw:Special:MyLanguage/Manual:$wgBlockAllowsUTEdit|$wgBlockAllowsUTEdit]]</var>).", "apihelp-block-param-reblock": "If the user is already blocked, overwrite the existing block.", "apihelp-block-param-watchuser": "Watch the user's or IP address's user and talk pages.", "apihelp-block-param-tags": "Change tags to apply to the entry in the block log.", @@ -290,7 +290,7 @@ "apihelp-opensearch-param-search": "Search string.", "apihelp-opensearch-param-limit": "Maximum number of results to return.", "apihelp-opensearch-param-namespace": "Namespaces to search.", - "apihelp-opensearch-param-suggest": "Do nothing if <var>[[mw:Manual:$wgEnableOpenSearchSuggest|$wgEnableOpenSearchSuggest]]</var> is false.", + "apihelp-opensearch-param-suggest": "Do nothing if <var>[[mw:Special:MyLanguage/Manual:$wgEnableOpenSearchSuggest|$wgEnableOpenSearchSuggest]]</var> is false.", "apihelp-opensearch-param-redirects": "How to handle redirects:\n;return:Return the redirect itself.\n;resolve:Return the target page. May return fewer than $1limit results.\nFor historical reasons, the default is \"return\" for $1format=json and \"resolve\" for other formats.", "apihelp-opensearch-param-format": "The format of the output.", "apihelp-opensearch-param-warningsaserror": "If warnings are raised with <kbd>format=json</kbd>, return an API error instead of ignoring them.", @@ -348,6 +348,7 @@ "apihelp-parse-paramvalue-prop-limitreportdata": "Gives the limit report in a structured way. Gives no data, when <var>$1disablelimitreport</var> is set.", "apihelp-parse-paramvalue-prop-limitreporthtml": "Gives the HTML version of the limit report. Gives no data, when <var>$1disablelimitreport</var> is set.", "apihelp-parse-paramvalue-prop-parsetree": "The XML parse tree of revision content (requires content model <code>$1</code>)", + "apihelp-parse-paramvalue-prop-parsewarnings": "Gives the warnings that occurred while parsing content.", "apihelp-parse-param-pst": "Do a pre-save transform on the input before parsing it. Only valid when used with text.", "apihelp-parse-param-onlypst": "Do a pre-save transform (PST) on the input, but don't parse it. Returns the same wikitext, after a PST has been applied. Only valid when used with <var>$1text</var>.", "apihelp-parse-param-effectivelanglinks": "Includes language links supplied by extensions (for use with <kbd>$1prop=langlinks</kbd>).", @@ -389,7 +390,7 @@ "apihelp-protect-example-unprotect": "Unprotect a page by setting restrictions to <kbd>all</kbd> (i.e. everyone is allowed to take the action).", "apihelp-protect-example-unprotect2": "Unprotect a page by setting no restrictions.", - "apihelp-purge-description": "Purge the cache for the given titles.\n\nRequires a POST request if the user is not logged in.", + "apihelp-purge-description": "Purge the cache for the given titles.", "apihelp-purge-param-forcelinkupdate": "Update the links tables.", "apihelp-purge-param-forcerecursivelinkupdate": "Update the links table, and update the links tables for any page that uses this page as a template.", "apihelp-purge-example-simple": "Purge the <kbd>Main Page</kbd> and the <kbd>API</kbd> page.", @@ -433,7 +434,7 @@ "apihelp-query+alldeletedrevisions-param-user": "Only list revisions by this user.", "apihelp-query+alldeletedrevisions-param-excludeuser": "Don't list revisions by this user.", "apihelp-query+alldeletedrevisions-param-namespace": "Only list pages in this namespace.", - "apihelp-query+alldeletedrevisions-param-miser-user-namespace": "<strong>Note:</strong> Due to [[mw:Manual:$wgMiserMode|miser mode]], using <var>$1user</var> and <var>$1namespace</var> together may result in fewer than <var>$1limit</var> results returned before continuing; in extreme cases, zero results may be returned.", + "apihelp-query+alldeletedrevisions-param-miser-user-namespace": "<strong>Note:</strong> Due to [[mw:Special:MyLanguage/Manual:$wgMiserMode|miser mode]], using <var>$1user</var> and <var>$1namespace</var> together may result in fewer than <var>$1limit</var> results returned before continuing; in extreme cases, zero results may be returned.", "apihelp-query+alldeletedrevisions-param-generatetitles": "When being used as a generator, generate titles rather than revision IDs.", "apihelp-query+alldeletedrevisions-example-user": "List the last 50 deleted contributions by user <kbd>Example</kbd>.", "apihelp-query+alldeletedrevisions-example-ns-main": "List the first 50 deleted revisions in the main namespace.", @@ -778,7 +779,7 @@ "apihelp-query+filearchive-example-simple": "Show a list of all deleted files.", "apihelp-query+filerepoinfo-description": "Return meta information about image repositories configured on the wiki.", - "apihelp-query+filerepoinfo-param-prop": "Which repository properties to get (there may be more available on some wikis):\n;apiurl:URL to the repository API - helpful for getting image info from the host.\n;name:The key of the repository - used in e.g. <var>[[mw:Manual:$wgForeignFileRepos|$wgForeignFileRepos]]</var> and [[Special:ApiHelp/query+imageinfo|imageinfo]] return values.\n;displayname:The human-readable name of the repository wiki.\n;rooturl:Root URL for image paths.\n;local:Whether that repository is the local one or not.", + "apihelp-query+filerepoinfo-param-prop": "Which repository properties to get (there may be more available on some wikis):\n;apiurl:URL to the repository API - helpful for getting image info from the host.\n;name:The key of the repository - used in e.g. <var>[[mw:Special:MyLanguage/Manual:$wgForeignFileRepos|$wgForeignFileRepos]]</var> and [[Special:ApiHelp/query+imageinfo|imageinfo]] return values.\n;displayname:The human-readable name of the repository wiki.\n;rooturl:Root URL for image paths.\n;local:Whether that repository is the local one or not.", "apihelp-query+filerepoinfo-example-simple": "Get information about file repositories.", "apihelp-query+fileusage-description": "Find all pages that use the given files.", @@ -1139,10 +1140,11 @@ "apihelp-query+siteinfo-paramvalue-prop-rightsinfo": "Returns wiki rights (license) information if available.", "apihelp-query+siteinfo-paramvalue-prop-restrictions": "Returns information on available restriction (protection) types.", "apihelp-query+siteinfo-paramvalue-prop-languages": "Returns a list of languages MediaWiki supports (optionally localised by using <var>$1inlanguagecode</var>).", + "apihelp-query+siteinfo-paramvalue-prop-languagevariants": "Returns a list of language codes for which [[mw:Special:MyLanguage/LanguageConverter|LanguageConverter]] is enabled, and the variants supported for each.", "apihelp-query+siteinfo-paramvalue-prop-skins": "Returns a list of all enabled skins (optionally localised by using <var>$1inlanguagecode</var>, otherwise in the content language).", "apihelp-query+siteinfo-paramvalue-prop-extensiontags": "Returns a list of parser extension tags.", "apihelp-query+siteinfo-paramvalue-prop-functionhooks": "Returns a list of parser function hooks.", - "apihelp-query+siteinfo-paramvalue-prop-showhooks": "Returns a list of all subscribed hooks (contents of <var>[[mw:Manual:$wgHooks|$wgHooks]]</var>).", + "apihelp-query+siteinfo-paramvalue-prop-showhooks": "Returns a list of all subscribed hooks (contents of <var>[[mw:Special:MyLanguage/Manual:$wgHooks|$wgHooks]]</var>).", "apihelp-query+siteinfo-paramvalue-prop-variables": "Returns a list of variable IDs.", "apihelp-query+siteinfo-paramvalue-prop-protocols": "Returns a list of protocols that are allowed in external links.", "apihelp-query+siteinfo-paramvalue-prop-defaultoptions": "Returns the default values for user preferences.", @@ -1217,7 +1219,7 @@ "apihelp-query+usercontribs-paramvalue-prop-flags": "Adds flags of the edit.", "apihelp-query+usercontribs-paramvalue-prop-patrolled": "Tags patrolled edits.", "apihelp-query+usercontribs-paramvalue-prop-tags": "Lists tags for the edit.", - "apihelp-query+usercontribs-param-show": "Show only items that meet these criteria, e.g. non minor edits only: <kbd>$2show=!minor</kbd>.\n\nIf <kbd>$2show=patrolled</kbd> or <kbd>$2show=!patrolled</kbd> is set, revisions older than <var>[[mw:Manual:$wgRCMaxAge|$wgRCMaxAge]]</var> ($1 {{PLURAL:$1|second|seconds}}) won't be shown.", + "apihelp-query+usercontribs-param-show": "Show only items that meet these criteria, e.g. non minor edits only: <kbd>$2show=!minor</kbd>.\n\nIf <kbd>$2show=patrolled</kbd> or <kbd>$2show=!patrolled</kbd> is set, revisions older than <var>[[mw:Special:MyLanguage/Manual:$wgRCMaxAge|$wgRCMaxAge]]</var> ($1 {{PLURAL:$1|second|seconds}}) won't be shown.", "apihelp-query+usercontribs-param-tag": "Only list revisions tagged with this tag.", "apihelp-query+usercontribs-param-toponly": "Only list changes which are the latest revision.", "apihelp-query+usercontribs-example-user": "Show contributions of user <kbd>Example</kbd>.", @@ -1228,6 +1230,7 @@ "apihelp-query+userinfo-paramvalue-prop-blockinfo": "Tags if the current user is blocked, by whom, and for what reason.", "apihelp-query+userinfo-paramvalue-prop-hasmsg": "Adds a tag <samp>messages</samp> if the current user has pending messages.", "apihelp-query+userinfo-paramvalue-prop-groups": "Lists all the groups the current user belongs to.", + "apihelp-query+userinfo-paramvalue-prop-groupmemberships": "Lists groups that the current user has been explicitly assigned to, including the expiry date of each group membership.", "apihelp-query+userinfo-paramvalue-prop-implicitgroups": "Lists all the groups the current user is automatically a member of.", "apihelp-query+userinfo-paramvalue-prop-rights": "Lists all the rights the current user has.", "apihelp-query+userinfo-paramvalue-prop-changeablegroups": "Lists the groups the current user can add to and remove from.", @@ -1249,6 +1252,7 @@ "apihelp-query+users-param-prop": "Which pieces of information to include:", "apihelp-query+users-paramvalue-prop-blockinfo": "Tags if the user is blocked, by whom, and for what reason.", "apihelp-query+users-paramvalue-prop-groups": "Lists all the groups each user belongs to.", + "apihelp-query+users-paramvalue-prop-groupmemberships": "Lists groups that each user has been explicitly assigned to, including the expiry date of each group membership.", "apihelp-query+users-paramvalue-prop-implicitgroups": "Lists all the groups a user is automatically a member of.", "apihelp-query+users-paramvalue-prop-rights": "Lists all the rights each user has.", "apihelp-query+users-paramvalue-prop-editcount": "Adds the user's edit count.", @@ -1318,7 +1322,7 @@ "apihelp-removeauthenticationdata-example-simple": "Attempt to remove the current user's data for <kbd>FooAuthenticationRequest</kbd>.", "apihelp-resetpassword-description": "Send a password reset email to a user.", - "apihelp-resetpassword-description-noroutes": "No password reset routes are available.\n\nEnable routes in <var>[[mw:Manual:$wgPasswordResetRoutes|$wgPasswordResetRoutes]]</var> to use this module.", + "apihelp-resetpassword-description-noroutes": "No password reset routes are available.\n\nEnable routes in <var>[[mw:Special:MyLanguage/Manual:$wgPasswordResetRoutes|$wgPasswordResetRoutes]]</var> to use this module.", "apihelp-resetpassword-param-user": "User being reset.", "apihelp-resetpassword-param-email": "Email address of the user being reset.", "apihelp-resetpassword-example-user": "Send a password reset email to user <kbd>Example</kbd>.", @@ -1361,7 +1365,7 @@ "apihelp-setnotificationtimestamp-example-allpages": "Reset the notification status for pages in the <kbd>{{ns:user}}</kbd> namespace.", "apihelp-setpagelanguage-description": "Change the language of a page.", - "apihelp-setpagelanguage-description-disabled": "Changing the language of a page is not allowed on this wiki.\n\nEnable <var>[[mw:Manual:$wgPageLanguageUseDB|$wgPageLanguageUseDB]]</var> to use this action.", + "apihelp-setpagelanguage-description-disabled": "Changing the language of a page is not allowed on this wiki.\n\nEnable <var>[[mw:Special:MyLanguage/Manual:$wgPageLanguageUseDB|$wgPageLanguageUseDB]]</var> to use this action.", "apihelp-setpagelanguage-param-title": "Title of the page whose language you wish to change. Cannot be used together with <var>$1pageid</var>.", "apihelp-setpagelanguage-param-pageid": "Page ID of the page whose language you wish to change. Cannot be used together with <var>$1title</var>.", "apihelp-setpagelanguage-param-lang": "Language code of the language to change the page to. Use <kbd>default</kbd> to reset the page to the wiki's default content language.", @@ -1406,7 +1410,7 @@ "apihelp-unblock-example-id": "Unblock block ID #<kbd>105</kbd>.", "apihelp-unblock-example-user": "Unblock user <kbd>Bob</kbd> with reason <kbd>Sorry Bob</kbd>.", - "apihelp-undelete-description": "Restore revisions of a deleted page.\n\nA list of deleted revisions (including timestamps) can be retrieved through [[Special:ApiHelp/query+deletedrevs|list=deletedrevs]], and a list of deleted file IDs can be retrieved through [[Special:ApiHelp/query+filearchive|list=filearchive]].", + "apihelp-undelete-description": "Restore revisions of a deleted page.\n\nA list of deleted revisions (including timestamps) can be retrieved through [[Special:ApiHelp/query+deletedrevisions|prop=deletedrevisions]], and a list of deleted file IDs can be retrieved through [[Special:ApiHelp/query+filearchive|list=filearchive]].", "apihelp-undelete-param-title": "Title of the page to restore.", "apihelp-undelete-param-reason": "Reason for restoring.", "apihelp-undelete-param-tags": "Change tags to apply to the entry in the deletion log.", @@ -1443,12 +1447,14 @@ "apihelp-userrights-description": "Change a user's group membership.", "apihelp-userrights-param-user": "User name.", "apihelp-userrights-param-userid": "User ID.", - "apihelp-userrights-param-add": "Add the user to these groups.", + "apihelp-userrights-param-add": "Add the user to these groups, or if they are already a member, update the expiry of their membership in that group.", + "apihelp-userrights-param-expiry": "Expiry timestamps. May be relative (e.g. <kbd>5 months</kbd> or <kbd>2 weeks</kbd>) or absolute (e.g. <kbd>2014-09-18T12:34:56Z</kbd>). If only one timestamp is set, it will be used for all groups passed to the <var>$1add</var> parameter. Use <kbd>infinite</kbd>, <kbd>indefinite</kbd>, <kbd>infinity</kbd>, or <kbd>never</kbd> for a never-expiring user group.", "apihelp-userrights-param-remove": "Remove the user from these groups.", "apihelp-userrights-param-reason": "Reason for the change.", "apihelp-userrights-param-tags": "Change tags to apply to the entry in the user rights log.", "apihelp-userrights-example-user": "Add user <kbd>FooBot</kbd> to group <kbd>bot</kbd>, and remove from groups <kbd>sysop</kbd> and <kbd>bureaucrat</kbd>.", "apihelp-userrights-example-userid": "Add the user with ID <kbd>123</kbd> to group <kbd>bot</kbd>, and remove from groups <kbd>sysop</kbd> and <kbd>bureaucrat</kbd>.", + "apihelp-userrights-example-expiry": "Add user <kbd>SometimeSysop</kbd> to group <kbd>sysop</kbd> for 1 month.", "apihelp-validatepassword-description": "Validate a password against the wiki's password policies.\n\nValidity is reported as <samp>Good</samp> if the password is acceptable, <samp>Change</samp> if the password may be used for login but must be changed, or <samp>Invalid</samp> if the password is not usable.", "apihelp-validatepassword-param-password": "Password to validate.", @@ -1484,8 +1490,8 @@ "apihelp-xmlfm-description": "Output data in XML format (pretty-print in HTML).", "api-format-title": "MediaWiki API result", - "api-format-prettyprint-header": "This is the HTML representation of the $1 format. HTML is good for debugging, but is unsuitable for application use.\n\nSpecify the <var>format</var> parameter to change the output format. To see the non-HTML representation of the $1 format, set <kbd>format=$2</kbd>.\n\nSee the [[mw:API|complete documentation]], or the [[Special:ApiHelp/main|API help]] for more information.", - "api-format-prettyprint-header-only-html": "This is an HTML representation intended for debugging, and is unsuitable for application use.\n\nSee the [[mw:API|complete documentation]], or the [[Special:ApiHelp/main|API help]] for more information.", + "api-format-prettyprint-header": "This is the HTML representation of the $1 format. HTML is good for debugging, but is unsuitable for application use.\n\nSpecify the <var>format</var> parameter to change the output format. To see the non-HTML representation of the $1 format, set <kbd>format=$2</kbd>.\n\nSee the [[mw:Special:MyLanguage/API|complete documentation]], or the [[Special:ApiHelp/main|API help]] for more information.", + "api-format-prettyprint-header-only-html": "This is an HTML representation intended for debugging, and is unsuitable for application use.\n\nSee the [[mw:Special:MyLanguage/API|complete documentation]], or the [[Special:ApiHelp/main|API help]] for more information.", "api-format-prettyprint-status": "This response would be returned with HTTP status $1 $2.", "api-pageset-param-titles": "A list of titles to work on.", @@ -1541,8 +1547,8 @@ "api-help-param-default-empty": "Default: <span class=\"apihelp-empty\">(empty)</span>", "api-help-param-token": "A \"$1\" token retrieved from [[Special:ApiHelp/query+tokens|action=query&meta=tokens]]", "api-help-param-token-webui": "For compatibility, the token used in the web UI is also accepted.", - "api-help-param-disabled-in-miser-mode": "Disabled due to [[mw:Manual:$wgMiserMode|miser mode]].", - "api-help-param-limited-in-miser-mode": "<strong>Note:</strong> Due to [[mw:Manual:$wgMiserMode|miser mode]], using this may result in fewer than <var>$1limit</var> results returned before continuing; in extreme cases, zero results may be returned.", + "api-help-param-disabled-in-miser-mode": "Disabled due to [[mw:Special:MyLanguage/Manual:$wgMiserMode|miser mode]].", + "api-help-param-limited-in-miser-mode": "<strong>Note:</strong> Due to [[mw:Special:MyLanguage/Manual:$wgMiserMode|miser mode]], using this may result in fewer than <var>$1limit</var> results returned before continuing; in extreme cases, zero results may be returned.", "api-help-param-direction": "In which direction to enumerate:\n;newer:List oldest first. Note: $1start has to be before $1end.\n;older:List newest first (default). Note: $1start has to be later than $1end.", "api-help-param-continue": "When more results are available, use this to continue.", "api-help-param-no-description": "<span class=\"apihelp-empty\">(no description)</span>", @@ -1594,6 +1600,7 @@ "apierror-blockedfrommail": "You have been blocked from sending email.", "apierror-blocked": "You have been blocked from editing.", "apierror-botsnotsupported": "This interface is not supported for bots.", + "apierror-cannot-async-upload-file": "The parameters <var>async</var> and <var>file</var> cannot be combined. If you want asynchronous processing of your uploaded file, first upload it to stash (using the <var>stash</var> parameter) and then publish the stashed file asynchronously (using <var>filekey</var> and <var>async</var>).", "apierror-cannotreauthenticate": "This action is not available as your identity cannot be verified.", "apierror-cannotviewtitle": "You are not allowed to view $1.", "apierror-cantblock-email": "You don't have permission to block users from sending email through the wiki.", @@ -1636,12 +1643,12 @@ "apierror-invalidexpiry": "Invalid expiry time \"$1\".", "apierror-invalid-file-key": "Not a valid file key.", "apierror-invalidlang": "Invalid language code for parameter <var>$1</var>.", - "apierror-invalidoldimage": "The oldimage parameter has invalid format.", + "apierror-invalidoldimage": "The <var>oldimage</var> parameter has an invalid format.", "apierror-invalidparammix-cannotusewith": "The <kbd>$1</kbd> parameter cannot be used with <kbd>$2</kbd>.", "apierror-invalidparammix-mustusewith": "The <kbd>$1</kbd> parameter may only be used with <kbd>$2</kbd>.", "apierror-invalidparammix-parse-new-section": "<kbd>section=new</kbd> cannot be combined with the <var>oldid</var>, <var>pageid</var> or <var>page</var> parameters. Please use <var>title</var> and <var>text</var>.", "apierror-invalidparammix": "The {{PLURAL:$2|parameters}} $1 can not be used together.", - "apierror-invalidsection": "The section parameter must be a valid section ID or <kbd>new</kbd>.", + "apierror-invalidsection": "The <var>section</var> parameter must be a valid section ID or <kbd>new</kbd>.", "apierror-invalidsha1base36hash": "The SHA1Base36 hash provided is not valid.", "apierror-invalidsha1hash": "The SHA1 hash provided is not valid.", "apierror-invalidtitle": "Bad title \"$1\".", @@ -1784,9 +1791,9 @@ "apiwarn-redirectsandrevids": "Redirect resolution cannot be used together with the <var>revids</var> parameter. Any redirects the <var>revids</var> point to have not been resolved.", "apiwarn-tokennotallowed": "Action \"$1\" is not allowed for the current user.", "apiwarn-tokens-origin": "Tokens may not be obtained when the same-origin policy is not applied.", - "apiwarn-toomanyvalues": "Too many values supplied for parameter <var>$1</var>: the limit is $2.", + "apiwarn-toomanyvalues": "Too many values supplied for parameter <var>$1</var>. The limit is $2.", "apiwarn-truncatedresult": "This result was truncated because it would otherwise be larger than the limit of $1 bytes.", - "apiwarn-unclearnowtimestamp": "Passing \"$2\" for timestamp parameter <var>$1</var> has been deprecated. If for some reason you need to explicitly specify the current time without calculating it client-side, use <kbd>now<kbd>.", + "apiwarn-unclearnowtimestamp": "Passing \"$2\" for timestamp parameter <var>$1</var> has been deprecated. If for some reason you need to explicitly specify the current time without calculating it client-side, use <kbd>now</kbd>.", "apiwarn-unrecognizedvalues": "Unrecognized {{PLURAL:$3|value|values}} for parameter <var>$1</var>: $2.", "apiwarn-unsupportedarray": "Parameter <var>$1</var> uses unsupported PHP array syntax.", "apiwarn-urlparamwidth": "Ignoring width value set in <var>$1urlparam</var> ($2) in favor of width value derived from <var>$1urlwidth</var>/<var>$1urlheight</var> ($3).", @@ -1799,6 +1806,7 @@ "api-feed-error-title": "Error ($1)", "api-usage-docref": "See $1 for API usage.", + "api-usage-mailinglist-ref": "Subscribe to the mediawiki-api-announce mailing list at <https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce> for notice of API deprecations and breaking changes.", "api-exception-trace": "$1 at $2($3)\n$4", "api-credits-header": "Credits", "api-credits": "API developers:\n* Yuri Astrakhan (creator, lead developer Sep 2006–Sep 2007)\n* Roan Kattouw (lead developer Sep 2007–2009)\n* Victor Vasiliev\n* Bryan Tong Minh\n* Sam Reed\n* Brad Jorsch (lead developer 2013–present)\n\nPlease send your comments, suggestions and questions to mediawiki-api@lists.wikimedia.org\nor file a bug report at https://phabricator.wikimedia.org/." diff --git a/includes/api/i18n/es.json b/includes/api/i18n/es.json index eb4206ceb775..a2fbb48f1b06 100644 --- a/includes/api/i18n/es.json +++ b/includes/api/i18n/es.json @@ -312,6 +312,7 @@ "apihelp-paraminfo-param-formatmodules": "Lista de los nombres del formato de los módulos (valor del parámetro <var>format</var>). Utiliza <var>$1modules</var> en su lugar.", "apihelp-paraminfo-example-1": "Mostrar información para <kbd>[[Special:ApiHelp/parse|action=parse]]</kbd>, <kbd>[[Special:ApiHelp/jsonfm|format=jsonfm]]</kbd>, <kbd>[[Special:ApiHelp/query+allpages|action=query&list=allpages]]</kbd> y <kbd>[[Special:ApiHelp/query+siteinfo|action=query&meta=siteinfo]]</kbd>.", "apihelp-paraminfo-example-2": "Mostrar información para todos los submódulos de <kbd>[[Special:ApiHelp/query|action=query]]</kbd>.", + "apihelp-parse-description": "Analiza el contenido y devuelve la salida del analizador sintáctico.\n\nVéanse los distintos módulos prop de <kbd>[[Special:ApiHelp/query|action=query]]</kbd> para obtener información de la versión actual de una página.\n\nHay varias maneras de especificar el texto que analizar:\n# Especificar una página o revisión, mediante <var>$1page</var>, <var>$1pageid</var> o <var>$1oldid</var>.\n# Especificar explícitamente el contenido, mediante <var>$1text</var>, <var>$1title</var> y <var>$1contentmodel</var>.\n# Especificar solamente un resumen que analizar. Se debería asignar a <var>$1prop</var> un valor vacío.", "apihelp-parse-param-title": "Título de la página a la que pertenece el texto. Si se omite se debe especificar <var>$1contentmodel</var> y se debe utilizar el [[API]] como título.", "apihelp-parse-param-text": "Texto a analizar. Utiliza <var>$1title</var> or <var>$1contentmodel</var> para controlar el modelo del contenido.", "apihelp-parse-param-summary": "Resumen a analizar.", @@ -346,6 +347,8 @@ "apihelp-parse-param-pst": "Guardar previamente los cambios antes de transformar la entrada antes de analizarla. Sólo es válido cuando se utiliza con el texto.", "apihelp-parse-param-onlypst": "Guardar previamente los cambios antes de transformar (PST) en la entrada. Devuelve el mismo wikitexto, después de que un PST se ha aplicado. Sólo es válido cuando se utiliza con <var>$1text</var>.", "apihelp-parse-param-effectivelanglinks": "Incluye enlaces de idiomas proporcionados por las extensiones (para utilizar con <kbd>$1prop=langlinks</kbd>).", + "apihelp-parse-param-section": "Analizar solo el contenido de este número de sección.\n\nSi el valor es <kbd>new</kbd>, analiza <var>$1text</var> y <var>$1sectiontitle</var> como si se añadiera una nueva sección a la página.\n\n<kbd>new</kbd> solo se permite cuando se especifique <var>text</var>.", + "apihelp-parse-param-sectiontitle": "Nuevo título de sección cuando <var>section</var> tiene el valor <kbd>new</kbd>.\n\nAl contrario que en la edición de páginas, no se sustituye por <var>summary</var> cuando se omite o su valor es vacío.", "apihelp-parse-param-disablelimitreport": "Omitir el informe de límite (\"NewPP limit report\") desde la salida del analizador.", "apihelp-parse-param-disablepp": "Usa <var>$1disablelimitreport</var> en su lugar.", "apihelp-parse-param-disableeditsection": "Omitir los enlaces de edición de sección de la salida del analizador.", @@ -355,6 +358,7 @@ "apihelp-parse-param-sectionpreview": "Analizar sección en modo de vista previa (también activa el modo de vista previa).", "apihelp-parse-param-disabletoc": "Omitir la tabla de contenidos en la salida.", "apihelp-parse-param-contentformat": "Formato de serialización de contenido utilizado para la introducción de texto. Sólo es válido cuando se utiliza con $1text.", + "apihelp-parse-param-contentmodel": "Modelo de contenido del texto de entrada. Si se omite, se debe especificar $1title, y el valor por defecto será el modelo del título especificado. Solo es válido cuando se use junto con $1text.", "apihelp-parse-example-page": "Analizar una página.", "apihelp-parse-example-text": "Analizar wikitexto.", "apihelp-parse-example-texttitle": "Analizar wikitexto, especificando el título de la página.", @@ -378,7 +382,7 @@ "apihelp-protect-example-protect": "Proteger una página", "apihelp-protect-example-unprotect": "Desproteger una página estableciendo la restricción a <kbd>all</kbd> («todos», es decir, cualquier usuario puede realizar la acción).", "apihelp-protect-example-unprotect2": "Desproteger una página anulando las restricciones.", - "apihelp-purge-description": "Purgar la caché de los títulos proporcionados.\n\nSe requiere una solicitud POST si el usuario no ha iniciado sesión.", + "apihelp-purge-description": "Purgar la caché de los títulos proporcionados.", "apihelp-purge-param-forcelinkupdate": "Actualizar las tablas de enlaces.", "apihelp-purge-param-forcerecursivelinkupdate": "Actualizar la tabla de enlaces y todas las tablas de enlaces de cualquier página que use esta página como una plantilla.", "apihelp-purge-example-simple": "Purgar la <kbd>Main Page</kbd> y la página <kbd>API</kbd>.", @@ -417,6 +421,7 @@ "apihelp-query+alldeletedrevisions-param-user": "Listar solo las revisiones de este usuario.", "apihelp-query+alldeletedrevisions-param-excludeuser": "No listar las revisiones de este usuario.", "apihelp-query+alldeletedrevisions-param-namespace": "Listar solo las páginas en este espacio de nombres.", + "apihelp-query+alldeletedrevisions-param-miser-user-namespace": "<strong>Nota:</strong> Debido al [[mw:Manual:$wgMiserMode|modo avaro]], usar juntos <var>$1user</var> y <var>$1namespace</var> puede dar lugar a que se devuelvan menos de <var>$1limit</var> antes de continuar. En casos extremos, podrían devolverse cero resultados.", "apihelp-query+alldeletedrevisions-param-generatetitles": "Cuando se utiliza como generador, generar títulos en lugar de identificadores de revisión.", "apihelp-query+alldeletedrevisions-example-user": "Listar las últimas 50 contribuciones borradas del usuario <kbd>Example</kbd>.", "apihelp-query+alldeletedrevisions-example-ns-main": "Listar las primeras 50 revisiones borradas en el espacio de nombres principal.", @@ -472,6 +477,7 @@ "apihelp-query+allmessages-param-prop": "Qué propiedades se obtendrán.", "apihelp-query+allmessages-param-enableparser": "Establecer para habilitar el analizador, se preprocesará el wikitexto del mensaje (sustitución de palabras mágicas, uso de plantillas, etc.).", "apihelp-query+allmessages-param-nocontent": "Si se establece, no incluya el contenido de los mensajes en la salida.", + "apihelp-query+allmessages-param-includelocal": "Incluir también los mensajes locales, es decir, aquellos que no existen en el propio software pero sí en el espacio de nombres {{ns:MediaWiki}}.\nEsto muestra todas las páginas del espacio de nombres {{ns:MediaWiki}}, así que también mostrará las que no son propiamente mensajes, como, por ejemplo, [[MediaWiki:Common.js|Common.js]].", "apihelp-query+allmessages-param-args": "Los argumentos que se sustituyen en el mensaje.", "apihelp-query+allmessages-param-filter": "Devolver solo mensajes con nombres que contengan esta cadena.", "apihelp-query+allmessages-param-customised": "Devolver solo mensajes en este estado de personalización.", @@ -505,10 +511,14 @@ "apihelp-query+allredirects-param-prefix": "Buscar todas las páginas de destino que empiecen con este valor.", "apihelp-query+allredirects-param-unique": "Mostrar solo títulos únicos de páginas de destino. No se puede usar junto con $1prop=ids|fragment|interwiki. Cuando se use como generador, devuelve páginas de destino en vez de páginas de origen.", "apihelp-query+allredirects-param-prop": "Qué piezas de información incluir:", + "apihelp-query+allredirects-paramvalue-prop-ids": "Añade el identificador de la página de redirección (no se puede usar junto con <var>$1unique</var>).", "apihelp-query+allredirects-paramvalue-prop-title": "Añade el título de la redirección.", + "apihelp-query+allredirects-paramvalue-prop-fragment": "Añade el fragmento de la redirección, si existe (no se puede usar junto con <var>$1unique</var>).", + "apihelp-query+allredirects-paramvalue-prop-interwiki": "Añade el prefijo interwiki de la redirección, si existe (no se puede usar junto con <var>$1unique</var>).", "apihelp-query+allredirects-param-namespace": "El espacio de nombres a enumerar.", "apihelp-query+allredirects-param-limit": "Cuántos elementos se devolverán.", "apihelp-query+allredirects-param-dir": "La dirección en la que se listará.", + "apihelp-query+allredirects-example-B": "Enumera las páginas de destino, incluyendo las páginas desaparecidas, con los identificadores de las páginas de las que provienen, empezando por <kbd>B</kbd>.", "apihelp-query+allredirects-example-unique": "La lista de páginas de destino.", "apihelp-query+allredirects-example-unique-generator": "Obtiene todas las páginas de destino, marcando los que faltan.", "apihelp-query+allredirects-example-generator": "Obtiene páginas que contienen las redirecciones.", @@ -536,6 +546,7 @@ "apihelp-query+alltransclusions-param-namespace": "El espacio de nombres que enumerar.", "apihelp-query+alltransclusions-param-limit": "Número de elementos que se desea obtener.", "apihelp-query+alltransclusions-param-dir": "La dirección en que ordenar la lista.", + "apihelp-query+alltransclusions-example-B": "Enumerar los títulos transcluidos, incluyendo los faltantes, junto con los identificadores de las páginas de las que provienen, empezando por <kbd>B</kbd>.", "apihelp-query+alltransclusions-example-unique": "Listar títulos transcluidos de forma única.", "apihelp-query+alltransclusions-example-unique-generator": "Obtiene todos los títulos transcluidos, marcando los que faltan.", "apihelp-query+alltransclusions-example-generator": "Obtiene las páginas que contienen las transclusiones.", @@ -561,13 +572,18 @@ "apihelp-query+allusers-param-attachedwiki": "Con <kbd>$1prop=centralids</kbd>, indicar también si el usuario está conectado con el wiki identificado por el ID.", "apihelp-query+allusers-example-Y": "Listar usuarios que empiecen por <kbd>Y</kbd>.", "apihelp-query+authmanagerinfo-description": "Recuperar información sobre el estado de autenticación actual.", + "apihelp-query+authmanagerinfo-param-requestsfor": "Obtener información sobre las peticiones de autentificación requeridas para la acción de autentificación especificada.", "apihelp-query+authmanagerinfo-example-login": "Captura de las solicitudes que puede ser utilizadas al comienzo de inicio de sesión.", + "apihelp-query+authmanagerinfo-example-login-merged": "Obtener las peticiones que podrían utilizarse al empezar un inicio de sesión, con los campos de formulario integrados.", + "apihelp-query+authmanagerinfo-example-securitysensitiveoperation": "Comprueba si la autentificación es suficiente para realizar la acción <kbd>foo</kbd>.", "apihelp-query+backlinks-description": "Encuentra todas las páginas que enlazan a la página dada.", + "apihelp-query+backlinks-param-title": "Título que buscar. No se puede usar junto con <var>$1pageid</var>.", "apihelp-query+backlinks-param-pageid": "Identificador de página que buscar. No puede usarse junto con <var>$1title</var>", "apihelp-query+backlinks-param-namespace": "El espacio de nombres que enumerar.", "apihelp-query+backlinks-param-dir": "La dirección en que ordenar la lista.", "apihelp-query+backlinks-param-filterredir": "Cómo filtrar redirecciones. Si se establece a <kbd>nonredirects</kbd> cuando está activo <var>$1redirect</var>, esto sólo se aplica al segundo nivel.", "apihelp-query+backlinks-param-limit": "Cuántas páginas en total se devolverán. Si está activo <var>$1redirect</var>, el límite aplica a cada nivel por separado (lo que significa que se pueden devolver hasta 2 * <var>$1limit</var> resultados).", + "apihelp-query+backlinks-param-redirect": "Si la página con el enlace es una redirección, encontrar también las páginas que enlacen a esa redirección. El límite máximo se reduce a la mitad.", "apihelp-query+backlinks-example-simple": "Mostrar enlaces a <kbd>Main page</kbd>.", "apihelp-query+backlinks-example-generator": "Obtener información acerca de las páginas enlazadas a <kbd>Main page</kbd>.", "apihelp-query+blocks-description": "Listar todos los usuarios y direcciones IP bloqueadas.", @@ -590,6 +606,7 @@ "apihelp-query+blocks-paramvalue-prop-flags": "Etiquetas la prohibición con (autoblock, anononly, etc.).", "apihelp-query+blocks-param-show": "Muestra solamente los elementos que cumplen estos criterios.\nPor ejemplo, para mostrar solamente los bloqueos indefinidos a direcciones IP, introduce <kbd>$1show=ip|!temp</kbd>.", "apihelp-query+blocks-example-simple": "Listar bloques.", + "apihelp-query+blocks-example-users": "Muestra los bloqueos de los usuarios <kbd>Alice</kbd> y <kbd>Bob</kbd>.", "apihelp-query+categories-description": "Enumera todas las categorías a las que pertenecen las páginas.", "apihelp-query+categories-param-prop": "Qué propiedades adicionales obtener para cada categoría:", "apihelp-query+categories-paramvalue-prop-sortkey": "Añade la clave de ordenación (cadena hexadecimal) y el prefijo de la clave de ordenación (la parte legible) de la categoría.", @@ -628,6 +645,11 @@ "apihelp-query+categorymembers-param-endsortkey": "Utilizar $1endhexsortkey en su lugar.", "apihelp-query+categorymembers-example-simple": "Obtener las primeras 10 páginas en <kbd>Category:Physics</kbd>.", "apihelp-query+categorymembers-example-generator": "Obtener información sobre las primeras 10 páginas de la <kbd>Category:Physics</kbd>.", + "apihelp-query+contributors-description": "Obtener la lista de contribuidores conectados y el número de contribuidores anónimos de una página.", + "apihelp-query+contributors-param-group": "Solo incluir usuarios de los grupos especificados. No incluye grupos implícitos o autopromocionados, como *, usuario o autoconfirmado.", + "apihelp-query+contributors-param-excludegroup": "Excluir usuarios de los grupos especificados. No incluye grupos implícitos o autopromocionados, como *, usuario o autoconfirmado.", + "apihelp-query+contributors-param-rights": "Solo incluir usuarios con los derechos especificados. No incluye derechos concedidos a grupos implícitos o autopromocionados, como *, usuario o autoconfirmado.", + "apihelp-query+contributors-param-excluderights": "Excluir usuarios con los derechos especificados. No incluye derechos concedidos a grupos implícitos o autopromocionados, como *, usuario o autoconfirmado.", "apihelp-query+contributors-param-limit": "Cuántos contribuyentes se devolverán.", "apihelp-query+contributors-example-simple": "Mostrar los contribuyentes de la página <kbd>Main Page</kbd>.", "apihelp-query+deletedrevisions-param-start": "Marca de tiempo por la que empezar la enumeración. Se ignora cuando se esté procesando una lista de ID de revisión.", @@ -635,7 +657,9 @@ "apihelp-query+deletedrevisions-param-tag": "Listar solo las revisiones con esta etiqueta.", "apihelp-query+deletedrevisions-param-user": "Listar solo las revisiones de este usuario.", "apihelp-query+deletedrevisions-param-excludeuser": "No listar las revisiones de este usuario.", + "apihelp-query+deletedrevisions-example-titles": "Muestra la lista de revisiones borradas de las páginas <kbd>Main Page</kbd> y <kbd>Talk:Main Page</kbd>, con su contenido.", "apihelp-query+deletedrevisions-example-revids": "Mostrar la información de la revisión borrada <kbd>123456</kbd>.", + "apihelp-query+deletedrevs-description": "Muestra la lista de revisiones borradas.\n\nOpera en tres modos:\n# Lista de revisiones borradas de los títulos dados, ordenadas por marca de tiempo.\n# Lista de contribuciones borradas del usuario dado, ordenadas por marca de tiempo.\n# Lista de todas las revisiones borradas en el espacio de nombres dado, ordenadas por título y marca de tiempo (donde no se ha especificado ningún título ni se ha fijado $1user).", "apihelp-query+deletedrevs-paraminfo-modes": "{{PLURAL:$1|Modo|Modos}}: $2", "apihelp-query+deletedrevs-param-start": "Marca de tiempo por la que empezar la enumeración.", "apihelp-query+deletedrevs-param-end": "Marca de tiempo por la que terminar la enumeración.", @@ -648,6 +672,10 @@ "apihelp-query+deletedrevs-param-excludeuser": "No listar las revisiones de este usuario.", "apihelp-query+deletedrevs-param-namespace": "Listar solo las páginas en este espacio de nombres.", "apihelp-query+deletedrevs-param-limit": "La cantidad máxima de revisiones que listar.", + "apihelp-query+deletedrevs-param-prop": "Propiedades que obtener:\n;revid: Añade el identificador de la revisión borrada.\n;parentid: Añade el identificador de la revisión anterior de la página.\n;user: Añade el usuario que hizo la revisión.\n;userid: Añade el identificador del usuario que hizo la revisión.\n;comment: Añade el comentario de la revisión.\n;parsedcomment: Añade el comentario de la revisión, pasado por el analizador sintáctico.\n;minor: Añade una etiqueta si la revisión es menor.\n;len: Añade la longitud (en bytes) de la revisión.\n;sha1: Añade el SHA-1 (base 16) de la revisión.\n;content: Añade el contenido de la revisión.\n;token:<span class=\"apihelp-deprecated\">Obsoleto.</span> Devuelve el token de edición.\n;tags: Etiquetas de la revisión.", + "apihelp-query+deletedrevs-example-mode1": "Muestra las últimas revisiones borradas de las páginas <kbd>Main Page</kbd> y <kbd>Talk:Main Page</kbd>, con contenido (modo 1).", + "apihelp-query+deletedrevs-example-mode2": "Muestra las últimas 50 contribuciones de <kbd>Bob</kbd> (modo 2).", + "apihelp-query+deletedrevs-example-mode3-main": "Muestra las primeras 50 revisiones borradas del espacio principal (modo 3).", "apihelp-query+deletedrevs-example-mode3-talk": "Listar las primeras 50 páginas en el espacio de nombres {{ns:talk}} (modo 3).", "apihelp-query+disabled-description": "Se ha desactivado el módulo de consulta.", "apihelp-query+duplicatefiles-description": "Enumerar todos los archivos que son duplicados de los archivos dados a partir de los valores hash.", @@ -710,8 +738,10 @@ "apihelp-query+fileusage-param-prop": "Qué propiedades se obtendrán:", "apihelp-query+fileusage-paramvalue-prop-pageid": "Identificador de cada página.", "apihelp-query+fileusage-paramvalue-prop-title": "Título de cada página.", + "apihelp-query+fileusage-paramvalue-prop-redirect": "Marcar si la página es una redirección.", "apihelp-query+fileusage-param-namespace": "Incluir solo páginas de estos espacios de nombres.", "apihelp-query+fileusage-param-limit": "Cuántos se devolverán.", + "apihelp-query+fileusage-param-show": "Muestra solo los elementos que cumplen estos criterios:\n;redirect: Muestra solamente redirecciones.\n;!redirect: Muestra solamente páginas que no son redirecciones.", "apihelp-query+fileusage-example-simple": "Obtener una lista de páginas que utilicen [[:File:Example.jpg]].", "apihelp-query+fileusage-example-generator": "Obtener información acerca de las páginas que utilicen [[:File:Example.jpg]].", "apihelp-query+imageinfo-description": "Devuelve información del archivo y su historial de subida.", @@ -722,6 +752,7 @@ "apihelp-query+imageinfo-paramvalue-prop-comment": "Comentarios sobre la versión.", "apihelp-query+imageinfo-paramvalue-prop-parsedcomment": "Analizar el comentario de la versión.", "apihelp-query+imageinfo-paramvalue-prop-canonicaltitle": "Agrega el título canónico del archivo.", + "apihelp-query+imageinfo-paramvalue-prop-url": "Devuelve la URL para el archivo y la página de descripción.", "apihelp-query+imageinfo-paramvalue-prop-dimensions": "Alias para el tamaño.", "apihelp-query+imageinfo-paramvalue-prop-sha1": "Añade el hash SHA-1 para la imagen.", "apihelp-query+imageinfo-paramvalue-prop-mime": "Añade el tipo MIME del archivo.", @@ -736,6 +767,7 @@ "apihelp-query+imageinfo-param-start": "Marca de tiempo por la que empezar la enumeración.", "apihelp-query+imageinfo-param-end": "Marca de tiempo por la que terminar la enumeración.", "apihelp-query+imageinfo-param-urlheight": "Similar a $1urlwidth.", + "apihelp-query+imageinfo-param-metadataversion": "Versión de los metadatos que se utilizará. Si se especifica <kbd>latest</kbd>, utilizará la última versión. El valor predeterminado es <kbd>1</kbd>, por motivo de retrocompatibilidad.", "apihelp-query+imageinfo-param-extmetadatafilter": "Si se especifica y no vacío, sólo estas claves serán devueltos por $1prop=extmetadata.", "apihelp-query+imageinfo-param-urlparam": "Un controlador específico de la cadena de parámetro. Por ejemplo, los archivos Pdf pueden utilizar <kbd>page15-100px</kbd>. <var>$1urlwidth</var> debe ser utilizado y debe ser consistente con <var>$1urlparam</var>.", "apihelp-query+imageinfo-param-localonly": "Buscar solo archivos en el repositorio local.", @@ -743,13 +775,18 @@ "apihelp-query+imageinfo-example-dated": "Obtener información sobre las versiones de [[:File:Test.jpg]] a partir de 2008.", "apihelp-query+images-description": "Devuelve todos los archivos contenidos en las páginas dadas.", "apihelp-query+images-param-limit": "Cuántos archivos se devolverán.", + "apihelp-query+images-param-images": "Mostrar solo estos archivos. Útil para comprobar si una determinada página tiene un determinado archivo.", "apihelp-query+images-param-dir": "La dirección en que ordenar la lista.", "apihelp-query+images-example-simple": "Obtener una lista de los archivos usados en la [[Main Page|Portada]].", "apihelp-query+images-example-generator": "Obtener información sobre todos los archivos empleados en [[Main Page]].", + "apihelp-query+imageusage-description": "Encontrar todas las páginas que usen el título de imagen dado.", "apihelp-query+imageusage-param-title": "Título a buscar. No puede usarse en conjunto con $1pageid.", "apihelp-query+imageusage-param-pageid": "ID de página a buscar. No puede usarse con $1title.", "apihelp-query+imageusage-param-namespace": "El espacio de nombres que enumerar.", "apihelp-query+imageusage-param-dir": "La dirección en que ordenar la lista.", + "apihelp-query+imageusage-param-filterredir": "Cómo filtrar las redirecciones. Si se establece a no redirecciones cuando está habilitado $1redirect, esto solo se aplica al segundo nivel.", + "apihelp-query+imageusage-param-limit": "Número de páginas que devolver. Si está habilitado <var>$1redirect</var>, el límite se aplica a cada nivel de forma separada (es decir, se pueden devolver hasta 2 * <var>$1limit</var>).", + "apihelp-query+imageusage-param-redirect": "Si la página con el enlace es una redirección, encontrar también las páginas que enlacen a esa redirección. El límite máximo se reduce a la mitad.", "apihelp-query+imageusage-example-simple": "Mostrar las páginas que usan [[:File:Albert Einstein Head.jpg]].", "apihelp-query+imageusage-example-generator": "Obtener información sobre las páginas que empleen [[:File:Albert Einstein Head.jpg]].", "apihelp-query+info-description": "Obtener información básica de la página.", @@ -761,9 +798,11 @@ "apihelp-query+info-paramvalue-prop-readable": "Si el usuario puede leer esta página.", "apihelp-query+info-paramvalue-prop-preload": "Muestra el texto devuelto por EditFormPreloadText.", "apihelp-query+info-paramvalue-prop-displaytitle": "Proporciona la manera en que se muestra realmente el título de la página", + "apihelp-query+info-param-testactions": "Comprobar su el usuario actual puede realizar determinadas acciones en la página.", "apihelp-query+info-param-token": "Usa [[Special:ApiHelp/query+tokens|action=query&meta=tokens]] en su lugar.", "apihelp-query+info-example-simple": "Obtener información acerca de la página <kbd>Main Page</kbd>.", "apihelp-query+info-example-protection": "Obtén información general y protección acerca de la página <kbd>Main Page</kbd>.", + "apihelp-query+iwbacklinks-description": "Encontrar todas las páginas que enlazan al enlace interwiki dado.\n\nPuede utilizarse para encontrar todos los enlaces con un prefijo, o todos los enlaces a un título (con un determinado prefijo). Si no se introduce ninguno de los parámetros, se entiende como «todos los enlaces interwiki».", "apihelp-query+iwbacklinks-param-prefix": "Prefijo para el interwiki.", "apihelp-query+iwbacklinks-param-title": "Enlace interlingüístico que buscar. Se debe usar junto con <var>$1blprefix</var>.", "apihelp-query+iwbacklinks-param-limit": "Cuántas páginas se devolverán.", @@ -788,6 +827,7 @@ "apihelp-query+langbacklinks-param-dir": "La dirección en que ordenar la lista.", "apihelp-query+langbacklinks-example-simple": "Obtener las páginas enlazadas a [[:fr:Test]]", "apihelp-query+langbacklinks-example-generator": "Obtener información acerca de las páginas enlazadas a [[:fr:Test]].", + "apihelp-query+langlinks-description": "Devuelve todos los enlaces interlingüísticos de las páginas dadas.", "apihelp-query+langlinks-param-limit": "Número de enlaces interlingüísticos que devolver.", "apihelp-query+langlinks-param-url": "Obtener la URL completa o no (no se puede usar con <var>$1prop</var>).", "apihelp-query+langlinks-param-prop": "Qué propiedades adicionales obtener para cada enlace interlingüe:", @@ -802,27 +842,50 @@ "apihelp-query+links-description": "Devuelve todos los enlaces de las páginas dadas.", "apihelp-query+links-param-namespace": "Mostrar solo los enlaces en estos espacios de nombres.", "apihelp-query+links-param-limit": "Cuántos enlaces se devolverán.", + "apihelp-query+links-param-titles": "Devolver solo los enlaces a estos títulos. Útil para comprobar si una determinada página enlaza a un determinado título.", "apihelp-query+links-param-dir": "La dirección en que ordenar la lista.", "apihelp-query+links-example-simple": "Obtener los enlaces de la página <kbd>Main Page</kbd>", + "apihelp-query+links-example-namespaces": "Obtener enlaces de la página <kbd>Main Page</kbd> de los espacios de nombres {{ns:user}} and {{ns:template}}.", + "apihelp-query+linkshere-description": "Buscar todas las páginas que enlazan a las páginas dadas.", "apihelp-query+linkshere-param-prop": "Qué propiedades se obtendrán:", "apihelp-query+linkshere-paramvalue-prop-pageid": "Identificador de cada página.", "apihelp-query+linkshere-paramvalue-prop-title": "Título de cada página.", "apihelp-query+linkshere-paramvalue-prop-redirect": "Indicar si la página es una redirección.", "apihelp-query+linkshere-param-namespace": "Incluir solo páginas de estos espacios de nombres.", "apihelp-query+linkshere-param-limit": "Cuántos se devolverán.", + "apihelp-query+linkshere-param-show": "Muestra solo los elementos que cumplen estos criterios:\n;redirect: Muestra solamente redirecciones.\n;!redirect: Muestra solamente páginas que no son redirecciones.", "apihelp-query+linkshere-example-simple": "Obtener una lista de páginas que enlacen a la [[Main Page]].", "apihelp-query+linkshere-example-generator": "Obtener información acerca de las páginas enlazadas a la [[Main Page|Portada]].", "apihelp-query+logevents-description": "Obtener eventos de los registros.", "apihelp-query+logevents-param-prop": "Qué propiedades se obtendrán:", "apihelp-query+logevents-paramvalue-prop-ids": "Agrega el identificador del evento de registro.", + "apihelp-query+logevents-paramvalue-prop-title": "Añade el título de la página para el evento del registro.", "apihelp-query+logevents-paramvalue-prop-type": "Añade el tipo del evento de registro.", + "apihelp-query+logevents-paramvalue-prop-user": "Añade el usuario responsable del evento del registro.", + "apihelp-query+logevents-paramvalue-prop-userid": "Agrega el identificador del usuario responsable del evento del registro.", + "apihelp-query+logevents-paramvalue-prop-timestamp": "Añade la marca de tiempo para el evento del registro.", + "apihelp-query+logevents-paramvalue-prop-comment": "Añade el comentario del evento del registro.", "apihelp-query+logevents-paramvalue-prop-parsedcomment": "Añade el comentario analizado del evento de registro.", + "apihelp-query+logevents-paramvalue-prop-details": "Muestra detalles adicionales sobre el evento del registro.", + "apihelp-query+logevents-paramvalue-prop-tags": "Muestra las etiquetas para el evento del registro.", + "apihelp-query+logevents-param-type": "Filtrar las entradas del registro solo a este tipo.", + "apihelp-query+logevents-param-action": "Filtrar las acciones del registro solo a esta acción. Reemplaza <var>$1type</var>. En la lista de valores posibles, los valores con el asterisco como carácter comodín tales como <kbd>action/*</kbd> pueden tener distintas cadenas después de la barra (/).", "apihelp-query+logevents-param-start": "Marca de tiempo por la que empezar la enumeración.", "apihelp-query+logevents-param-end": "Marca de tiempo por la que terminar la enumeración.", + "apihelp-query+logevents-param-user": "Filtrar entradas a aquellas realizadas por el usuario dado.", + "apihelp-query+logevents-param-title": "Filtrar entradas a aquellas relacionadas con una página.", + "apihelp-query+logevents-param-namespace": "Filtrar entradas a aquellas en el espacio de nombres dado.", + "apihelp-query+logevents-param-prefix": "Filtrar entradas que empiezan por este prefijo.", + "apihelp-query+logevents-param-tag": "Solo mostrar las entradas de eventos con esta etiqueta.", + "apihelp-query+logevents-param-limit": "Número total de entradas de eventos que devolver.", + "apihelp-query+logevents-example-simple": "Mostrar los eventos recientes del registro.", "apihelp-query+pagepropnames-description": "Mostrar todos los nombres de propiedades de página utilizados en el wiki.", + "apihelp-query+pagepropnames-param-limit": "Número máximo de nombres que devolver.", + "apihelp-query+pagepropnames-example-simple": "Obtener los 10 primeros nombres de propiedades.", "apihelp-query+pageprops-description": "Obtener diferentes propiedades de página definidas en el contenido de la página.", "apihelp-query+pageprops-param-prop": "Sólo listar estas propiedades de página (<kbd>[[Special:ApiHelp/query+pagepropnames|action=query&list=pagepropnames]]</kbd> devuelve los nombres de las propiedades de página en uso). Útil para comprobar si las páginas usan una determinada propiedad de página.", "apihelp-query+pageprops-example-simple": "Obtener las propiedades de las páginas <kbd>Main Page</kbd> y <kbd>MediaWiki</kbd>.", + "apihelp-query+pageswithprop-description": "Mostrar todas las páginas que usen una propiedad de página.", "apihelp-query+pageswithprop-param-propname": "Propiedad de página para la cual enumerar páginas (<kbd>[[Special:ApiHelp/query+pagepropnames|action=query&list=pagepropnames]]</kbd> devuelve los nombres de las propiedades de página en uso).", "apihelp-query+pageswithprop-param-prop": "Qué piezas de información incluir:", "apihelp-query+pageswithprop-paramvalue-prop-ids": "Añade el identificador de página.", @@ -842,6 +905,8 @@ "apihelp-query+protectedtitles-param-namespace": "Listar solo los títulos en estos espacios de nombres.", "apihelp-query+protectedtitles-param-level": "Listar solo títulos con estos niveles de protección.", "apihelp-query+protectedtitles-param-limit": "Cuántas páginas se devolverán.", + "apihelp-query+protectedtitles-param-start": "Empezar la enumeración en esta marca de tiempo de protección.", + "apihelp-query+protectedtitles-param-end": "Terminar la enumeración en esta marca de tiempo de protección.", "apihelp-query+protectedtitles-param-prop": "Qué propiedades se obtendrán:", "apihelp-query+protectedtitles-paramvalue-prop-timestamp": "Añade la marca de tiempo de cuando se añadió la protección.", "apihelp-query+protectedtitles-paramvalue-prop-user": "Agrega el usuario que agregó la protección.", @@ -855,6 +920,8 @@ "apihelp-query+querypage-param-page": "El nombre de la página especial. Recuerda, es sensible a mayúsculas y minúsculas.", "apihelp-query+querypage-param-limit": "Número de resultados que se devolverán.", "apihelp-query+querypage-example-ancientpages": "Devolver resultados de [[Special:Ancientpages]].", + "apihelp-query+random-description": "Obtener un conjunto de páginas aleatorias.\n\nLas páginas aparecen enumeradas en una secuencia fija, solo que el punto de partida es aleatorio. Esto quiere decir que, si, por ejemplo, <samp>Portada</samp> es la primera página aleatoria de la lista, <samp>Lista de monos ficticios</samp> <em>siempre</em> será la segunda, <samp>Lista de personas en sellos de Vanuatu</samp> la tercera, etc.", + "apihelp-query+random-param-namespace": "Devolver solo las páginas de estos espacios de nombres.", "apihelp-query+random-param-limit": "Limita el número de páginas aleatorias que se devolverán.", "apihelp-query+random-param-redirect": "Usa <kbd>$1filterredir=redirects</kbd> en su lugar.", "apihelp-query+random-param-filterredir": "Cómo filtrar las redirecciones.", @@ -868,6 +935,7 @@ "apihelp-query+recentchanges-param-excludeuser": "No listar cambios de este usuario.", "apihelp-query+recentchanges-param-tag": "Listar solo los cambios con esta etiqueta.", "apihelp-query+recentchanges-param-prop": "Incluir piezas adicionales de información:", + "apihelp-query+recentchanges-paramvalue-prop-user": "Añade el usuario responsable de la edición y añade una etiqueta si se trata de una IP.", "apihelp-query+recentchanges-paramvalue-prop-userid": "Añade el identificador del usuario responsable de la edición.", "apihelp-query+recentchanges-paramvalue-prop-comment": "Añade el comentario de la edición.", "apihelp-query+recentchanges-paramvalue-prop-parsedcomment": "Añade el comentario analizado para la edición.", @@ -881,9 +949,13 @@ "apihelp-query+recentchanges-paramvalue-prop-loginfo": "Añade información de registro (identificador de registro, tipo de registro, etc.) a las entradas de registro.", "apihelp-query+recentchanges-paramvalue-prop-tags": "Muestra las etiquetas de la entrada.", "apihelp-query+recentchanges-param-token": "Usa <kbd>[[Special:ApiHelp/query+tokens|action=query&meta=tokens]]</kbd> en su lugar.", + "apihelp-query+recentchanges-param-show": "Muestra solo los elementos que cumplan estos criterios. Por ejemplo, para ver solo ediciones menores realizadas por usuarios conectados, introduce $1show=minor|!anon.", "apihelp-query+recentchanges-param-limit": "Cuántos cambios en total se devolverán.", "apihelp-query+recentchanges-param-type": "Cuántos tipos de cambios se mostrarán.", + "apihelp-query+recentchanges-param-toponly": "Enumerar solo las modificaciones que sean las últimas revisiones.", + "apihelp-query+recentchanges-param-generaterevisions": "Cuando se utilice como generador, genera identificadores de revisión en lugar de títulos. Las entradas en la lista de cambios recientes que no tengan identificador de revisión asociado (por ejemplo, la mayoría de las entradas de registro) no generarán nada.", "apihelp-query+recentchanges-example-simple": "Lista de cambios recientes.", + "apihelp-query+recentchanges-example-generator": "Obtener información de página de cambios recientes no patrullados.", "apihelp-query+redirects-description": "Devuelve todas las redirecciones a las páginas dadas.", "apihelp-query+redirects-param-prop": "Qué propiedades se obtendrán:", "apihelp-query+redirects-paramvalue-prop-pageid": "Identificador de página de cada redirección.", @@ -893,11 +965,20 @@ "apihelp-query+redirects-param-limit": "Cuántas redirecciones se devolverán.", "apihelp-query+redirects-example-simple": "Mostrar una lista de las redirecciones a la [[Main Page|Portada]]", "apihelp-query+redirects-example-generator": "Obtener información sobre todas las redirecciones a la [[Main Page|Portada]].", + "apihelp-query+revisions-paraminfo-singlepageonly": "Solo se puede usar con una sola página (modo n.º 2).", + "apihelp-query+revisions-param-startid": "Identificador de revisión a partir del cual empezar la enumeración.", + "apihelp-query+revisions-param-endid": "Identificador de revisión en el que detener la enumeración.", + "apihelp-query+revisions-param-start": "Marca de tiempo a partir de la cual empezar la enumeración.", "apihelp-query+revisions-param-end": "Enumerar hasta esta marca de tiempo.", "apihelp-query+revisions-param-user": "Incluir solo las revisiones realizadas por el usuario.", "apihelp-query+revisions-param-excludeuser": "Excluir las revisiones realizadas por el usuario.", "apihelp-query+revisions-param-tag": "Mostrar solo revisiones marcadas con esta etiqueta.", + "apihelp-query+revisions-example-content": "Obtener datos con el contenido de la última revisión de los títulos <kbd>API</kbd> y <kbd>Main Page</kbd>.", "apihelp-query+revisions-example-last5": "Mostrar las últimas 5 revisiones de la <kbd>Main Page</kbd>.", + "apihelp-query+revisions-example-first5": "Obtener las primeras 5 revisiones de <kbd>Main Page</kbd>.", + "apihelp-query+revisions-example-first5-after": "Obtener las primeras 5 revisiones de <kbd>Main Page</kbd> realizadas después de 2006-05-01.", + "apihelp-query+revisions-example-first5-not-localhost": "Obtener las primeras 5 revisiones de <kbd>Main Page</kbd> que no fueron realizadas por el usuario anónimo <kbd>127.0.0.1</kbd>.", + "apihelp-query+revisions-example-first5-user": "Obtener las primeras 5 revisiones de <kbd>Main Page</kbd> que fueron realizadas por el usuario <kbd>MediaWiki default</kbd>.", "apihelp-query+revisions+base-param-prop": "Las propiedades que se obtendrán para cada revisión:", "apihelp-query+revisions+base-paramvalue-prop-ids": "El identificador de la revisión.", "apihelp-query+revisions+base-paramvalue-prop-flags": "Marcas de revisión (menor).", @@ -908,9 +989,17 @@ "apihelp-query+revisions+base-paramvalue-prop-sha1": "SHA-1 (base 16) de la revisión.", "apihelp-query+revisions+base-paramvalue-prop-contentmodel": "Identificador del modelo de contenido de la revisión.", "apihelp-query+revisions+base-paramvalue-prop-comment": "Comentario del usuario para la revisión.", + "apihelp-query+revisions+base-paramvalue-prop-parsedcomment": "Comentario analizado del usuario para la revisión.", "apihelp-query+revisions+base-paramvalue-prop-content": "Texto de la revisión.", "apihelp-query+revisions+base-paramvalue-prop-tags": "Etiquetas para la revisión.", + "apihelp-query+revisions+base-paramvalue-prop-parsetree": "El árbol de análisis sintáctico XML del contenido de la revisión (requiere el modelo de contenido <code>$1</code>).", "apihelp-query+revisions+base-param-limit": "Limitar la cantidad de revisiones que se devolverán.", + "apihelp-query+revisions+base-param-expandtemplates": "Expandir las plantillas en el contenido de la revisión (requiere $1prop=content).", + "apihelp-query+revisions+base-param-generatexml": "Generar el árbol de análisis sintáctico XML para el contenido de la revisión (requiere $1prop=content; reemplazado por <kbd>$1prop=parsetree</kbd>).", + "apihelp-query+revisions+base-param-parse": "Analizar el contenido de la revisión (requiere $1prop=content). Por motivos de rendimiento, si se utiliza esta opción, el valor de $1limit es forzado a 1.", + "apihelp-query+revisions+base-param-section": "Recuperar solamente el contenido de este número de sección.", + "apihelp-query+revisions+base-param-contentformat": "Formato de serialización utilizado para <var>$1difftotext</var> y esperado para la salida de contenido.", + "apihelp-query+search-description": "Realizar una búsqueda de texto completa.", "apihelp-query+search-param-namespace": "Buscar sólo en estos espacios de nombres.", "apihelp-query+search-param-what": "Tipo de búsqueda que realizar.", "apihelp-query+search-param-info": "Qué metadatos devolver.", @@ -956,16 +1045,22 @@ "apihelp-query+siteinfo-paramvalue-prop-variables": "Devuelve una lista de identificadores variables.", "apihelp-query+siteinfo-paramvalue-prop-protocols": "Devuelve una lista de los protocolos que se permiten en los enlaces externos.", "apihelp-query+siteinfo-paramvalue-prop-defaultoptions": "Devuelve los valores predeterminados de las preferencias del usuario.", + "apihelp-query+siteinfo-param-filteriw": "Devuelve solo entradas locales o solo entradas no locales del mapa interwiki.", + "apihelp-query+siteinfo-param-numberingroup": "Muestra el número de usuarios en los grupos de usuarios.", + "apihelp-query+siteinfo-param-inlanguagecode": "Código de idioma para los nombres localizados de los idiomas (en el mejor intento posible) y apariencias.", "apihelp-query+siteinfo-example-simple": "Obtener información del sitio.", + "apihelp-query+siteinfo-example-interwiki": "Obtener una lista de prefijos interwiki locales.", "apihelp-query+stashimageinfo-description": "Devuelve información del archivo para archivos escondidos.", "apihelp-query+stashimageinfo-param-sessionkey": "Alias de $1filekey, para retrocompatibilidad.", "apihelp-query+stashimageinfo-example-simple": "Devuelve información para un archivo escondido.", "apihelp-query+stashimageinfo-example-params": "Devuelve las miniaturas de dos archivos escondidos.", + "apihelp-query+tags-description": "Enumerar las etiquetas de modificación.", "apihelp-query+tags-param-limit": "El número máximo de etiquetas para enumerar.", "apihelp-query+tags-param-prop": "Qué propiedades se obtendrán:", "apihelp-query+tags-paramvalue-prop-name": "Añade el nombre de la etiqueta.", "apihelp-query+tags-paramvalue-prop-displayname": "Agrega el mensaje de sistema para la etiqueta.", "apihelp-query+tags-paramvalue-prop-description": "Añade la descripción de la etiqueta.", + "apihelp-query+tags-paramvalue-prop-hitcount": "Añade el número de revisiones y entradas de registro que tienen esta etiqueta.", "apihelp-query+tags-paramvalue-prop-defined": "Indicar si la etiqueta está definida.", "apihelp-query+tags-paramvalue-prop-source": "Obtiene las fuentes de la etiqueta, que pueden incluir <samp>extension</samp> para etiquetas definidas por extensiones y <samp>manual</samp> para etiquetas que pueden aplicarse manualmente por los usuarios.", "apihelp-query+tags-paramvalue-prop-active": "Si la etiqueta aún se sigue aplicando.", @@ -973,17 +1068,27 @@ "apihelp-query+templates-description": "Devuelve todas las páginas transcluidas en las páginas dadas.", "apihelp-query+templates-param-namespace": "Mostrar plantillas solamente en estos espacios de nombres.", "apihelp-query+templates-param-limit": "Cuántas plantillas se devolverán.", + "apihelp-query+templates-param-templates": "Mostrar solo estas plantillas. Útil para comprobar si una determinada página utiliza una determinada plantilla.", "apihelp-query+templates-param-dir": "La dirección en que ordenar la lista.", + "apihelp-query+templates-example-simple": "Obtener las plantillas que se usan en la página <kbd>Portada</kbd>.", + "apihelp-query+templates-example-generator": "Obtener información sobre las páginas de las plantillas utilizadas en <kbd>Main Page</kbd>.", + "apihelp-query+templates-example-namespaces": "Obtener las páginas de los espacios de nombres {{ns:user}} y {{ns:template}} que están transcluidas en la página <kbd>Main Page</kbd>.", "apihelp-query+transcludedin-description": "Encuentra todas las páginas que transcluyan las páginas dadas.", "apihelp-query+transcludedin-param-prop": "Qué propiedades se obtendrán:", "apihelp-query+transcludedin-paramvalue-prop-pageid": "Identificador de cada página.", "apihelp-query+transcludedin-paramvalue-prop-title": "Título de cada página.", + "apihelp-query+transcludedin-paramvalue-prop-redirect": "Marcar si la página es una redirección.", "apihelp-query+transcludedin-param-namespace": "Incluir solo las páginas en estos espacios de nombres.", "apihelp-query+transcludedin-param-limit": "Cuántos se devolverán.", + "apihelp-query+transcludedin-param-show": "Muestra solo los elementos que cumplen estos criterios:\n;redirect: Muestra solamente redirecciones.\n;!redirect: Muestra solamente páginas que no son redirecciones.", "apihelp-query+transcludedin-example-simple": "Obtener una lista de páginas transcluyendo <kbd>Main Page</kbd>.", "apihelp-query+transcludedin-example-generator": "Obtener información sobre las páginas que transcluyen <kbd>Main Page</kbd>.", "apihelp-query+usercontribs-description": "Obtener todas las ediciones realizadas por un usuario.", "apihelp-query+usercontribs-param-limit": "Número máximo de contribuciones que se devolverán.", + "apihelp-query+usercontribs-param-user": "Los usuarios para los cuales se desea recuperar las contribuciones. No se puede utilizar junto con <var>$1userids</var> o <var>$1userprefix</var>.", + "apihelp-query+usercontribs-param-userprefix": "Recuperar las contribuciones de todos los usuarios cuyos nombres comienzan con este valor. No se puede utilizar junto con <var>$1user</var> o <var>$1userids</var>.", + "apihelp-query+usercontribs-param-userids": "Los identificadores de los usuarios para los cuales se desea recuperar las contribuciones. No se puede utilizar junto con <var>$1userids</var> o <var>$1userprefix</var>.", + "apihelp-query+usercontribs-param-namespace": "Enumerar solo las contribuciones en estos espacios de nombres.", "apihelp-query+usercontribs-param-prop": "Incluir piezas adicionales de información:", "apihelp-query+usercontribs-paramvalue-prop-ids": "Añade el identificador de página y el de revisión.", "apihelp-query+usercontribs-paramvalue-prop-title": "Agrega el título y el identificador del espacio de nombres de la página.", @@ -992,21 +1097,30 @@ "apihelp-query+usercontribs-paramvalue-prop-parsedcomment": "Añade el comentario analizado de la edición.", "apihelp-query+usercontribs-paramvalue-prop-size": "Añade el nuevo tamaño de la edición.", "apihelp-query+usercontribs-paramvalue-prop-sizediff": "Añade la diferencia de tamaño de la edición respecto de su progenitora.", + "apihelp-query+usercontribs-paramvalue-prop-flags": "Añade las marcas de la edición.", "apihelp-query+usercontribs-paramvalue-prop-patrolled": "Etiqueta ediciones verificadas.", "apihelp-query+usercontribs-paramvalue-prop-tags": "Lista las etiquetas para la edición.", "apihelp-query+usercontribs-param-show": "Mostrar solo los elementos que coinciden con estos criterios. Por ejemplo, solo ediciones no menores: <kbd>$2show=!minor</kbd>.\n\nSi se establece <kbd>$2show=patrolled</kbd> o <kbd>$2show=!patrolled</kbd>, las revisiones más antiguas que <var>[[mw:Manual:$wgRCMaxAge|$wgRCMaxAge]]</var> ($1 {{PLURAL:$1|segundo|segundos}}) no se mostrarán.", + "apihelp-query+usercontribs-param-tag": "Enumerar solo las revisiones con esta etiqueta.", + "apihelp-query+usercontribs-param-toponly": "Enumerar solo las modificaciones que sean las últimas revisiones.", "apihelp-query+usercontribs-example-user": "Mostrar contribuciones del usuario <kbd>Example</kbd>.", "apihelp-query+usercontribs-example-ipprefix": "Mostrar las contribuciones de todas las direcciones IP con el prefijo <kbd>192.0.2.</kbd>.", "apihelp-query+userinfo-description": "Obtener información sobre el usuario actual.", "apihelp-query+userinfo-param-prop": "Qué piezas de información incluir:", + "apihelp-query+userinfo-paramvalue-prop-blockinfo": "Etiqueta si el usuario está bloqueado, por quién y por qué motivo.", + "apihelp-query+userinfo-paramvalue-prop-hasmsg": "Añade una etiqueta <samp>messages</samp> si el usuario actual tiene mensajes pendientes.", "apihelp-query+userinfo-paramvalue-prop-groups": "Lista todos los grupos al que pertenece el usuario actual.", + "apihelp-query+userinfo-paramvalue-prop-implicitgroups": "Enumera todos los grupos a los que pertenece automáticamente el usuario actual.", "apihelp-query+userinfo-paramvalue-prop-rights": "Lista todos los permisos que tiene el usuario actual.", + "apihelp-query+userinfo-paramvalue-prop-changeablegroups": "Enumera los grupos a los que el usuario actual se puede unir o retirar.", "apihelp-query+userinfo-paramvalue-prop-options": "Lista todas las preferencias que haya establecido el usuario actual.", "apihelp-query+userinfo-paramvalue-prop-editcount": "Añade el número de ediciones del usuario actual.", "apihelp-query+userinfo-paramvalue-prop-ratelimits": "Lista todos los límites de velocidad aplicados al usuario actual.", "apihelp-query+userinfo-paramvalue-prop-realname": "Añade el nombre real del usuario.", "apihelp-query+userinfo-paramvalue-prop-email": "Añade la dirección de correo electrónico del usuario y la fecha de autenticación por correo.", + "apihelp-query+userinfo-paramvalue-prop-acceptlang": "Reenvía la cabecera <code>Accept-Language</code> enviada por el cliente en un formato estructurado.", "apihelp-query+userinfo-paramvalue-prop-registrationdate": "Añade la fecha de registro del usuario.", + "apihelp-query+userinfo-paramvalue-prop-unreadcount": "Añade el recuento de páginas no leídas de la lista de seguimiento del usuario (máximo $1, devuelve <samp>$2</samp> si el número es mayor).", "apihelp-query+userinfo-example-simple": "Obtener información sobre el usuario actual.", "apihelp-query+userinfo-example-data": "Obtener información adicional sobre el usuario actual.", "apihelp-query+users-description": "Obtener información sobre una lista de usuarios.", @@ -1017,12 +1131,20 @@ "apihelp-query+users-paramvalue-prop-rights": "Enumera todos los permisos que tiene cada usuario.", "apihelp-query+users-paramvalue-prop-editcount": "Añade el número de ediciones del usuario.", "apihelp-query+users-paramvalue-prop-registration": "Añade la marca de tiempo del registro del usuario.", + "apihelp-query+users-paramvalue-prop-emailable": "Marca si el usuario puede y quiere recibir correo electrónico a través de [[Special:Emailuser]].", "apihelp-query+users-paramvalue-prop-gender": "Etiqueta el género del usuario. Devuelve \"masculino\", \"femenino\" o \"desconocido\".", + "apihelp-query+users-paramvalue-prop-cancreate": "Indica si se puede crear una cuenta para nombres de usuario válidos pero no registrados.", + "apihelp-query+users-param-users": "Una lista de usuarios de los que obtener información.", + "apihelp-query+users-param-userids": "Una lista de identificadores de usuarios de los que obtener información.", + "apihelp-query+users-param-token": "Usa <kbd>[[Special:ApiHelp/query+tokens|action=query&meta=tokens]]</kbd> en su lugar.", "apihelp-query+users-example-simple": "Devolver información del usuario <kbd>Example</kbd>.", + "apihelp-query+watchlist-description": "Obtener los cambios recientes de las páginas de la lista de seguimiento del usuario actual.", "apihelp-query+watchlist-param-start": "El sello de tiempo para comenzar la enumeración", "apihelp-query+watchlist-param-end": "El sello de tiempo para finalizar la enumeración.", "apihelp-query+watchlist-param-namespace": "Filtrar cambios solamente a los espacios de nombres dados.", + "apihelp-query+watchlist-param-user": "Mostrar solamente los cambios de este usuario.", "apihelp-query+watchlist-param-excludeuser": "No listar cambios de este usuario.", + "apihelp-query+watchlist-param-limit": "Número de resultados que devolver en cada petición.", "apihelp-query+watchlist-param-prop": "Qué propiedades adicionales se obtendrán:", "apihelp-query+watchlist-paramvalue-prop-ids": "Añade identificadores de revisiones y de páginas.", "apihelp-query+watchlist-paramvalue-prop-title": "Añade el título de la página.", @@ -1036,40 +1158,76 @@ "apihelp-query+watchlist-paramvalue-prop-sizes": "Añade la longitud vieja y la nueva de la página.", "apihelp-query+watchlist-paramvalue-prop-notificationtimestamp": "Añade fecha y hora de cuando el usuario fue notificado por última vez acerca de la edición.", "apihelp-query+watchlist-paramvalue-prop-loginfo": "Añade información del registro cuando corresponda.", + "apihelp-query+watchlist-param-show": "Muestra solo los elementos que cumplan estos criterios. Por ejemplo, para ver solo ediciones menores realizadas por usuarios conectados, introduce $1show=minor|!anon.", "apihelp-query+watchlist-param-type": "Qué tipos de cambios mostrar:", "apihelp-query+watchlist-paramvalue-type-edit": "Ediciones comunes a páginas", "apihelp-query+watchlist-paramvalue-type-external": "Cambios externos.", "apihelp-query+watchlist-paramvalue-type-new": "Creaciones de páginas.", "apihelp-query+watchlist-paramvalue-type-log": "Entradas del registro.", + "apihelp-query+watchlist-paramvalue-type-categorize": "Cambios de pertenencia a categorías.", "apihelp-query+watchlist-param-owner": "Utilizado junto con $1token para acceder a la lista de seguimiento de otro usuario.", + "apihelp-query+watchlist-example-simple": "Enumera la última revisión de las páginas con cambios recientes de la lista de seguimiento del usuario actual.", + "apihelp-query+watchlist-example-props": "Obtener información adicional sobre la última revisión de páginas con cambios recientes en la lista de seguimiento del usuario actual.", + "apihelp-query+watchlist-example-allrev": "Obtener información sobre todos los cambios recientes de páginas de la lista de seguimiento del usuario actual.", + "apihelp-query+watchlist-example-generator": "Obtener información de página de las páginas con cambios recientes de la lista de seguimiento del usuario actual.", + "apihelp-query+watchlist-example-generator-rev": "Obtener información de revisión de los cambios recientes de páginas de la lista de seguimiento del usuario actual.", + "apihelp-query+watchlist-example-wlowner": "Enumerar la última revisión de páginas con cambios recientes de la lista de seguimiento del usuario <kbd>Example</kbd>.", "apihelp-query+watchlistraw-description": "Obtener todas las páginas de la lista de seguimiento del usuario actual.", "apihelp-query+watchlistraw-param-namespace": "Mostrar solamente las páginas de los espacios de nombres dados.", "apihelp-query+watchlistraw-param-limit": "Número de resultados que devolver en cada petición.", "apihelp-query+watchlistraw-param-prop": "Qué propiedades adicionales se obtendrán:", + "apihelp-query+watchlistraw-paramvalue-prop-changed": "Añade la marca de tiempo de la última notificación al usuario sobre la edición.", "apihelp-query+watchlistraw-param-show": "Sólo listar los elementos que cumplen estos criterios.", "apihelp-query+watchlistraw-param-owner": "Utilizado junto con $1token para acceder a la lista de seguimiento de otro usuario.", "apihelp-query+watchlistraw-param-dir": "La dirección en la que se listará.", "apihelp-query+watchlistraw-param-fromtitle": "Título (con el prefijo de espacio de nombres) desde el que se empezará a enumerar.", "apihelp-query+watchlistraw-param-totitle": "Título (con el prefijo de espacio de nombres) desde el que se dejará de enumerar.", "apihelp-query+watchlistraw-example-simple": "Listar las páginas de la lista de seguimiento del usuario actual.", + "apihelp-query+watchlistraw-example-generator": "Obtener información de las páginas de la lista de seguimiento del usuario actual.", "apihelp-removeauthenticationdata-description": "Elimina los datos de autentificación del usuario actual.", + "apihelp-removeauthenticationdata-example-simple": "Trata de eliminar los datos del usuario actual para <kbd>FooAuthenticationRequest</kbd>.", "apihelp-resetpassword-description": "Enviar un email de reinicialización de la contraseña a un usuario.", + "apihelp-resetpassword-param-user": "Usuario en proceso de reinicialización", + "apihelp-resetpassword-param-email": "Dirección de correo electrónico del usuario que se va a reinicializar", + "apihelp-resetpassword-example-user": "Enviar un correo de recuperación de contraseña al usuario <kbd>Ejemplo</kbd>.", + "apihelp-resetpassword-example-email": "Enviar un correo de recuperación de contraseña para todos los usuarios con dirección de correo electrónico <kbd>usuario@ejemplo.com</kbd>.", "apihelp-revisiondelete-description": "Eliminar y restaurar revisiones", + "apihelp-revisiondelete-param-target": "Título de la página para el borrado de la revisión, en caso de ser necesario para ese tipo.", + "apihelp-revisiondelete-param-ids": "Identificadores de las revisiones para borrar.", "apihelp-revisiondelete-param-hide": "Qué ocultar en cada revisión.", "apihelp-revisiondelete-param-show": "Qué mostrar en cada revisión.", "apihelp-revisiondelete-param-reason": "Motivo de la eliminación o restauración.", + "apihelp-revisiondelete-param-tags": "Etiquetas que aplicar a la entrada en el registro de borrados.", "apihelp-revisiondelete-example-revision": "Ocultar el contenido de la revisión <kbd>12345</kbd> de la página <kbd>Main Page</kbd>.", "apihelp-revisiondelete-example-log": "Ocultar todos los datos de la entrada de registro <kbd>67890</kbd> con el motivo <kbd>BLP violation</kbd>.", "apihelp-rollback-description": "Deshacer la última edición de la página.\n\nSi el último usuario que editó la página hizo varias ediciones consecutivas, todas ellas serán revertidas.", + "apihelp-rollback-param-title": "Título de la página que revertir. No se puede usar junto con <var>$1pageid</var>.", + "apihelp-rollback-param-pageid": "Identificador de la página que revertir. No se puede usar junto con <var>$1title</var>.", + "apihelp-rollback-param-tags": "Etiquetas que aplicar a la reversión.", + "apihelp-rollback-param-user": "Nombre del usuario cuyas ediciones se van a revertir.", "apihelp-rollback-param-summary": "Resumen de edición personalizado. Si se deja vacío se utilizará el predeterminado.", + "apihelp-rollback-param-markbot": "Marcar las acciones revertidas y la reversión como ediciones por bots.", + "apihelp-rollback-param-watchlist": "Añadir o borrar incondicionalmente la página de la lista de seguimiento del usuario actual, usar preferencias o no cambiar seguimiento.", + "apihelp-rollback-example-simple": "Revertir las últimas ediciones de la página <kbd>Main Page</kbd> por el usuario <kbd>Example</kbd>.", + "apihelp-rollback-example-summary": "Revertir las últimas ediciones de la página <kbd>Main Page</kbd> por el usuario de IP <kbd>192.0.2.5</kbd> con resumen <kbd>Reverting vandalism</kbd>, y marcar esas ediciones y la reversión como ediciones realizadas por bots.", "apihelp-rsd-description": "Exportar un esquema RSD (Really Simple Discovery; Descubrimiento Muy Simple).", "apihelp-rsd-example-simple": "Exportar el esquema RSD.", + "apihelp-setnotificationtimestamp-description": "Actualizar la marca de tiempo de notificación de las páginas en la lista de seguimiento.\n\nEsto afecta a la función de resaltado de las páginas modificadas en la lista de seguimiento y al envío de correo electrónico cuando la preferencia \"{{int:tog-enotifwatchlistpages}}\" está habilitada.", + "apihelp-setnotificationtimestamp-param-entirewatchlist": "Trabajar en todas las páginas en seguimiento.", + "apihelp-setnotificationtimestamp-param-timestamp": "Marca de tiempo en la que fijar la marca de tiempo de notificación.", + "apihelp-setnotificationtimestamp-param-torevid": "Revisión a la que fijar la marca de tiempo de notificación (una sola página).", + "apihelp-setnotificationtimestamp-param-newerthanrevid": "Revisión a la que fijar la marca de tiempo de notificación más reciente (una sola página).", + "apihelp-setnotificationtimestamp-example-all": "Restablecer el estado de notificación para la totalidad de la lista de seguimiento.", + "apihelp-setnotificationtimestamp-example-page": "Restablecer el estado de notificación de <kbd>Main page</kbd>.", + "apihelp-setnotificationtimestamp-example-pagetimestamp": "Fijar la marca de tiempo de notificación de <kbd>Main page</kbd> para que todas las ediciones posteriores al 1 de enero de 2012 estén consideradas como no vistas.", + "apihelp-setnotificationtimestamp-example-allpages": "Restablecer el estado de notificación de las páginas del espacio de nombres <kbd>{{ns:user}}</kbd>.", "apihelp-setpagelanguage-description": "Cambiar el idioma de una página.", "apihelp-setpagelanguage-description-disabled": "En este wiki no se permite modificar el idioma de las páginas.\n\nActiva <var>[[mw:Manual:$wgPageLanguageUseDB|$wgPageLanguageUseDB]]</var> para utilizar esta acción.", "apihelp-setpagelanguage-param-title": "Título de la página cuyo idioma deseas cambiar. No se puede usar junto con <var>$1pageid</var>.", "apihelp-setpagelanguage-param-pageid": "Identificador de la página cuyo idioma deseas cambiar. No se puede usar junto con <var>$1title</var>.", "apihelp-setpagelanguage-param-lang": "Código del idioma al que se desea cambiar la página. Usa <kbd>default</kbd> para restablecer la página al idioma predeterminado para el contenido del wiki.", "apihelp-setpagelanguage-param-reason": "Motivo del cambio.", + "apihelp-setpagelanguage-param-tags": "Cambiar las etiquetas que aplicar a la entrada de registro resultante de esta acción.", "apihelp-setpagelanguage-example-language": "Cambiar el idioma de <kbd>Main Page</kbd> al euskera.", "apihelp-setpagelanguage-example-default": "Cambiar el idioma de la página con identificador 123 al idioma predeterminado para el contenido del wiki.", "apihelp-stashedit-param-title": "Título de la página que se está editando.", @@ -1080,53 +1238,98 @@ "apihelp-stashedit-param-contentformat": "Formato de serialización de contenido utilizado para el texto de entrada.", "apihelp-stashedit-param-baserevid": "Identificador de la revisión de base.", "apihelp-stashedit-param-summary": "Resumen de cambios.", + "apihelp-tag-description": "Añadir o borrar etiquetas de modificación de revisiones individuales o entradas de registro.", + "apihelp-tag-param-rcid": "Uno o más identificadores de cambios recientes a los que añadir o borrar la etiqueta.", + "apihelp-tag-param-revid": "Uno o más identificadores de revisión a los que añadir o borrar la etiqueta.", "apihelp-tag-param-logid": "Uno o más identificadores de entradas del registro a los que agregar o eliminar la etiqueta.", + "apihelp-tag-param-add": "Etiquetas que añadir. Solo se pueden añadir etiquetas definidas manualmente.", + "apihelp-tag-param-remove": "Etiquetas que borrar. Solo se pueden borrar etiquetas definidas manualmente o completamente indefinidas.", "apihelp-tag-param-reason": "Motivo del cambio.", + "apihelp-tag-param-tags": "Etiquetas que aplicar a la entrada de registro que se generará como resultado de esta acción.", "apihelp-tag-example-rev": "Añadir la etiqueta <kbd>vandalism</kbd> al identificador de revisión 123 sin especificar un motivo", "apihelp-tag-example-log": "Eliminar la etiqueta <kbd>spam</kbd> de la entrada del registro con identificador 123 con el motivo <kbd>Wrongly applied</kbd>", "apihelp-unblock-description": "Desbloquear un usuario.", - "apihelp-unblock-param-user": "Nombre de usuario, dirección IP o intervalo de direcciones IP para desbloquear. No se puede utilizar junto con <var>$1id</var> o <var>$luserid</var>.", + "apihelp-unblock-param-id": "Identificador del bloqueo que se desea desbloquear (obtenido mediante <kbd>list=blocks</kbd>). No se puede usar junto con with <var>$1user</var> o <var>$1userid</var>.", + "apihelp-unblock-param-user": "Nombre de usuario, dirección IP o intervalo de direcciones IP para desbloquear. No se puede utilizar junto con <var>$1id</var> o <var>$1userid</var>.", "apihelp-unblock-param-userid": "ID de usuario que desbloquear. No se puede utilizar junto con <var>$1id</var> o <var>$1user</var>.", "apihelp-unblock-param-reason": "Motivo del desbloqueo.", + "apihelp-unblock-param-tags": "Cambiar las etiquetas que aplicar a la entrada en el registro de bloqueos.", "apihelp-unblock-example-id": "Desbloquear el bloqueo de ID #<kbd>105</kbd>", "apihelp-unblock-example-user": "Desbloquear al usuario <kbd>Bob</kbd> con el motivo <kbd>Sorry Bob</kbd>", "apihelp-undelete-param-title": "Título de la página que restaurar.", "apihelp-undelete-param-reason": "Motivo de la restauración.", + "apihelp-undelete-param-tags": "Cambiar las etiquetas para aplicar a la entrada en el registro de borrados.", + "apihelp-undelete-param-timestamps": "Marcas de tiempo de las revisiones que se desea restaurar. Si tanto <var>$1timestamps</var> como <var>$1fileids</var> están vacíos, se restaurarán todas.", + "apihelp-undelete-param-fileids": "Identificadores de las revisiones que se desea restaurar. Si tanto <var>$1timestamps</var> como <var>$1fileids</var> están vacíos, se restaurarán todas.", + "apihelp-undelete-example-page": "Restaurar la página <kbd>Main page</kbd>.", "apihelp-undelete-example-revisions": "Restaurar dos revisiones de la página <kbd>Main Page</kbd>.", "apihelp-upload-param-filename": "Nombre del archivo de destino.", "apihelp-upload-param-tags": "Cambiar etiquetas para aplicar a la entrada del registro de subidas y a la revisión de página de archivo.", + "apihelp-upload-param-text": "Texto de página inicial para archivos nuevos.", "apihelp-upload-param-watch": "Vigilar la página.", + "apihelp-upload-param-watchlist": "Añadir o borrar incondicionalmente la página de la lista de seguimiento del usuario actual, utilizar las preferencias o no cambiar el estado de seguimiento.", "apihelp-upload-param-ignorewarnings": "Ignorar las advertencias.", "apihelp-upload-param-file": "Contenido del archivo.", "apihelp-upload-param-url": "URL de la que obtener el archivo.", + "apihelp-upload-param-sessionkey": "Idéntico a $1filekey, mantenido por razones de retrocompatibilidad.", "apihelp-upload-param-filesize": "Tamaño de archivo total de la carga.", "apihelp-upload-param-offset": "Posición del fragmento en bytes.", "apihelp-upload-param-chunk": "Contenido del fragmento.", "apihelp-upload-param-async": "Realizar de forma asíncrona las operaciones de archivo potencialmente grandes cuando sea posible.", "apihelp-upload-example-url": "Subir desde una URL.", + "apihelp-upload-example-filekey": "Completar una subida que falló debido a advertencias.", + "apihelp-userrights-description": "Cambiar la pertenencia a grupos de un usuario.", "apihelp-userrights-param-user": "Nombre de usuario.", "apihelp-userrights-param-userid": "ID de usuario.", - "apihelp-userrights-param-add": "Agregar el usuario a estos grupos.", + "apihelp-userrights-param-add": "Agregar el usuario a estos grupos, o, si ya es miembro, actualizar la fecha de expiración de su pertenencia a ese grupo.", + "apihelp-userrights-param-expiry": "Marcas de tiempo de expiración. Pueden ser relativas (por ejemplo, <kbd>5 months</kbd> o <kbd>2 weeks</kbd>) o absolutas (por ejemplo, <kbd>2014-09-18T12:34:56Z</kbd>). Si sólo se fija una marca de tiempo, se utilizará para todos los grupos que se pasen al parámetro <var>$1añadir</var>. Usa <kbd>infinite</kbd>, <kbd>indefinite</kbd>, <kbd>infinity</kbd>, o <kbd>never</kbd> para que la pertenencia al grupo no tenga fecha de expiración.", "apihelp-userrights-param-remove": "Eliminar el usuario de estos grupos.", "apihelp-userrights-param-reason": "Motivo del cambio.", "apihelp-userrights-param-tags": "Cambia las etiquetas que aplicar a la entrada del registro de derechos del usuario.", "apihelp-userrights-example-user": "Agregar al usuario <kbd>FooBot</kbd> al grupo <kbd>bot</kbd> y eliminarlo de los grupos <kbd>sysop</kbd> y <kbd>bureaucrat</kbd>.", + "apihelp-userrights-example-userid": "Añade el usuario con identificador <kbd>123</kbd> al grupo <kbd>bot</kbd>, y lo borra de los grupos <kbd>sysop</kbd> y <kbd>bureaucrat</kbd>.", + "apihelp-userrights-example-expiry": "Añadir al usuario <kbd>SometimeSysop</kbd> al grupo <kbd>sysop</kbd> por 1 mes.", + "apihelp-validatepassword-description": "Valida una contraseña contra las políticas de contraseñas del wiki.\n\nLa validez es <samp>Good</samp> si la contraseña es aceptable, <samp>Change</samp> y la contraseña se puede usar para iniciar sesión pero debe cambiarse o <samp>Invalid</samp> si la contraseña no se puede usar.", + "apihelp-validatepassword-param-password": "Contraseña para validar.", + "apihelp-validatepassword-param-user": "Nombre de usuario, para pruebas de creación de cuentas. El usuario nombrado no debe existir.", + "apihelp-validatepassword-param-email": "Dirección de correo electrónico, para pruebas de creación de cuentas.", + "apihelp-validatepassword-param-realname": "Nombre real, para pruebas de creación de cuentas.", + "apihelp-validatepassword-example-1": "Validar la contraseña <kbd>foobar</kbd> para el usuario actual.", + "apihelp-validatepassword-example-2": "Validar la contraseña <kbd>qwerty</kbd> para la creación del usuario <kbd>Example</kbd>.", + "apihelp-watch-description": "Añadir o borrar páginas de la lista de seguimiento del usuario actual.", + "apihelp-watch-param-title": "La página que seguir o dejar de seguir. Usa <var>$1titles</var> en su lugar.", + "apihelp-watch-param-unwatch": "Si se define, en vez de seguir la página, se dejará de seguir.", "apihelp-watch-example-watch": "Vigilar la página <kbd>Main Page</kbd>.", "apihelp-watch-example-unwatch": "Dejar de vigilar la <kbd>Main Page</kbd>.", + "apihelp-watch-example-generator": "Seguir las primeras páginas del espacio de nombres principal.", "apihelp-format-example-generic": "Devolver el resultado de la consulta en formato $1.", + "apihelp-format-param-wrappedhtml": "Devolver el HTML con resaltado sintáctico y los módulos ResourceLoader asociados en forma de objeto JSON.", "apihelp-json-description": "Extraer los datos de salida en formato JSON.", "apihelp-json-param-callback": "Si se especifica, envuelve la salida dentro de una llamada a una función dada. Por motivos de seguridad, cualquier dato específico del usuario estará restringido.", "apihelp-json-param-utf8": "Si se especifica, codifica la mayoría (pero no todos) de los caracteres no pertenecientes a ASCII como UTF-8 en lugar de reemplazarlos por secuencias de escape hexadecimal. Toma el comportamiento por defecto si <var>formatversion</var> no es <kbd>1</kbd>.", "apihelp-json-param-ascii": "Si se especifica, codifica todos los caracteres no pertenecientes a ASCII mediante secuencias de escape hexadecimal. Toma el comportamiento por defecto si <var>formatversion</var> no es <kbd>1</kbd>.", "apihelp-json-param-formatversion": "Formato de salida:\n;1: Formato retrocompatible (booleanos con estilo XML, claves <samp>*</samp> para nodos de contenido, etc.).\n;2: Formato moderno experimental. ¡Atención, las especificaciones pueden cambiar!\n;latest: Utiliza el último formato (actualmente <kbd>2</kbd>). Puede cambiar sin aviso.", + "apihelp-jsonfm-description": "Producir los datos de salida en formato JSON (con resaltado sintáctico en HTML).", "apihelp-none-description": "No extraer nada.", "apihelp-php-description": "Extraer los datos de salida en formato serializado PHP.", + "apihelp-php-param-formatversion": "Formato de salida:\n;1: Formato retrocompatible (booleanos con estilo XML, claves <samp>*</samp> para nodos de contenido, etc.).\n;2: Formato moderno experimental. ¡Atención, las especificaciones pueden cambiar!\n;latest: Utilizar el último formato (actualmente <kbd>2</kbd>). Puede cambiar sin aviso.", + "apihelp-phpfm-description": "Producir los datos de salida en formato PHP serializado (con resaltado sintáctico en HTML).", "apihelp-rawfm-description": "Extraer los datos de salida, incluidos los elementos de depuración, en formato JSON (embellecido en HTML).", + "apihelp-xml-description": "Producir los datos de salida en formato XML.", "apihelp-xml-param-xslt": "Si se especifica, añade la página nombrada como una hoja de estilo XSL. El valor debe ser un título en el espacio de nombres {{ns:MediaWiki}} que termine en <code>.xsl</code>.", "apihelp-xml-param-includexmlnamespace": "Si se especifica, añade un espacio de nombres XML.", + "apihelp-xmlfm-description": "Producir los datos de salida en formato XML (con resaltado sintáctico en HTML).", "api-format-title": "Resultado de la API de MediaWiki", "api-format-prettyprint-header": "Esta es la representación en HTML del formato $1. HTML es adecuado para realizar tareas de depuración, pero no para utilizarlo en aplicaciones.\n\nUtiliza el parámetro <var>format</var> para modificar el formato de salida. Para ver la representación no HTML del formato $1, emplea <kbd>format=$2</kbd>.\n\nPara obtener más información, consulta la [[mw:API|documentación completa]] o la [[Special:ApiHelp/main|ayuda de API]].", + "api-format-prettyprint-header-only-html": "Esta es una representación en HTML destinada a la depuración, y no es adecuada para el uso de la aplicación.\n\nVéase la [[mw:API|documentación completa]] o la [[Special:ApiHelp/main|página de ayuda de la API]] para más información.", "api-format-prettyprint-status": "Esta respuesta se devolvería con el estado HTTP $1 $2.", + "api-pageset-param-titles": "Una lista de títulos en los que trabajar.", + "api-pageset-param-pageids": "Una lista de identificadores de páginas en las que trabajar.", + "api-pageset-param-revids": "Una lista de identificadores de revisiones en las que trabajar.", + "api-pageset-param-generator": "Obtener la lista de páginas en las que trabajar mediante la ejecución del módulo de consulta especificado.\n\n<strong>Nota:</strong> Los nombres de los parámetros del generador deben prefijarse con una «g», véanse los ejemplos.", + "api-pageset-param-redirects-generator": "Resolver automáticamente las redirecciones en <var>$1titles</var>, <var>$1pageids</var>, y <var>$1revids</var> y en las páginas devueltas por <var>$1generator</var>.", + "api-pageset-param-redirects-nogenerator": "Resolver automáticamente las redirecciones en <var>$1titles</var>, <var>$1pageids</var> y <var>$1revids</var>.", + "api-pageset-param-converttitles": "Convertir los títulos a otras variantes, si es necesario. Solo funciona si el idioma del contenido del wiki admite la conversión entre variantes. La conversión entre variantes está habilitada en idiomas tales como $1.", "api-help-title": "Ayuda de la API de MediaWiki", "api-help-lead": "Esta es una página de documentación autogenerada de la API de MediaWiki.\n\nDocumentación y ejemplos: https://www.mediawiki.org/wiki/API", "api-help-main-header": "Módulo principal", @@ -1145,13 +1348,13 @@ "api-help-param-deprecated": "En desuso.", "api-help-param-required": "Este parámetro es obligatorio.", "api-help-datatypes-header": "Tipos de datos", - "api-help-datatypes": "Algunos tipos de parámetros en las solicitudes de API necesita más explicación:\n;boolean\n:Los parámetros booleanos trabajo como HTML casillas de verificación: si el parámetro se especifica, independientemente de su valor, se considera verdadero. Para un valor false, se omite el parámetro completo.\n;marca de tiempo\n:Las marcas de tiempo se puede especificar en varios formatos. ISO 8601 la fecha y la hora se recomienda. Todas las horas están en UTC, la inclusión de la zona horaria es ignorado.\n:* ISO 8601 la fecha y la hora, <kbd><var>2001</var>-<var>01</var>-<var>15</var>T<var>14</var>:<var>56</var>:<var>00</var>Z</kbd> (signos de puntuación y <kbd>Z</kbd> son opcionales)\n:* ISO 8601 la fecha y la hora (se omite) fracciones de segundos, <kbd><var>2001</var>-<var>01</var>-<var>15</var>T<var>14</var>:<var>56</var>:<var>00</var>.<var>00001</var>Z</kbd> (guiones, dos puntos, y, <kbd>Z</kbd> son opcionales)\n:* MediaWiki formato, <kbd><var>2001</var><var>01</var><var>15</var><var>14</var><var>56</var><var>00</var></kbd>\n:* Genérico formato numérico, <kbd><var>2001</var>-<var>01</var>-<var>15</var> <var>14</var>:<var>56</var>:<var>00</var></kbd> (opcional en la zona horaria de <kbd>GMT</kbd>, <kbd>+<var>##</var></kbd>, o <kbd>-<var>##</var></kbd> se omite)\n:* El formato EXIF, <kbd><var>2001</var>:<var>01</var>:<var>15</var> <var>14</var>:<var>56</var>:<var>00</var></kbd>\n:*Formato RFC 2822 (zona horaria se puede omitir), <kbd><var>Mon</var>, <var>15</var> <var>Ene</var> <var>2001</var> <var>14</var>:<var>56</var>:<var>00</var></kbd>\n:* Formato RFC 850 (zona horaria se puede omitir), <kbd><var>lunes</var>, <var>15</var>-<var>Ene</var>-<var>2001</var> <var>14</var>:<var>56</var>:<var>00</var></kbd>\n:* C ctime formato, <kbd><var>Mon</var> <var>Jan</var> <var>15</var> <var>14</var>:<var>56</var>:<var>00</var> <var>de 2001</var></kbd>\n:* Segundos desde 1970-01-01T00:00:00Z como la 1 a la 13 dígito entero (excepto <kbd>0</kbd>)\n:* La cadena de <kbd>ahora</kbd>", + "api-help-datatypes": "Las entradas en MediaWiki deberían estar en UTF-8 según la norma NFC. MediaWiki puede tratar de convertir otros formatos, pero esto puede provocar errores en algunas operaciones (tales como las [[Special:ApiHelp/edit|ediciones]] con controles MD5).\n\nAlgunos tipos de parámetros en las solicitudes de API requieren de una explicación más detallada:\n;boolean\n:Los parámetros booleanos trabajo como cajas de verificación de HTML: si el parámetro está definido, independientemente de su valor, se considera verdadero. Para un valor falso, se debe omitir el parámetro por completo.\n;marca de tiempo\n:Las marcas de tiempo se pueden definir en varios formatos. Se recomienda seguir la norma ISO 8601 de fecha y hora. Todas las horas están en UTC, ignorándose cualquier indicación de zona horaria.\n:* Fecha y hora en ISO 8601, <kbd><var>2001</var>-<var>01</var>-<var>15</var>T<var>14</var>:<var>56</var>:<var>00</var>Z</kbd> (los signos de puntuación y la <kbd>Z</kbd> son opcionales)\n:* Fecha y hora en ISO 8601 con fracciones de segundo (que se omiten), <kbd><var>2001</var>-<var>01</var>-<var>15</var>T<var>14</var>:<var>56</var>:<var>00</var>.<var>00001</var>Z</kbd> (los guiones, los dos puntos y la <kbd>Z</kbd> son opcionales)\n:* Formato MediaWiki, <kbd><var>2001</var><var>01</var><var>15</var><var>14</var><var>56</var><var>00</var></kbd>\n:* Formato genérico de número, <kbd><var>2001</var>-<var>01</var>-<var>15</var> <var>14</var>:<var>56</var>:<var>00</var></kbd> (la zona horaria opcional, sea <kbd>GMT</kbd>, <kbd>+<var>##</var></kbd> o <kbd>-<var>##</var></kbd> se omite)\n:* Formato EXIF, <kbd><var>2001</var>:<var>01</var>:<var>15</var> <var>14</var>:<var>56</var>:<var>00</var></kbd>\n:*Formato RFC 2822 (la zona horaria es opcional), <kbd><var>lun</var>, <var>15</var> <var>ene</var> <var>2001</var> <var>14</var>:<var>56</var>:<var>00</var></kbd>\n:* Formato RFC 850 (la zona horaria es opcional), <kbd><var>lunes</var>, <var>15</var>-<var>ene</var>-<var>2001</var> <var>14</var>:<var>56</var>:<var>00</var></kbd>\n:* Formato ctime de C, <kbd><var>Mon</var> <var>Jan</var> <var>15</var> <var>14</var>:<var>56</var>:<var>00</var> <var>2001</var></kbd>\n:* Número de segundos desde 1970-01-01T00:00:00Z en forma de número entero de entre 1 y 13 cifras (sin <kbd>0</kbd>)\n:* La cadena <kbd>now</kbd>\n\n;separador alternativo de valores múltiples\n:Los parámetros que toman valores múltiples se envían normalmente utilizando la barra vertical para separar los valores, p. ej., <kbd>param=valor1|valor2</kbd> o <kbd>param=valor1%7Cvalor2</kbd>. Si un valor tiene que contener el carácter de barra vertical, utiliza U+001F (separador de unidades) como separador ''y'' prefija el valor con, p. ej. <kbd>param=%1Fvalor1%1Fvalor2</kbd>.", "api-help-param-type-limit": "Tipo: entero o <kbd>max</kbd>", "api-help-param-type-integer": "Tipo: {{PLURAL:$1|1=entero|2=lista de enteros}}", "api-help-param-type-boolean": "Tipo: booleano/lógico ([[Special:ApiHelp/main#main/datatypes|detalles]])", "api-help-param-type-timestamp": "Tipo: {{PLURAL:$1|1=timestamp|2=lista de timestamps}} ([[Special:ApiHelp/main#main/datatypes|formatos permitidos]])", "api-help-param-type-user": "Tipo: {{PLURAL:$1|1=nombre de usuario|2=lista de nombres de usuarios}}", - "api-help-param-list": "{{PLURAL:$1|1=Uno de los siguientes valores|2=Valores (separados por <kbd>{{!}}</kbd>)}}: $2", + "api-help-param-list": "{{PLURAL:$1|1=Uno de los siguientes valores|2=Valores (separados por <kbd>{{!}}</kbd> u [[Special:ApiHelp/main#main/datatypes|otro separador]])}}: $2", "api-help-param-list-can-be-empty": "{{PLURAL:$1|0=Debe estar vacío|Puede estar vacío, o $2}}", "api-help-param-limit": "No se permite más de $1.", "api-help-param-limit2": "No se permite más de $1 ($2 para los bots).", @@ -1163,19 +1366,33 @@ "api-help-param-multi-all": "Para especificar todos los valores, utiliza <kbd>$1</kbd>.", "api-help-param-default": "Predeterminado: $1", "api-help-param-default-empty": "Predeterminado: <span class=\"apihelp-empty\">(vacío)</span>", + "api-help-param-disabled-in-miser-mode": "Deshabilitado debido al [[mw:Manual:$wgMiserMode|modo avaro]].", + "api-help-param-limited-in-miser-mode": "<strong>Nota:</strong> Debido al [[mw:Manual:$wgMiserMode|modo avaro]], usar esto puede dar lugar a que se devuelvan menos de <var>$1limit</var> antes de continuar. En casos extremos, podrían devolverse cero resultados.", + "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": "<span class=\"apihelp-empty\">(sin descripción)</span>", "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", + "api-help-right-apihighlimits": "Usa límites más altos para consultas a través de la API (consultas lentas: $1; consultas rápidas: $2). Los límites para las consultas lentas también se aplican a los parámetros multivalorados.", + "api-help-open-in-apisandbox": "<small>[abrir en la zona de pruebas]</small>", "api-help-authmanagerhelper-messageformat": "Formato utilizado para los mensajes devueltos.", + "api-help-authmanagerhelper-mergerequestfields": "Combinar la información de los campos para todas las peticiones de autentificación en una matriz.", "api-help-authmanagerhelper-preservestate": "Preservar el estado de un intento fallido anterior de inicio de sesión, si es posible.", + "apierror-allimages-redirect": "Usar <kbd>gaifilterredir=nonredirects</kbd> en lugar de <var>redirects</var> cuando se use <kbd>allimages</kbd> como generador.", + "apierror-allpages-generator-redirects": "Usar <kbd>gaifilterredir=nonredirects</kbd> en lugar de <var>redirects</var> cuando se use <kbd>allpages</kbd> como generador.", + "apierror-appendnotsupported": "No se puede añadir a las páginas que utilizan el modelo de contenido $1.", + "apierror-articleexists": "El artículo que intentaste crear ya estaba creado.", "apierror-assertbotfailed": "La aserción de que el usuario tiene el derecho <code>bot</code> falló.", "apierror-assertnameduserfailed": "La aserción de que el usuario es «$1» falló.", "apierror-assertuserfailed": "La aserción de que el usuario está conectado falló.", "apierror-autoblocked": "Tu dirección IP ha sido bloqueada automáticamente porque fue utilizada por un usuario bloqueado.", "apierror-badconfig-resulttoosmall": "El valor de <code>$wgAPIMaxResultSize</code> en este wiki es demasiado pequeño como para contener información básica de resultados.", + "apierror-badcontinue": "Parámetro continue no válido. Debes pasar el valor original devuelto por la consulta anterior.", "apierror-baddiff": "La comparación no puede recuperarse. Una o ambas revisiones no existen o no tienes permiso para verlas.", + "apierror-baddiffto": "<var>$1diffto</var> debe fijarse a un número no negativo, <kbd>prev</kbd>, <kbd>next</kbd> or <kbd>cur</kbd>.", + "apierror-badformat-generic": "El formato solicitado $1 no es compatible con el modelo de contenido $2.", + "apierror-badformat": "El formato solicitado $1 no es compatible con el modelo de contenido $2 utilizado por $3.", "apierror-badgenerator-notgenerator": "El módulo <kbd>$1</kbd> no puede utilizarse como un generador.", "apierror-badgenerator-unknown": "<kbd>generator=$1</kbd> desconocido.", "apierror-badip": "El parámetro IP no es válido.", @@ -1185,72 +1402,162 @@ "apierror-badparameter": "Valor no válido para el parámetro <var>$1</var>.", "apierror-badquery": "La consulta no es válida.", "apierror-badtimestamp": "Valor no válido \"$2\" para el parámetro de marca de tiempo <var>$1</var>.", + "apierror-badupload": "El parámetro de subida de archivo <var>$1</var> no es una subida de archivo. Asegúrate de usar <code>multipart/form-data</code> para tu POST e introduce un nombre de archivo en la cabecera <code>Content-Disposition</code>.", "apierror-badurl": "Valor no válido \"$2\" para el parámetro de URL <var>$1</var>.", "apierror-baduser": "Valor no válido \"$2\" para el parámetro de usuario <var>$1</var>.", + "apierror-badvalue-notmultivalue": "El separador multivalor U+001F solo se puede utilizar en parámetros multivalorados.", "apierror-blockedfrommail": "Se te ha bloqueado de enviar email.", "apierror-blocked": "Se te ha bloqueado de editar.", + "apierror-botsnotsupported": "Esta interfaz no está disponible para bots.", + "apierror-cannotreauthenticate": "Esta acción no está disponible, ya que tu identidad no se puede verificar.", + "apierror-cannotviewtitle": "No tienes permiso para ver $1.", "apierror-cantblock-email": "No tienes permiso para bloquear a los usuarios el envío de correo electrónico a través de la wiki.", "apierror-cantblock": "No tienes permiso para bloquear usuarios.", "apierror-cantchangecontentmodel": "No tienes permiso para cambiar el modelo de contenido de una página.", "apierror-canthide": "No tienes permiso para ocultar nombres de usuario del registro de bloqueos.", "apierror-cantimport-upload": "No tienes permiso para importar páginas subidas.", "apierror-cantimport": "No tienes permiso para importar páginas.", + "apierror-cantoverwrite-sharedfile": "El fichero objetivo existe en un repositorio compartido y no tienes permiso para reemplazarlo.", + "apierror-cantsend": "No estás conectado, no tienes una dirección de correo electrónico confirmada o no tienes permiso para enviar correo electrónico a otros usuarios, así que no puedes enviar correo electrónico.", + "apierror-cantundelete": "No se ha podido restaurar: puede que las revisiones solicitadas no existan o que ya se hayan restaurado.", + "apierror-changeauth-norequest": "No se ha podido crear la petición de modificación.", + "apierror-compare-inputneeded": "Se necesita un título, un identificador de página o un número de revisión tanto para el parámetro <var>from</var> como para el parámetro <var>to</var>.", + "apierror-contentserializationexception": "La serialización de contenido falló: $1", + "apierror-contenttoobig": "El contenido que has suministrado supera el tamaño máximo de archivo de $1 {{PLURAL:$1|kilobytes|kilobytes}}.", + "apierror-create-titleexists": "Los títulos existentes no se pueden proteger con <kbd>create</kbd>.", + "apierror-csp-report": "Error de procesamiento del informe CSP: $1.", "apierror-databaseerror": "[$1] Error en la consulta de la base de datos.", "apierror-deletedrevs-param-not-1-2": "El parámetro <var>$1</var> no se puede utilizar en los modos 1 o 2.", + "apierror-deletedrevs-param-not-3": "El parámetro <var>$1</var> no se puede usar en modo 3.", + "apierror-emptynewsection": "Crear secciones vacías no es posible.", + "apierror-emptypage": "Crear páginas vacías no está permitido.", "apierror-exceptioncaught": "[$1] Excepción capturada: $2", "apierror-filedoesnotexist": "El archivo no existe.", + "apierror-fileexists-sharedrepo-perm": "El archivo objetivo existe en un repositorio compartido. Usa el parámetro <var>ignorewarnings</var> para reemplazarlo.", + "apierror-filenopath": "No se pudo obtener la ruta local del archivo.", "apierror-filetypecannotberotated": "El tipo de archivo no se puede girar.", + "apierror-formatphp": "Esta respuesta no se puede representar con <kbd>format=php</kbd>. Véase https://phabricator.wikimedia.org/T68776.", "apierror-imageusage-badtitle": "El título de <kbd>$1</kbd> debe ser un archivo.", "apierror-import-unknownerror": "Error desconocido en la importación: $1.", + "apierror-integeroutofrange-abovebotmax": "<var>$1</var> no puede ser mayor que $2 (fijado a $3) para bots o administradores de sistema.", + "apierror-integeroutofrange-abovemax": "<var>$1</var> no puede ser mayor que $2 (fijado a $3) para usuarios.", + "apierror-integeroutofrange-belowminimum": "<var>$1</var> no puede ser menor que $2 (fijado a $3).", + "apierror-invalidcategory": "El nombre de la categoría que has introducido no es válida.", "apierror-invalidexpiry": "Tiempo de expiración \"$1\" no válido.", + "apierror-invalidlang": "Código de idioma no válido para el parámetro <var>$1</var>.", "apierror-invalidparammix-cannotusewith": "El parámetro <kbd>$1</kbd> no se puede utilizar junto con <kbd>$2</kbd>.", "apierror-invalidparammix-mustusewith": "El parámetro <kbd>$1</kbd> solo se puede utilizar junto con <kbd>$2</kbd>.", "apierror-invalidparammix-parse-new-section": "<kbd>section=new</kbd> no se puede combinar con los parámetros <var>oldid</var>, <var>pageid</var> y <var>page</var>. Por favor, utiliza <var>title</var> y <var>text</var>.", "apierror-invalidparammix": "{{PLURAL:$2|Los parámetros}} $1 no se pueden utilizar juntos.", - "apierror-invalidsection": "El parámetro de sección debe ser un ID de sección válido, o bien <kbd>new</kbd>.", + "apierror-invalidsection": "El parámetro <var>section</var> debe ser un identificador de sección válido, o bien <kbd>new</kbd>.", "apierror-invalidsha1base36hash": "El hash SHA1Base36 proporcionado no es válido.", "apierror-invalidsha1hash": "El hash SHA1 proporcionado no es válido.", "apierror-invalidtitle": "Título incorrecto \"$1\".", + "apierror-invalidurlparam": "Valor no válido para <var>$1urlparam</var> (<kbd>$2=$3</kbd>).", "apierror-invaliduser": "Nombre de usuario «$1» no válido.", "apierror-invaliduserid": "El identificador de usuario <var>$1</var> no es válido.", + "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.", "apierror-missingparam-one-of": "{{PLURAL:$2|El parámetro|Uno de los parámetros}} $1 es necesario.", "apierror-missingparam": "Se debe establecer el parámetro <var>$1</var>.", "apierror-missingrev-pageid": "No hay ninguna revisión actual de la página con ID $1.", + "apierror-missingtitle-createonly": "Los títulos faltantes solo se pueden proteger con <kbd>create</kbd>.", "apierror-missingtitle": "El título especificado no existe.", "apierror-missingtitle-byname": "La página $1 no existe.", "apierror-moduledisabled": "El módulo <kbd>$1</kbd> ha sido deshabilitado.", "apierror-multival-only-one-of": "Solo {{PLURAL:$3|se permite el valor|se permiten los valores}} $2 para el parámetro <var>$1</var>.", "apierror-multival-only-one": "Solo se permite un valor para el parámetro <var>$1</var>.", "apierror-multpages": "<var>$1</var> no se puede utilizar más que con una sola página.", + "apierror-mustbeloggedin-changeauth": "Debes estar conectado para poder cambiar los datos de autentificación.", + "apierror-mustbeloggedin-generic": "Debes estar conectado.", + "apierror-mustbeloggedin-linkaccounts": "Debes estar conectado para enlazar cuentas.", + "apierror-mustbeloggedin-removeauth": "Debes estar conectado para borrar datos de autentificación.", + "apierror-mustbeloggedin": "Debes estar conectado para $1.", + "apierror-mustbeposted": "El módulo <kbd>$1</kbd> requiere una petición POST.", + "apierror-mustpostparams": "Se {{PLURAL:$2|encontró el siguiente parámetro|encontraron los siguientes parámetros}} en la cadena de la consulta, pero deben estar en el cuerpo del POST: $1.", + "apierror-noapiwrite": "La edición de este wiki a través de la API está deshabilitada. Asegúrate de que la declaración <code>$wgEnableWriteAPI=true;</code> está incluida en el archivo <code>LocalSettings.php</code> del wiki.", + "apierror-nochanges": "No se solicitó ningún cambio.", + "apierror-nodeleteablefile": "No existe tal versión antigua del archivo.", + "apierror-no-direct-editing": "La edición directa a través de la API no es compatible con el modelo de contenido $1 utilizado por $2.", "apierror-noedit-anon": "Los usuarios anónimos no pueden editar páginas.", "apierror-noedit": "No tienes permiso para editar páginas.", + "apierror-noimageredirect-anon": "Los usuarios anónimos no pueden crear redirecciones de imágenes.", + "apierror-noimageredirect": "No tienes permiso para crear redirecciones de imágenes.", + "apierror-nosuchlogid": "No hay ninguna entrada de registro con identificador $1.", + "apierror-nosuchpageid": "No hay ninguna página con identificador $1.", + "apierror-nosuchrcid": "No hay ningún cambio reciente con identificador $1.", + "apierror-nosuchrevid": "No hay ninguna revisión con identificador $1.", + "apierror-nosuchsection": "No hay ninguna sección $1.", + "apierror-nosuchsection-what": "No hay ninguna sección $1 en $2.", "apierror-nosuchuserid": "No hay ningún usuario con ID $1.", + "apierror-notarget": "No has especificado un destino válido para esta acción.", + "apierror-notpatrollable": "La revisión r$1 no se puede patrullar por ser demasiado antigua.", + "apierror-opensearch-json-warnings": "No se pueden representar los avisos en formato JSON de OpenSearch.", + "apierror-pagecannotexist": "En este espacio de nombres no se permiten páginas reales.", + "apierror-pagedeleted": "La página ha sido borrada en algún momento desde que obtuviste su marca de tiempo.", "apierror-pagelang-disabled": "En este wiki no se puede cambiar el idioma de una página.", "apierror-paramempty": "El parámetro <var>$1</var> no puede estar vacío.", + "apierror-parsetree-notwikitext": "<kbd>prop=parsetree</kbd> solo es compatible con el contenido en wikitexto.", + "apierror-parsetree-notwikitext-title": "<kbd>prop=parsetree</kbd> solo es compatible con el contenido en wikitexto. $1 usa el modelo de contenido $2.", "apierror-permissiondenied": "No tienes permiso para $1.", "apierror-permissiondenied-generic": "Permiso denegado.", "apierror-permissiondenied-unblock": "No tienes permiso para desbloquear usuarios.", + "apierror-prefixsearchdisabled": "La búsqueda por prefijo está deshabilitada en el modo avaro.", + "apierror-promised-nonwrite-api": "La cabecera HTTP <code>Promise-Non-Write-API-Action</code> no se puede enviar a módulos de la API en modo escritura.", "apierror-protect-invalidaction": "Tipo de protección «$1» no válido.", "apierror-protect-invalidlevel": "Nivel de protección «$1» no válido.", "apierror-readapidenied": "Necesitas permiso de lectura para utilizar este módulo.", "apierror-readonly": "El wiki está actualmente en modo de solo lectura.", + "apierror-reauthenticate": "No te has autentificado recientemente en esta sesión. Por favor, vuelve a autentificarte.", + "apierror-revdel-mutuallyexclusive": "No se puede usar el mismo campo en <var>hide</var> y <var>show</var>.", + "apierror-revdel-paramneeded": "Se requiere al menos un valor para <var>hide</var> y/o <var>show</var>.", + "apierror-revisions-norevids": "El parámetro <var>revids</var> no se puede utilizar junto con las opciones de lista (<var>$1limit</var>, <var>$1startid</var>, <var>$1endid</var>, <kbd>$1dir=newer</kbd>, <var>$1user</var>, <var>$1excludeuser</var>, <var>$1start</var> y <var>$1end</var>).", + "apierror-revisions-singlepage": "Se utilizó <var>titles</var>, <var>pageids</var> o un generador para proporcionar múltiples páginas, pero los parámetros <var>$1limit</var>, <var>$1startid</var>, <var>$1endid</var>, <kbd>$1dir=newer</kbd>, <var>$1user</var>, <var>$1excludeuser</var>, <var>$1start</var> y <var>$1end</var> solo se pueden utilizar en una sola página.", "apierror-revwrongpage": "r$1 no es una revisión de $2.", + "apierror-sectionreplacefailed": "No se ha podido combinar la sección actualizada.", + "apierror-sectionsnotsupported": "Las secciones no son compatibles con el modelo de contenido $1.", + "apierror-sectionsnotsupported-what": "Las secciones no son compatibles con $1.", + "apierror-show": "Parámetro incorrecto: no se pueden proporcionar valores mutuamente excluyentes.", + "apierror-siteinfo-includealldenied": "No se puede ver la información de todos los servidores a menos que <var>$wgShowHostNames</var> tenga valor verdadero.", + "apierror-sizediffdisabled": "La diferencia de tamaño está deshabilitada en el modo avaro.", + "apierror-spamdetected": "Tu edición fue rechazada por contener un fragmento de spam: <code>$1</code>.", "apierror-specialpage-cantexecute": "No tienes permiso para ver los resultados de esta página especial.", + "apierror-stashwrongowner": "Propietario incorrecto: $1", "apierror-systemblocked": "Has sido bloqueado automáticamente por el software MediaWiki.", + "apierror-templateexpansion-notwikitext": "La expansión de plantillas solo es compatible con el contenido en wikitexto. $1 usa el modelo de contenido $2.", "apierror-unknownaction": "La acción especificada, <kbd>$1</kbd>, no está reconocida.", + "apierror-unknownerror-editpage": "Error de EditPage desconocido: $1.", "apierror-unknownerror-nocode": "Error desconocido.", "apierror-unknownerror": "Error desconocido: «$1»", "apierror-unknownformat": "Formato no reconocido «$1».", "apierror-unrecognizedparams": "{{PLURAL:$2|Parámetro no reconocido|Parámetros no reconocidos}}: $1.", "apierror-unrecognizedvalue": "Valor no reconocido para el parámetro <var>$1</var>: $2.", + "apierror-unsupportedrepo": "El repositorio local de archivos no permite consultar todas las imágenes.", + "apierror-urlparamnormal": "No se pudieron normalizar los parámetros de imagen de $1.", "apierror-writeapidenied": "No tienes permiso para editar este wiki a través de la API.", + "apiwarn-alldeletedrevisions-performance": "Para conseguir un mejor rendimiento a la hora de generar títulos, establece <kbd>$1dir=newer</kbd>.", + "apiwarn-badurlparam": "No se pudo analizar <var>$1urlparam</var> para $2. Se utilizarán solamente la anchura y altura.", + "apiwarn-badutf8": "El valor pasado para <var>$1</var> contiene datos no válidos o no normalizados. Los datos textuales deberían estar en Unicode válido, normalizado en NFC y sin caracteres de control C0 excepto HT (\\t), LF (\\n) y CR (\\r).", + "apiwarn-deprecation-deletedrevs": "<kbd>list=deletedrevs</kbd> ha quedado obsoleto. En su lugar, utiliza <kbd>prop=deletedrevisions</kbd> o <kbd>list=alldeletedrevisions</kbd>.", + "apiwarn-deprecation-expandtemplates-prop": "Como no se ha especificado ningún valor para el parámetro <var>prop</var>, se ha utilizado un formato heredado para la salida. Este formato está en desuso y, en el futuro, el parámetro <var>prop</var> tendrá un valor predeterminado, de forma que siempre se utilizará el formato nuevo.", "apiwarn-deprecation-httpsexpected": "Se ha utilizado HTTP cuando se esperaba HTTPS.", + "apiwarn-deprecation-login-botpw": "El inicio de sesión con la cuenta principal mediante <kbd>action=login</kbd> está en desuso y puede dejar de funcionar sin aviso previo. Para proseguir el inicio de sesión mediante <kbd>action=login</kbd>, véase [[Special:BotPasswords]]. Para proseguir el inicio de sesión con la cuenta principal de forma segura, véase <kbd>action=clientlogin</kbd>.", + "apiwarn-deprecation-login-nobotpw": "El inicio de sesión con la cuenta principal mediante <kbd>action=login</kbd> está en desuso y puede dejar de funcionar sin aviso previo. Para iniciar sesión de forma segura, véase <kbd>action=clientlogin</kbd>.", + "apiwarn-deprecation-parameter": "El parámetro <var>$1</var> ha quedado obsoleto.", + "apiwarn-deprecation-parse-headitems": "<kbd>prop=headitems</kbd> está en desuso desde MediaWiki 1.28. Usa <kbd>prop=headhtml</kbd> cuando crees nuevos documentos HTML, o <kbd>prop=módulos|jsconfigvars</kbd> cuando actualices un documento en el lado del cliente.", + "apiwarn-deprecation-purge-get": "El uso de <kbd>action=purge</kbd> mediante GET está obsoleto. Usa POST en su lugar.", + "apiwarn-deprecation-withreplacement": "<kbd>$1</kbd> ha quedado obsoleto. En su lugar, utiliza <kbd>$2</kbd>.", "apiwarn-invalidcategory": "\"$1\" no es una categoría.", "apiwarn-invalidtitle": "«$1» no es un título válido.", + "apiwarn-invalidxmlstylesheetext": "Las hojas de estilo deben tener la extensión <code>.xsl</code>.", "apiwarn-invalidxmlstylesheet": "La hoja de estilos especificada no es válida o no existe.", "apiwarn-invalidxmlstylesheetns": "La hoja de estilos debería estar en el espacio de nombres {{ns:MediaWiki}}.", + "apiwarn-moduleswithoutvars": "La propiedad <kbd>modules</kbd> está definida, pero no lo está <kbd>jsconfigvars</kbd> ni <kbd>encodedjsconfigvars</kbd>. Las variables de configuración son necesarias para el correcto uso del módulo.", "apiwarn-notfile": "\"$1\" no es un archivo.", + "apiwarn-parse-nocontentmodel": "No se proporcionó <var>title</var> ni <var>contentmodel</var>. Se asume $1.", + "apiwarn-tokennotallowed": "La acción «$1» no está permitida para el usuario actual.", + "apiwarn-truncatedresult": "Se ha truncado este resultado porque de otra manera sobrepasaría el límite de $1 bytes.", "apiwarn-unclearnowtimestamp": "El paso de «$2» para el parámetro <var>$1</var> de la marca de tiempo ha quedado obsoleto. Si por alguna razón necesitas especificar de forma explícita la hora actual sin calcularla desde el lado del cliente, utiliza <kbd>now</kbd> («ahora»).", "apiwarn-unrecognizedvalues": "{{PLURAL:$3|Valor no reconocido|Valores no reconocidos}} para el parámetro <var>$1</var>: $2.", "apiwarn-validationfailed-badchars": "caracteres no válidos en la clave (solamente se admiten los caracteres <code>a-z</code>, <code>A-Z</code>, <code>0-9</code>, <code>_</code> y <code>-</code>).", diff --git a/includes/api/i18n/fr.json b/includes/api/i18n/fr.json index 051b879255f3..fb4c37f8fdfa 100644 --- a/includes/api/i18n/fr.json +++ b/includes/api/i18n/fr.json @@ -30,10 +30,10 @@ "The RedBurn" ] }, - "apihelp-main-description": "<div class=\"hlist plainlinks api-main-links\">\n* [[mw:API:Main_page|Documentation]]\n* [[mw:API:FAQ|FAQ]]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api Liste de diffusion]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce Annonces de l’API]\n* [https://phabricator.wikimedia.org/maniphest/query/GebfyV4uCaLd/#R Bogues et demandes]\n</div>\n<strong>État :</strong> Toutes les fonctionnalités affichées sur cette page devraient fonctionner, mais l’API est encore en cours de développement et peut changer à tout moment. Inscrivez-vous à [https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ la liste de diffusion mediawiki-api-announce] pour être informé des mises à jour.\n\n<strong>Requêtes erronées :</strong> Si des requêtes erronées sont envoyées à l’API, un en-tête HTTP sera renvoyé avec la clé « MediaWiki-API-Error ». La valeur de cet en-tête et le code d’erreur renvoyé prendront la même valeur. Pour plus d’information, voyez [[mw:API:Errors_and_warnings|API: Errors and warnings]].\n\n<strong>Test :</strong> Pour faciliter le test des requêtes de l’API, voyez [[Special:ApiSandbox]].", + "apihelp-main-description": "<div class=\"hlist plainlinks api-main-links\">\n* [[mw:Special:MyLanguage/API:Main_page|Documentation]]\n* [[mw:Special:MyLanguage/API:FAQ|FAQ]]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api Liste de diffusion]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce Annonces de l’API]\n* [https://phabricator.wikimedia.org/maniphest/query/GebfyV4uCaLd/#R Bogues et demandes]\n</div>\n<strong>État :</strong> Toutes les fonctionnalités affichées sur cette page devraient fonctionner, mais l’API est encore en cours de développement et peut changer à tout moment. Inscrivez-vous à [https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ la liste de diffusion mediawiki-api-announce] pour être informé des mises à jour.\n\n<strong>Requêtes erronées :</strong> Si des requêtes erronées sont envoyées à l’API, un entête HTTP sera renvoyé avec la clé « MediaWiki-API-Error ». La valeur de cet entête et le code d’erreur renvoyé prendront la même valeur. Pour plus d’information, voyez [[mw:Special:MyLanguage/API:Errors_and_warnings|API: Errors and warnings]].\n\n<strong>Test :</strong> Pour faciliter le test des requêtes de l’API, voyez [[Special:ApiSandbox]].", "apihelp-main-param-action": "Quelle action effectuer.", "apihelp-main-param-format": "Le format de sortie.", - "apihelp-main-param-maxlag": "La latence maximale peut être utilisée quand MédiaWiki est installé sur un cluster de base de données répliqué. Pour éviter des actions provoquant un supplément de latence de réplication de site, ce paramètre peut faire attendre le client jusqu’à ce que la latence de réplication soit inférieure à une valeur spécifiée. En cas de latence excessive, le code d’erreur <samp>maxlag</samp> est renvoyé avec un message tel que <samp>Attente de $host : $lag secondes de délai</samp>.<br />Voyez [[mw:Manual:Maxlag_parameter|Manuel: Maxlag parameter]] pour plus d’information.", + "apihelp-main-param-maxlag": "La latence maximale peut être utilisée quand MédiaWiki est installé sur un cluster de base de données répliqué. Pour éviter des actions provoquant un supplément de latence de réplication de site, ce paramètre peut faire attendre le client jusqu’à ce que la latence de réplication soit inférieure à une valeur spécifiée. En cas de latence excessive, le code d’erreur <samp>maxlag</samp> est renvoyé avec un message tel que <samp>Attente de $host : $lag secondes de délai</samp>.<br />Voyez [[mw:Special:MyLanguage/Manual:Maxlag_parameter|Manuel: Maxlag parameter]] pour plus d’information.", "apihelp-main-param-smaxage": "Fixer l’entête HTTP de contrôle de cache <code>s-maxage</code> à ce nombre de secondes. Les erreurs ne sont jamais mises en cache.", "apihelp-main-param-maxage": "Fixer l’entête HTTP de contrôle de cache <code>max-age</code> à ce nombre de secondes. Les erreurs ne sont jamais mises en cache.", "apihelp-main-param-assert": "Vérifier si l’utilisateur est connecté si positionné à <kbd>user</kbd>, ou s'il a le droit d'un utilisateur robot si positionné à <kbd>bot</kbd>.", @@ -57,7 +57,7 @@ "apihelp-block-param-autoblock": "Bloquer automatiquement la dernière adresse IP utilisée, et toute les adresses IP subséquentes depuis lesquelles ils ont essayé de se connecter.", "apihelp-block-param-noemail": "Empêcher l’utilisateur d’envoyer des courriels via le wiki (nécessite le droit <code>blockemail</code>).", "apihelp-block-param-hidename": "Masque le nom de l’utilisateur dans le journal des blocages (nécessite le droit <code>hideuser</code>).", - "apihelp-block-param-allowusertalk": "Autoriser les utilisateurs à modifier leur propre page de discussion (dépend de <var>[[mw:Manual:$wgBlockAllowsUTEdit|$wgBlockAllowsUTEdit]]</var>).", + "apihelp-block-param-allowusertalk": "Autoriser les utilisateurs à modifier leur propre page de discussion (dépend de <var>[[mw:Special:MyLanguage/Manual:$wgBlockAllowsUTEdit|$wgBlockAllowsUTEdit]]</var>).", "apihelp-block-param-reblock": "Si l’utilisateur est déjà bloqué, écraser le blocage existant.", "apihelp-block-param-watchuser": "Surveiller les pages utilisateur et de discussion de l’utilisateur ou de l’adresse IP.", "apihelp-block-param-tags": "Modifier les balises à appliquer à l’entrée du journal des blocages.", @@ -131,7 +131,7 @@ "apihelp-edit-param-watch": "Ajouter la page à la liste de suivi de l'utilisateur actuel.", "apihelp-edit-param-unwatch": "Supprimer la page de la liste de suivi de l'utilisateur actuel.", "apihelp-edit-param-watchlist": "Ajouter ou supprimer sans condition la page de votre liste de suivi, utiliser les préférences ou ne pas changer le suivi.", - "apihelp-edit-param-md5": "Le hachage MD5 du paramètre $1text, ou les paramètres $1prependtext et $1appendtext concaténés. Si défini, la modification ne sera pas effectuée à moins que le hachage ne soit correct.", + "apihelp-edit-param-md5": "Le hachage MD5 du paramètre $1text, ou les paramètres $1prependtext et $1appendtext concaténés. Si défini, la modification ne sera pas effectuée sauf si le hachage est correct.", "apihelp-edit-param-prependtext": "Ajouter ce texte au début de la page. Écrase $1text.", "apihelp-edit-param-appendtext": "Ajouter ce texte à la fin de la page. Écrase $1text.\n\nUtiliser $1section=new pour ajouter une nouvelle section, plutôt que ce paramètre.", "apihelp-edit-param-undo": "Annuler cette révision. Écrase $1text, $1prependtext et $1appendtext.", @@ -182,15 +182,15 @@ "apihelp-feedrecentchanges-description": "Renvoie un fil de modifications récentes.", "apihelp-feedrecentchanges-param-feedformat": "Le format du flux.", "apihelp-feedrecentchanges-param-namespace": "Espace de noms auquel limiter les résultats.", - "apihelp-feedrecentchanges-param-invert": "Tous les espaces de nom sauf le sélectionné.", + "apihelp-feedrecentchanges-param-invert": "Tous les espaces de noms sauf celui sélectionné.", "apihelp-feedrecentchanges-param-associated": "Inclure l’espace de noms associé (discussion ou principal).", "apihelp-feedrecentchanges-param-days": "Jours auxquels limiter le résultat.", "apihelp-feedrecentchanges-param-limit": "Nombre maximal de résultats à renvoyer.", "apihelp-feedrecentchanges-param-from": "Afficher les modifications depuis lors.", "apihelp-feedrecentchanges-param-hideminor": "Masquer les modifications mineures.", "apihelp-feedrecentchanges-param-hidebots": "Masquer les modifications faites par des robots.", - "apihelp-feedrecentchanges-param-hideanons": "Masquer les modifications faites par des utilisateurs anonymes.", - "apihelp-feedrecentchanges-param-hideliu": "Masquer les modifications faites par des utilisateurs enregistrés.", + "apihelp-feedrecentchanges-param-hideanons": "Masquer les modifications faites par les utilisateurs anonymes.", + "apihelp-feedrecentchanges-param-hideliu": "Masquer les modifications faites par les utilisateurs enregistrés.", "apihelp-feedrecentchanges-param-hidepatrolled": "Masquer les modifications contrôlées.", "apihelp-feedrecentchanges-param-hidemyself": "Masquer les modifications faites par l'utilisateur actuel.", "apihelp-feedrecentchanges-param-hidecategorization": "Masquer les changements de la catégorie d'appartenance.", @@ -204,7 +204,7 @@ "apihelp-feedwatchlist-description": "Renvoie un flux de liste de suivi.", "apihelp-feedwatchlist-param-feedformat": "Le format du flux.", "apihelp-feedwatchlist-param-hours": "Lister les pages modifiées lors de ce nombre d’heures depuis maintenant.", - "apihelp-feedwatchlist-param-linktosections": "Lier directement pour modifier les sections si possible.", + "apihelp-feedwatchlist-param-linktosections": "Lier directement vers les sections modifées si possible.", "apihelp-feedwatchlist-example-default": "Afficher le flux de la liste de suivi", "apihelp-feedwatchlist-example-all6hrs": "Afficher toutes les modifications sur les pages suivies dans les dernières 6 heures", "apihelp-filerevert-description": "Rétablir un fichier dans une ancienne version.", @@ -217,13 +217,13 @@ "apihelp-help-param-submodules": "Inclure l’aide pour les sous-modules du module nommé.", "apihelp-help-param-recursivesubmodules": "Inclure l’aide pour les sous-modules de façon récursive.", "apihelp-help-param-helpformat": "Format de sortie de l’aide.", - "apihelp-help-param-wrap": "Inclut la sortie dans une structure de réponse API standard.", - "apihelp-help-param-toc": "Inclure une table des matières dans la sortir HTML.", + "apihelp-help-param-wrap": "Inclut la sortie dans une structure standard de réponse API.", + "apihelp-help-param-toc": "Inclure une table des matières dans la sortie HTML.", "apihelp-help-example-main": "Aide pour le module principal", "apihelp-help-example-submodules": "Aide pour <kbd>action=query</kbd> et tous ses sous-modules.", - "apihelp-help-example-recursive": "Toute l’aide sur une page", - "apihelp-help-example-help": "Aide pour le module d’aide lui-même", - "apihelp-help-example-query": "Aide pour deux sous-modules de recherche", + "apihelp-help-example-recursive": "Toute l’aide sur une page.", + "apihelp-help-example-help": "Aide pour le module d’aide lui-même.", + "apihelp-help-example-query": "Aide pour deux sous-modules de recherche.", "apihelp-imagerotate-description": "Faire pivoter une ou plusieurs images.", "apihelp-imagerotate-param-rotation": "Degrés de rotation de l’image dans le sens des aiguilles d’une montre.", "apihelp-imagerotate-param-tags": "Balises à appliquer à l’entrée dans le journal de téléchargement.", @@ -274,31 +274,31 @@ "apihelp-move-description": "Déplacer une page.", "apihelp-move-param-from": "Titre de la page à renommer. Impossible de l’utiliser avec <var>$1fromid</var>.", "apihelp-move-param-fromid": "ID de la page à renommer. Impossible à utiliser avec <var>$1from</var>.", - "apihelp-move-param-to": "Titre de la page renommée.", + "apihelp-move-param-to": "Nouveau titre de la page.", "apihelp-move-param-reason": "Motif du renommage.", "apihelp-move-param-movetalk": "Renommer la page de discussion, si elle existe.", "apihelp-move-param-movesubpages": "Renommer les sous-pages, le cas échéant.", "apihelp-move-param-noredirect": "Ne pas créer une redirection.", - "apihelp-move-param-watch": "Ajouter une page et la redirection à liste de suivi de l'utilisateur actuel.", + "apihelp-move-param-watch": "Ajouter la page et la redirection, à la liste de suivi de l'utilisateur actuel.", "apihelp-move-param-unwatch": "Supprimer la page et la redirection de la liste de suivi de l'utilisateur actuel.", "apihelp-move-param-watchlist": "Ajouter ou supprimer sans condition la page de la liste de suivi de l'utilisateur actuel, utiliser les préférences ou ne pas changer le suivi.", "apihelp-move-param-ignorewarnings": "Ignorer tous les avertissements.", "apihelp-move-param-tags": "Modifier les balises à appliquer à l'entrée du journal des renommages et à la version zéro de la page de destination.", - "apihelp-move-example-move": "Déplacer <kbd>Badtitle</kbd> en <kbd>Goodtitle</kbd> sans garder de redirection.", + "apihelp-move-example-move": "Renommer <kbd>Badtitle</kbd> en <kbd>Goodtitle</kbd> sans garder de redirection.", "apihelp-opensearch-description": "Rechercher dans le wiki en utilisant le protocole OpenSearch.", - "apihelp-opensearch-param-search": "Chaîne de recherche.", + "apihelp-opensearch-param-search": "Chaîne de caractères cherchée.", "apihelp-opensearch-param-limit": "Nombre maximal de résultats à renvoyer.", "apihelp-opensearch-param-namespace": "Espaces de nom à rechercher.", - "apihelp-opensearch-param-suggest": "Ne rien faire si <var>[[mw:Manual:$wgEnableOpenSearchSuggest|$wgEnableOpenSearchSuggest]]</var> vaut faux.", + "apihelp-opensearch-param-suggest": "Ne rien faire si <var>[[mw:Special:MyLanguage/Manual:$wgEnableOpenSearchSuggest|$wgEnableOpenSearchSuggest]]</var> vaut faux.", "apihelp-opensearch-param-redirects": "Comment gérer les redirections :\n;return:Renvoie la redirection elle-même.\n;resolve:Renvoie la page cible. Peut renvoyer moins de $1limit résultats.\nPour des raisons historiques, la valeur par défaut est « return » pour $1format=json et « resolve » pour les autres formats.", "apihelp-opensearch-param-format": "Le format de sortie.", - "apihelp-opensearch-param-warningsaserror": "Si des avertissements sont levés avec <kbd>format=json</kbd>, renvoyer une erreur d’API au lieu de les ignorer.", + "apihelp-opensearch-param-warningsaserror": "Si des avertissements apparaissent avec <kbd>format=json</kbd>, renvoyer une erreur d’API au lieu de les ignorer.", "apihelp-opensearch-example-te": "Trouver les pages commençant par <kbd>Te</kbd>.", "apihelp-options-description": "Modifier les préférences de l’utilisateur courant.\n\nSeules les options enregistrées dans le cœur ou dans l’une des extensions installées, ou les options avec des clés préfixées par <code>userjs-</code> (devant être utilisées dans les scripts utilisateur), peuvent être définies.", - "apihelp-options-param-reset": "Réinitialise les préférences aux valeurs par défaut du site.", + "apihelp-options-param-reset": "Réinitialise les préférences avec les valeurs par défaut du site.", "apihelp-options-param-resetkinds": "Liste des types d’option à réinitialiser quand l’option <var>$1reset</var> est définie.", "apihelp-options-param-change": "Liste des modifications, au format nom=valeur (par ex. skin=vector). Si aucune valeur n’est fournie (pas même un signe égal), par ex., nomoption|autreoption|…, l’option sera réinitialisée à sa valeur par défaut. Pour toute valeur passée contenant une barre verticale (<kbd>|</kbd>), utiliser le [[Special:ApiHelp/main#main/datatypes|séparateur alternatif de valeur multiple]] pour que l'opération soit correcte.", - "apihelp-options-param-optionname": "Un nom d’option qui doit être fixé à la valeur fournie par <var>$1optionvalue</var>.", + "apihelp-options-param-optionname": "Nom de l’option qui doit être définie avec la valeur fournie par <var>$1optionvalue</var>.", "apihelp-options-param-optionvalue": "La valeur de l'option spécifiée par <var>$1optionname</var>.", "apihelp-options-example-reset": "Réinitialiser toutes les préférences", "apihelp-options-example-change": "Modifier les préférences <kbd>skin</kbd> et <kbd>hideminor</kbd>.", @@ -306,7 +306,7 @@ "apihelp-paraminfo-description": "Obtenir des informations sur les modules de l’API.", "apihelp-paraminfo-param-modules": "Liste des noms de module (valeurs des paramètres <var>action</var> et <var>format</var>, ou <kbd>main</kbd>). Peut spécifier des sous-modules avec un <kbd>+</kbd>, ou tous les sous-modules avec <kbd>+*</kbd>, ou tous les sous-modules récursivement avec <kbd>+**</kbd>.", "apihelp-paraminfo-param-helpformat": "Format des chaînes d’aide.", - "apihelp-paraminfo-param-querymodules": "Liste des noms de module de requêtage (valeur des paramètres <var>prop</var>, <var>meta</var> ou <var>list</var>=). Utiliser <kbd>$1modules=query+foo</kbd> au lieu de <kbd>$1querymodules=foo</kbd>.", + "apihelp-paraminfo-param-querymodules": "Liste des noms des modules de requête (valeur des paramètres <var>prop</var>, <var>meta</var> ou <var>list</var>). Utiliser <kbd>$1modules=query+foo</kbd> au lieu de <kbd>$1querymodules=foo</kbd>.", "apihelp-paraminfo-param-mainmodule": "Obtenir aussi des informations sur le module principal (niveau supérieur). Utiliser plutôt <kbd>$1modules=main</kbd>.", "apihelp-paraminfo-param-pagesetmodule": "Obtenir aussi des informations sur le module pageset (en fournissant titles= et ses amis).", "apihelp-paraminfo-param-formatmodules": "Liste des noms de module de mise en forme (valeur du paramètre <var>format</var>). Utiliser plutôt <var>$1modules</var>.", @@ -344,6 +344,7 @@ "apihelp-parse-paramvalue-prop-limitreportdata": "Fournit le rapport de limite d’une manière structurée. Ne fournit aucune donnée, si <var>$1disablelimitreport</var> est positionné.", "apihelp-parse-paramvalue-prop-limitreporthtml": "Fournit la version HTML du rapport de limite. Ne fournit aucune donnée, si <var>$1disablelimitreport</var> est positionné.", "apihelp-parse-paramvalue-prop-parsetree": "L’arbre d’analyse XML du contenu de la révision (nécessite le modèle de contenu <code>$1</code>)", + "apihelp-parse-paramvalue-prop-parsewarnings": "Fournit les messages d'avertissement qui sont apparus lors de l'analyse de contenu.", "apihelp-parse-param-pst": "Faire une transformation avant enregistrement de l’entrée avant de l’analyser. Valide uniquement quand utilisé avec du texte.", "apihelp-parse-param-onlypst": "Faire une transformation avant enregistrement (PST) de l’entrée, mais ne pas l’analyser. Renvoie le même wikitexte, après que la PST a été appliquée. Valide uniquement quand utilisé avec <var>$1text</var>.", "apihelp-parse-param-effectivelanglinks": "Inclut les liens de langue fournis par les extensions (à utiliser avec <kbd>$1prop=langlinks</kbd>).", @@ -376,19 +377,19 @@ "apihelp-protect-param-expiry": "Horodatages d’expiration. Si un seul horodatage est fourni, il sera utilisé pour toutes les protections. Utiliser <kbd>infinite</kbd>, <kbd>indefinite</kbd>, <kbd>infinity</kbd> ou <kbd>never</kbd> pour une protection sans expiration.", "apihelp-protect-param-reason": "Motif de (dé)protection.", "apihelp-protect-param-tags": "Modifier les balises à appliquer à l’entrée dans le journal de protection.", - "apihelp-protect-param-cascade": "Activer la protection en cascade (c’est-à-dire protéger les modèles transclus et les images utilisées dans cette page). Ignoré si aucun des niveaux de protection fournis ne prend en charge la mise en cascade.", + "apihelp-protect-param-cascade": "Activer la protection en cascade (c’est-à-dire protéger les modèles transclus et les images utilisés dans cette page). Ignoré si aucun des niveaux de protection fournis ne prend en charge la mise en cascade.", "apihelp-protect-param-watch": "Si activé, ajouter la page (dé)protégée à la liste de suivi de l'utilisateur actuel.", "apihelp-protect-param-watchlist": "Ajouter ou supprimer sans condition la page de la liste de suivi de l'utilisateur actuel, utiliser les préférences ou ne pas modifier le suivi.", "apihelp-protect-example-protect": "Protéger une page", "apihelp-protect-example-unprotect": "Enlever la protection d’une page en mettant les restrictions à <kbd>all</kbd> (c'est à dire tout le monde est autorisé à faire l'action).", "apihelp-protect-example-unprotect2": "Enlever la protection de la page en ne mettant aucune restriction", - "apihelp-purge-description": "Vider le cache des titres fournis.\n\nNécessite une requête POST si l’utilisateur n’est pas connecté.", + "apihelp-purge-description": "Vider le cache des titres fournis.", "apihelp-purge-param-forcelinkupdate": "Mettre à jour les tables de liens.", "apihelp-purge-param-forcerecursivelinkupdate": "Mettre à jour la table des liens, et mettre à jour les tables de liens pour toute page qui utilise cette page comme modèle", "apihelp-purge-example-simple": "Purger les pages <kbd>Main Page</kbd> et <kbd>API</kbd>.", "apihelp-purge-example-generator": "Purger les 10 premières pages de l’espace de noms principal", "apihelp-query-description": "Extraire des données de et sur MediaWiki.\n\nToutes les modifications de données devront d’abord utiliser une requête pour obtenir un jeton, afin d’éviter les abus de la part de sites malveillants.", - "apihelp-query-param-prop": "Quelles propriétés obtenir des pages demandées.", + "apihelp-query-param-prop": "Quelles propriétés obtenir pour les pages demandées.", "apihelp-query-param-list": "Quelles listes obtenir.", "apihelp-query-param-meta": "Quelles métadonnées obtenir.", "apihelp-query-param-indexpageids": "Inclure une section pageids supplémentaire listant tous les IDs de page renvoyés.", @@ -396,13 +397,13 @@ "apihelp-query-param-exportnowrap": "Renvoyer le XML exporté sans l’inclure dans un résultat XML (même format que [[Special:Export]]). Utilisable uniquement avec $1export.", "apihelp-query-param-iwurl": "S’il faut obtenir l’URL complète si le titre est un lien interwiki.", "apihelp-query-param-rawcontinue": "Renvoyer les données <samp>query-continue</samp> brutes pour continuer.", - "apihelp-query-example-revisions": "Récupérer [[Special:ApiHelp/query+siteinfo|l’info du site]] et [[Special:ApiHelp/query+revisions|les révisions]] de <kbd>Page principale</kbd>.", + "apihelp-query-example-revisions": "Récupérer [[Special:ApiHelp/query+siteinfo|l’info du site]] et [[Special:ApiHelp/query+revisions|les révisions]] de <kbd>Main Page</kbd>.", "apihelp-query-example-allpages": "Récupérer les révisions des pages commençant par <kbd>API/</kbd>.", "apihelp-query+allcategories-description": "Énumérer toutes les catégories.", "apihelp-query+allcategories-param-from": "La catégorie depuis laquelle démarrer l’énumération.", "apihelp-query+allcategories-param-to": "La catégorie à laquelle terminer l’énumération.", "apihelp-query+allcategories-param-prefix": "Rechercher tous les titres de catégorie qui commencent avec cette valeur.", - "apihelp-query+allcategories-param-dir": "Direction dans laquelle trier.", + "apihelp-query+allcategories-param-dir": "Ordre dans lequel trier.", "apihelp-query+allcategories-param-min": "Renvoyer uniquement les catégories avec au moins ce nombre de membres.", "apihelp-query+allcategories-param-max": "Renvoyer uniquement les catégories avec au plus ce nombre de membres.", "apihelp-query+allcategories-param-limit": "Combien de catégories renvoyer.", @@ -423,31 +424,31 @@ "apihelp-query+alldeletedrevisions-param-user": "Lister uniquement les révisions par cet utilisateur.", "apihelp-query+alldeletedrevisions-param-excludeuser": "Ne pas lister les révisions par cet utilisateur.", "apihelp-query+alldeletedrevisions-param-namespace": "Lister uniquement les pages dans cet espace de noms.", - "apihelp-query+alldeletedrevisions-param-miser-user-namespace": "<strong>REMARQUE :</strong> Du fait du [[mw:Manual:$wgMiserMode|mode minimal]], utiliser <var>$1user</var> et <var>$1namespace</var> ensemble peut aboutir à moins de résultats renvoyés que <var>$1limit</var> avant de continuer ; dans les cas extrêmes, zéro résultats peuvent être renvoyés.", + "apihelp-query+alldeletedrevisions-param-miser-user-namespace": "<strong>REMARQUE :</strong> du fait du [[mw:Special:MyLanguage/Manual:$wgMiserMode|mode minimal]], utiliser <var>$1user</var> et <var>$1namespace</var> ensemble peut aboutir à avoir moins de résultats renvoyés que <var>$1limit</var> avant de continuer ; dans les cas extrêmes, zéro résultats peuvent être renvoyés.", "apihelp-query+alldeletedrevisions-param-generatetitles": "Utilisé comme générateur, générer des titres plutôt que des IDs de révision.", "apihelp-query+alldeletedrevisions-example-user": "Lister les 50 dernières contributions supprimées par l'utilisateur <kbd>Example</kbd>.", "apihelp-query+alldeletedrevisions-example-ns-main": "Lister les 50 premières révisions supprimées dans l’espace de noms principal.", - "apihelp-query+allfileusages-description": "Lister toutes les utilisations de fichier, y compris ceux n’existant pas.", + "apihelp-query+allfileusages-description": "Lister toutes les utilisations de fichiers, y compris ceux n’existant pas.", "apihelp-query+allfileusages-param-from": "Le titre du fichier depuis lequel commencer l’énumération.", "apihelp-query+allfileusages-param-to": "Le titre du fichier auquel arrêter l’énumération.", "apihelp-query+allfileusages-param-prefix": "Rechercher tous les fichiers dont le titre commence par cette valeur.", - "apihelp-query+allfileusages-param-unique": "Afficher uniquement les titres de fichier distincts. Impossible à utiliser avec $1prop=ids.\nQuand utilisé comme générateur, produit les pages cibles au lieu des sources.", + "apihelp-query+allfileusages-param-unique": "Afficher uniquement les titres de fichiers distincts. Impossible à utiliser avec $1prop=ids.\nQuand il est utilisé comme générateur, il produit les pages cible au lieu des pages source.", "apihelp-query+allfileusages-param-prop": "Quelles informations inclure :", - "apihelp-query+allfileusages-paramvalue-prop-ids": "Ajoute les IDs de page des pages l’utilisant (impossible à utiliser avec $1unique).", + "apihelp-query+allfileusages-paramvalue-prop-ids": "Ajoute l'ID des pages qui l’utilisent (incompatible avec $1unique).", "apihelp-query+allfileusages-paramvalue-prop-title": "Ajoute le titre du fichier.", "apihelp-query+allfileusages-param-limit": "Combien d’éléments renvoyer au total.", - "apihelp-query+allfileusages-param-dir": "La direction dans laquelle lister.", - "apihelp-query+allfileusages-example-B": "Lister les titres de fichier, y compris les manquants, avec les IDs de page d’où ils proviennent, en commençant à <kbd>B</kbd>.", - "apihelp-query+allfileusages-example-unique": "Lister les titres de fichier uniques", - "apihelp-query+allfileusages-example-unique-generator": "Obtient tous les titres de fichier, en marquant les manquants", - "apihelp-query+allfileusages-example-generator": "Obtient les pages contenant les fichiers", + "apihelp-query+allfileusages-param-dir": "L'ordre dans lequel lister.", + "apihelp-query+allfileusages-example-B": "Lister les titres des fichiers, y compris ceux manquants, avec les IDs de page d’où ils proviennent, en commençant à <kbd>B</kbd>.", + "apihelp-query+allfileusages-example-unique": "Lister les titres de fichier uniques.", + "apihelp-query+allfileusages-example-unique-generator": "Obtient tous les titres de fichier, en marquant les manquants.", + "apihelp-query+allfileusages-example-generator": "Obtient les pages contenant les fichiers.", "apihelp-query+allimages-description": "Énumérer toutes les images séquentiellement.", "apihelp-query+allimages-param-sort": "Propriété par laquelle trier.", - "apihelp-query+allimages-param-dir": "La direction dans laquelle lister.", + "apihelp-query+allimages-param-dir": "L'ordre dans laquel lister.", "apihelp-query+allimages-param-from": "Le titre de l’image depuis laquelle démarrer l’énumération. Ne peut être utilisé qu’avec $1sort=name.", "apihelp-query+allimages-param-to": "Le titre de l’image auquel arrêter l’énumération. Ne peut être utilisé qu’avec $1sort=name.", "apihelp-query+allimages-param-start": "L’horodatage depuis lequel énumérer. Ne peut être utilisé qu’avec $1sort=timestamp.", - "apihelp-query+allimages-param-end": "L’horodatage de fin de l’énumération. Ne peut être utilisé qu’avec $1sort=timestamp.", + "apihelp-query+allimages-param-end": "L’horodatage de la fin d’énumération. Ne peut être utilisé qu’avec $1sort=timestamp.", "apihelp-query+allimages-param-prefix": "Rechercher toutes les images dont le titre commence par cette valeur. Utilisable uniquement avec $1sort=name.", "apihelp-query+allimages-param-minsize": "Restreindre aux images avec au moins ce nombre d’octets.", "apihelp-query+allimages-param-maxsize": "Restreindre aux images avec au plus ce nombre d’octets.", @@ -458,7 +459,7 @@ "apihelp-query+allimages-param-mime": "Quels types MIME rechercher, par ex. <kbd>image/jpeg</kbd>.", "apihelp-query+allimages-param-limit": "Combien d’images renvoyer au total.", "apihelp-query+allimages-example-B": "Afficher une liste des fichiers commençant par la lettre <kbd>B</kbd>.", - "apihelp-query+allimages-example-recent": "Afficher une liste des fichiers récemment téléchargés semblable à [[Special:NewFiles]]", + "apihelp-query+allimages-example-recent": "Afficher une liste de fichiers récemment téléchargés, semblable à [[Special:NewFiles]].", "apihelp-query+allimages-example-mimetypes": "Afficher une liste de fichiers avec le type MIME <kbd>image/png</kbd> ou <kbd>image/gif</kbd>", "apihelp-query+allimages-example-generator": "Afficher l’information sur 4 fichiers commençant par la lettre <kbd>T</kbd>.", "apihelp-query+alllinks-description": "Énumérer tous les liens pointant vers un espace de noms donné.", @@ -471,15 +472,15 @@ "apihelp-query+alllinks-paramvalue-prop-title": "Ajoute le titre du lien.", "apihelp-query+alllinks-param-namespace": "L’espace de noms à énumérer.", "apihelp-query+alllinks-param-limit": "Combien d’éléments renvoyer au total.", - "apihelp-query+alllinks-param-dir": "La direction dans laquelle lister.", - "apihelp-query+alllinks-example-B": "Lister les titres liés, y compris les manquants, avec les IDs des pages d’où ils proviennent, en démarrant à <kbd>B</kbd>.", + "apihelp-query+alllinks-param-dir": "L'ordre dans lequel lister.", + "apihelp-query+alllinks-example-B": "Lister les titres liés, y compris ceux manquants, avec les IDs des pages d’où ils proviennent, en démarrant à <kbd>B</kbd>.", "apihelp-query+alllinks-example-unique": "Lister les titres liés uniques", "apihelp-query+alllinks-example-unique-generator": "Obtient tous les titres liés, en marquant les manquants", "apihelp-query+alllinks-example-generator": "Obtient les pages contenant les liens", "apihelp-query+allmessages-description": "Renvoyer les messages depuis ce site.", "apihelp-query+allmessages-param-messages": "Quels messages sortir. <kbd>*</kbd> (par défaut) signifie tous les messages.", "apihelp-query+allmessages-param-prop": "Quelles propriétés obtenir.", - "apihelp-query+allmessages-param-enableparser": "Si positionné pour activer l’analyseur, traitera en avance le wikitexte du message (substitution des mots magiques, gestion des modèles, etc.).", + "apihelp-query+allmessages-param-enableparser": "Positionner pour activer l’analyseur, traitera en avance le wikitexte du message (substitution des mots magiques, gestion des modèles, etc.).", "apihelp-query+allmessages-param-nocontent": "Si positionné, ne pas inclure le contenu des messages dans la sortie.", "apihelp-query+allmessages-param-includelocal": "Inclure aussi les messages locaux, c’est-à-dire les messages qui n’existent pas dans le logiciel mais dans l’espace de noms {{ns:MediaWiki}}.\nCela liste toutes les pages de l’espace de noms {{ns:MediaWiki}}, donc aussi celles qui ne sont pas vraiment des messages, telles que [[MediaWiki:Common.js|Common.js]].", "apihelp-query+allmessages-param-args": "Arguments à substituer dans le message.", @@ -504,7 +505,7 @@ "apihelp-query+allpages-param-prlevel": "Filtrer les protections basées sur le niveau de protection (doit être utilisé avec le paramètre $1prtype=).", "apihelp-query+allpages-param-prfiltercascade": "Filtrer les protections d’après leur cascade (ignoré si $1prtype n’est pas positionné).", "apihelp-query+allpages-param-limit": "Combien de pages renvoyer au total.", - "apihelp-query+allpages-param-dir": "La direction dans laquelle lister.", + "apihelp-query+allpages-param-dir": "L'ordre dans lequel lister.", "apihelp-query+allpages-param-filterlanglinks": "Filtrer si une page a des liens de langue. Noter que cela ne prend pas en compte les liens de langue ajoutés par des extensions.", "apihelp-query+allpages-param-prexpiry": "Quelle expiration de protection sur laquelle filtrer la page :\n;indefinite:N’obtenir que les pages avec une expiration de protection infinie.\n;definite:N’obtenir que les pages avec une expiration de protection définie (spécifique).\n;all:Obtenir toutes les pages avec une expiration de protection.", "apihelp-query+allpages-example-B": "Afficher une liste des pages commençant par la lettre <kbd>B</kbd>.", @@ -523,7 +524,7 @@ "apihelp-query+allredirects-param-namespace": "L’espace de noms à énumérer.", "apihelp-query+allredirects-param-limit": "Combien d’éléments renvoyer au total.", "apihelp-query+allredirects-param-dir": "La direction dans laquelle lister.", - "apihelp-query+allredirects-example-B": "Lister les pages cible, y compris les manquantes, avec les IDs de page d’où ils proviennent, en commençant à <kbd>B</kbd>.", + "apihelp-query+allredirects-example-B": "Lister les pages cible, y compris celles manquantes, avec les IDs de page d’où ils proviennent, en commençant à <kbd>B</kbd>.", "apihelp-query+allredirects-example-unique": "Lister les pages cible unique", "apihelp-query+allredirects-example-unique-generator": "Obtient toutes les pages cible, en marquant les manquantes", "apihelp-query+allredirects-example-generator": "Obtient les pages contenant les redirections", @@ -534,7 +535,7 @@ "apihelp-query+allrevisions-param-excludeuser": "Ne pas lister les révisions faites par cet utilisateur.", "apihelp-query+allrevisions-param-namespace": "Lister uniquement les pages dans cet espace de noms.", "apihelp-query+allrevisions-param-generatetitles": "Utilisé comme générateur, génère des titres plutôt que des IDs de révision.", - "apihelp-query+allrevisions-example-user": "Lister les 50 dernières contributions de l’utilisateur <kbd>Exemple</kbd>.", + "apihelp-query+allrevisions-example-user": "Lister les 50 dernières contributions de l’utilisateur <kbd>Example</kbd>.", "apihelp-query+allrevisions-example-ns-main": "Lister les 50 premières révisions dans l’espace de noms principal.", "apihelp-query+mystashedfiles-description": "Obtenir une liste des fichiers dans le cache de téléchargement de l’utilisateur actuel", "apihelp-query+mystashedfiles-param-prop": "Quelles propriétés récupérer pour les fichiers.", @@ -552,11 +553,11 @@ "apihelp-query+alltransclusions-paramvalue-prop-title": "Ajoute le titre de la transclusion.", "apihelp-query+alltransclusions-param-namespace": "L’espace de noms à énumérer.", "apihelp-query+alltransclusions-param-limit": "Combien d’éléments renvoyer au total.", - "apihelp-query+alltransclusions-param-dir": "La direction dans laquelle lister.", + "apihelp-query+alltransclusions-param-dir": "L'ordre dans lequel lister.", "apihelp-query+alltransclusions-example-B": "Lister les titres inclus, y compris les manquants, avec les IDs des pages d’où ils viennent, en commençant à <kbd>B</kbd>.", "apihelp-query+alltransclusions-example-unique": "Lister les titres inclus uniques", - "apihelp-query+alltransclusions-example-unique-generator": "Obtient tous les titres inclus, en marquant les manquants", - "apihelp-query+alltransclusions-example-generator": "Obtient les pages contenant des transclusions", + "apihelp-query+alltransclusions-example-unique-generator": "Obtient tous les titres inclus, en marquant les manquants.", + "apihelp-query+alltransclusions-example-generator": "Obtient les pages contenant les transclusions.", "apihelp-query+allusers-description": "Énumérer tous les utilisateurs enregistrés.", "apihelp-query+allusers-param-from": "Le nom d’utilisateur auquel démarrer l’énumération.", "apihelp-query+allusers-param-to": "Le nom d’utilisateur auquel stopper l’énumération.", @@ -569,7 +570,7 @@ "apihelp-query+allusers-paramvalue-prop-blockinfo": "Ajoute l’information sur le bloc actuel d’un utilisateur.", "apihelp-query+allusers-paramvalue-prop-groups": "Liste des groupes auxquels appartient l’utilisateur. Cela utilise beaucoup de ressources du serveur et peut renvoyer moins de résultats que la limite.", "apihelp-query+allusers-paramvalue-prop-implicitgroups": "Liste tous les groupes auxquels l’utilisateur est affecté automatiquement.", - "apihelp-query+allusers-paramvalue-prop-rights": "Liste les droits qu’à l’utilisateur.", + "apihelp-query+allusers-paramvalue-prop-rights": "Liste les droits qu’a l’utilisateur.", "apihelp-query+allusers-paramvalue-prop-editcount": "Ajoute le compteur de modifications de l’utilisateur.", "apihelp-query+allusers-paramvalue-prop-registration": "Ajoute l’horodatage de l’inscription de l’utilisateur, s’il est disponible (peut être vide).", "apihelp-query+allusers-paramvalue-prop-centralids": "Ajoute les IDs centraux et l’état d’attachement de l’utilisateur.", @@ -590,8 +591,8 @@ "apihelp-query+backlinks-param-namespace": "L’espace de noms à énumérer.", "apihelp-query+backlinks-param-dir": "La direction dans laquelle lister.", "apihelp-query+backlinks-param-filterredir": "Comment filtrer les redirections. Si positionné à <kbd>nonredirects</kbd> quand <var>$1redirect</var> est activé, cela ne s’applique qu’au second niveau.", - "apihelp-query+backlinks-param-limit": "Combien de pages renvoyer au total. Si $1redirect est activé, la limite s’applique à chaque niveau séparément (ce qui signifie jusqu’à 2 * limite résultats peut être retourné).", - "apihelp-query+backlinks-param-redirect": "Si le lien vers une page est une redirection, trouver toutes les pages qui ont un lien vers cette redirection aussi. La limite maximale est divisée par deux.", + "apihelp-query+backlinks-param-limit": "Combien de pages renvoyer au total. Si <var>$1redirect</var> est activé, la limite s’applique à chaque niveau séparément (ce qui signifie jusqu’à 2 * <var>$1limit</var> résultats pouvant être retournés).", + "apihelp-query+backlinks-param-redirect": "Si le lien vers une page est une redirection, trouver également toutes les pages qui ont un lien vers cette redirection. La limite maximale est divisée par deux.", "apihelp-query+backlinks-example-simple": "Afficher les liens vers <kbd>Main page</kbd>.", "apihelp-query+backlinks-example-generator": "Obtenir des informations sur les pages ayant un lien vers <kbd>Main page</kbd>.", "apihelp-query+blocks-description": "Lister tous les utilisateurs et les adresses IP bloqués.", @@ -622,7 +623,7 @@ "apihelp-query+categories-paramvalue-prop-hidden": "Marque les catégories cachées avec <code>__HIDDENCAT__</code>.", "apihelp-query+categories-param-show": "Quelle sorte de catégories afficher.", "apihelp-query+categories-param-limit": "Combien de catégories renvoyer.", - "apihelp-query+categories-param-categories": "Lister uniquement ces catégories. Utile pour vérifier si une certaine page est dans une certaine catégorie.", + "apihelp-query+categories-param-categories": "Lister uniquement ces catégories. Utile pour vérifier si une certaine page est dans une catégorie donnée.", "apihelp-query+categories-param-dir": "La direction dans laquelle lister.", "apihelp-query+categories-example-simple": "Obtenir une liste des catégories auxquelles appartient la page <kbd>Albert Einstein</kbd>.", "apihelp-query+categories-example-generator": "Obtenir des informations sur toutes les catégories utilisées dans la page <kbd>Albert Einstein</kbd>.", @@ -654,7 +655,7 @@ "apihelp-query+categorymembers-example-simple": "Obtenir les 10 premières pages de <kbd>Category:Physics</kbd>.", "apihelp-query+categorymembers-example-generator": "Obtenir l’information sur les 10 premières pages de <kbd>Category:Physics</kbd>.", "apihelp-query+contributors-description": "Obtenir la liste des contributeurs connectés et le nombre de contributeurs anonymes d’une page.", - "apihelp-query+contributors-param-group": "Inclure uniquement les utilisateurs dans les groupes donnés. Ne pas inclure les groupes implicites ou auto-promus comme *, user ou autoconfirmed.", + "apihelp-query+contributors-param-group": "Inclut uniquement les utilisateurs dans les groupes donnés. N'inclut pas les groupes implicites ou auto-promus comme *, user ou autoconfirmed.", "apihelp-query+contributors-param-excludegroup": "Exclure les utilisateurs des groupes donnés. Ne pas inclure les groupes implicites ou auto-promus comme *, user ou autoconfirmed.", "apihelp-query+contributors-param-rights": "Inclure uniquement les utilisateurs ayant les droits donnés. Ne pas inclure les droits accordés par les groupes implicites ou auto-promus comme *, user ou autoconfirmed.", "apihelp-query+contributors-param-excluderights": "Exclure les utilisateurs ayant les droits donnés. Ne pas inclure les droits accordés par les groupes implicites ou auto-promus comme *, user ou autoconfirmed.", @@ -682,16 +683,16 @@ "apihelp-query+deletedrevs-param-namespace": "Lister uniquement les pages dans cet espace de noms.", "apihelp-query+deletedrevs-param-limit": "Le nombre maximal de révisions à lister.", "apihelp-query+deletedrevs-param-prop": "Quelles propriétés obtenir :\n;revid:Ajoute l’ID de la révision supprimée.\n;parentid:Ajoute l’ID de la révision précédente de la page.\n;user:Ajoute l’utilisateur ayant fait la révision.\n;userid:Ajoute l’ID de l’utilisateur qui a fait la révision.\n;comment:Ajoute le commentaire de la révision.\n;parsedcomment:Ajoute le commentaire analysé de la révision.\n;minor:Marque si la révision est mineure.\n;len:Ajoute la longueur (en octets) de la révision.\n;sha1:Ajoute le SHA-1 (base 16) de la révision.\n;content:Ajoute le contenu de la révision.\n;token:<span class=\"apihelp-deprecated\">Obsolète.</span> Fournit le jeton de modification.\n;tags:Balises pour la révision.", - "apihelp-query+deletedrevs-example-mode1": "Lister les dernières révisions supprimées de des pages <kbd>Main Page</kbd> et <kbd>Talk:Main Page</kbd>, avec le contenu (mode 1).", + "apihelp-query+deletedrevs-example-mode1": "Lister les dernières révisions supprimées des pages <kbd>Main Page</kbd> et <kbd>Talk:Main Page</kbd>, avec le contenu (mode 1).", "apihelp-query+deletedrevs-example-mode2": "Lister les 50 dernières contributions de <kbd>Bob</kbd> supprimées (mode 2).", "apihelp-query+deletedrevs-example-mode3-main": "Lister les 50 premières révisions supprimées dans l’espace de noms principal (mode 3)", "apihelp-query+deletedrevs-example-mode3-talk": "Lister les 50 premières pages supprimées dans l’espace de noms {{ns:talk}} (mode 3).", "apihelp-query+disabled-description": "Ce module de requête a été désactivé.", - "apihelp-query+duplicatefiles-description": "Lister tous les fichiers qui sont des doublons des fichiers donnés d’après leurs valeurs de hachage.", + "apihelp-query+duplicatefiles-description": "Lister d’après leurs valeurs de hachage, tous les fichiers qui sont des doublons de fichiers donnés.", "apihelp-query+duplicatefiles-param-limit": "Combien de fichiers dupliqués à renvoyer.", "apihelp-query+duplicatefiles-param-dir": "La direction dans laquelle lister.", "apihelp-query+duplicatefiles-param-localonly": "Rechercher les fichiers uniquement dans le référentiel local.", - "apihelp-query+duplicatefiles-example-simple": "Rechercher les doublons de [[:File:Albert Einstein Head.jpg]]", + "apihelp-query+duplicatefiles-example-simple": "Rechercher les doublons de [[:File:Albert Einstein Head.jpg]].", "apihelp-query+duplicatefiles-example-generated": "Rechercher les doublons de tous les fichiers", "apihelp-query+embeddedin-description": "Trouver toutes les pages qui incluent (par transclusion) le titre donné.", "apihelp-query+embeddedin-param-title": "Titre à rechercher. Impossible à utiliser avec $1pageid.", @@ -701,10 +702,10 @@ "apihelp-query+embeddedin-param-filterredir": "Comment filtrer les redirections.", "apihelp-query+embeddedin-param-limit": "Combien de pages renvoyer au total.", "apihelp-query+embeddedin-example-simple": "Afficher les pages incluant <kbd>Template:Stub</kbd>.", - "apihelp-query+embeddedin-example-generator": "Obteir des informations sur les pages incluant <kbd>Template:Stub</kbd>.", + "apihelp-query+embeddedin-example-generator": "Obtenir des informations sur les pages incluant <kbd>Template:Stub</kbd>.", "apihelp-query+extlinks-description": "Renvoyer toutes les URLs externes (non interwikis) des pages données.", "apihelp-query+extlinks-param-limit": "Combien de liens renvoyer.", - "apihelp-query+extlinks-param-protocol": "Protocole de l’URL. Si vide et <var>$1query</var> est positionné, le protocole est <kbd>http</kbd>. Laisser à la fois ceci et <var>$1query</var> vide pour lister tous les liens externes.", + "apihelp-query+extlinks-param-protocol": "Protocole de l’URL. Si vide et <var>$1query</var> est positionné, le protocole est <kbd>http</kbd>. Laisser à la fois ceci et <var>$1query</var> vides pour lister tous les liens externes.", "apihelp-query+extlinks-param-query": "Rechercher une chaîne sans protocole. Utile pour vérifier si une certaine page contient une certaine URL externe.", "apihelp-query+extlinks-param-expandurl": "Étendre les URLs relatives au protocole avec le protocole canonique.", "apihelp-query+extlinks-example-simple": "Obtenir une liste des liens externes de <kbd>Main Page</kbd>.", @@ -714,7 +715,7 @@ "apihelp-query+exturlusage-paramvalue-prop-title": "Ajoute le titre et l’ID de l’espace de noms de la page.", "apihelp-query+exturlusage-paramvalue-prop-url": "Ajoute l’URL utilisée dans la page.", "apihelp-query+exturlusage-param-protocol": "Protocole de l’URL. Si vide et que <var>$1query</var> est rempli, le protocole est <kbd>http</kbd>. Le laisser avec <var>$1query</var> vide pour lister tous les liens externes.", - "apihelp-query+exturlusage-param-query": "Rechercher une chaîne sans protocole. Voyez [[Special:LinkSearch]]. Le laisser vide liste tous les liens externes.", + "apihelp-query+exturlusage-param-query": "Rechercher une chaîne sans protocole. Voyez [[Special:LinkSearch]]. Le laisser vide pour lister tous les liens externes.", "apihelp-query+exturlusage-param-namespace": "Les espaces de nom à énumérer.", "apihelp-query+exturlusage-param-limit": "Combien de pages renvoyer.", "apihelp-query+exturlusage-param-expandurl": "Étendre les URLs relatives au protocole avec le protocole canonique.", @@ -729,7 +730,7 @@ "apihelp-query+filearchive-param-sha1base36": "Hachage SHA1 de l’image en base 36 (utilisé dans MédiaWiki).", "apihelp-query+filearchive-param-prop": "Quelle information obtenir sur l’image :", "apihelp-query+filearchive-paramvalue-prop-sha1": "Ajoute le hachage SHA-1 pour l’image.", - "apihelp-query+filearchive-paramvalue-prop-timestamp": "Ajoute l÷’horodatage pour la version téléchargée.", + "apihelp-query+filearchive-paramvalue-prop-timestamp": "Ajoute l’horodatage pour la version téléchargée.", "apihelp-query+filearchive-paramvalue-prop-user": "Ajoute l’utilisateur qui a téléchargé la version de l’image.", "apihelp-query+filearchive-paramvalue-prop-size": "Ajoute la taille de l’image en octets et la hauteur, la largeur et le nombre de page (si c’est applicable).", "apihelp-query+filearchive-paramvalue-prop-dimensions": "Alias pour la taille.", @@ -741,9 +742,9 @@ "apihelp-query+filearchive-paramvalue-prop-bitdepth": "Ajoute la profondeur de bit de la version.", "apihelp-query+filearchive-paramvalue-prop-archivename": "Ajoute le nom de fichier de la version d’archive pour les versions autres que la dernière.", "apihelp-query+filearchive-example-simple": "Afficher une liste de tous les fichiers supprimés", - "apihelp-query+filerepoinfo-description": "Renvoyer les méta-informations sur les référentiels d’image configurés dans le wiki.", - "apihelp-query+filerepoinfo-param-prop": "Quelles propriétés du référentiel récupérer (il peut y en avoir plus de disponibles sur certains wikis) :\n;apiurl:URL de l’API du référentiel - utile pour obtenir les infos de l’image depuis l’hôte.\n;name:La clé du référentiel - utilisé par ex. dans les valeurs de retour de <var>[[mw:Manual:$wgForeignFileRepos|$wgForeignFileRepos]]</var> et [[Special:ApiHelp/query+imageinfo|imageinfo]].\n;displayname:Le nom lisible du wiki référentiel.\n;rooturl:URL racine des chemins d’image.\n;local:Si ce référentiel est le référentiel local ou non.", - "apihelp-query+filerepoinfo-example-simple": "Obtenir l’information sur les référentiels de fichier", + "apihelp-query+filerepoinfo-description": "Renvoyer les méta-informations sur les référentiels d’images configurés dans le wiki.", + "apihelp-query+filerepoinfo-param-prop": "Quelles propriétés du référentiel récupérer (il peut y en avoir plus de disponibles sur certains wikis) :\n;apiurl:URL de l’API du référentiel - utile pour obtenir les infos de l’image depuis l’hôte.\n;name:La clé du référentiel - utilisé par ex. dans les valeurs de retour de <var>[[mw:Special:MyLanguage/Manual:$wgForeignFileRepos|$wgForeignFileRepos]]</var> et [[Special:ApiHelp/query+imageinfo|imageinfo]].\n;displayname:Le nom lisible du wiki référentiel.\n;rooturl:URL racine des chemins d’image.\n;local:Si ce référentiel est le référentiel local ou non.", + "apihelp-query+filerepoinfo-example-simple": "Obtenir des informations sur les référentiels de fichier.", "apihelp-query+fileusage-description": "Trouver toutes les pages qui utilisent les fichiers donnés.", "apihelp-query+fileusage-param-prop": "Quelles propriétés obtenir :", "apihelp-query+fileusage-paramvalue-prop-pageid": "ID de chaque page.", @@ -755,7 +756,7 @@ "apihelp-query+fileusage-example-simple": "Obtenir une liste des pages utilisant [[:File:Example.jpg]]", "apihelp-query+fileusage-example-generator": "Obtenir l’information sur les pages utilisant [[:File:Example.jpg]]", "apihelp-query+imageinfo-description": "Renvoyer l’information de fichier et l’historique de téléchargement.", - "apihelp-query+imageinfo-param-prop": "Quelles informations obtenir du fichier :\n;timestamp:Ajoute l’horodatage de la version téléchargée.\n;user:Ajoute l’utilisateur qui a téléchargé chaque version du fichier.\n;userid:Ajoute l’ID de l’utilisateur qui a téléchargé chaque version du fichier.\n;comment:Commentaire sur la version.\n;parsedcomment:Analyser le commentaire sur cette version.\n;canonicaltitle:Ajoute le titre canonique du fichier.\n;url:Fournit l’URL du fichier et la page de description.\n;size:Ajoute la taille du fichier en octets et la hauteur, la largeur et le nombre de pages (si applicable).\n;dimensions:Alias pour la taille.\n;sha1:Ajoute le hachage SHA-1 pour le fichier.\n;mime:Ajoute le type MIME du fichier.\n;thumbmime:Ajoute le type MIME de la vignette de l’image (nécessite l’URL et le paramètre $1urlwidth).\n;mediatype:Ajoute le type de média du fichier.\n;metadata:Liste les métadonnées Exif de la version du fichier.\n;commonmetadata:Liste les métadonnées génériques du format du fichier pour la version du fichier.\n;extmetadata:Liste les métadonnées mises en forme combinées depuis différentes sources. Les résultats sont au format HTML.\n;archivename:Ajoute le nom de fichier de la version d’archive pour les versions autres que la dernière.\n;bitdepth:Ajoute la profondeur de bit de la version.\n;uploadwarning:Utilisé par la page Special:Upload pour obtenir de l’information sur un fichier existant. Non prévu pour être utilisé en dehors du cœur de MédiaWiki.", + "apihelp-query+imageinfo-param-prop": "Quelle information obtenir du fichier :", "apihelp-query+imageinfo-paramvalue-prop-timestamp": "Ajoute l’horodatage à la version téléchargée.", "apihelp-query+imageinfo-paramvalue-prop-user": "Ajoute l’utilisateur qui a téléchargé chaque version du fichier.", "apihelp-query+imageinfo-paramvalue-prop-userid": "Ajouter l’ID de l’utilisateur qui a téléchargé chaque version du fichier.", @@ -763,7 +764,7 @@ "apihelp-query+imageinfo-paramvalue-prop-parsedcomment": "Analyser le commentaire de la version.", "apihelp-query+imageinfo-paramvalue-prop-canonicaltitle": "Ajoute le titre canonique du fichier.", "apihelp-query+imageinfo-paramvalue-prop-url": "Fournit l’URL du fichier et de la page de description.", - "apihelp-query+imageinfo-paramvalue-prop-size": "Ajoute la taille du fichier en octets et sa hauteur, largeur et compteur de page (le cas échéant).", + "apihelp-query+imageinfo-paramvalue-prop-size": "Ajoute la taille du fichier en octets et sa hauteur, sa largeur et le compteur de page (le cas échéant).", "apihelp-query+imageinfo-paramvalue-prop-dimensions": "Alias pour la taille.", "apihelp-query+imageinfo-paramvalue-prop-sha1": "Ajoute le hachage SH1-1 du fichier.", "apihelp-query+imageinfo-paramvalue-prop-mime": "Ajoute le type MIME du fichier.", @@ -776,7 +777,7 @@ "apihelp-query+imageinfo-paramvalue-prop-bitdepth": "Ajoute la profondeur de bits de la version.", "apihelp-query+imageinfo-paramvalue-prop-uploadwarning": "Utilisé par la page Special:Upload pour obtenir de l’information sur un fichier existant. Non prévu pour être utilisé en dehors du cœur de MédiaWiki.", "apihelp-query+imageinfo-paramvalue-prop-badfile": "Ajoute l'indication que le fichier est sur [[MediaWiki:Bad image list]]", - "apihelp-query+imageinfo-param-limit": "Combien de révision de fichier renvoyer par fichier.", + "apihelp-query+imageinfo-param-limit": "Combien de révisions de fichier renvoyer par fichier.", "apihelp-query+imageinfo-param-start": "Horodatage auquel démarrer la liste.", "apihelp-query+imageinfo-param-end": "Horodatage auquel arrêter la liste.", "apihelp-query+imageinfo-param-urlwidth": "Si $2prop=url est défini, une URL vers une image à l’échelle de cette largeur sera renvoyée.\nPour des raisons de performance si cette option est utilisée, pas plus de $1 images mises à l’échelle seront renvoyées.", @@ -785,11 +786,11 @@ "apihelp-query+imageinfo-param-extmetadatalanguage": "Quelle langue pour analyser extmetadata. Cela affecte à la fois quelle traduction analyser, s’il y en a plusieurs, et comment les choses comme les nombres et d’autres valeurs sont mises en forme.", "apihelp-query+imageinfo-param-extmetadatamultilang": "Si des traductions pour la propriété extmetadata sont disponibles, les analyser toutes.", "apihelp-query+imageinfo-param-extmetadatafilter": "Si spécifié et non vide, seules ces clés seront renvoyées pour $1prop=extmetadata.", - "apihelp-query+imageinfo-param-urlparam": "Une chaîne de paramètre spécifique à l’analyseur. Par exemple, les PDFs peuvent utiliser <kbd>page15-100px</kbd>. <var>$1urlwidth</var> doit être utilisé et être cohérent avec <var>$1urlparam</var>.", + "apihelp-query+imageinfo-param-urlparam": "Une chaîne de paramètres spécifique à l’analyseur. Par exemple, les PDFs peuvent utiliser <kbd>page15-100px</kbd>. <var>$1urlwidth</var> doit être utilisé et être cohérent avec <var>$1urlparam</var>.", "apihelp-query+imageinfo-param-badfilecontexttitle": "Si <kbd>$2prop=badfile</kbd> est positionné, il s'agit du titre de la page utilisé pour évaluer la [[MediaWiki:Bad image list]]", "apihelp-query+imageinfo-param-localonly": "Rechercher les fichiers uniquement dans le référentiel local.", - "apihelp-query+imageinfo-example-simple": "Analyser les informations sur la version actuelle de [[:File:Albert Einstein Head.jpg]]", - "apihelp-query+imageinfo-example-dated": "Analyser les informations sur les versions de [[:File:Test.jpg]] depuis 2008", + "apihelp-query+imageinfo-example-simple": "Analyser les informations sur la version actuelle de [[:File:Albert Einstein Head.jpg]].", + "apihelp-query+imageinfo-example-dated": "Analyser les informations sur les versions de [[:File:Test.jpg]] depuis 2008.", "apihelp-query+images-description": "Renvoie tous les fichiers contenus dans les pages fournies.", "apihelp-query+images-param-limit": "Combien de fichiers renvoyer.", "apihelp-query+images-param-images": "Lister uniquement ces fichiers. Utile pour vérifier si une page donnée contient un fichier donné.", @@ -807,7 +808,7 @@ "apihelp-query+imageusage-example-simple": "Afficher les pages utilisant [[:File:Albert Einstein Head.jpg]]", "apihelp-query+imageusage-example-generator": "Obtenir des informations sur les pages utilisant [[:File:Albert Einstein Head.jpg]]", "apihelp-query+info-description": "Obtenir les informations de base sur la page.", - "apihelp-query+info-param-prop": "Quelles propriétés supplémentaires récupérer :\n;protection:Liste de niveau de protection de chaque page.\n;talkid:L’ID de la page de discussion pour chaque page qui n’est pas une page de discussion.\n;watched:Liste de l’état de suivi de chaque page.\n;watchers:Le nombre d’observateurs, si c&est autorisé.\n;notificationtimestamp:L’horodatage de notification de la liste de suivi de chaque page.\n;subjectid:L’ID de la page parente de chaque page de discussion.\n;url:Fournit une URL complète, une URL de modification, et l’URL canonique pour chaque page.\n;readable:Si l’utilisateur peut lire cette page.\n;preload:Fournit le texte renvoyé par EditFormPreloadText.\n;displaytitle:Fournit la manière dont le titre de la page est vraiment affiché.", + "apihelp-query+info-param-prop": "Quelles propriétés supplémentaires récupérer :", "apihelp-query+info-paramvalue-prop-protection": "Lister le niveau de protection de chaque page.", "apihelp-query+info-paramvalue-prop-talkid": "L’ID de la page de discussion de chaque page qui n’est pas de discussion.", "apihelp-query+info-paramvalue-prop-watched": "Lister l’état de suivi de chaque page.", @@ -822,8 +823,8 @@ "apihelp-query+info-param-testactions": "Tester si l’utilisateur actuel peut effectuer certaines actions sur la page.", "apihelp-query+info-param-token": "Utiliser plutôt [[Special:ApiHelp/query+tokens|action=query&meta=tokens]].", "apihelp-query+info-example-simple": "Obtenir des informations sur la page <kbd>Main Page</kbd>.", - "apihelp-query+info-example-protection": "Obtenir des informations générale et de protection sur la page <kbd>Main Page</kbd>.", - "apihelp-query+iwbacklinks-description": "Trouver toutes les pages qui ont un lien vers le lien interwiki indiqué.\n\nPeut être utilisé pour trouver tous les liens avec un préfixe, ou tous les liens vers un titre (avec un préfixe donné). N’utiliser aucun paramètre revient en pratique à « tous les liens interwiki ».", + "apihelp-query+info-example-protection": "Obtenir des informations générales et de protection sur la page <kbd>Main Page</kbd>.", + "apihelp-query+iwbacklinks-description": "Trouver toutes les pages qui ont un lien vers le lien interwiki indiqué.\n\nPeut être utilisé pour trouver tous les liens avec un préfixe, ou tous les liens vers un titre (avec un préfixe donné). Sans paramètre, équivaut à « tous les liens interwiki ».", "apihelp-query+iwbacklinks-param-prefix": "Préfixe pour l’interwiki.", "apihelp-query+iwbacklinks-param-title": "Lien interwiki à rechercher. Doit être utilisé avec <var>$1blprefix</var>.", "apihelp-query+iwbacklinks-param-limit": "Combien de pages renvoyer.", @@ -831,10 +832,10 @@ "apihelp-query+iwbacklinks-paramvalue-prop-iwprefix": "Ajoute le préfixe de l’interwiki.", "apihelp-query+iwbacklinks-paramvalue-prop-iwtitle": "Ajoute le titre de l’interwiki.", "apihelp-query+iwbacklinks-param-dir": "La direction dans laquelle lister.", - "apihelp-query+iwbacklinks-example-simple": "Obtenir les pages ayant un lien vers [[wikibooks:Test]]", - "apihelp-query+iwbacklinks-example-generator": "Obtenir des informations sur les pages ayant un lien vers [[wikibooks:Test]]", + "apihelp-query+iwbacklinks-example-simple": "Obtenir les pages qui ont un lien vers [[wikibooks:Test]].", + "apihelp-query+iwbacklinks-example-generator": "Obtenir des informations sur les pages qui ont un lien vers [[wikibooks:Test]].", "apihelp-query+iwlinks-description": "Renvoie tous les liens interwiki des pages indiquées.", - "apihelp-query+iwlinks-param-url": "S&il faut obtenir l’URL complète (impossible à utiliser avec $1prop).", + "apihelp-query+iwlinks-param-url": "S'il faut obtenir l’URL complète (impossible à utiliser avec $1prop).", "apihelp-query+iwlinks-param-prop": "Quelles propriétés supplémentaires obtenir pour chaque lien interlangue :", "apihelp-query+iwlinks-paramvalue-prop-url": "Ajoute l’URL complète.", "apihelp-query+iwlinks-param-limit": "Combien de liens interwiki renvoyer.", @@ -850,8 +851,8 @@ "apihelp-query+langbacklinks-paramvalue-prop-lllang": "Ajoute le code de langue du lien de langue.", "apihelp-query+langbacklinks-paramvalue-prop-lltitle": "Ajoute le titre du lien de langue.", "apihelp-query+langbacklinks-param-dir": "La direction dans laquelle lister.", - "apihelp-query+langbacklinks-example-simple": "Obtenir les pages avec un lien avec [[:fr:Test]]", - "apihelp-query+langbacklinks-example-generator": "Obtenir des informations sur les pages ayant un lien vers [[:fr:Test]]", + "apihelp-query+langbacklinks-example-simple": "Obtenir les pages ayant un lien vers [[:fr:Test]].", + "apihelp-query+langbacklinks-example-generator": "Obtenir des informations sur les pages ayant un lien vers [[:fr:Test]].", "apihelp-query+langlinks-description": "Renvoie tous les liens interlangue des pages fournies.", "apihelp-query+langlinks-param-limit": "Combien de liens interlangue renvoyer.", "apihelp-query+langlinks-param-url": "S’il faut récupérer l’URL complète (impossible à utiliser avec <var>$1prop</var>).", @@ -865,19 +866,19 @@ "apihelp-query+langlinks-param-inlanguagecode": "Code de langue pour les noms de langue localisés.", "apihelp-query+langlinks-example-simple": "Obtenir les liens interlangue de la page <kbd>Main Page</kbd>.", "apihelp-query+links-description": "Renvoie tous les liens des pages fournies.", - "apihelp-query+links-param-namespace": "Afficher les liens uniquement dans ces espaces de nom.", + "apihelp-query+links-param-namespace": "Afficher les liens uniquement dans ces espaces de noms.", "apihelp-query+links-param-limit": "Combien de liens renvoyer.", "apihelp-query+links-param-titles": "Lister uniquement les liens vers ces titres. Utile pour vérifier si une certaine page a un lien vers un titre donné.", "apihelp-query+links-param-dir": "La direction dans laquelle lister.", "apihelp-query+links-example-simple": "Obtenir les liens de la page <kbd>Main Page</kbd>", "apihelp-query+links-example-generator": "Obtenir des informations sur tous les liens de page dans <kbd>Main Page</kbd>.", - "apihelp-query+links-example-namespaces": "Obtenir les liens de la page <kbd>Accueil</kbd> dans les espaces de nom {{ns:user}} et {{ns:template}}.", + "apihelp-query+links-example-namespaces": "Obtenir les liens de la page <kbd>Main Page</kbd> dans les espaces de nom {{ns:user}} et {{ns:template}}.", "apihelp-query+linkshere-description": "Trouver toutes les pages ayant un lien vers les pages données.", "apihelp-query+linkshere-param-prop": "Quelles propriétés obtenir :", "apihelp-query+linkshere-paramvalue-prop-pageid": "ID de chaque page.", "apihelp-query+linkshere-paramvalue-prop-title": "Titre de chaque page.", "apihelp-query+linkshere-paramvalue-prop-redirect": "Indique si la page est une redirection.", - "apihelp-query+linkshere-param-namespace": "Inclure uniquement les pages dans ces espaces de nom.", + "apihelp-query+linkshere-param-namespace": "Inclure uniquement les pages dans ces espaces de noms.", "apihelp-query+linkshere-param-limit": "Combien de résultats renvoyer.", "apihelp-query+linkshere-param-show": "Afficher uniquement les éléments qui correspondent à ces critères :\n;redirect:Afficher uniquement les redirections.\n;!redirect:Afficher uniquement les non-redirections.", "apihelp-query+linkshere-example-simple": "Obtenir une liste des pages liées à [[Main Page]]", @@ -885,8 +886,8 @@ "apihelp-query+logevents-description": "Obtenir des événements des journaux.", "apihelp-query+logevents-param-prop": "Quelles propriétés obtenir :", "apihelp-query+logevents-paramvalue-prop-ids": "Ajoute l’ID de l’événement.", - "apihelp-query+logevents-paramvalue-prop-title": "Ajoute le titre de la page pour l’événement.", - "apihelp-query+logevents-paramvalue-prop-type": "Ajoute le type de l’événement.", + "apihelp-query+logevents-paramvalue-prop-title": "Ajoute le titre de la page pour l’événement enregistré.", + "apihelp-query+logevents-paramvalue-prop-type": "Ajoute le type de l’événement enregistré.", "apihelp-query+logevents-paramvalue-prop-user": "Ajoute l’utilisateur responsable de l’événement.", "apihelp-query+logevents-paramvalue-prop-userid": "Ajoute l’ID de l’utilisateur responsable de l’événement.", "apihelp-query+logevents-paramvalue-prop-timestamp": "Ajoute l’horodatage de l’événement.", @@ -894,13 +895,13 @@ "apihelp-query+logevents-paramvalue-prop-parsedcomment": "Ajoute le commentaire analysé de l’événement.", "apihelp-query+logevents-paramvalue-prop-details": "Liste les détails supplémentaires sur l’événement.", "apihelp-query+logevents-paramvalue-prop-tags": "Liste les balises de l’événement.", - "apihelp-query+logevents-param-type": "Filtrer les entrées du journal à ce seul type.", - "apihelp-query+logevents-param-action": "Filtrer les actions du journal à cette seule action. Écrase <var>$1type</var>. La présence d'une valeur avec un astérisque dans la liste, comme <var>$1type</var>, indique qu'une chaîne arbitraire peut être passée dans dans la requête à la place de l'astérisque.", + "apihelp-query+logevents-param-type": "Filtrer les entrées du journal sur ce seul type.", + "apihelp-query+logevents-param-action": "Filtrer les actions du journal sur cette seule action. Écrase <var>$1type</var>. Dans la liste des valeurs possibles, les valeurs suivies d'un astérisque, comme <kbd>action/*</kbd>, peuvent avoir différentes chaînes après le slash.", "apihelp-query+logevents-param-start": "L’horodatage auquel démarrer l’énumération.", "apihelp-query+logevents-param-end": "L’horodatage auquel arrêter l’énumération.", "apihelp-query+logevents-param-user": "Restreindre aux entrées générées par l’utilisateur spécifié.", "apihelp-query+logevents-param-title": "Restreindre aux entrées associées à une page donnée.", - "apihelp-query+logevents-param-namespace": "Restreindre aux entrées dans l’espace de nom spécifié.", + "apihelp-query+logevents-param-namespace": "Restreindre aux entrées dans l’espace de noms spécifié.", "apihelp-query+logevents-param-prefix": "Restreindre aux entrées commençant par ce préfixe.", "apihelp-query+logevents-param-tag": "Lister seulement les entrées ayant cette balise.", "apihelp-query+logevents-param-limit": "Combien d'entrées renvoyer au total.", @@ -923,7 +924,7 @@ "apihelp-query+pageswithprop-example-generator": "Obtenir des informations supplémentaires sur les 10 premières pages utilisant <code>__NOTOC__</code>.", "apihelp-query+prefixsearch-description": "Effectuer une recherche de préfixe sur les titres de page.\n\nMalgré les similarités dans le nom, ce module n’est pas destiné à être l’équivalent de [[Special:PrefixIndex]] ; pour cela, voyez <kbd>[[Special:ApiHelp/query+allpages|action=query&list=allpages]]</kbd> avec le paramètre <kbd>apprefix</kbd>. Le but de ce module est similaire à <kbd>[[Special:ApiHelp/opensearch|action=opensearch]]</kbd> : prendre l’entrée utilisateur et fournir les meilleurs titres s’en approchant. Selon le serveur du moteur de recherche, cela peut inclure corriger des fautes de frappe, éviter des redirections, ou d’autres heuristiques.", "apihelp-query+prefixsearch-param-search": "Chaîne de recherche.", - "apihelp-query+prefixsearch-param-namespace": "Espaces de nom à rechercher.", + "apihelp-query+prefixsearch-param-namespace": "Espaces de noms à rechercher.", "apihelp-query+prefixsearch-param-limit": "Nombre maximal de résultats à renvoyer.", "apihelp-query+prefixsearch-param-offset": "Nombre de résultats à sauter.", "apihelp-query+prefixsearch-example-simple": "Rechercher les titres de page commençant par <kbd>meaning</kbd>.", @@ -943,35 +944,35 @@ "apihelp-query+protectedtitles-paramvalue-prop-expiry": "Ajoute l’horodatage de levée de la protection.", "apihelp-query+protectedtitles-paramvalue-prop-level": "Ajoute le niveau de protection.", "apihelp-query+protectedtitles-example-simple": "Lister les titres protégés", - "apihelp-query+protectedtitles-example-generator": "Trouver les liens vers les titres protégés dans l’espace de noms principal", - "apihelp-query+querypage-description": "Obtenir une liste fournie par une page spéciale basée sur QueryPage", - "apihelp-query+querypage-param-page": "Le nom de la page spéciale. Remarque, ce nom est sensible à la casse.", + "apihelp-query+protectedtitles-example-generator": "Trouver les liens vers les titres protégés dans l’espace de noms principal.", + "apihelp-query+querypage-description": "Obtenir une liste fournie par une page spéciale basée sur QueryPage.", + "apihelp-query+querypage-param-page": "Le nom de la page spéciale. Notez que ce nom est sensible à la casse.", "apihelp-query+querypage-param-limit": "Nombre de résultats à renvoyer.", "apihelp-query+querypage-example-ancientpages": "Renvoyer les résultats de [[Special:Ancientpages]].", "apihelp-query+random-description": "Obtenir un ensemble de pages au hasard.\n\nLes pages sont listées dans un ordre prédéterminé, seul le point de départ est aléatoire. Par exemple, cela signifie que si la première page dans la liste est <samp>Accueil</samp>, la seconde sera <em>toujours</em> <samp>Liste des singes de fiction</samp>, la troisième <samp>Liste de personnes figurant sur les timbres de Vanuatu</samp>, etc.", "apihelp-query+random-param-namespace": "Renvoyer seulement des pages de ces espaces de noms.", - "apihelp-query+random-param-limit": "Limite sur le nombre de pages aléatoires renvoyées.", + "apihelp-query+random-param-limit": "Limiter le nombre de pages aléatoires renvoyées.", "apihelp-query+random-param-redirect": "Utilisez <kbd>$1filterredir=redirects</kbd> au lieu de ce paramètre.", "apihelp-query+random-param-filterredir": "Comment filtrer les redirections.", - "apihelp-query+random-example-simple": "Obtenir deux pages aléatoires de l’espace principal", - "apihelp-query+random-example-generator": "Renvoyer les informations de la page sur deux pages au hasard de l’espace de noms principal", + "apihelp-query+random-example-simple": "Obtenir deux pages aléatoires de l’espace de noms principal.", + "apihelp-query+random-example-generator": "Renvoyer les informations de la page sur deux pages au hasard de l’espace de noms principal.", "apihelp-query+recentchanges-description": "Énumérer les modifications récentes.", "apihelp-query+recentchanges-param-start": "L’horodatage auquel démarrer l’énumération.", "apihelp-query+recentchanges-param-end": "L’horodatage auquel arrêter l’énumération.", - "apihelp-query+recentchanges-param-namespace": "Filtrer les modifications uniquement sur ces espaces de nom.", - "apihelp-query+recentchanges-param-user": "Lister uniquement les modifications par cet utilisateur.", - "apihelp-query+recentchanges-param-excludeuser": "Ne pas lister les modifications par cet utilisateur.", + "apihelp-query+recentchanges-param-namespace": "Filtrer les modifications uniquement sur ces espaces de noms.", + "apihelp-query+recentchanges-param-user": "Lister uniquement les modifications faites par cet utilisateur.", + "apihelp-query+recentchanges-param-excludeuser": "Ne pas lister les modifications faites par cet utilisateur.", "apihelp-query+recentchanges-param-tag": "Lister uniquement les modifications marquées avec cette balise.", "apihelp-query+recentchanges-param-prop": "Inclure des informations supplémentaires :", - "apihelp-query+recentchanges-paramvalue-prop-user": "Ajoute l’utilisateur responsable de la modification et marque si c’est une adresse IP.", + "apihelp-query+recentchanges-paramvalue-prop-user": "Ajoute l’utilisateur responsable de la modification et marque s'il s'agit d'une adresse IP.", "apihelp-query+recentchanges-paramvalue-prop-userid": "Ajoute l’ID de l’utilisateur responsable de la modification.", "apihelp-query+recentchanges-paramvalue-prop-comment": "Ajoute le commentaire de la modification.", "apihelp-query+recentchanges-paramvalue-prop-parsedcomment": "Ajoute le commentaire analysé pour la modification.", "apihelp-query+recentchanges-paramvalue-prop-flags": "Ajoute les balises de la modification.", "apihelp-query+recentchanges-paramvalue-prop-timestamp": "Ajoute l’horodatage de la modification.", "apihelp-query+recentchanges-paramvalue-prop-title": "Ajoute le titre de la page modifiée.", - "apihelp-query+recentchanges-paramvalue-prop-ids": "Ajoute l’ID de la page, l’ID des modifications récentes et l’ID de l’ancienne et la nouvelle révisions.", - "apihelp-query+recentchanges-paramvalue-prop-sizes": "Ajoute l’ancienne et la nouvelle tailles de la page en octets.", + "apihelp-query+recentchanges-paramvalue-prop-ids": "Ajoute l’ID de la page, l’ID des modifications récentes et l’ID de l’ancienne et de la nouvelle révision.", + "apihelp-query+recentchanges-paramvalue-prop-sizes": "Ajoute l’ancienne et la nouvelle taille de la page en octets.", "apihelp-query+recentchanges-paramvalue-prop-redirect": "Marque la modification si la page est une redirection.", "apihelp-query+recentchanges-paramvalue-prop-patrolled": "Marque les modifications patrouillables comme patrouillées ou non.", "apihelp-query+recentchanges-paramvalue-prop-loginfo": "Ajoute les informations du journal (Id du journal, type de trace, etc.) aux entrées du journal.", @@ -990,7 +991,7 @@ "apihelp-query+redirects-paramvalue-prop-pageid": "ID de page de chaque redirection.", "apihelp-query+redirects-paramvalue-prop-title": "Titre de chaque redirection.", "apihelp-query+redirects-paramvalue-prop-fragment": "Fragment de chaque redirection, s’il y en a un.", - "apihelp-query+redirects-param-namespace": "Inclure uniquement les pages dans ces espaces de nom.", + "apihelp-query+redirects-param-namespace": "Inclure uniquement les pages dans ces espaces de noms.", "apihelp-query+redirects-param-limit": "Combien de redirections renvoyer.", "apihelp-query+redirects-param-show": "Afficher uniquement les éléments correspondant à ces critères :\n;fragment:Afficher uniquement les redirections avec un fragment.\n;!fragment:Afficher uniquement les redirections sans fragment.", "apihelp-query+redirects-example-simple": "Obtenir une liste des redirections vers [[Main Page]]", @@ -1005,7 +1006,7 @@ "apihelp-query+revisions-param-excludeuser": "Exclure les révisions faites par l’utilisateur.", "apihelp-query+revisions-param-tag": "Lister uniquement les révisions marquées avec cette balise.", "apihelp-query+revisions-param-token": "Quels jetons obtenir pour chaque révision.", - "apihelp-query+revisions-example-content": "Obtenir des données avec le contenu pour la dernière révision des titres <kbd>API</kbd> et <kbd>Page principale</kbd>.", + "apihelp-query+revisions-example-content": "Obtenir des données avec le contenu pour la dernière révision des titres <kbd>API</kbd> et <kbd>Main Page</kbd>.", "apihelp-query+revisions-example-last5": "Obtenir les 5 dernières révisions de la <kbd>Main Page</kbd>.", "apihelp-query+revisions-example-first5": "Obtenir les 5 premières révisions de la <kbd>Page principale</kbd>.", "apihelp-query+revisions-example-first5-after": "Obtenir les 5 premières révisions de la <kbd>Page principale</kbd> faites après le 01/05/2006.", @@ -1030,13 +1031,13 @@ "apihelp-query+revisions+base-param-generatexml": "Générer l’arbre d’analyse XML pour le contenu de la révision (nécessite $1prop=content ; remplacé par <kbd>$1prop=parsetree</kbd>).", "apihelp-query+revisions+base-param-parse": "Analyser le contenu de la révision (nécessite $1prop=content). Pour des raisons de performance, si cette option est utilisée, $1limit est forcé à 1.", "apihelp-query+revisions+base-param-section": "Récupérer uniquement le contenu de ce numéro de section.", - "apihelp-query+revisions+base-param-diffto": "ID de révision à comparer à chaque révision. Utiliser <kbd>prev</kbd>, <kbd>next</kbd> et <kbd>cur</kbd> pour la version précédente, suivante et actuelle respectivement.", - "apihelp-query+revisions+base-param-difftotext": "Texte auquel comparer chaque révision. Compare uniquement un nombre limité de révisions. Écrase <var>$1diffto</var>. Si <var>$1section</var> est positionné, seule cette section sera comparée avec ce texte", + "apihelp-query+revisions+base-param-diffto": "ID de révision à prendre pour comparer chaque révision. Utiliser <kbd>prev</kbd>, <kbd>next</kbd> et <kbd>cur</kbd> pour la version précédente, suivante et actuelle respectivement.", + "apihelp-query+revisions+base-param-difftotext": "Texte auquel comparer chaque révision. Compare uniquement un nombre limité de révisions. Écrase <var>$1diffto</var>. Si <var>$1section</var> est positionné, seule cette section sera comparée avec ce texte.", "apihelp-query+revisions+base-param-difftotextpst": "Effectuer une transformation avant enregistrement sur le texte avant de le comparer. Valide uniquement quand c’est utilisé avec <var>$1difftotext</var>.", "apihelp-query+revisions+base-param-contentformat": "Format de sérialisation utilisé pour <var>$1difftotext</var> et attendu pour la sortie du contenu.", "apihelp-query+search-description": "Effectuer une recherche en texte intégral.", "apihelp-query+search-param-search": "Rechercher les titres de page ou le contenu correspondant à cette valeur. Vous pouvez utiliser la chaîne de recherche pour invoquer des fonctionnalités de recherche spéciales, selon ce que le serveur de recherche du wiki implémente.", - "apihelp-query+search-param-namespace": "Rechercher uniquement dans ces espaces de nom.", + "apihelp-query+search-param-namespace": "Rechercher uniquement dans ces espaces de noms.", "apihelp-query+search-param-what": "Quel type de recherche effectuer.", "apihelp-query+search-param-info": "Quelles métadonnées renvoyer.", "apihelp-query+search-param-prop": "Quelles propriétés renvoyer :", @@ -1052,25 +1053,25 @@ "apihelp-query+search-paramvalue-prop-sectiontitle": "Ajoute le titre de la section correspondante.", "apihelp-query+search-paramvalue-prop-categorysnippet": "Ajoute un extrait analysé de la catégorie correspondante.", "apihelp-query+search-paramvalue-prop-isfilematch": "Ajoute un booléen indiquant si la recherche correspond au contenu du fichier.", - "apihelp-query+search-paramvalue-prop-score": "<span class=\"apihelp-deprecated\">Désuet et ignoré.</span>", + "apihelp-query+search-paramvalue-prop-score": "<span class=\"apihelp-deprecated\">Obsolète et ignoré.</span>", "apihelp-query+search-paramvalue-prop-hasrelated": "<span class=\"apihelp-deprecated\">Obsolète et ignoré.</span>", "apihelp-query+search-param-limit": "Combien de pages renvoyer au total.", "apihelp-query+search-param-interwiki": "Inclure les résultats interwiki dans la recherche, s’ils sont disponibles.", "apihelp-query+search-param-backend": "Quel serveur de recherche utiliser, si ce n’est pas celui par défaut.", "apihelp-query+search-param-enablerewrites": "Activer la réécriture interne de la requête. Les serveurs de recherche peuvent changer la requête en une autre dont ils estiment qu'elle donne de meilleurs résultats, par exemple en corrigeant l'orthographe.", "apihelp-query+search-example-simple": "Rechercher <kbd>meaning</kbd>.", - "apihelp-query+search-example-text": "Rechercher des textes pour <kbd>signification</kbd>.", + "apihelp-query+search-example-text": "Rechercher des textes pour <kbd>meaning</kbd>.", "apihelp-query+search-example-generator": "Obtenir les informations sur les pages renvoyées par une recherche de <kbd>meaning</kbd>.", "apihelp-query+siteinfo-description": "Renvoyer les informations générales sur le site.", "apihelp-query+siteinfo-param-prop": "Quelles informations obtenir :", "apihelp-query+siteinfo-paramvalue-prop-general": "Information globale du système.", - "apihelp-query+siteinfo-paramvalue-prop-namespaces": "Liste des espaces de nom déclarés et leur nom canonique.", - "apihelp-query+siteinfo-paramvalue-prop-namespacealiases": "Liste des alias des espaces de nom déclarés.", + "apihelp-query+siteinfo-paramvalue-prop-namespaces": "Liste des espaces de noms déclarés avec leur nom canonique.", + "apihelp-query+siteinfo-paramvalue-prop-namespacealiases": "Liste des alias des espaces de noms déclarés.", "apihelp-query+siteinfo-paramvalue-prop-specialpagealiases": "Liste des alias des pages spéciales.", "apihelp-query+siteinfo-paramvalue-prop-magicwords": "Liste des mots magiques et leurs alias.", "apihelp-query+siteinfo-paramvalue-prop-statistics": "Renvoie les statistiques du site.", "apihelp-query+siteinfo-paramvalue-prop-interwikimap": "Renvoie la correspondance interwiki (éventuellement filtrée, éventuellement localisée en utilisant <var>$1inlanguagecode</var>).", - "apihelp-query+siteinfo-paramvalue-prop-dbrepllag": "Renvoie le serveur de base de donnée avec la plus grande latence de réplication.", + "apihelp-query+siteinfo-paramvalue-prop-dbrepllag": "Renvoie le serveur de base de données ayant la plus grande latence de réplication.", "apihelp-query+siteinfo-paramvalue-prop-usergroups": "Renvoie les groupes utilisateur et les droits associés.", "apihelp-query+siteinfo-paramvalue-prop-libraries": "Renvoie les bibliothèques installées sur le wiki.", "apihelp-query+siteinfo-paramvalue-prop-extensions": "Renvoie les extensions installées sur le wiki.", @@ -1078,26 +1079,27 @@ "apihelp-query+siteinfo-paramvalue-prop-rightsinfo": "Renvoie l’information sur les droits du wiki (sa licence), si elle est disponible.", "apihelp-query+siteinfo-paramvalue-prop-restrictions": "Renvoie l’information sur les types de restriction disponibles (protection).", "apihelp-query+siteinfo-paramvalue-prop-languages": "Renvoie une liste des langues que MédiaWiki prend en charge (éventuellement localisée en utilisant <var>$1inlanguagecode</var>).", + "apihelp-query+siteinfo-paramvalue-prop-languagevariants": "Renvoie une liste de codes de langue pour lesquels [[mw:Special:MyLanguage/LanguageConverter|LanguageConverter]] est activé, et les variantes supportées pour chacun.", "apihelp-query+siteinfo-paramvalue-prop-skins": "Renvoie une liste de tous les habillages activés (éventuellement localisé en utilisant <var>$1inlanguagecode</var>, sinon dans la langue du contenu).", "apihelp-query+siteinfo-paramvalue-prop-extensiontags": "Renvoie une liste des balises d’extension de l’analyseur.", "apihelp-query+siteinfo-paramvalue-prop-functionhooks": "Renvoie une liste des accroches de fonction de l’analyseur.", - "apihelp-query+siteinfo-paramvalue-prop-showhooks": "Renvoie une liste de toutes les accroches souscrites (contenu de <var>[[mw:Manual:$wgHooks|$wgHooks]]</var>).", - "apihelp-query+siteinfo-paramvalue-prop-variables": "Renvoie une liste des IDs de variable.", - "apihelp-query+siteinfo-paramvalue-prop-protocols": "Renvoie une liste des protocoles qui sont autorisés dans les liens externes.", + "apihelp-query+siteinfo-paramvalue-prop-showhooks": "Renvoie une liste de toutes les accroches souscrites (contenu de <var>[[mw:Special:MyLanguage/Manual:$wgHooks|$wgHooks]]</var>).", + "apihelp-query+siteinfo-paramvalue-prop-variables": "Renvoie une liste d'IDs de variable.", + "apihelp-query+siteinfo-paramvalue-prop-protocols": "Renvoie une liste de protocoles autorisés dans les liens externes.", "apihelp-query+siteinfo-paramvalue-prop-defaultoptions": "Renvoie les valeurs par défaut pour les préférences utilisateur.", "apihelp-query+siteinfo-paramvalue-prop-uploaddialog": "Renvoie la configuration du dialogue de téléversement.", "apihelp-query+siteinfo-param-filteriw": "Renvoyer uniquement les entrées locales ou uniquement les non locales de la correspondance interwiki.", "apihelp-query+siteinfo-param-showalldb": "Lister tous les serveurs de base de données, pas seulement celui avec la plus grande latence.", "apihelp-query+siteinfo-param-numberingroup": "Liste le nombre d’utilisateurs dans les groupes.", "apihelp-query+siteinfo-param-inlanguagecode": "Code de langue pour les noms de langue localisés (du mieux possible) et les noms d’habillage.", - "apihelp-query+siteinfo-example-simple": "Extraire les informations du site", - "apihelp-query+siteinfo-example-interwiki": "Extraire une liste des préfixes interwiki locaux", - "apihelp-query+siteinfo-example-replag": "Vérifier la latence de réplication actuelle", + "apihelp-query+siteinfo-example-simple": "Extraire les informations du site.", + "apihelp-query+siteinfo-example-interwiki": "Extraire une liste des préfixes interwiki locaux.", + "apihelp-query+siteinfo-example-replag": "Vérifier la latence de réplication actuelle.", "apihelp-query+stashimageinfo-description": "Renvoie les informations de fichier des fichiers mis en réserve.", "apihelp-query+stashimageinfo-param-filekey": "Clé qui identifie un téléchargement précédent qui a été temporairement mis en réserve.", - "apihelp-query+stashimageinfo-param-sessionkey": "Alias pour $1filekey, pour la compatibilité descendante.", + "apihelp-query+stashimageinfo-param-sessionkey": "Alias pour $1filekey, pour la compatibilité ascendante.", "apihelp-query+stashimageinfo-example-simple": "Renvoie les informations sur un fichier mis en réserve.", - "apihelp-query+stashimageinfo-example-params": "Renvoie les vignettes pour deux fichiers mis en réserve", + "apihelp-query+stashimageinfo-example-params": "Renvoie les vignettes pour deux fichiers mis de côté.", "apihelp-query+tags-description": "Lister les balises de modification.", "apihelp-query+tags-param-limit": "Le nombre maximal de balises à lister.", "apihelp-query+tags-param-prop": "Quelles propriétés récupérer :", @@ -1106,7 +1108,7 @@ "apihelp-query+tags-paramvalue-prop-description": "Ajoute la description de la balise.", "apihelp-query+tags-paramvalue-prop-hitcount": "Ajoute le nombre de révisions et d’entrées du journal qui ont cette balise.", "apihelp-query+tags-paramvalue-prop-defined": "Indique si la balise est définie.", - "apihelp-query+tags-paramvalue-prop-source": "Obtient les sources de la balise, ce qui comprend <samp>extension</samp> pour les balises définies par une extension et <samp>manual</samp> pour les balises pouvant être appliquées manuellement par les utilisateurs.", + "apihelp-query+tags-paramvalue-prop-source": "Retourne les sources de la balise, ce qui comprend <samp>extension</samp> pour les balises définies par une extension et <samp>manual</samp> pour les balises pouvant être appliquées manuellement par les utilisateurs.", "apihelp-query+tags-paramvalue-prop-active": "Si la balise est encore appliquée.", "apihelp-query+tags-example-simple": "Lister les balises disponibles", "apihelp-query+templates-description": "Renvoie toutes les pages incluses dans les pages fournies.", @@ -1131,7 +1133,7 @@ "apihelp-query+transcludedin-param-show": "Afficher uniquement les éléments qui correspondent à ces critères:\n;redirect:Afficher uniquement les redirections.\n;!redirect:Afficher uniquement les non-redirections.", "apihelp-query+transcludedin-example-simple": "Obtenir une liste des pages incluant <kbd>Main Page</kbd>.", "apihelp-query+transcludedin-example-generator": "Obtenir des informations sur les pages incluant <kbd>Main Page</kbd>.", - "apihelp-query+usercontribs-description": "Obtenir toutes les modifications par un utilisateur.", + "apihelp-query+usercontribs-description": "Obtenir toutes les modifications d'un utilisateur.", "apihelp-query+usercontribs-param-limit": "Le nombre maximal de contributions à renvoyer.", "apihelp-query+usercontribs-param-start": "L’horodatage auquel démarrer le retour.", "apihelp-query+usercontribs-param-end": "L’horodatage auquel arrêter le retour.", @@ -1150,20 +1152,21 @@ "apihelp-query+usercontribs-paramvalue-prop-flags": "Ajoute les marques de la modification.", "apihelp-query+usercontribs-paramvalue-prop-patrolled": "Marque les modifications patrouillées.", "apihelp-query+usercontribs-paramvalue-prop-tags": "Liste les balises de la modification.", - "apihelp-query+usercontribs-param-show": "Afficher uniquement les éléments correspondant à ces critères, par ex. les modifications non mineures uniquement : <kbd>$2show=!minor</kbd>.\n\nSi <kbd>$2show=patrolled</kbd> ou <kbd>$2show=!patrolled</kbd> est positionné, les révisions plus anciennes que <var>[[mw:Manual:$wgRCMaxAge|$wgRCMaxAge]]</var> ($1 {{PLURAL:$1|seconde|secondes}}) ne seront pas affichées.", + "apihelp-query+usercontribs-param-show": "Afficher uniquement les éléments correspondant à ces critères, par ex. les modifications non mineures uniquement : <kbd>$2show=!minor</kbd>.\n\nSi <kbd>$2show=patrolled</kbd> ou <kbd>$2show=!patrolled</kbd> est positionné, les révisions plus anciennes que <var>[[mw:Special:MyLanguage/Manual:$wgRCMaxAge|$wgRCMaxAge]]</var> ($1 {{PLURAL:$1|seconde|secondes}}) ne seront pas affichées.", "apihelp-query+usercontribs-param-tag": "Lister uniquement les révisions marquées avec cette balise.", - "apihelp-query+usercontribs-param-toponly": "Lister uniquement les modifications qui sont la dernière révision.", + "apihelp-query+usercontribs-param-toponly": "Lister uniquement les modifications de la dernière révision.", "apihelp-query+usercontribs-example-user": "Afficher les contributions de l'utilisateur <kbd>Exemple</kbd>.", "apihelp-query+usercontribs-example-ipprefix": "Afficher les contributions de toutes les adresses IP avec le préfixe <kbd>192.0.2.</kbd>.", - "apihelp-query+userinfo-description": "Obtenir de l’information sur l’utilisateur courant.", + "apihelp-query+userinfo-description": "Obtenir des informations sur l’utilisateur courant.", "apihelp-query+userinfo-param-prop": "Quelles informations inclure :", "apihelp-query+userinfo-paramvalue-prop-blockinfo": "Marque si l’utilisateur actuel est bloqué, par qui, et pour quelle raison.", "apihelp-query+userinfo-paramvalue-prop-hasmsg": "Ajoute une balise <samp>messages</samp> si l’utilisateur actuel a des messages en cours.", "apihelp-query+userinfo-paramvalue-prop-groups": "Liste tous les groupes auxquels appartient l’utilisateur actuel.", + "apihelp-query+userinfo-paramvalue-prop-groupmemberships": "Liste les groupes auxquels l’utilisateur actuel a été explicitement affecté, avec la date d’expiration de chaque appartenance au groupe.", "apihelp-query+userinfo-paramvalue-prop-implicitgroups": "Liste tous les groupes dont l’utilisateur actuel est automatiquement membre.", "apihelp-query+userinfo-paramvalue-prop-rights": "Liste tous les droits qu’a l’utilisateur actuel.", "apihelp-query+userinfo-paramvalue-prop-changeablegroups": "Liste les groupes pour lesquels l’utilisateur actuel peut ajouter ou supprimer.", - "apihelp-query+userinfo-paramvalue-prop-options": "Liste toutes les préférences qu’a défini l’utilisateur actuel.", + "apihelp-query+userinfo-paramvalue-prop-options": "Liste toutes les préférences qu’a définies l’utilisateur actuel.", "apihelp-query+userinfo-paramvalue-prop-preferencestoken": "<span class=\"apihelp-deprecated\">Obsolete.</span> Obtenir un jeton pour modifier les préférences de l’utilisateur actuel.", "apihelp-query+userinfo-paramvalue-prop-editcount": "Ajoute le compteur de modifications de l’utilisateur actuel.", "apihelp-query+userinfo-paramvalue-prop-ratelimits": "Liste toutes les limites de débit s’appliquant à l’utilisateur actuel.", @@ -1174,12 +1177,13 @@ "apihelp-query+userinfo-paramvalue-prop-unreadcount": "Ajoute le compteur de pages non lues de la liste de suivi de l’utilisateur (au maximum $1 ; renvoie <samp>$2</samp> s’il y en a plus).", "apihelp-query+userinfo-paramvalue-prop-centralids": "Ajoute les IDs centraux et l’état d’attachement de l’utilisateur.", "apihelp-query+userinfo-param-attachedwiki": "Avec <kbd>$1prop=centralids</kbd>, indiquer si l’utilisateur est attaché au wiki identifié par cet ID.", - "apihelp-query+userinfo-example-simple": "Obtenir de l’information sur l’utilisateur actuel", + "apihelp-query+userinfo-example-simple": "Obtenir des informations sur l’utilisateur actuel.", "apihelp-query+userinfo-example-data": "Obtenir des informations supplémentaires sur l’utilisateur actuel", - "apihelp-query+users-description": "Obtenir des information sur une liste d’utilisateurs", + "apihelp-query+users-description": "Obtenir des informations sur une liste d’utilisateurs", "apihelp-query+users-param-prop": "Quelles informations inclure :", "apihelp-query+users-paramvalue-prop-blockinfo": "Marque si l’utilisateur est bloqué, par qui, et pour quelle raison.", - "apihelp-query+users-paramvalue-prop-groups": "Liste tous les groupes auquel appartient chaque utilisateur.", + "apihelp-query+users-paramvalue-prop-groups": "Liste tous les groupes auxquels appartient chaque utilisateur.", + "apihelp-query+users-paramvalue-prop-groupmemberships": "Liste les groupes auxquels chaque utilisateur a été explicitement affecté, avec la date d’expiration de l’appartenance à chaque groupe.", "apihelp-query+users-paramvalue-prop-implicitgroups": "Liste tous les groupes dont un utilisateur est automatiquement membre.", "apihelp-query+users-paramvalue-prop-rights": "Liste tous les droits qu’a un utilisateur.", "apihelp-query+users-paramvalue-prop-editcount": "Ajoute le compteur de modifications de l’utilisateur.", @@ -1223,11 +1227,11 @@ "apihelp-query+watchlist-paramvalue-type-categorize": "Modifications d’appartenance aux catégories.", "apihelp-query+watchlist-param-owner": "Utilisé avec $1token pour accéder à la liste de suivi d’un autre utilisateur.", "apihelp-query+watchlist-param-token": "Un jeton de sécurité (disponible dans les [[Special:Preferences#mw-prefsection-watchlist|préférences]] de l’utilsiateur) pour autoriser l’accès à la liste de suivi d&un autre utilisateur.", - "apihelp-query+watchlist-example-simple": "Lister la révision de tête des pages récemment modifiées dans la liste de suivi de l’utilisateur actuel", - "apihelp-query+watchlist-example-props": "Chercher des informations supplémentaires sur la révision de tête des pages récemment modifiées de la liste de suivi de l’utilisateur actuel", + "apihelp-query+watchlist-example-simple": "Lister la révision de tête des pages récemment modifiées dans la liste de suivi de l’utilisateur actuel.", + "apihelp-query+watchlist-example-props": "Chercher des informations supplémentaires sur la révision de tête des pages récemment modifiées de la liste de suivi de l’utilisateur actuel.", "apihelp-query+watchlist-example-allrev": "Chercher les informations sur toutes les modifications récentes des pages de la liste de suivi de l’utilisateur actuel", "apihelp-query+watchlist-example-generator": "Chercher l’information de la page sur les pages récemment modifiées de la liste de suivi de l’utilisateur actuel", - "apihelp-query+watchlist-example-generator-rev": "Chercher l’information de la révision pour les modifications récentes des pages de la liste de suivi de l’utilisateur actuel", + "apihelp-query+watchlist-example-generator-rev": "Chercher l’information de la révision pour les modifications récentes des pages de la liste de suivi de l’utilisateur actuel.", "apihelp-query+watchlist-example-wlowner": "Lister la révision de tête des pages récemment modifiées de la liste de suivi de l'utilisateur <kbd>Exemple</kbd>.", "apihelp-query+watchlistraw-description": "Obtenir toutes les pages de la liste de suivi de l’utilisateur actuel.", "apihelp-query+watchlistraw-param-namespace": "Lister uniquement les pages dans les espaces de nom fournis.", @@ -1240,17 +1244,17 @@ "apihelp-query+watchlistraw-param-dir": "Le sens dans lequel lister.", "apihelp-query+watchlistraw-param-fromtitle": "Démarrer l'énumération avec ce Titre (inclure le préfixe d'espace de noms) :", "apihelp-query+watchlistraw-param-totitle": "Terminer l'énumération avec ce Titre (inclure le préfixe d'espace de noms) :", - "apihelp-query+watchlistraw-example-simple": "Lister les pages dans la liste de suivi de l’utilisateur actuel", - "apihelp-query+watchlistraw-example-generator": "Chercher l’information sur les pages de la liste de suivi de l’utilisateur actuel", + "apihelp-query+watchlistraw-example-simple": "Lister les pages dans la liste de suivi de l’utilisateur actuel.", + "apihelp-query+watchlistraw-example-generator": "Chercher l’information sur les pages de la liste de suivi de l’utilisateur actuel.", "apihelp-removeauthenticationdata-description": "Supprimer les données d’authentification pour l’utilisateur actuel.", "apihelp-removeauthenticationdata-example-simple": "Tentative de suppression des données de l’utilisateur pour <kbd>FooAuthenticationRequest</kbd>.", "apihelp-resetpassword-description": "Envoyer un courriel de réinitialisation du mot de passe à un utilisateur.", - "apihelp-resetpassword-description-noroutes": "Aucun chemin pour réinitialiser le mot de passe n’est disponible.\n\nActiver les chemins dans <var>[[mw:Manual:$wgPasswordResetRoutes|$wgPasswordResetRoutes]]</var> pour utiliser ce module.", + "apihelp-resetpassword-description-noroutes": "Aucun chemin pour réinitialiser le mot de passe n’est disponible.\n\nActiver les chemins dans <var>[[mw:Special:MyLanguage/Manual:$wgPasswordResetRoutes|$wgPasswordResetRoutes]]</var> pour utiliser ce module.", "apihelp-resetpassword-param-user": "Utilisateur ayant été réinitialisé.", "apihelp-resetpassword-param-email": "Adresse courriel de l’utilisateur ayant été réinitialisé.", "apihelp-resetpassword-example-user": "Envoyer un courriel de réinitialisation du mot de passe à l’utilisateur <kbd>Exemple</kbd>.", "apihelp-resetpassword-example-email": "Envoyer un courriel pour la réinitialisation de mot de passe à tous les utilisateurs avec une adresse email <kbd>user@example.com</kbd>.", - "apihelp-revisiondelete-description": "Supprimer et annuler la suppression des révisions.", + "apihelp-revisiondelete-description": "Supprimer et rétablir des révisions.", "apihelp-revisiondelete-param-type": "Type de suppression de révision en cours de traitement.", "apihelp-revisiondelete-param-target": "Titre de page pour la suppression de révision, s’il est nécessaire pour le type.", "apihelp-revisiondelete-param-ids": "Identifiants pour les révisions à supprimer.", @@ -1283,7 +1287,7 @@ "apihelp-setnotificationtimestamp-example-pagetimestamp": "Fixer l’horodatage de notification pour <kbd>Page principale</kbd> afin que toutes les modifications depuis le 1 janvier 2012 soient non vues", "apihelp-setnotificationtimestamp-example-allpages": "Réinitialiser l’état de notification sur les pages dans l’espace de noms <kbd>{{ns:user}}</kbd>.", "apihelp-setpagelanguage-description": "Modifier la langue d’une page.", - "apihelp-setpagelanguage-description-disabled": "Il n’est pas possible de modifier la langue d’une page sur ce wiki.\n\nActiver <var>[[mw:Manual:$wgPageLanguageUseDB|$wgPageLanguageUseDB]]</var> pour utiliser cette action.", + "apihelp-setpagelanguage-description-disabled": "Il n’est pas possible de modifier la langue d’une page sur ce wiki.\n\nActiver <var>[[mw:Special:MyLanguage/Manual:$wgPageLanguageUseDB|$wgPageLanguageUseDB]]</var> pour utiliser cette action.", "apihelp-setpagelanguage-param-title": "Titre de la page dont vous souhaitez modifier la langue. Ne peut pas être utilisé avec <var>$1pageid</var>.", "apihelp-setpagelanguage-param-pageid": "Identifiant (ID) de la page dont vous souhaitez modifier la langue. Ne peut être utilisé avec <var>$1title</var>.", "apihelp-setpagelanguage-param-lang": "Code de langue vers lequel la page doit être changée. Utiliser <kbd>defaut</kbd> pour réinitialiser la page sur la langue par défaut du contenu du wiki.", @@ -1316,14 +1320,14 @@ "apihelp-tokens-example-edit": "Récupérer un jeton de modification (par défaut).", "apihelp-tokens-example-emailmove": "Récupérer un jeton de courriel et un jeton de déplacement.", "apihelp-unblock-description": "Débloquer un utilisateur.", - "apihelp-unblock-param-id": "ID du blocage à lever (obtenu via <kbd>list=blocks</kbd>). Impossible à utiliser avec <var>$1user</var> ou <var>$luserid</var>.", - "apihelp-unblock-param-user": "Nom d’utilisateur, adresse IP ou plage d’adresses IP à débloquer. Impossible à utiliser en même temps que <var>$1id</var> ou <var>$luserid</var>.", + "apihelp-unblock-param-id": "ID du blocage à lever (obtenu via <kbd>list=blocks</kbd>). Impossible à utiliser avec <var>$1user</var> ou <var>$1userid</var>.", + "apihelp-unblock-param-user": "Nom d’utilisateur, adresse IP ou plage d’adresses IP à débloquer. Impossible à utiliser en même temps que <var>$1id</var> ou <var>$1userid</var>.", "apihelp-unblock-param-userid": "ID de l'utilisateur à débloquer. Ne peut être utilisé avec <var>$1id</var> ou <var>$1user</var>.", "apihelp-unblock-param-reason": "Motif de déblocage.", "apihelp-unblock-param-tags": "Modifier les balises à appliquer à l’entrée dans le journal de blocage.", "apihelp-unblock-example-id": "Lever le blocage d’ID #<kbd>105</kbd>.", "apihelp-unblock-example-user": "Débloquer l’utilisateur <kbd>Bob</kbd> avec le motif <kbd>Désolé Bob</kbd>.", - "apihelp-undelete-description": "Restaurer les révisions d’une page supprimée.\n\nUne liste des révisions supprimées (avec les horodatages) peut être récupérée via [[Special:ApiHelp/query+deletedrevs|list=deletedrevs]], et une liste d’IDs de fichier supprimé peut être récupérée via [[Special:ApiHelp/query+filearchive|list=filearchive]].", + "apihelp-undelete-description": "Restaurer les révisions d’une page supprimée.\n\nUne liste des révisions supprimées (avec les horodatages) peut être récupérée via [[Special:ApiHelp/query+deletedrevisions|prop=deletedrevisions]], et une liste d’IDs de fichier supprimé peut être récupérée via [[Special:ApiHelp/query+filearchive|list=filearchive]].", "apihelp-undelete-param-title": "Titre de la page à restaurer.", "apihelp-undelete-param-reason": "Motif de restauration.", "apihelp-undelete-param-tags": "Modifier les balises à appliquer à l’entrée dans le journal de suppression.", @@ -1357,12 +1361,14 @@ "apihelp-userrights-description": "Modifier l’appartenance d’un utilisateur à un groupe.", "apihelp-userrights-param-user": "Nom d’utilisateur.", "apihelp-userrights-param-userid": "ID de l’utilisateur.", - "apihelp-userrights-param-add": "Ajouter l’utilisateur à ces groupes.", + "apihelp-userrights-param-add": "Ajouter l’utilisateur à ces groupes, ou s’ils sont déjà membres, mettre à jour la date d’expiration de leur appartenance à ce groupe.", + "apihelp-userrights-param-expiry": "Horodatages d’expiration. Peuvent être relatifs (par ex. <kbd>5 mois</kbd> ou <kbd>2 semaines</kbd>) ou absolus (par ex. <kbd>2014-09-18T12:34:56Z</kbd>). Si uniquement un horodatage est fixé, il sera utilisé pour tous les groupes passés au paramètre <var>$1add</var>. Utiliser <kbd>infinite</kbd>, <kbd>indefinite</kbd>, <kbd>infinity</kbd>, ou <kbd>never</kbd> pour une lien utilisateur-groupe qui n’expire jamais.", "apihelp-userrights-param-remove": "Supprimer l’utilisateur de ces groupes.", "apihelp-userrights-param-reason": "Motif pour la modification.", "apihelp-userrights-param-tags": "Modifier les balises à appliquer à l’entrée dans le journal des droits utilisateur.", "apihelp-userrights-example-user": "Ajouter l’utilisateur <kbd>FooBot</kbd> au groupe <kbd>bot</kbd>, et le supprimer des groupes <kbd>sysop</kbd> et <kbd>bureaucrat</kbd>.", "apihelp-userrights-example-userid": "Ajouter l’utilisateur d’ID <kbd>123</kbd> au groupe <kbd>robot</kbd>, et le supprimer des groupes <kbd>sysop</kbd> et <kbd>bureaucrate</kbd>.", + "apihelp-userrights-example-expiry": "Ajouter l'utilisateur <kbd>SometimeSysop</kbd> au groupe <kbd>sysop</kbd> pour 1 mois.", "apihelp-validatepassword-description": "Valider un mot de passe en suivant les règles des mots de passe du wiki.\n\nLa validation est <samp>Good</samp> si le mot de passe est acceptable, <samp>Change</samp> s'il peut être utilisé pour se connecter et doit être changé, ou <samp>Invalid</samp> s'il n'est pas utilisable.", "apihelp-validatepassword-param-password": "Mot de passe à valider.", "apihelp-validatepassword-param-user": "Nom de l'utilisateur, pour tester la création de compte. L'utilisateur ne doit pas déja exister.", @@ -1394,8 +1400,8 @@ "apihelp-xml-param-includexmlnamespace": "Si spécifié, ajoute un espace de noms XML.", "apihelp-xmlfm-description": "Extraire les données au format XML (affiché proprement en HTML).", "api-format-title": "Résultat de l’API de MediaWiki", - "api-format-prettyprint-header": "Voici la représentation HTML du format $1. HTML est utile pour le débogage, mais inapproprié pour être utilisé dans une application.\n\nSpécifiez le paramètre <var>format</var> pour modifier le format de sortie. Pour voir la représentation non HTML du format $1, mettez <kbd>format=$2</kbd>.\n\nVoyez la [[mw:API|documentation complète]], ou l’[[Special:ApiHelp/main|aide de l’API]] pour plus d’information.", - "api-format-prettyprint-header-only-html": "Ceci est une représentation HTML à des fins de déboguage, et n’est pas approprié à une utilisation applicative.\n\nVoir la [[mw:API|documentation complète]], ou l’[[Special:ApiHelp/main|aide de l’API]] pour plus d’information.", + "api-format-prettyprint-header": "Voici la représentation HTML du format $1. HTML est utile pour le débogage, mais inapproprié pour être utilisé dans une application.\n\nSpécifiez le paramètre <var>format</var> pour modifier le format de sortie. Pour voir la représentation non HTML du format $1, mettez <kbd>format=$2</kbd>.\n\nVoyez la [[mw:Special:MyLanguage/API|documentation complète]], ou l’[[Special:ApiHelp/main|aide de l’API]] pour plus d’information.", + "api-format-prettyprint-header-only-html": "Ceci est une représentation HTML à des fins de débogage, et n’est pas approprié pour une utilisation applicative.\n\nVoir la [[mw:Special:MyLanguage/API|documentation complète]], ou l’[[Special:ApiHelp/main|aide de l’API]] pour plus d’information.", "api-format-prettyprint-status": "Cette réponse serait retournée avec l'état HTTP $1 $2.", "api-pageset-param-titles": "Une liste des titres sur lesquels travailler.", "api-pageset-param-pageids": "Une liste des IDs de page sur lesquelles travailler.", @@ -1443,8 +1449,8 @@ "api-help-param-default-empty": "Par défaut : <span class=\"apihelp-empty\">(vide)</span>", "api-help-param-token": "Un jeton « $1 » récupéré par [[Special:ApiHelp/query+tokens|action=query&meta=tokens]]", "api-help-param-token-webui": "Pour rester compatible, le jeton utilisé dans l’IHM web est aussi accepté.", - "api-help-param-disabled-in-miser-mode": "Désactivé à cause du [[mw:Manual:$wgMiserMode|mode minimal]].", - "api-help-param-limited-in-miser-mode": "<strong>NOTE :</strong> Du fait du [[mw:Manual:$wgMiserMode|mode minimal]], utiliser cela peut aboutir à moins de résultats que <var>$1limit</var> renvoyés avant de continuer ; dans les cas extrêmes, zéro résultats peuvent être renvoyés.", + "api-help-param-disabled-in-miser-mode": "Désactivé à cause du [[mw:Special:MyLanguage/Manual:$wgMiserMode|mode minimal]].", + "api-help-param-limited-in-miser-mode": "<strong>NOTE :</strong> Du fait du [[mw:Special:MyLanguage/Manual:$wgMiserMode|mode minimal]], utiliser cela peut aboutir à moins de résultats que <var>$1limit</var> renvoyés avant de continuer ; dans les cas extrêmes, zéro résultats peuvent être renvoyés.", "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": "<span class=\"apihelp-empty\">(aucune description)</span>", @@ -1494,6 +1500,7 @@ "apierror-blockedfrommail": "Vous avez été bloqué pour l’envoi de courriel.", "apierror-blocked": "Vous avez été bloqué pour modifier.", "apierror-botsnotsupported": "Cette interface n’est pas supportée pour les robots.", + "apierror-cannot-async-upload-file": "Les paramètres <var>async</var> et <var>file</var> ne peuvent pas être combinés. Si vous voulez un traitement asynchrone de votre fichier téléchargé, importez-le d’abord dans la réserve (en utilisant le paramètre <var>stash</var>) puis publiez le fichier importé de façon asynchrone (en utilisant <var>filekey</var> et <var>async</var>).", "apierror-cannotreauthenticate": "Cette action n’est pas disponible car votre identité ne peut pas être vérifiée.", "apierror-cannotviewtitle": "Vous n’êtes pas autorisé à voir $1.", "apierror-cantblock-email": "Vous n’avez pas le droit de bloquer des utilisateurs pour envoyer des courriels via ce wiki.", @@ -1536,12 +1543,12 @@ "apierror-invalidexpiry": "Heure d'expiration invalide \"$1\".", "apierror-invalid-file-key": "Ne correspond pas à une clé valide de fichier.", "apierror-invalidlang": "Code de langue non valide pour le paramètre <var>$1</var>.", - "apierror-invalidoldimage": "Le paramètre oldimage a un format non valide.", + "apierror-invalidoldimage": "Le paramètre <var>oldimage</var> a un format non valide.", "apierror-invalidparammix-cannotusewith": "Le paramètre <kbd>$1</kbd> ne peut pas être utilisé avec <kbd>$2</kbd>.", "apierror-invalidparammix-mustusewith": "Le paramètre <kbd>$1</kbd> ne peut être utilisé qu’avec <kbd>$2</kbd>.", "apierror-invalidparammix-parse-new-section": "<kbd>section=new</kbd> ne peut pas être combiné avec le paramètre <var>oldid</var>, <var>pageid</var> ou <var>page</var>. Veuillez utiliser <var>title</var> et <var>text</var>.", "apierror-invalidparammix": "{{PLURAL:$2|Les paramètres}} $1 ne peuvent pas être utilisés ensemble.", - "apierror-invalidsection": "Le paramètre section doit être un ID de section valide ou <kbd>new</kbd>.", + "apierror-invalidsection": "Le paramètre <var>section</var> doit être un ID de section valide ou <kbd>new</kbd>.", "apierror-invalidsha1base36hash": "Le hachage SHA1Base36 fourni n’est pas valide.", "apierror-invalidsha1hash": "Le hachage SHA1 fourni n’est pas valide.", "apierror-invalidtitle": "Mauvais titre « $1 ».", @@ -1682,9 +1689,9 @@ "apiwarn-redirectsandrevids": "La résolution de la redirection ne peut pas être utilisée avec le paramètre <var>revids</var>. Toutes les redirections vers lesquelles pointent <var>revids</var> n’ont pas été résolues.", "apiwarn-tokennotallowed": "L'action « $1 » n'est pas autorisée pour l'utilisateur actuel.", "apiwarn-tokens-origin": "Les jetons ne peuvent pas être obtenus quand la politique de même origine n’est pas appliquée.", - "apiwarn-toomanyvalues": "Trop de valeurs fournies pour le paramètre <var>$1</var>: la limite est $2.", + "apiwarn-toomanyvalues": "Trop de valeurs fournies pour le paramètre <var>$1</var>. La limite est $2.", "apiwarn-truncatedresult": "Ce résultat a été tronqué parce que sinon, il dépasserait la limite de $1 octets.", - "apiwarn-unclearnowtimestamp": "Passer « $2 » comme paramètre d’horodatage <var>$1</var> a été rendu obsolète. Si, pour une raison quelconque, vous avez besoin de spécifier explicitement l’heure courante sans la recalculer du côté client, utilisez <kbd>now<kbd>.", + "apiwarn-unclearnowtimestamp": "Passer « $2 » comme paramètre d’horodatage <var>$1</var> a été rendu obsolète. Si, pour une raison quelconque, vous avez besoin de spécifier explicitement l’heure courante sans la recalculer du côté client, utilisez <kbd>now</kbd>.", "apiwarn-unrecognizedvalues": "{{PLURAL:$3|Valeur non reconnue|Valeurs non reconnues}} pour le paramètre <var>$1</var> : $2.", "apiwarn-unsupportedarray": "Le paramètre <var>$1</var> utilise une syntaxe PHP de tableau non prise en charge.", "apiwarn-urlparamwidth": "Valeur de la largeur définie dans <var>$1urlparam</var> ($2) ignorée en faveur de la largeur calculée à partir de <var>$1urlwidth</var>/<var>$1urlheight</var> ($3).", @@ -1696,6 +1703,7 @@ "apiwarn-wgDebugAPI": "<strong>Avertissement de sécurité</strong>: <var>$wgDebugAPI</var> est activé.", "api-feed-error-title": "Erreur ($1)", "api-usage-docref": "Voir $1 concernant l'utilisation de l'API.", + "api-usage-mailinglist-ref": "S’abonner à la liste de diffusion mediawiki-api-announce sur <https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce> pour les signalisations d’obsolescence de l’API ou de modifications en rupture.", "api-exception-trace": "$1 à $2($3)\n$4", "api-credits-header": "Remerciements", "api-credits": "Développeurs de l’API :\n* Roan Kattouw (développeur en chef Sept. 2007–2009)\n* Victor Vasiliev\n* Bryan Tong Minh\n* Sam Reed\n* Yuri Astrakhan (créateur, développeur en chef Sept. 2006–Sept. 2007)\n* Brad Jorsch (développeur en chef depuis 2013)\n\nVeuillez envoyer vos commentaires, suggestions et questions à mediawiki-api@lists.wikimedia.org\nou remplir un rapport de bogue sur https://phabricator.wikimedia.org/." diff --git a/includes/api/i18n/gl.json b/includes/api/i18n/gl.json index 14f5512ba055..bfaef7a10e7d 100644 --- a/includes/api/i18n/gl.json +++ b/includes/api/i18n/gl.json @@ -14,10 +14,10 @@ "Hamilton Abreu" ] }, - "apihelp-main-description": "<div class=\"hlist plainlinks api-main-links\">\n* [[mw:API:Main_page|Documentación]]\n* [[mw:API:FAQ|FAQ]]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api Lista de discusión]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce Anuncios da API]\n* [https://phabricator.wikimedia.org/maniphest/query/GebfyV4uCaLd/#R Erros e solicitudes]\n</div>\n<strong>Estado:</strong> Tódalas funcionalidades mostradas nesta páxina deberían estar funcionanado, pero a API aínda está desenrolo, e pode ser modificada en calquera momento. Apúntese na [https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ lista de discusión mediawiki-api-announce] para estar informado acerca das actualizacións.\n\n<strong>Solicitudes incorrectas:</strong> Cando se envían solicitudes incorrectas á API, envíase unha cabeceira HTTP coa chave \"MediaWiki-API-Error\" e, a seguir, tanto o valor da cabeceira como o código de erro retornado serán definidos co mesmo valor. Para máis información, consulte [[mw:API:Errors_and_warnings|API: Erros e avisos]].\n\n<strong>Test:</strong> Para facilitar as probas das peticións da API, consulte [[Special:ApiSandbox]].", + "apihelp-main-description": "<div class=\"hlist plainlinks api-main-links\">\n* [[mw:Special:MyLanguage/API:Main_page|Documentación]]\n* [[mw:Special:MyLanguage/API:FAQ|FAQ]]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api Lista de discusión]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce Anuncios da API]\n* [https://phabricator.wikimedia.org/maniphest/query/GebfyV4uCaLd/#R Erros e solicitudes]\n</div>\n<strong>Estado:</strong> Tódalas funcionalidades mostradas nesta páxina deberían estar funcionanado, pero a API aínda está desenrolo, e pode ser modificada en calquera momento. Apúntese na [https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ lista de discusión mediawiki-api-announce] para estar informado acerca das actualizacións.\n\n<strong>Solicitudes incorrectas:</strong> Cando se envían solicitudes incorrectas á API, envíase unha cabeceira HTTP coa chave \"MediaWiki-API-Error\" e, a seguir, tanto o valor da cabeceira como o código de erro retornado serán definidos co mesmo valor. Para máis información, consulte [[mw:Special:MyLanguage/API:Errors_and_warnings|API: Erros e avisos]].\n\n<strong>Test:</strong> Para facilitar as probas das peticións da API, consulte [[Special:ApiSandbox]].", "apihelp-main-param-action": "Que acción se realizará.", "apihelp-main-param-format": "O formato de saída.", - "apihelp-main-param-maxlag": "O retardo máximo pode usarse cando MediaWiki está instalada nun cluster de base de datos replicadas. Para gardar accións que causen calquera retardo máis de replicación do sitio, este parámetro pode facer que o cliente espere ata que o retardo de replicación sexa menor que o valor especificado. No caso de retardo excesivo, é devolto o código de erro <samp>maxlag</samp> cunha mensaxe como <samp>esperando por $host: $lag segundos de retardo</samp>.<br />Para máis información, ver [[mw:Manual:Maxlag_parameter|Manual: Maxlag parameter]].", + "apihelp-main-param-maxlag": "O retardo máximo pode usarse cando MediaWiki está instalada nun cluster de base de datos replicadas. Para gardar accións que causen calquera retardo máis de replicación do sitio, este parámetro pode facer que o cliente espere ata que o retardo de replicación sexa menor que o valor especificado. No caso de retardo excesivo, é devolto o código de erro <samp>maxlag</samp> cunha mensaxe como <samp>esperando por $host: $lag segundos de retardo</samp>.<br />Para máis información, ver [[mw:Special:MyLanguage/Manual:Maxlag_parameter|Manual: Maxlag parameter]].", "apihelp-main-param-smaxage": "Fixar a cabeceira HTTP de control de caché <code>s-maxage</code> a esos segundos. Os erros nunca se gardan na caché.", "apihelp-main-param-maxage": "Fixar a cabeceira HTTP de control de caché <code>max-age</code> a esos segundos. Os erros nunca se gardan na caché.", "apihelp-main-param-assert": "Verificar se o usuario está conectado como <kbd>usuario</kbd> ou ten a marca de <kbd>bot</kbd>.", @@ -29,6 +29,8 @@ "apihelp-main-param-origin": "Cando se accede á API usando unha petición AJAX entre-dominios (CORS), inicializar o parámetro co dominio orixe. Isto debe incluírse en calquera petición pre-flight, e polo tanto debe ser parte da petición URI (non do corpo POST). Para peticións autenticadas, isto debe coincidir exactamente cunha das orixes na cabeceira <code>Origin</code>, polo que ten que ser fixado a algo como <kbd>https://en.wikipedia.org</kbd> ou <kbd>https://meta.wikimedia.org</kbd>. Se este parámetro non coincide coa cabeceira <code>Origin</code>, devolverase unha resposta 403. Se este parámetro coincide coa cabeceira <code>Origin</code> e a orixe está na lista branca, as cabeceiras <code>Access-Control-Allow-Origin</code> e <code>Access-Control-Allow-Credentials</code> serán fixadas.\n\nPara peticións non autenticadas, especifique o valor <kbd>*</kbd>. Isto fará que se fixe a cabeceira <code>Access-Control-Allow-Origin</code>, pero <code>Access-Control-Allow-Credentials</code> será <code>false</code> e todos os datos específicos do usuario serán ocultados.", "apihelp-main-param-uselang": "Linga a usar para a tradución de mensaxes. <kbd>[[Special:ApiHelp/query+siteinfo|action=query&meta=siteinfo]]</kbd> con <kbd>siprop=languages</kbd> devolve unha lista de códigos de lingua, ou especificando <kbd>user</kbd> coa preferencia de lingua do usuario actual, ou especificando <kbd>content</kbd> para usar a lingua do contido desta wiki.", "apihelp-main-param-errorformat": "Formato a usar para a saída do texto de aviso e de erroː\n; plaintext: texto wiki sen as etiquetas HTML e coas entidades substituídas.\n; wikitext: texto wiki sen analizar.\n; html: HTML.\n; raw: Clave de mensaxe e parámetros.\n; none: Sen saída de texto, só os códigos de erro.\n; bc: Formato utilizado antes de MediaWiki 1.29. <var>errorlang</var> e <var>errorsuselocal</var> non se teñen en conta.", + "apihelp-main-param-errorlang": "Lingua usada para advertencias e erros. <kbd>[[Special:ApiHelp/query+siteinfo|action=query&meta=siteinfo]]</kbd> con <kbd>siprop=languages</kbd> devolve unha lista de códigos de lingua. Pode especificar <kbd>content</kbd> para utilizar a lingua do contido deste wiki ou <kbd>uselang</kbd> para utilizar o mesmo valor que o do parámetro <var>uselang</var>.", + "apihelp-main-param-errorsuselocal": "Se se indica, os textos de erro empregarán mensaxes adaptadas á lingua do espazo de nomes {{ns:MediaWiki}}.", "apihelp-block-description": "Bloquear un usuario.", "apihelp-block-param-user": "Nome de usuario, dirección ou rango de IPs que quere bloquear. Non pode usarse xunto con <var>$1userid</var>", "apihelp-block-param-userid": "Identificador de usuario a bloquear. Non pode usarse xunto con <var>$1user</var>.", @@ -39,9 +41,10 @@ "apihelp-block-param-autoblock": "Bloquear automaticamente o último enderezo IP utilizado, e calquera outro enderezo desde o que intente conectarse.", "apihelp-block-param-noemail": "Impide que o usuario envíe correos electrónicos a través da wiki. (Require o permiso <code>blockemail</code>).", "apihelp-block-param-hidename": "Ocultar o nome de usuario do rexistro de bloqueos. (Precisa do permiso <code>hideuser</code>).", - "apihelp-block-param-allowusertalk": "Permitir que o usuario edite a súa propia páxina de conversa (depende de <var>[[mw:Manual:$wgBlockAllowsUTEdit|$wgBlockAllowsUTEdit]]</var>).", + "apihelp-block-param-allowusertalk": "Permitir que o usuario edite a súa propia páxina de conversa (depende de <var>[[mw:Special:MyLanguage/Manual:$wgBlockAllowsUTEdit|$wgBlockAllowsUTEdit]]</var>).", "apihelp-block-param-reblock": "Se o usuario xa está bloqueado, sobreescribir o bloqueo existente.", "apihelp-block-param-watchuser": "Vixiar a páxina de usuario ou direccións IP e a de conversa deste usuario", + "apihelp-block-param-tags": "Cambiar as etiquetas a aplicar á entrada no rexistro de bloqueos.", "apihelp-block-example-ip-simple": "Bloquear dirección IP <kbd>192.0.2.5</kbd> durante tres días coa razón <kbd>Primeiro aviso</kbd>.", "apihelp-block-example-user-complex": "Bloquear indefinidamente ó usuario <kbd>Vandal</kbd> coa razón <kbd>Vandalism</kbd>, e impedir a creación de novas contas e envío de correos electrónicos.", "apihelp-changeauthenticationdata-description": "Cambiar os datos de autenticación do usuario actual.", @@ -207,6 +210,7 @@ "apihelp-help-example-query": "Axuda para dous submódulos de consulta.", "apihelp-imagerotate-description": "Xirar unha ou máis imaxes.", "apihelp-imagerotate-param-rotation": "Graos a rotar a imaxe no sentido do reloxio.", + "apihelp-imagerotate-param-tags": "Etiquetas aplicar á entrada no rexistro de subas.", "apihelp-imagerotate-example-simple": "Rotar <kbd>File:Example.png</kbd> <kbd>90</kbd> graos.", "apihelp-imagerotate-example-generator": "Rotar tódalas imaxes en <kbd>Category:Flip</kbd> <kbd>180</kbd> graos", "apihelp-import-description": "Importar unha páxina doutra wiki, ou dun ficheiro XML.\n\nDecátese de que o POST HTTP debe facerse como unha carga de ficheiro (p. ex. usando multipart/form-data) cando se envíe un ficheiro para o parámetro <var>xml</var>.", @@ -218,6 +222,7 @@ "apihelp-import-param-templates": "Para importacións interwiki: importar tódolos modelos incluídos.", "apihelp-import-param-namespace": "Importar a este espazo de nomes. Non se pode usar de forma conxunta con <var>$1rootpage</var>.", "apihelp-import-param-rootpage": "Importar como subpáxina desta páxina. Non se pode usar de forma conxunta con <var>$1namespace</var>.", + "apihelp-import-param-tags": "Cambiar as etiquetas a aplicar á entrada no rexistro de importacións e á revisión nula das páxinas importadas.", "apihelp-import-example-import": "Importar [[meta:Help:ParserFunctions]] ó espazo de nomes 100 con todo o historial.", "apihelp-linkaccount-description": "Vincular unha conta dun provedor externo ó usuario actual.", "apihelp-linkaccount-example-link": "Comezar o proceso de vincular a unha conta de <kbd>Exemplo</kbd>.", @@ -236,6 +241,7 @@ "apihelp-managetags-param-tag": "Etiqueta para crear, borrar, activar ou desactivar. Para a creación da etiqueta, a etiqueta non pode existir previamente. Para o borrado da etiqueta, a etiqueta debe existir. Para a activación da etiqueta, a etiqueta debe existir e non pode ser usada por unha extensión. Para desactivar unha etiqueta, a etiqueta debe estar activa e definida manualmente.", "apihelp-managetags-param-reason": "Un motivo opcional para crear, borrar, activar ou desactivar a etiqueta.", "apihelp-managetags-param-ignorewarnings": "Ignorar calquera aviso que apareza durante a operación.", + "apihelp-managetags-param-tags": "Cambiar as etiquetas a aplicar á entrada no rexistro de xestión das etiquetas.", "apihelp-managetags-example-create": "Crear unha etiqueta chamada <kbd>spam</kbd> coa razón <kbd>For use in edit patrolling</kbd>", "apihelp-managetags-example-delete": "Borrar a etiqueta <kbd>vandalismo</kbd> coa razón <kbd>Erros ortográficos</kbd>", "apihelp-managetags-example-activate": "Activar a etiqueta chamada <kbd>spam</kbd> coa razón <kbd>For use in edit patrolling</kbd>", @@ -261,12 +267,13 @@ "apihelp-move-param-unwatch": "Eliminar a páxina e a redirección da páxina de vixiancia do usuario actual.", "apihelp-move-param-watchlist": "Engadir ou eliminar sen condicións a páxina da lista de vixiancia do usuario actual, use as preferencias ou non cambie a vixiancia.", "apihelp-move-param-ignorewarnings": "Ignorar as advertencias.", + "apihelp-move-param-tags": "Cambiar as etiquetas a aplicar á entrada do rexistro de traslados e na revisión nula da páxina de destino.", "apihelp-move-example-move": "Mover <kbd>Badtitle</kbd> a <kbd>Goodtitle</kbd> sen deixar unha redirección.", "apihelp-opensearch-description": "Buscar no wiki mediante o protocolo OpenSearch.", "apihelp-opensearch-param-search": "Buscar texto.", "apihelp-opensearch-param-limit": "Número máximo de resultados a visualizar.", "apihelp-opensearch-param-namespace": "Espazo de nomes no que buscar.", - "apihelp-opensearch-param-suggest": "Non facer nada se <var>[[mw:Manual:$wgEnableOpenSearchSuggest|$wgEnableOpenSearchSuggest]]</var> é falso.", + "apihelp-opensearch-param-suggest": "Non facer nada se <var>[[mw:Special:MyLanguage/Manual:$wgEnableOpenSearchSuggest|$wgEnableOpenSearchSuggest]]</var> é falso.", "apihelp-opensearch-param-redirects": "Como xestionar as redireccións:\n;return:Devolve a mesma redirección.\n;resolve:Devolve a páxina á que apunta. Pode devolver menos de $1limit resultados.\nPor razóns históricas, o valor por defecto para $1format=json é \"return\" e \"resolve\" para outros formatos.", "apihelp-opensearch-param-format": "O formato de saída.", "apihelp-opensearch-param-warningsaserror": "Se os avisos son recibidos con <kbd>format=json</kbd>, devolver un erro de API no canto de ignoralos.", @@ -359,7 +366,7 @@ "apihelp-protect-example-protect": "Protexer unha páxina", "apihelp-protect-example-unprotect": "Desprotexer unha páxina poñendo as restricións a <kbd>all</kbd>. (isto quere dicir que todo o mundo pode realizar a acción).", "apihelp-protect-example-unprotect2": "Desprotexer unha páxina quitando as restricións.", - "apihelp-purge-description": "Borrar a caché para os títulos indicados.\n\nPrecisa dunha petición POST se o usuario non está conectado.", + "apihelp-purge-description": "Borrar a caché para os títulos indicados.", "apihelp-purge-param-forcelinkupdate": "Actualizar as táboas de ligazóns.", "apihelp-purge-param-forcerecursivelinkupdate": "Actualizar a táboa de ligazóns, e actualizar as táboas de ligazóns para calquera páxina que use esta páxina como modelo.", "apihelp-purge-example-simple": "Purgar a <kbd>Main Page</kbd> e páxina da <kbd>API</kbd>.", @@ -400,7 +407,7 @@ "apihelp-query+alldeletedrevisions-param-user": "Só listar revisións deste usuario.", "apihelp-query+alldeletedrevisions-param-excludeuser": "Non listar revisións deste usuario.", "apihelp-query+alldeletedrevisions-param-namespace": "Só listar páxinas neste espazo de nomes.", - "apihelp-query+alldeletedrevisions-param-miser-user-namespace": "<strong>Nota:</strong> Debido ó [[mw:Manual:$wgMiserMode|modo minimal]], ó usar á vez <var>$1user</var> e <var>$1namespace</var> pode devolver menos resultados de <var>$1limit</var> antes de continuar, en casos extremos, pode que non devolva resultados.", + "apihelp-query+alldeletedrevisions-param-miser-user-namespace": "<strong>Nota:</strong> Debido ó [[mw:Special:MyLanguage/Manual:$wgMiserMode|modo minimal]], ó usar á vez <var>$1user</var> e <var>$1namespace</var> pode devolver menos resultados de <var>$1limit</var> antes de continuar, en casos extremos, pode que non devolva resultados.", "apihelp-query+alldeletedrevisions-param-generatetitles": "Usado como xenerador, xenera títulos no canto de IDs de revisión.", "apihelp-query+alldeletedrevisions-example-user": "Listar as últimas 50 contribucións borradas do usuario <kbd>Example</kbd>.", "apihelp-query+alldeletedrevisions-example-ns-main": "Listar as 50 primeiras revisións borradas no espazo de nomes principal.", @@ -719,7 +726,7 @@ "apihelp-query+filearchive-paramvalue-prop-archivename": "Engade o nome do ficheiro da versión do ficheiro para as versións que non son a última.", "apihelp-query+filearchive-example-simple": "Mostrar unha lista de tódolos fichieiros eliminados.", "apihelp-query+filerepoinfo-description": "Devolver a meta información sobre os repositorios de imaxes configurados na wiki.", - "apihelp-query+filerepoinfo-param-prop": "Que propiedades do repositorio mostrar (pode haber máis dispoñible nalgunhas wikis):\n;apiurl:URL ó API do repositorio - útil para obter información das imaxes no host.\n;name:A clave do repositorio - usada p. ex. nas variables de retorno de <var>[[mw:Manual:$wgForeignFileRepos|$wgForeignFileRepos]]</var> e [[Special:ApiHelp/query+imageinfo|imageinfo]]\n;displayname:O nome lexible do wiki repositorio.\n;rooturl:URL raíz dos camiños de imaxe.\n;local:Se o repositorio é o repositorio local ou non.", + "apihelp-query+filerepoinfo-param-prop": "Que propiedades do repositorio mostrar (pode haber máis dispoñible nalgunhas wikis):\n;apiurl:URL ó API do repositorio - útil para obter información das imaxes no host.\n;name:A clave do repositorio - usada p. ex. nas variables de retorno de <var>[[mw:Special:MyLanguage/Manual:$wgForeignFileRepos|$wgForeignFileRepos]]</var> e [[Special:ApiHelp/query+imageinfo|imageinfo]]\n;displayname:O nome lexible do wiki repositorio.\n;rooturl:URL raíz dos camiños de imaxe.\n;local:Se o repositorio é o repositorio local ou non.", "apihelp-query+filerepoinfo-example-simple": "Obter infomación sobre os repositorios de ficheiros", "apihelp-query+fileusage-description": "Atopar tódalas páxinas que usan os ficheiros dados.", "apihelp-query+fileusage-param-prop": "Que propiedades obter:", @@ -763,6 +770,7 @@ "apihelp-query+imageinfo-param-extmetadatamultilang": "Se as traducións para a propiedade extmetadata están dispoñibles, búscaas todas.", "apihelp-query+imageinfo-param-extmetadatafilter": "Se está especificado e non baleiro, só se devolverán esas claves para $1prop=extmetadata.", "apihelp-query+imageinfo-param-urlparam": "Unha cadea de parámetro específico no analizador. Por exemplo, os PDFs poden usar <kbd>page15-100px</kbd>. Debe usarse <var>$1urlwidth</var> que debe ser coherente con <var>$1urlparam</var>.", + "apihelp-query+imageinfo-param-badfilecontexttitle": "Se <kbd>$2prop=badfile</kbd> está definido, este é o título da páxina usado para avaliar a [[MediaWiki:Bad image list]]", "apihelp-query+imageinfo-param-localonly": "Só buscar ficheiros no repositorio local.", "apihelp-query+imageinfo-example-simple": "Busca a información sobre a versión actual de [[:File:Albert Einstein Head.jpg]].", "apihelp-query+imageinfo-example-dated": "Busca información sobre as versións de [[:File:Test.jpg]] posteriores a 2008.", @@ -1054,10 +1062,11 @@ "apihelp-query+siteinfo-paramvalue-prop-rightsinfo": "Devolve a información dos dereitos (licenza) da wiki se está dispoñible.", "apihelp-query+siteinfo-paramvalue-prop-restrictions": "Devolve información dos tipos de restricións (protección) dispoñibles.", "apihelp-query+siteinfo-paramvalue-prop-languages": "Devolve unha lista dos idiomas que soporta Mediawiki (opcionalmente pode localizarse usando <var>$1inlanguagecode</var>).", + "apihelp-query+siteinfo-paramvalue-prop-languagevariants": "Devolve unha lista de códigos de lingua para os que [[mw:Special:MyLanguage/LanguageConverter|LanguageConverter]] está activo, e as variantes soportadas para cada un.", "apihelp-query+siteinfo-paramvalue-prop-skins": "Devolve unha lista de todas as aparencias dispoñibles (opcionalmente pode localizarse usando <var>$1inlanguagecode</var>, noutro caso no idioma do contido).", "apihelp-query+siteinfo-paramvalue-prop-extensiontags": "Devolve unha lista de etiquetas de extensión de analizador.", "apihelp-query+siteinfo-paramvalue-prop-functionhooks": "Devolve unha lista de ganchos de función de analizador.", - "apihelp-query+siteinfo-paramvalue-prop-showhooks": "Devolve unha lista de todos os ganchos subscritos (contido de <var>[[mw:Manual:$wgHooks|$wgHooks]]</var>).", + "apihelp-query+siteinfo-paramvalue-prop-showhooks": "Devolve unha lista de todos os ganchos subscritos (contido de <var>[[mw:Special:MyLanguage/Manual:$wgHooks|$wgHooks]]</var>).", "apihelp-query+siteinfo-paramvalue-prop-variables": "Devolve unha lista de identificadores de variable.", "apihelp-query+siteinfo-paramvalue-prop-protocols": "Devolve unha lista de protocolos que están permitidos nas ligazóns externas.", "apihelp-query+siteinfo-paramvalue-prop-defaultoptions": "Devolve os valores por defecto das preferencias de usuario.", @@ -1111,8 +1120,9 @@ "apihelp-query+usercontribs-param-limit": "Máximo número de contribucións a mostar.", "apihelp-query+usercontribs-param-start": "Selo de tempo de comezo ó que volver.", "apihelp-query+usercontribs-param-end": "Selo de tempo de fin ó que volver.", - "apihelp-query+usercontribs-param-user": "Usuarios para os que recuperar as contribucións.", - "apihelp-query+usercontribs-param-userprefix": "Recuperar as contribucións de todos os usuarios cuxo nome comece por este valor. Ignora $1user.", + "apihelp-query+usercontribs-param-user": "Usuarios para os que recuperar as contribucións. Non pode ser usado con <var>$1userids</var> ou <var>$1userprefix</var>.", + "apihelp-query+usercontribs-param-userprefix": "Recuperar as contribucións de todos os usuarios cuxo nome comece por este valor. Non pode usarse con <var>$1user</var> nin con <var>$1userids</var>.", + "apihelp-query+usercontribs-param-userids": "IDs de usuarios para os que recuperar as contribucións. Non pode ser usado con <var>$1user</var> nin con <var>$1userprefix</var>.", "apihelp-query+usercontribs-param-namespace": "Só listar contribucións nestes espazos de nomes.", "apihelp-query+usercontribs-param-prop": "Engade información adicional:", "apihelp-query+usercontribs-paramvalue-prop-ids": "Engade os identificadores de páxina e modificación.", @@ -1125,7 +1135,7 @@ "apihelp-query+usercontribs-paramvalue-prop-flags": "Engade os indicadores da modificación.", "apihelp-query+usercontribs-paramvalue-prop-patrolled": "Marca as modificacións vixiadas.", "apihelp-query+usercontribs-paramvalue-prop-tags": "Lista as etiquetas da modificación.", - "apihelp-query+usercontribs-param-show": "Só mostrar elementos que cumpran estos criterios, p.ex. só edicións menores: <kbd>$2show=!minor</kbd>.\n\nSe está fixado <kbd>$2show=patrolled</kbd> ou <kbd>$2show=!patrolled</kbd>, as modificacións máis antigas que <var>[[mw:Manual:$wgRCMaxAge|$wgRCMaxAge]]</var> ($1 {{PLURAL:$1|segundo|segundos}}) non se mostrarán.", + "apihelp-query+usercontribs-param-show": "Só mostrar elementos que cumpran estos criterios, p.ex. só edicións menores: <kbd>$2show=!minor</kbd>.\n\nSe está fixado <kbd>$2show=patrolled</kbd> ou <kbd>$2show=!patrolled</kbd>, as modificacións máis antigas que <var>[[mw:Special:MyLanguage/Manual:$wgRCMaxAge|$wgRCMaxAge]]</var> ($1 {{PLURAL:$1|segundo|segundos}}) non se mostrarán.", "apihelp-query+usercontribs-param-tag": "Só listar revisións marcadas con esta etiqueta.", "apihelp-query+usercontribs-param-toponly": "Listar só cambios que son a última revisión.", "apihelp-query+usercontribs-example-user": "Mostrar as contribucións do usuario <kbd>Exemplo</kbd>.", @@ -1135,6 +1145,7 @@ "apihelp-query+userinfo-paramvalue-prop-blockinfo": "Marca se o usuario actual está bloqueado, por que, e por que razón.", "apihelp-query+userinfo-paramvalue-prop-hasmsg": "Engade unha etiqueta <samp>messages</samp> (mensaxe) se o usuario actual ten mensaxes pendentes.", "apihelp-query+userinfo-paramvalue-prop-groups": "Lista todos os grupos ós que pertence o usuario actual.", + "apihelp-query+userinfo-paramvalue-prop-groupmemberships": "Lista os grupos ós que o usuario actual foi asignado explicitamente, incluíndo a data de caducidade de afiliación a cada grupo.", "apihelp-query+userinfo-paramvalue-prop-implicitgroups": "Lista todos so grupos dos que o usuario actual é membro automaticamente.", "apihelp-query+userinfo-paramvalue-prop-rights": "Lista todos os dereitos que ten o usuario actual.", "apihelp-query+userinfo-paramvalue-prop-changeablegroups": "Lista os grupos ós que o usuario pode engadir ou eliminar a outros usuarios.", @@ -1155,6 +1166,7 @@ "apihelp-query+users-param-prop": "Que información incluír:", "apihelp-query+users-paramvalue-prop-blockinfo": "Etiquetas se o usuario está bloqueado, por quen, e por que razón.", "apihelp-query+users-paramvalue-prop-groups": "Lista todos os grupos ós que pertence cada usuario.", + "apihelp-query+users-paramvalue-prop-groupmemberships": "Lista os grupos ós que foi asignado explicitamente cada usuario, incluíndo a data de caducidade de afiliación a cada grupo.", "apihelp-query+users-paramvalue-prop-implicitgroups": "Lista os grupos dos que un usuario é membro de forma automatica.", "apihelp-query+users-paramvalue-prop-rights": "Lista todos os dereitos que ten cada usuario.", "apihelp-query+users-paramvalue-prop-editcount": "Engade o contador de edicións do usuario.", @@ -1165,6 +1177,7 @@ "apihelp-query+users-paramvalue-prop-cancreate": "Indica se unha conta pode ser creada para nomes de usuario válidos pero non rexistrados.", "apihelp-query+users-param-attachedwiki": "Con <kbd>$1prop=centralids</kbd>, \nindica que o usuario está acoplado á wiki identificada por este identificador.", "apihelp-query+users-param-users": "Lista de usuarios para os que obter información.", + "apihelp-query+users-param-userids": "Unha lista de identificadores de usuarios dos que obter información.", "apihelp-query+users-param-token": "Usar <kbd>[[Special:ApiHelp/query+tokens|action=query&meta=tokens]]</kbd> no canto diso.", "apihelp-query+users-example-simple": "Mostar información para o usuario <kbd>Example</kbd>.", "apihelp-query+watchlist-description": "Ver os cambios recentes das páxinas na lista de vixiancia do usuario actual.", @@ -1219,7 +1232,7 @@ "apihelp-removeauthenticationdata-description": "Elimina os datos de autenticación do usuario actual.", "apihelp-removeauthenticationdata-example-simple": "Intenta eliminar os datos de usuario actual para <kbd>FooAuthenticationRequest</kbd>.", "apihelp-resetpassword-description": "Envía un correo de inicialización de contrasinal a un usuario.", - "apihelp-resetpassword-description-noroutes": "Non están dispoñibles as rutas de reinicio de contrasinal \n\nActive as rutas en <var>[[mw:Manual:$wgPasswordResetRoutes|$wgPasswordResetRoutes]]</var> para usar este módulo.", + "apihelp-resetpassword-description-noroutes": "Non están dispoñibles as rutas de reinicio de contrasinal \n\nActive as rutas en <var>[[mw:Special:MyLanguage/Manual:$wgPasswordResetRoutes|$wgPasswordResetRoutes]]</var> para usar este módulo.", "apihelp-resetpassword-param-user": "Usuario sendo reinicializado.", "apihelp-resetpassword-param-email": "Está reinicializándose o enderezo de correo electrónico do usuario.", "apihelp-resetpassword-example-user": "Enviar un correo de reinicialización de contrasinal ó usuario <kbd>Exemplo</kbd>.", @@ -1232,6 +1245,7 @@ "apihelp-revisiondelete-param-show": "Que mostrar para cada revisión.", "apihelp-revisiondelete-param-suppress": "Eliminar os datos dos administradores así coma dos doutros.", "apihelp-revisiondelete-param-reason": "Razón para o borrado ou restaurado.", + "apihelp-revisiondelete-param-tags": "Etiquetas a aplicar á entrada no rexistro de borrados.", "apihelp-revisiondelete-example-revision": "Ocultar contido para revisión <kbd>12345</kbd> na <kbd>Páxina Principal</kbd>.", "apihelp-revisiondelete-example-log": "Ocultar todos os datos da entrada de rexistro <kbd>67890</kbd> coa razón <kbd>BLP violation</kbd>.", "apihelp-rollback-description": "Desfacer a última modificación da páxina.\n\nSe o último usuario que modificou a páxina fixo varias modificacións nunha fila, desfaranse todas.", @@ -1256,7 +1270,12 @@ "apihelp-setnotificationtimestamp-example-pagetimestamp": "Fixar o selo de tempo de notificación para a <kbd>Main page</kbd> de forma que todas as edicións dende o 1 se xaneiro de 2012 queden sen revisar.", "apihelp-setnotificationtimestamp-example-allpages": "Restaurar o estado de notificación para as páxinas no espazo de nomes de <kbd>{{ns:user}}</kbd>.", "apihelp-setpagelanguage-description": "Cambiar a lingua dunha páxina.", + "apihelp-setpagelanguage-description-disabled": "Neste wiki non se permite modificar a lingua das páxinas.\n\nActive <var>[[mw:Special:MyLanguage/Manual:$wgPageLanguageUseDB|$wgPageLanguageUseDB]]</var> para utilizar esta acción.", + "apihelp-setpagelanguage-param-title": "Título da páxina cuxa lingua quere cambiar. Non se pode usar xunto con <var>$1pageid</var>.", + "apihelp-setpagelanguage-param-pageid": "Identificador da páxina cuxa lingua quere cambiar. Non se pode usar xunto con <var>$1title</var>.", + "apihelp-setpagelanguage-param-lang": "Código da lingua á que se quere cambiar a páxina. Use <kbd>default</kbd> para restablecer a páxina á lingua por defecto do contido da wiki.", "apihelp-setpagelanguage-param-reason": "Motivo do cambio.", + "apihelp-setpagelanguage-param-tags": "Cambiar as etiquetas a aplicar á entrada de rexistro resultante desta acción.", "apihelp-setpagelanguage-example-language": "Cambiar a lingua de <kbd>Main Page</kbd> ó éuscaro.", "apihelp-setpagelanguage-example-default": "Cambiar a lingua da páxina con identificador 123 á lingua predeterminada para o contido da wiki.", "apihelp-stashedit-description": "Preparar unha edición na caché compartida.\n\nEstá previsto que sexa usado vía AJAX dende o formulario de edición para mellorar o rendemento de gardado da páxina.", @@ -1276,6 +1295,7 @@ "apihelp-tag-param-add": "Etiquetas a engadir. Só poden engadirse etiquetas definidas manualmente.", "apihelp-tag-param-remove": "Etiquetas a eliminar. Só se poden eliminar as etiquetas definidas manualmente ou que non teñen ningunha definición.", "apihelp-tag-param-reason": "Razón para o cambio.", + "apihelp-tag-param-tags": "Etiquetas a aplicar á entrada de rexistro que será creada como resultado desta acción.", "apihelp-tag-example-rev": "Engadir a etiqueta <kbd>vandalismo</kbd> á revisión con identificador 123 sen indicar un motivo", "apihelp-tag-example-log": "Eliminar a etiqueta <kbd>publicidade</kbd> da entrada do rexistro con identificador 123 co motivo <kbd>aplicada incorrectamente</kbd>", "apihelp-tokens-description": "Obter os identificadores para accións de modificación de datos.\n\nEste módulo está obsoleto e foi reemprazado por [[Special:ApiHelp/query+tokens|action=query&meta=tokens]].", @@ -1283,14 +1303,14 @@ "apihelp-tokens-example-edit": "Recuperar un identificador de modificación (por defecto).", "apihelp-tokens-example-emailmove": "Recuperar un identificador de correo e un identificador de movemento.", "apihelp-unblock-description": "Desbloquear un usuario.", - "apihelp-unblock-param-id": "ID do bloque a desbloquear (obtido de <kbd>list=blocks</kbd>). Non pode usarse xunto con <var>$1user</var> ou <var>$luserid</var>.", - "apihelp-unblock-param-user": "Nome de usuario, enderezo IP ou rango de enderezos IP a desbloquear. Non pode usarse xunto con <var>$1id</var> ou <var>$luserid</var>.", + "apihelp-unblock-param-id": "ID do bloque a desbloquear (obtido de <kbd>list=blocks</kbd>). Non pode usarse xunto con <var>$1user</var> ou <var>$1userid</var>.", + "apihelp-unblock-param-user": "Nome de usuario, enderezo IP ou rango de enderezos IP a desbloquear. Non pode usarse xunto con <var>$1id</var> ou <var>$1userid</var>.", "apihelp-unblock-param-userid": "ID de usuario a desbloquear. Non pode usarse xunto con <var>$1id</var> ou <var>$1user</var>.", "apihelp-unblock-param-reason": "Razón para desbloquear.", "apihelp-unblock-param-tags": "Cambiar as etiquetas a aplicar na entrada do rexistro de bloqueo.", "apihelp-unblock-example-id": "Desbloquear bloqueo ID #<kbd>105</kbd>.", "apihelp-unblock-example-user": "Desbloquear usuario <kbd>Bob</kbd> con razón <kbd>Síntoo Bob</kbd>.", - "apihelp-undelete-description": "Restaurar modificacións dunha páxina borrada.\n\nUnha lista de modificacións borradas (incluíndo os seus selos de tempo) pode consultarse a través de [[Special:ApiHelp/query+deletedrevs|list=deletedrevs]], e unha lista de IDs de ficheiros borrados pode consultarse a través de [[Special:ApiHelp/query+filearchive|list=filearchive]].", + "apihelp-undelete-description": "Restaurar modificacións dunha páxina borrada.\n\nUnha lista de modificacións borradas (incluíndo os seus selos de tempo) pode consultarse a través de [[Special:ApiHelp/query+deletedrevisions|prop=deletedrevisions]], e unha lista de IDs de ficheiros borrados pode consultarse a través de [[Special:ApiHelp/query+filearchive|list=filearchive]].", "apihelp-undelete-param-title": "Título da páxina a restaurar.", "apihelp-undelete-param-reason": "Razón para restaurar.", "apihelp-undelete-param-tags": "Cambiar as etiquetas a aplicar na entrada do rexistro de borrado.", @@ -1324,13 +1344,21 @@ "apihelp-userrights-description": "Cambiar a pertencia dun usuario a un grupo.", "apihelp-userrights-param-user": "Nome de usuario.", "apihelp-userrights-param-userid": "ID de usuario.", - "apihelp-userrights-param-add": "Engadir o usuario a estes grupos.", + "apihelp-userrights-param-add": "Engadir o usuario a estes grupos, ou se xa é membro, actualizar a caducidade da súa afiliación.", + "apihelp-userrights-param-expiry": "Marcas de tempo de caducidade. Poden ser relativas (por exemplo, <kbd>5 meses</kbd> ou <kbd>2 semanas</kbd>) ou absolutas (por exemplo, <kbd>2014-09-18T12:34:56Z</kbd>). Se só se fixa unha marca de tempo, utilizarase para tódolos grupos que se pasen ó parámetro <var>$1add</var>. Use <kbd>infinite</kbd>, <kbd>indefinite</kbd>, <kbd>infinity</kbd>, ou <kbd>never</kbd> para que a pertenza ó grupo non teña data de caducidade.", "apihelp-userrights-param-remove": "Eliminar o usuario destes grupos.", "apihelp-userrights-param-reason": "Motivo para o cambio.", "apihelp-userrights-param-tags": "Cambia as etiquetas a aplicar á entrada do rexistro de dereitos de usuario.", "apihelp-userrights-example-user": "Engadir o usuario <kbd>FooBot</kbd> ó grupo <kbd>bot</kbd>, e eliminar dos grupos <kbd>sysop</kbd> e <kbd>bureaucrat</kbd>.", "apihelp-userrights-example-userid": "Engadir ó usuario con ID <kbd>123</kbd> ó grupo <kbd>bot</kbd>, e borralo dos grupos <kbd>sysop</kbd> e <kbd>burócrata</kbd>.", + "apihelp-userrights-example-expiry": "Engadir o usuario <kbd>SometimeSysop</kbd> ó grupo <kbd>sysop</kbd> por 1 mes.", + "apihelp-validatepassword-description": "Valida un contrasinal contra as políticas de contrasinais da wiki.\n\nA validez é <samp>Good</samp> se o contrasinal é aceptable, <samp>Change</samp> se o contrasinal pode usarse para iniciar sesión pero debe cambiarse ou <samp>Invalid</samp> se o contrasinal non se pode usar.", "apihelp-validatepassword-param-password": "Contrasinal a validar.", + "apihelp-validatepassword-param-user": "Nome de usuario, para probas de creación de contas. O usuario nomeado non debe existir.", + "apihelp-validatepassword-param-email": "Enderezo de correo electrónico, para probas de creación de contas.", + "apihelp-validatepassword-param-realname": "Nome real, para probas de creación de contas.", + "apihelp-validatepassword-example-1": "Validar o contrasinal <kbd>foobar</kbd> para o usuario actual.", + "apihelp-validatepassword-example-2": "Validar o contrasinal <kbd>qwerty</kbd> para a creación do usuario <kbd>Example</kbd>.", "apihelp-watch-description": "Engadir ou borrar páxinas da lista de vixiancia do usuario actual.", "apihelp-watch-param-title": "Páxina a vixiar/deixar de vixiar. Usar no canto <var>$1titles</var>.", "apihelp-watch-param-unwatch": "Se está definido, a páxina deixará de estar vixiada en vez de vixiada.", @@ -1355,8 +1383,8 @@ "apihelp-xml-param-includexmlnamespace": "Se está indicado, engade un espazo de nomes XML.", "apihelp-xmlfm-description": "Datos de saída en formato XML(impresión en HTML).", "api-format-title": "Resultado de API de MediaWiki", - "api-format-prettyprint-header": "Esta é a representación HTML do formato $1. HTML é bó para depurar, pero non é axeitado para usar nunha aplicación.\n\nEspecifique o parámetro <var>format</var> para cambiar o formato de saída. Para ver a representación non-HTML do formato $1, fixe <kbd>format=$2</kbd>.\n\n\nRevise a [[mw:API|documentación completa]], ou a [[Special:ApiHelp/main|axuda da API]] para obter máis información.", - "api-format-prettyprint-header-only-html": "Esta é unha representación HTML empregada para a depuración de erros, e non é axeitada para o uso de aplicacións.\n\nVexa a [[mw:API|documentación completa]], ou a [[Special:ApiHelp/main|axuda da API]] para máis información.", + "api-format-prettyprint-header": "Esta é a representación HTML do formato $1. HTML é bó para depurar, pero non é axeitado para usar nunha aplicación.\n\nEspecifique o parámetro <var>format</var> para cambiar o formato de saída. Para ver a representación non-HTML do formato $1, fixe <kbd>format=$2</kbd>.\n\n\nRevise a [[mw:Special:MyLanguage/API|documentación completa]], ou a [[Special:ApiHelp/main|axuda da API]] para obter máis información.", + "api-format-prettyprint-header-only-html": "Esta é unha representación HTML empregada para a depuración de erros, e non é axeitada para o uso de aplicacións.\n\nVexa a [[mw:Special:MyLanguage/API|documentación completa]], ou a [[Special:ApiHelp/main|axuda da API]] para máis información.", "api-format-prettyprint-status": "Esta resposta será devolta co estado de HTTP $1 $2.", "api-pageset-param-titles": "Lista de títulos nos que traballar.", "api-pageset-param-pageids": "Lista de identificadores de páxina nos que traballar.", @@ -1404,8 +1432,8 @@ "api-help-param-default-empty": "Por defecto: <span class=\"apihelp-empty\">(baleiro)</span>", "api-help-param-token": "Un identificador \"$1\" recuperado por [[Special:ApiHelp/query+tokens|action=query&meta=tokens]]", "api-help-param-token-webui": "Por compatibilidade, o identificador usado na web UI tamén é aceptado.", - "api-help-param-disabled-in-miser-mode": "Desactivado debido ó [[mw:Manual:$wgMiserMode|modo minimal]].", - "api-help-param-limited-in-miser-mode": "<strong>Nota:</strong> Debido ó [[mw:Manual:$wgMiserMode|modo minimal]], usar isto pode devolver menos de <var>$1limit</var> resultados antes de seguir, en casos extremos, pode que non se devolvan resultados.", + "api-help-param-disabled-in-miser-mode": "Desactivado debido ó [[mw:Special:MyLanguage/Manual:$wgMiserMode|modo minimal]].", + "api-help-param-limited-in-miser-mode": "<strong>Nota:</strong> Debido ó [[mw:Special:MyLanguage/Manual:$wgMiserMode|modo minimal]], usar isto pode devolver menos de <var>$1limit</var> resultados antes de seguir, en casos extremos, pode que non se devolvan resultados.", "api-help-param-direction": "En que dirección enumerar:\n;newer:Lista os máis antigos primeiro. Nota: $1start ten que estar antes que $1end.\n;older:Lista os máis novos primeiro (por defecto). Nota: $1start ten que estar despois que $1end.", "api-help-param-continue": "Cando estean dispoñibles máis resultados, use isto para continuar.", "api-help-param-no-description": "<span class=\"apihelp-empty\">(sen descrición)</span>", @@ -1423,7 +1451,21 @@ "api-help-authmanagerhelper-returnurl": "Devolve o URL para os fluxos de autenticación de terceiros, que debe ser absoluto. Este ou <var>$1continue</var> é obrigatorio.\n\nLogo da recepción dunha resposta <samp>REDIRECT</samp>, vostede normalmente abrirá un navegador web ou un visor web para ver a URL <samp>redirecttarget</samp> especificada para un fluxo de autenticación de terceiros. Cando isto se complete, a aplicación de terceiros enviará ó navegador web ou visor web a esta URL. Vostede debe eliminar calquera consulta ou parámetros POST da URL e pasalos como unha consulta <var>$1continue</var> a este módulo API.", "api-help-authmanagerhelper-continue": "Esta petición é unha continucación despois dun resposta precedente <samp>UI</samp> ou <samp>REDIRECT</samp>. Esta ou <var>$1returnurl</var> é requirida.", "api-help-authmanagerhelper-additional-params": "Este módulo acepta parámetros adicionais dependendo das consultas de autenticación dispoñibles. Use <kbd>[[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]]</kbd> con <kbd>amirequestsfor=$1</kbd> (ou unha resposta previa deste módulo, se aplicable) para determinar as consultas dispoñibles e os campos que usan.", + "apierror-allimages-redirect": "Usar <kbd>gaifilterredir=nonredirects</kbd> no canto de <var>redirects</var> cando <kbd>allimages</kbd> é usado como xerador.", + "apierror-allpages-generator-redirects": "Usar <kbd>gapfilterredir=nonredirects</kbd> no canto de <var>redirects</var> cando <kbd>allpages</kbd> é usado como xerador.", + "apierror-appendnotsupported": "Non pode anexarse a páxinas que usan o modelo de contido $1.", "apierror-articleexists": "O artigo que intentou crear xa existe.", + "apierror-assertbotfailed": "A verificación de que o usuario ten o dereito de <code>bot</code> fallou.", + "apierror-assertnameduserfailed": "A verificación de que o usuario é «$1» fallou.", + "apierror-assertuserfailed": "A verificación de que o usuario está conectado fallou.", + "apierror-autoblocked": "O seu enderezo IP foi bloqueado automaticamente porque foi utilizado por un usuario bloqueado.", + "apierror-badconfig-resulttoosmall": "O valor de <code>$wgAPIMaxResultSize</code> neste wiki é demasiado pequeno como para conter información de resultados básicos.", + "apierror-badcontinue": "Parámetro de continuación non válido. Debe pasar o valor orixinal devolto pola consulta precedente.", + "apierror-baddiff": "A comparación non pode recuperarse. Unha ou ambas revisións non existen ou non ten permiso para velas.", + "apierror-baddiffto": "<var>$1diffto</var> debe fixarse cun número non negativo, <kbd>prev</kbd>, <kbd>next</kbd> ou <kbd>cur</kbd>.", + "apierror-badformat-generic": "O formato solicitado $1 non está soportado polo modelo de contido $2.", + "apierror-badformat": "O formato solicitado $1 non está soportado polo modelo de contido $2 utilizado por $3.", + "apierror-badgenerator-notgenerator": "O módulo <kbd>$1</kbd> non pode utilizarse como xerador.", "apierror-badgenerator-unknown": "<kbd>generator=$1</kbd> descoñecido.", "apierror-badip": "O parámetro IP non é válido.", "apierror-badmd5": "O código hash MD5 non era incorrecto.", @@ -1433,13 +1475,25 @@ "apierror-badquery": "A consulta non é válida.", "apierror-badtimestamp": "Valor \"$2\" non válido para o parámetro de data e hora <var>$1</var>.", "apierror-badtoken": "Identificador CSRF non válido.", + "apierror-badupload": "O parámetro de suba de ficheiro <var>$1</var> non é unha suba de ficheiro, asegúrese de usar <code>multipart/form-data</code> para o seu POST e de incluír un nome de ficheiro na cabeceira <code>Content-Disposition</code>.", "apierror-badurl": "Valor \"$2\" non válido para o parámetro de URL <var>$1</var>.", "apierror-baduser": "Valor \"$2\" non válido para o parámetro de usuario <var>$1</var>.", + "apierror-badvalue-notmultivalue": "O separador multivalor U+001F só pode utilizarse en parámetros multivalorados.", + "apierror-bad-watchlist-token": "Identificador de lista de vixilancia proporcionado incorrecto. Por favor, obteña un identificador correcto en [[Special:Preferences]].", + "apierror-blockedfrommail": "Foi bloqueado para o envío de correos electrónicos.", "apierror-blocked": "Foi bloqueado fronte á edición.", "apierror-botsnotsupported": "Esta interface non está dispoñible para bots.", + "apierror-cannotreauthenticate": "Esta acción non está dispoñible xa que súa identidade non se pode verificar.", "apierror-cannotviewtitle": "Non está autorizado para ver $1.", + "apierror-cantblock-email": "Non ten permiso para bloquear ós usuarios o envío de correo electrónico a través da wiki.", "apierror-cantblock": "Non ten permisos para bloquear usuarios.", + "apierror-cantchangecontentmodel": "Non ten permiso para cambiar o modelo de contido dunha páxina.", + "apierror-canthide": "Non ten permiso para ocultar nomes de usuario do rexistro de bloqueos.", + "apierror-cantimport-upload": "Non ten permiso para importar páxinas subidas.", "apierror-cantimport": "Non ten permisos para importar páxinas.", + "apierror-cantoverwrite-sharedfile": "O ficheiro obxectivo existe nun repositorio compartido e non ten permiso para substituílo.", + "apierror-cantsend": "Non está conectado na súa conta, non ten un enderezo de correo electrónico confirmado, ou non ten permiso para enviar correos electrónicos a outros usuarios, polo que non pode enviar correo electrónico.", + "apierror-cantundelete": "Non se puido restaurarː pode que as revisións solicitadas non existan, ou pode que xa se restauraran.", "apierror-changeauth-norequest": "Erro ó crear a petición de modificación.", "apierror-chunk-too-small": "O tamaño mínimo dun segmento é de $1 {{PLURAL:$1|byte|bytes}} para os segmentos non finais.", "apierror-cidrtoobroad": "Os rangos CIDR $1 maiores que /$2 non son aceptados.", @@ -1471,45 +1525,142 @@ "apierror-invalidexpiry": "Hora de caducidade incorrecta \"$1\".", "apierror-invalid-file-key": "Non se corresponde cunha clave válida de ficheiro.", "apierror-invalidlang": "Código de lingua incorrecto para o parámetro <var>$1</var>.", - "apierror-invalidoldimage": "O parámetro oldimage ten un formato incorrecto.", + "apierror-invalidoldimage": "O parámetro <var>oldimage</var> ten un formato incorrecto.", "apierror-invalidparammix-cannotusewith": "O parámetro <kbd>$1</kbd> non pode usarse xunto con <kbd>$2</kbd>.", "apierror-invalidparammix-mustusewith": "O parámetro <kbd>$1</kbd> só pode usarse xunto con <kbd>$2</kbd>.", "apierror-invalidparammix-parse-new-section": "<kbd>section=new</kbd> non se pode combinar cos parámetros <var>oldid</var>, <var>pageid</var> e <var>page</var>. Por favor, utilice <var>title</var> e <var>text</var>.", "apierror-invalidparammix": "{{PLURAL:$2|Os parámetros}} $1 non poden usarse xuntos.", - "apierror-invalidsection": "O parámetro sección debe ser un ID de sección válido ou <kbd>new</kbd>.", + "apierror-invalidsection": "O parámetro <var>section</var> debe ser un ID de sección válido ou <kbd>new</kbd>.", "apierror-invalidsha1base36hash": "O código hash SHA1Base36 proporcionado non é correcto.", "apierror-invalidsha1hash": "O código hash SHA1 proporcionado non é correcto.", "apierror-invalidtitle": "Título incorrecto \"$1\".", "apierror-invalidurlparam": "Valor non válido para <var>$1urlparam</var> (<kbd>$2=$3</kbd>).", "apierror-invaliduser": "Nome de usuario incorrecto \"$1\".", + "apierror-invaliduserid": "O identificador de usuario <var>$1</var> non é válido.", "apierror-maxlag-generic": "Esparando por un servidor de base de datosː $1 {{PLURAL:$1|segundo|segundos}} de atraso.", "apierror-maxlag": "Esperando por $2: $1 {{PLURAL:$1|segundo|segundos}} de atraso.", - "apierror-mimesearchdisabled": "A busca MIME está desactivada no modo Miser (tacaño).", + "apierror-mimesearchdisabled": "A busca MIME está desactivada no modo Miser (avaro).", + "apierror-missingcontent-pageid": "Falta contido para a páxina con identificador $1.", + "apierror-missingparam-at-least-one-of": "{{PLURAL:$2|O parámetro|Polo menos un dos parámetros}} $1 é necesario.", + "apierror-missingparam-one-of": "{{PLURAL:$2|O parámetro|Un dos parámetros}} $1 é necesario.", "apierror-missingparam": "O parámetro <var>$1</var> debe estar definido.", + "apierror-missingrev-pageid": "Non hai ningunha revisión actual da páxina con ID $1.", + "apierror-missingtitle-createonly": "Os títulos faltantes só se poden protexer con <kbd>create</kbd>.", "apierror-missingtitle": "A páxina que especificou non existe.", "apierror-missingtitle-byname": "A páxina $1 non existe.", "apierror-moduledisabled": "O módulo <kbd>$1</kbd> foi deshabilitado.", "apierror-multival-only-one-of": "Só {{PLURAL:$3|se permite o valor|se permiten os valores}} $2 para o parámetro <var>$1</var>.", "apierror-multival-only-one": "Só se permite un valor para o parámetro <var>$1</var>.", "apierror-multpages": "<var>$1</var> non se pode utilizar máis que con unha soa páxina.", + "apierror-mustbeloggedin-changeauth": "Debe estar conectado para poder cambiar os datos de autentificación.", + "apierror-mustbeloggedin-generic": "Debe estar conectado.", + "apierror-mustbeloggedin-linkaccounts": "Debe estar conectado para ligar contas.", + "apierror-mustbeloggedin-removeauth": "Debe estar conectado para borrar datos de autentificación.", + "apierror-mustbeloggedin": "Debe estar conectado para $1.", + "apierror-mustbeposted": "O módulo <kbd>$1</kbd> require unha petición POST.", + "apierror-mustpostparams": "{{PLURAL:$2|Atopouse o seguinte parámetro|Atopáronse os seguintes parámetros}} na cadea da consulta, pero deben estar no corpo do POST: $1.", + "apierror-noapiwrite": "A edición deste wiki a través da API está deshabilitada. Asegúrese de que a declaración <code>$wgEnableWriteAPI=true;</code> está incluída no ficheiro <code>LocalSettings.php</code> da wiki.", + "apierror-nochanges": "Non se solicitou ningún cambio.", + "apierror-nodeleteablefile": "Non existe esa versión antiga do ficheiro.", + "apierror-no-direct-editing": "A edición directa a través da API non é compatible co modelo de contido $1 utilizado por $2.", "apierror-noedit-anon": "Os usuarios anónimos non poden editar páxinas.", "apierror-noedit": "Non ten permisos para editar páxinas.", + "apierror-noimageredirect-anon": "Os usuarios anónimos non poden crear redireccións de imaxes.", + "apierror-noimageredirect": "Non ten permiso para crear redireccións de imaxes.", + "apierror-nosuchlogid": "Non hai ningunha entrada de rexistro con identificador $1.", + "apierror-nosuchpageid": "Non hai ningunha páxina con identificador $1.", + "apierror-nosuchrcid": "Non hai ningún cambio recente con identificador $1.", + "apierror-nosuchrevid": "Non hai ningunha revisión con identificador $1.", "apierror-nosuchsection": "Non hai ningunha sección $1.", "apierror-nosuchsection-what": "Non hai ningunha sección $1 en $2.", + "apierror-nosuchuserid": "Non hai ningún usuario con identificador $1.", + "apierror-notarget": "Non indicou un destino válido para esta acción.", + "apierror-notpatrollable": "A revisión r$1 non pode patrullarse por ser demasiado antiga.", + "apierror-opensearch-json-warnings": "Non se poden representar os avisos en formato JSON de OpenSearch.", + "apierror-pagecannotexist": "O espazo de nomes non permite as páxinas actuais.", + "apierror-pagedeleted": "A páxina foi borrada dende que obtivo o selo de tempo.", + "apierror-pagelang-disabled": "Neste wiki non se pode cambiar a lingua dunha páxina.", + "apierror-paramempty": "O parámetro <var>$1</var> non pode estar baleiro.", + "apierror-parsetree-notwikitext": "<kbd>prop=parsetree</kbd> só está soportado para o contido wikitexto.", + "apierror-parsetree-notwikitext-title": "<kbd>prop=parsetree</kbd> só está soportado para o contido wikitexto. $1 usa o modelo de contido $2.", + "apierror-pastexpiry": "A tempo de caducidade \"$1\" está no pasado.", + "apierror-permissiondenied": "Non ten permiso para $1.", "apierror-permissiondenied-generic": "Permisos rexeitados.", + "apierror-permissiondenied-patrolflag": "Necesita o permiso <code>patrol</code> ou <code>patrolmarks</code> para solicitar a marca de patrullado.", + "apierror-permissiondenied-unblock": "Non ten permiso para desbloquear usuarios.", + "apierror-prefixsearchdisabled": "A busca de prefixo está desactivada no modo Miser (avaro).", + "apierror-promised-nonwrite-api": "A cabeceira HTTP <code>Promise-Non-Write-API-Action</code> non se pode enviar a módulos da API en modo escritura.", "apierror-protect-invalidaction": "Tipo de protección \"$1\" non válido.", "apierror-protect-invalidlevel": "Nivel de protección \"$1\" non válido.", + "apierror-ratelimited": "Superou o seu límite de rango. Agarde uns minutos e inténteo de novo", "apierror-readapidenied": "Necesita permiso de lectura para utilizar ese módulo.", "apierror-readonly": "A wiki está actualmente en modo de só lectura.", + "apierror-reauthenticate": "Non se autentificou recentemente nesta sesión. Por favor, volva a autentificarse.", + "apierror-revdel-mutuallyexclusive": "Non se pode usar o mesmo campo en <var>hide</var> e <var>show</var>.", + "apierror-revdel-needtarget": "É necesario un título obxectivo para este tipo RevDel.", + "apierror-revdel-paramneeded": "Requírese polo menos un valor para <var>hide</var> e/ou <var>show</var>.", + "apierror-revisions-norevids": "O parámetro <var>revids</var> non se pode utilizar xunto coas opción de lista (<var>$1limit</var>, <var>$1startid</var>, <var>$1endid</var>, <kbd>$1dir=newer</kbd>, <var>$1user</var>, <var>$1excludeuser</var>, <var>$1start</var> e <var>$1end</var>).", + "apierror-revisions-singlepage": "Utilizouse <var>titles</var>, <var>pageids</var> ou un xerador para proporcionar múltiples páxinas, pero os parámetros <var>$1limit</var>, <var>$1startid</var>, <var>$1endid</var>, <kbd>$1dir=newer</kbd>, <var>$1user</var>, <var>$1excludeuser</var>, <var>$1start</var> e <var>$1end</var> só poden utilizarse nunha soa páxina.", + "apierror-revwrongpage": "r$1 non é unha revisión de $2.", + "apierror-searchdisabled": "A busca <var>$1</var> está desactivada.", + "apierror-sectionreplacefailed": "Non se puido combinar a sección actualizada.", + "apierror-sectionsnotsupported": "As seccións non son compatibles co modelo de contido $1.", + "apierror-sectionsnotsupported-what": "As seccións non son compatibles con $1.", + "apierror-show": "Parámetro incorrecto - non se poden proporcionar valores mutuamente excluíntes.", + "apierror-siteinfo-includealldenied": "Non se pode ver a información de tódolos servidores a menos que <var>$wgShowHostNames</var> teña valor verdadeiro.", + "apierror-sizediffdisabled": "A diferenza de tamaño está deshabilitada no modo Miser.", + "apierror-spamdetected": "A súa edición foi rexeitada por conter un fragmento de publicidade: <code>$1</code>.", + "apierror-specialpage-cantexecute": "Non ten permiso para ver os resultados desta páxina especial.", + "apierror-stashedfilenotfound": "Non se puido atopar o ficheiro na reserva: $1.", + "apierror-stashfailed-complete": "A suba por partes completouse, revise o estado para obter máis detalles.", + "apierror-stashfailed-nosession": "Non hai sesión de suba por partes con esa clave.", + "apierror-stashfilestorage": "Non se puido almacenar a suba na reservaː $1", + "apierror-stashinvalidfile": "Ficheiro de reserva incorrecto.", + "apierror-stashpathinvalid": "Clave de ficheiro con formato incorrecto ou non válidaː $1.", "apierror-stashwrongowner": "Erro de propietarioː $1", + "apierror-stashzerolength": "Ficheiro de lonxitude cero, non pode ser almacenado na reservaː $1.", + "apierror-systemblocked": "Foi bloqueado automaticamente polo software MediaWiki.", + "apierror-templateexpansion-notwikitext": "A expansión de modelos só é compatible co contido en wikitexto. $1 usa o modelo de contido $2.", + "apierror-unknownaction": "A acción especificada, <kbd>$1</kbd>, non está recoñecida.", + "apierror-unknownerror-editpage": "Erro descoñecido EditPageː $1.", "apierror-unknownerror-nocode": "Erro descoñecido.", "apierror-unknownerror": "Erro descoñecido: \"$1\".", "apierror-unknownformat": "Formato descoñecido \"$1\".", + "apierror-unrecognizedparams": "{{PLURAL:$2|Parámetro non recoñecido|Parámetros non recoñecidos}}: $1.", + "apierror-unrecognizedvalue": "Valor non recoñecido para o parámetro <var>$1</var>: $2.", + "apierror-unsupportedrepo": "O repositorio local de ficheiros non permite consultar tódalas imaxes.", + "apierror-upload-filekeyneeded": "Debe proporcionar un <var>filekey</var> cando <var>offset</var> é distinto de cero.", + "apierror-upload-filekeynotallowed": "Non pode proporcionar <var>filekey</var> cando <var>offset</var> é 0.", + "apierror-upload-inprogress": "A suba dende a reserva está en progreso.", + "apierror-upload-missingresult": "Non hai resultado nos datos de estado.", + "apierror-urlparamnormal": "Non se puideron normalizar os parámetros de imaxe de $1.", + "apierror-writeapidenied": "Non ten permiso para editar este wiki a través da API.", + "apiwarn-alldeletedrevisions-performance": "Para ter un mellor rendemento á hora de xerar títulos, estableza <kbd>$1dir=newer</kbd>.", + "apiwarn-badurlparam": "Non se puido analizar <var>$1urlparam</var> para $2. Só se usará a anchura e a altura.", + "apiwarn-badutf8": "O valor pasado para <var>$1</var> contén datos non válidos ou non normalizados. Os datos de texto deberían estar en formato Unicode válido, normalizado en NFC e sen caracteres de control C0 distintos de HT (\\t), LF (\\n) e CR (\\r).", + "apiwarn-deprecation-deletedrevs": "<kbd>list=deletedrevs</kbd> quedou obsoleto. No seu lugar, utilice <kbd>prop=deletedrevisions</kbd> ou <kbd>list=alldeletedrevisions</kbd>.", "apiwarn-deprecation-httpsexpected": "Utilizouse HTTP cando esperábase HTTPS.", "apiwarn-deprecation-parameter": "O parámetro <var>$1</var> está obsoleto.", + "apiwarn-deprecation-purge-get": "O uso de <kbd>action=purge</kbd> mediante GET está obsoleto. Use POST no seu lugar.", + "apiwarn-deprecation-withreplacement": "<kbd>$1</kbd> está obsoleto. No seu lugar, utilice <kbd>$2</kbd>.", "apiwarn-invalidcategory": "\"$1\" non é unha categoría.", "apiwarn-invalidtitle": "\"$1\" non é un título válido.", + "apiwarn-invalidxmlstylesheetext": "As follas de estilo deben ter a extensión <code>.xsl</code>.", + "apiwarn-invalidxmlstylesheet": "A folla de estilos especificada non é válida ou non existe.", + "apiwarn-invalidxmlstylesheetns": "A folla de estilos debería estar no espazo de nomes {{ns:MediaWiki}}.", + "apiwarn-moduleswithoutvars": "A propiedade <kbd>modules</kbd> está definida, pero non o está <kbd>jsconfigvars</kbd> nin <kbd>encodedjsconfigvars</kbd>. As variables de configuración son necesarias para o correcto uso do módulo.", "apiwarn-notfile": "\"$1\" non é un ficheiro.", + "apiwarn-parse-nocontentmodel": "Non se proporcionou <var>title</var> nin <var>contentmodel</var>, asúmese $1.", + "apiwarn-tokennotallowed": "A acción \"$1\" non está permitida para o usuario actual.", + "apiwarn-toomanyvalues": "Demasiados valores para o parámetro <var>$1</var>. O límite é $2.", + "apiwarn-truncatedresult": "Truncouse este resultado porque doutra maneira sobrepasaría o límite de $1 bytes.", + "apiwarn-unrecognizedvalues": "{{PLURAL:$3|Valor non recoñecido|Valores non recoñecidos}} para o parámetro <var>$1</var>: $2.", + "apiwarn-validationfailed-badchars": "caracteres non válidos na clave (só se admiten os caracteres <code>a-z</code>, <code>A-Z</code>, <code>0-9</code>, <code>_</code> e <code>-</code>).", + "apiwarn-validationfailed-badpref": "non é unha preferencia válida.", + "apiwarn-validationfailed-cannotset": "non pode ser establecido por este módulo.", + "apiwarn-validationfailed-keytoolong": "clave demasiado longa (non pode ter máis de $1 bytes).", + "apiwarn-validationfailed": "Erro de validación de <kbd>$1</kbd>: $2", + "apiwarn-wgDebugAPI": "<strong>Aviso de seguridade</strong>: <var>$wgDebugAPI</var> está habilitado.", "api-feed-error-title": "Erro ($1)", "api-usage-docref": "Consulte $1 para ver o uso da API.", "api-exception-trace": "$1 en $2($3)\n$4", diff --git a/includes/api/i18n/he.json b/includes/api/i18n/he.json index 26dfc99b277a..a948c850c594 100644 --- a/includes/api/i18n/he.json +++ b/includes/api/i18n/he.json @@ -12,13 +12,15 @@ "Umherirrender", "Macofe", "MojoMann", - "Mikey641" + "Mikey641", + "Esh77", + "שמזן" ] }, - "apihelp-main-description": "<div class=\"hlist plainlinks api-main-links\">\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</div>\n<strong>מצב:</strong> כל האפשרויות שמוצגות בדף הזה אמורות לעבוד, אבל ה־API עדיין בפיתוח פעיל, ויכול להשתנות בכל זמן. עשו מינוי ל[https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ רשימת הדיוור mediawiki-api-announce] להודעות על עדכונים.\n\n<strong>בקשות שגויות:</strong> כשבקשות שגויות נשלחות ל־API, תישלח כותרת HTTP עם המפתח \"MediaWiki-API-Error\" ואז גם הערך של הכותרת וגם קוד השגיאה יוגדרו לאותו ערך. למידע נוסף ר' [[mw:API:Errors_and_warnings|API: שגיאות ואזהרות]].\n\n<strong>בדיקה:</strong> לבדיקה קלה יותר של בקשות ר' [[Special:ApiSandbox]].", + "apihelp-main-description": "<div class=\"hlist plainlinks api-main-links\">\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</div>\n<strong>מצב:</strong> כל האפשרויות שמוצגות בדף הזה אמורות לעבוד, אבל ה־API עדיין בפיתוח פעיל, ויכול להשתנות בכל זמן. עשו מינוי ל[https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ רשימת הדיוור mediawiki-api-announce] להודעות על עדכונים.\n\n<strong>בקשות שגויות:</strong> כשבקשות שגויות נשלחות ל־API, תישלח כותרת HTTP עם המפתח \"MediaWiki-API-Error\" ואז גם הערך של הכותרת וגם קוד השגיאה יוגדרו לאותו ערך. למידע נוסף ר' [[mw:Special:MyLanguage/API:Errors_and_warnings|API: שגיאות ואזהרות]].\n\n<strong>בדיקה:</strong> לבדיקה קלה יותר של בקשות ר' [[Special:ApiSandbox]].", "apihelp-main-param-action": "איזו פעולה לבצע.", "apihelp-main-param-format": "תסדיר הפלט.", - "apihelp-main-param-maxlag": "שיהוי מרבי יכול לשמש כשמדיה־ויקי מותקנת בצביר עם מסד נתונים משוכפל. כדי לחסוך בפעולות שגורמות יותר שיהוי בשכפול אתר, הפרמטר הזה יכול לגרום ללקוח להמתין עד ששיהוי השכפול יורד מתחת לערך שצוין. במקרה של שיהוי מוגזם, קוד השגיאה <samp>maxlag</samp> מוחזר עם הודעה כמו <samp>Waiting for $host: $lag seconds lagged</samp>.<br />ר' [[mw:Manual:Maxlag_parameter|מדריך למשתמש: פרמטר maxlag]] למידע נוסף.", + "apihelp-main-param-maxlag": "שיהוי מרבי יכול לשמש כשמדיה־ויקי מותקנת בצביר עם מסד נתונים משוכפל. כדי לחסוך בפעולות שגורמות יותר שיהוי בשכפול אתר, הפרמטר הזה יכול לגרום ללקוח להמתין עד ששיהוי השכפול יורד מתחת לערך שצוין. במקרה של שיהוי מוגזם, קוד השגיאה <samp>maxlag</samp> מוחזר עם הודעה כמו <samp>Waiting for $host: $lag seconds lagged</samp>.<br />ר' [[mw:Special:MyLanguage/Manual:Maxlag_parameter|מדריך למשתמש: פרמטר maxlag]] למידע נוסף.", "apihelp-main-param-smaxage": "הגדרת כותרת בקרת מטמון HTTP <code>s-maxage</code> למספר כזה של שניות.", "apihelp-main-param-maxage": "הגדרת כותרת בקרת מטמון HTTP <code>max-age</code> למספר כזה של שניות.", "apihelp-main-param-assert": "לוודא שהמשתמש נכנס אם זה מוגדר ל־<kbd>user</kbd>, או שיש לו הרשאת בוט אם זה <kbd>bot</kbd>.", @@ -42,9 +44,10 @@ "apihelp-block-param-autoblock": "חסימה אוטומטית גם של כתובת ה־IP האחרונה שהשתמש בה ושל כל כתובת IP שינסה להשתמש בה בעתיד.", "apihelp-block-param-noemail": "למנוע ממשתמש לשלוח דואר אלקטרוני דרך הוויקי. (דורש את ההרשאה <code>blockemail</code>).", "apihelp-block-param-hidename": "הסרת השם מיומן החסימות. (דורש את ההרשאה <code>hideuser</code>.)", - "apihelp-block-param-allowusertalk": "לאפשר למשתמש לערוך את דף השיחה שלו או שלה (תלוי ב־<var>[[mw:Manual:$wgBlockAllowsUTEdit|$wgBlockAllowsUTEdit]]</var>).", + "apihelp-block-param-allowusertalk": "לאפשר למשתמש לערוך את דף השיחה שלו או שלה (תלוי ב־<var>[[mw:Special:MyLanguage/Manual:$wgBlockAllowsUTEdit|$wgBlockAllowsUTEdit]]</var>).", "apihelp-block-param-reblock": "אם המשתמש כבר חסום, לדרוס את החסימה הנוכחית.", "apihelp-block-param-watchuser": "לעקוב אחרי דף המשתמש ודף השיחה של המשתמש או של כתובת ה־IP.", + "apihelp-block-param-tags": "תגי שינוי שיחולו על העיול ביומן החסימה.", "apihelp-block-example-ip-simple": "חסימת כתובת ה־IP <kbd>192.0.2.5</kbd> לשלושה ימים עם הסיבה <kbd>First strike</kbd>.", "apihelp-block-example-user-complex": "חסימת המשתמש <kbd>Vandal</kbd> ללא הגבלת זמן עם הסיבה <kbd>Vandalism</kbd>, ומניעת יצירת חשבונות חדשים ושליחת דוא\"ל.", "apihelp-changeauthenticationdata-description": "שינוי נתוני אימות עבור המשתמש הנוכחי.", @@ -88,7 +91,7 @@ "apihelp-delete-param-title": "כותרת העמוד למחיקה. לא ניתן להשתמש בשילוב עם <var>$1pageid</var>.", "apihelp-delete-param-pageid": "מס׳ הזיהוי של העמוד למחיקה. לא ניתן להשתמש בשילוב עם <var>$1title</var>.", "apihelp-delete-param-reason": "סיבת המחיקה. אם לא הוגדרה, תתווסף סיבה שנוצרה אוטומטית.", - "apihelp-delete-param-tags": "לשנות את התגים כדי שיחולו על העיול ביומן המחיקה.", + "apihelp-delete-param-tags": "תגי שינוי שיחולו על העיול ביומן המחיקה.", "apihelp-delete-param-watch": "הוספת העמוד לרשימת המעקב של המשתמש הנוכחי.", "apihelp-delete-param-watchlist": "הוספה או הסרה של הדף ללא תנאי מרשימת המעקב של המשתמש הנוכחי, להשתמש בהעדפות או לא לשנות את המעקב.", "apihelp-delete-param-unwatch": "הסרת הדף מרשימת המעקב של של המשתמש הנוכחי.", @@ -222,6 +225,7 @@ "apihelp-import-param-templates": "ליבוא בין אתרי ויקי: לייבא גם את כל התבניות המוכללות.", "apihelp-import-param-namespace": "לייבא למרחב השם הזה. לא ניתן להשתמש בזה יחד עם <var>$1rootpage</var>.", "apihelp-import-param-rootpage": "לייבא בתור תת־משנה של הדף הזה. לא ניתן להשתמש בזה יחד עם <var>$1namespace</var>.", + "apihelp-import-param-tags": "תגי שינוי שיחולו על העיול ביומן הייבוא ולגרסה הריקה בדפים המיובאים.", "apihelp-import-example-import": "לייבא את [[meta:Help:ParserFunctions]] למרחב השם 100 עם היסטוריה מלאה.", "apihelp-linkaccount-description": "קישור חשבון של ספק צד־שלישי למשתמש הנוכחי.", "apihelp-linkaccount-example-link": "תחילת תהליך הקישור לחשבון מ־<kbd>Example</kbd>.", @@ -240,6 +244,7 @@ "apihelp-managetags-param-tag": "תג ליצירה, מחיקה, הפעלה או כיבוי. ליצירת תג, התג לא צריך להיות קיים. למחיקת תג, התג צריך להיות קיים. להפעלת תג, התג צריך להתקיים ולא להיות בשימוש של הרחבה. לכיבוי תג, התג צריך להיות קיים ומוגדר ידנית.", "apihelp-managetags-param-reason": "סיבה אופציונלית ליצירה, מחיקה, הפעלה או כיבוי של תג.", "apihelp-managetags-param-ignorewarnings": "האם להתעלם מכל האזהרות שמופיעות תוך כדי הפעולה.", + "apihelp-managetags-param-tags": "תגי השינוי שיחולו על העיול ביומן ניהול התגים.", "apihelp-managetags-example-create": "יצירת תג בשם <kbd>spam</kbd> עם הסיבה <kbd>For use in edit patrolling</kbd>", "apihelp-managetags-example-delete": "מחיקת התג <kbd>vandlaism</kbd> עם הסיבה <kbd>Misspelt</kbd>", "apihelp-managetags-example-activate": "הפעלת התג <kbd>spam</kbd> עם הסיבה <kbd>For use in edit patrolling</kbd>", @@ -265,12 +270,13 @@ "apihelp-move-param-unwatch": "הסרת הדף וההפניה מרשימת המעקב של המשתמש הנוכחי.", "apihelp-move-param-watchlist": "הוספה או הסרה של הדף ללא תנאי מרשימת המעקב של המשתמש הנוכחי, להשתמש בהעדפות או לא לשנות את המעקב.", "apihelp-move-param-ignorewarnings": "להתעלם מכל האזהרות.", + "apihelp-move-param-tags": "תגי שינוי שיחולו על העיול ביומן ההעברות ולגרסה הריקה בדף היעד.", "apihelp-move-example-move": "העברת <kbd>Badtitle</kbd> ל־<kbd>Goodtitle</kbd> בלי להשאיר הפניה.", "apihelp-opensearch-description": "חיפוש בוויקי בפרוטוקול OpenSearch.", "apihelp-opensearch-param-search": "מחרוזת לחיפוש.", "apihelp-opensearch-param-limit": "המספר המרבי של התוצאות שתוחזרנה.", "apihelp-opensearch-param-namespace": "שמות מתחם לחיפוש.", - "apihelp-opensearch-param-suggest": "לא לעשות דבר אם <var>[[mw:Manual:$wgEnableOpenSearchSuggest|$wgEnableOpenSearchSuggest]]</var> הוא false.", + "apihelp-opensearch-param-suggest": "לא לעשות דבר אם <var>[[mw:Special:MyLanguage/Manual:$wgEnableOpenSearchSuggest|$wgEnableOpenSearchSuggest]]</var> הוא false.", "apihelp-opensearch-param-redirects": "איך לטפל בהפניות:\n;return:להחזיר את ההפניה עצמה.\n;resolve:להחזיר את דף היעד. יכול להחזיר פחות מ־$1limit תוצאות.\nמסיבות היסטוריות, בררת המחדל היא \"return\" עבור $1format=json ו־\"resolve\" עבור תסדירים אחרים.", "apihelp-opensearch-param-format": "תסדיר הפלט.", "apihelp-opensearch-param-warningsaserror": "אם אזהרות מוּעלות עם <kbd>format=json</kbd>, להחזיר שגיאת API במקום להתעלם מהן.", @@ -325,6 +331,7 @@ "apihelp-parse-paramvalue-prop-limitreportdata": "נותן דו\"ח הגבלות בדרך מובנית. לא נותן שום נתונים כאשר מוגדר <var>$1disablelimitreport</var>.", "apihelp-parse-paramvalue-prop-limitreporthtml": "נותן את גרסת ה־HTML של דו\"ח ההגבלות. לא נותן שום נתונים כאשר מוגדר <var>$1disablelimitreport</var>.", "apihelp-parse-paramvalue-prop-parsetree": "עץ פענוח XML של תוכן הגרסה (דורש מודל תוכן <code>$1</code>)", + "apihelp-parse-paramvalue-prop-parsewarnings": "נותן אזהרות שאירעו בזמן פענוח התוכן.", "apihelp-parse-param-pst": "לעשות התמרה לפני שמירה על הקלט לפני פענוחו. תקין רק בשימוש עם טקסט.", "apihelp-parse-param-onlypst": "לעשות התמרה לפני שמירה (pre-save transform, PST) על הקלט, אבל לא לפענח אותו. מחזיר את אותו קוד הוויקי אחרי החלת PST. תקף רק בשימוש עם <var>$1text</var>.", "apihelp-parse-param-effectivelanglinks": "כולל קישור שפה שמספקות הרחבות (לשימוש עם <kbd>$1prop=langlinks</kbd>).", @@ -348,7 +355,7 @@ "apihelp-patrol-param-rcid": "מזהה שינויים אחרונים לניטור.", "apihelp-patrol-param-revid": "מזהה גרסה לניטור.", "apihelp-patrol-param-tags": "תגי שינוי שיחולו על העיול ביומן הניטור.", - "apihelp-patrol-example-rcid": "לנטר עיול משינויים אחרונים.", + "apihelp-patrol-example-rcid": "לנטר רשומה משינויים אחרונים.", "apihelp-patrol-example-revid": "לנטר גרסה.", "apihelp-protect-description": "לשנות את רמת ההגנה של דף.", "apihelp-protect-param-title": "כותרת הדף להגנה או הסרת הגנה. לא ניתן להשתמש בזה יחד עם $1pageid.", @@ -363,7 +370,7 @@ "apihelp-protect-example-protect": "הגנה על דף.", "apihelp-protect-example-unprotect": "להסיר את ההגנה מהדף על־ידי הגדרת מגבלות על <kbd>all</kbd> (למשל: כולם מורשים לבצע את הפעולה).", "apihelp-protect-example-unprotect2": "הסרת הגנה מדף על־ידי הגדרה של אפס הגבלות.", - "apihelp-purge-description": "ניקוי המטמון לכותרות שניתנו.\n\nדורש בקשת POST אם המשתמש לא נכנס לחשבון.", + "apihelp-purge-description": "ניקוי המטמון לכותרות שניתנו.", "apihelp-purge-param-forcelinkupdate": "עדכון טבלאות הקישורים.", "apihelp-purge-param-forcerecursivelinkupdate": "עדכון טבלת הקישורים ועדכון טבלאות הקישורים עבור כל דף שמשתמש בדף הזה בתור תבנית.", "apihelp-purge-example-simple": "ניקוי המטמון של הדפים <kbd>Main Page</kbd> ו־<kbd>API</kbd>.", @@ -404,7 +411,7 @@ "apihelp-query+alldeletedrevisions-param-user": "לרשום רק גרסאות מאת המשתמש הזה.", "apihelp-query+alldeletedrevisions-param-excludeuser": "לא לרשום גרסאות מאת המשתמש הזה.", "apihelp-query+alldeletedrevisions-param-namespace": "לרשום רק דפים במרחב השם הזה.", - "apihelp-query+alldeletedrevisions-param-miser-user-namespace": "<strong>לתשומת לבך:</strong> בשל [[mw:Manual:$wgMiserMode|מצב חיסכון]], שימוש ב־<var>$1user</var> וב־<var>$1namespace</var> ביחד עלול להניב החזרה של פחות מ־<var>$1limit</var> תוצאות לפני המשך; במצבים קיצוניים יכולות להיות מוחזרות אפס תוצאות.", + "apihelp-query+alldeletedrevisions-param-miser-user-namespace": "<strong>לתשומת לבך:</strong> בשל [[mw:Special:MyLanguage/Manual:$wgMiserMode|מצב חיסכון]], שימוש ב־<var>$1user</var> וב־<var>$1namespace</var> ביחד עלול להניב החזרה של פחות מ־<var>$1limit</var> תוצאות לפני המשך; במצבים קיצוניים יכולות להיות מוחזרות אפס תוצאות.", "apihelp-query+alldeletedrevisions-param-generatetitles": "בעת שימוש בתור מחולל, לחולל כותרת במקום מזהי גרסה.", "apihelp-query+alldeletedrevisions-example-user": "לרשום את 50 התרומות המחוקות האחרונות של משתמש <kbd>Example</kbd>.", "apihelp-query+alldeletedrevisions-example-ns-main": "רשימת 50 הגרסאות המחוקות הראשונות במרחב הראשי.", @@ -723,7 +730,7 @@ "apihelp-query+filearchive-paramvalue-prop-archivename": "הוספת שם הקובץ של גרסה מאורכבת עבור גרסאות שאינן האחרונה.", "apihelp-query+filearchive-example-simple": "הצגת רשימת כל הקבצים המחוקים.", "apihelp-query+filerepoinfo-description": "החזרת מידע מטא על מאגרי תמונות שמוגדרים בוויקי.", - "apihelp-query+filerepoinfo-param-prop": "אילו מאפייני מאגר לקבל (יכולים להיות יותר מזה באתרי ויקי אחדים):\n;apiurl:URL ל־API של המאגר – מועיל לקבלת מידע על התמונה מהמארח.\n;name:המפתח של המאגר – משמש למשל בערכים המוחזרים מ־<var>[[mw:Manual:$wgForeignFileRepos|$wgForeignFileRepos]]</var> ומ־[[Special:ApiHelp/query+imageinfo|imageinfo]].\n;displayname:שם קריא של אתר הוויקי של המאגר.\n;rooturl:URL שורש לנתיבי תמונות.\n;local:האם המאגר הוא מקומי או לא.", + "apihelp-query+filerepoinfo-param-prop": "אילו מאפייני מאגר לקבל (יכולים להיות יותר מזה באתרי ויקי אחדים):\n;apiurl:URL ל־API של המאגר – מועיל לקבלת מידע על התמונה מהמארח.\n;name:המפתח של המאגר – משמש למשל בערכים המוחזרים מ־<var>[[mw:Special:MyLanguage/Manual:$wgForeignFileRepos|$wgForeignFileRepos]]</var> ומ־[[Special:ApiHelp/query+imageinfo|imageinfo]].\n;displayname:שם קריא של אתר הוויקי של המאגר.\n;rooturl:URL שורש לנתיבי תמונות.\n;local:האם המאגר הוא מקומי או לא.", "apihelp-query+filerepoinfo-example-simple": "קבלת מידע על מאגרי קבצים.", "apihelp-query+fileusage-description": "מציאת כל הדפים שמשתמשים בקבצים הנתונים.", "apihelp-query+fileusage-param-prop": "אילו מאפיינים לקבל:", @@ -1059,10 +1066,11 @@ "apihelp-query+siteinfo-paramvalue-prop-rightsinfo": "החזרת הזכויות (הרישיון) של הוויקי, אם זמין.", "apihelp-query+siteinfo-paramvalue-prop-restrictions": "החזרת מידע על ההגבלות (ההגנות) הזמינות.", "apihelp-query+siteinfo-paramvalue-prop-languages": "החזרת השפות שמדיה־ויקי תומכת בהן (זה יכול להיות מותאם מקומים עם <var>$1inlanguagecode</var>).", + "apihelp-query+siteinfo-paramvalue-prop-languagevariants": "מחזיר רשימת קודי שפה שמופעל עבורם ממיר שפה ([[mw:Special:MyLanguage/LanguageConverter|LanguageConverter]]), וההגוונים הנתמכים עבור כל אחת מהן.", "apihelp-query+siteinfo-paramvalue-prop-skins": "החזרת רשימת כל העיצובים הזמינים (זה יכול להיות מותאם מקומית באמצעות <var>$1inlanguagecode</var>, אחרת זה יהיה בשפת התוכן).", "apihelp-query+siteinfo-paramvalue-prop-extensiontags": "החזרת רשימת תגי הרחבת מפענח.", "apihelp-query+siteinfo-paramvalue-prop-functionhooks": "החזרת hook־ים של הרחבות מפענח.", - "apihelp-query+siteinfo-paramvalue-prop-showhooks": "החזרת כל ה־hook־ים המנויים (תוכן של <var>[[mw:Manual:$wgHooks|$wgHooks]]</var>).", + "apihelp-query+siteinfo-paramvalue-prop-showhooks": "החזרת כל ה־hook־ים המנויים (תוכן של <var>[[mw:Special:MyLanguage/Manual:$wgHooks|$wgHooks]]</var>).", "apihelp-query+siteinfo-paramvalue-prop-variables": "החזרת מזהי משתנים.", "apihelp-query+siteinfo-paramvalue-prop-protocols": "החזרת רשימת הפרוטוקולים המותרים בקישורים חיצוניים.", "apihelp-query+siteinfo-paramvalue-prop-defaultoptions": "החזרת הערכים ההתחלתיים של העדפות משתמש.", @@ -1118,6 +1126,7 @@ "apihelp-query+usercontribs-param-end": "באיזה חותם־הזמן לסיים", "apihelp-query+usercontribs-param-user": "עבור אילו משתמשים לאחזר תרומות. לא יכול לשמש עם <var>$1userids</var> או <var>$1userprefix</var>.", "apihelp-query+usercontribs-param-userprefix": "אחזור תרומות עבור כל המשתמשים שהשמות שלהם מתחילים בערך הזה. לא יכול לשמש עם <var>$1user</var> או <var>$1userids</var>.", + "apihelp-query+usercontribs-param-userids": "מזהי המשתמשים לאחזור תרומות. לא יכול לשמש עם <var>$1user</var> או <var>$1userprefix</var>.", "apihelp-query+usercontribs-param-namespace": "לרשום רק תרומות במרחבי השם האלה.", "apihelp-query+usercontribs-param-prop": "לכלול פריטי מידע נוספים:", "apihelp-query+usercontribs-paramvalue-prop-ids": "הוספת מזהה הדף ומזהה הגרסה.", @@ -1130,7 +1139,7 @@ "apihelp-query+usercontribs-paramvalue-prop-flags": "הוספת הדגלים של העריכה.", "apihelp-query+usercontribs-paramvalue-prop-patrolled": "מתייג עריכות בדוקות.", "apihelp-query+usercontribs-paramvalue-prop-tags": "רשימת תגים עבור עריכות.", - "apihelp-query+usercontribs-param-show": "הצגה רק של פריטים שמתאימים לאמות המידה האלה, למשל רק עריכות לא־משניות.\n\nאם מוגדר <kbd>$2show=patrolled</kbd> או <kbd>$2show=!patrolled</kbd>, גרסאות ישנות מ־<var dir=\"ltr\">[[mw:Manual:$wgRCMaxAge|$wgRCMaxAge]]</var> ({{PLURAL:$1|שנייה אחת|$1 שניות}}) לא תוצגנה.", + "apihelp-query+usercontribs-param-show": "הצגה רק של פריטים שמתאימים לאמות המידה האלה, למשל רק עריכות לא־משניות.\n\nאם מוגדר <kbd>$2show=patrolled</kbd> או <kbd>$2show=!patrolled</kbd>, גרסאות ישנות מ־<var dir=\"ltr\">[[mw:Special:MyLanguage/Manual:$wgRCMaxAge|$wgRCMaxAge]]</var> ({{PLURAL:$1|שנייה אחת|$1 שניות}}) לא תוצגנה.", "apihelp-query+usercontribs-param-tag": "לרשום רק גרסאות עם התג הזה.", "apihelp-query+usercontribs-param-toponly": "לרשום רק שינויים שהם הגרסה האחרונה.", "apihelp-query+usercontribs-example-user": "הצגת התרומות של המשתמש <kbd>Example</kbd>.", @@ -1140,6 +1149,7 @@ "apihelp-query+userinfo-paramvalue-prop-blockinfo": "מתייג אם המשתמש הנוכחי נחסם, על־ידי מי ומאיזו סיבה.", "apihelp-query+userinfo-paramvalue-prop-hasmsg": "הוספת התג <samp>messages</samp> אם למשתמש הנוכחי יש הודעות ממתינות.", "apihelp-query+userinfo-paramvalue-prop-groups": "רשימת כל הקבוצות שהמשתמש שייך אליהן.", + "apihelp-query+userinfo-paramvalue-prop-groupmemberships": "לרשום קבוצות שהמשתמש הנוכחי משויך אליהן במפורש, כולל תאריך תפוגה לחברות בכל קבוצה.", "apihelp-query+userinfo-paramvalue-prop-implicitgroups": "רשימת כל הקבוצות שהמשתמש שייך אליהן באופן אוטומטי.", "apihelp-query+userinfo-paramvalue-prop-rights": "רשימת כל ההרשאות שיש למשתמש הזה.", "apihelp-query+userinfo-paramvalue-prop-changeablegroups": "רשימת הקבוצות שהמשתמש הנוכחי יכול להוסיף אליהן ולגרוע מהן.", @@ -1160,6 +1170,7 @@ "apihelp-query+users-param-prop": "אילו חלקי מידע לקבל:", "apihelp-query+users-paramvalue-prop-blockinfo": "מתייג אם המשתמש חסום, על־ידי מי, ומאיזו סיבה.", "apihelp-query+users-paramvalue-prop-groups": "רשימת כל הקבוצות שהמשתמש שייך אליהן.", + "apihelp-query+users-paramvalue-prop-groupmemberships": "לרשום קבוצות שכל משתמש משויך אליהן במפורש, כולל תאריך תפוגה לחברות בכל קבוצה.", "apihelp-query+users-paramvalue-prop-implicitgroups": "רשימת כל הקבוצות שהמשתמש חבר בהן אוטומטית.", "apihelp-query+users-paramvalue-prop-rights": "רשימת כל ההרשאות שיש למשתמש.", "apihelp-query+users-paramvalue-prop-editcount": "הוספת מניין העריכות של המשתמש.", @@ -1225,7 +1236,7 @@ "apihelp-removeauthenticationdata-description": "הסרת נתוני אימות עבור המשתמש הנוכחי.", "apihelp-removeauthenticationdata-example-simple": "לנסות להסיר את נתוני המשתמש הנוכחי בשביל <kbd>FooAuthenticationRequest</kbd>.", "apihelp-resetpassword-description": "שליחת דוא\"ל איפוס סיסמה למשתמש.", - "apihelp-resetpassword-description-noroutes": "אין מסלולים לאיפוס ססמה.\n\nכדי להשתמש במודול הזה, יש להפעיל מסלולים ב־<var>[[mw:Manual:$wgPasswordResetRoutes|$wgPasswordResetRoutes]]</var>.", + "apihelp-resetpassword-description-noroutes": "אין מסלולים לאיפוס ססמה.\n\nכדי להשתמש במודול הזה, יש להפעיל מסלולים ב־<var>[[mw:Special:MyLanguage/Manual:$wgPasswordResetRoutes|$wgPasswordResetRoutes]]</var>.", "apihelp-resetpassword-param-user": "המשתמש שמאופס.", "apihelp-resetpassword-param-email": "כתובת הדוא\"ל של המשתמש שהסיסמה שלו מאופסת.", "apihelp-resetpassword-example-user": "שליחת מכתב איפוס ססמה למשתמש <kbd>Example</kbd>.", @@ -1240,7 +1251,7 @@ "apihelp-revisiondelete-param-reason": "סיבה למחיקה או לשחזור ממחיקה.", "apihelp-revisiondelete-param-tags": "אילו תגים להחיל על העיול ביומן המחיקה.", "apihelp-revisiondelete-example-revision": "הסתרת התוכן של הגרסה <kbd>12345</kbd> בדף <kbd>Main Page</kbd>.", - "apihelp-revisiondelete-example-log": "הסתרת כל הנתוהים על עיול היומן <kbd>67890</kbd> עם הסיבה <kbd>BLP violation</kbd>.", + "apihelp-revisiondelete-example-log": "הסתרת כל הנתונים על רשומת היומן <kbd>67890</kbd> עם הסיבה <kbd>BLP violation</kbd>.", "apihelp-rollback-description": "ביטול העריכה האחרונה לדף.\n\nאם המשמש האחרון שערך את הדף עשה מספר עריכות זו אחר זו, הן תשוחזרנה.", "apihelp-rollback-param-title": "שם הדף לשחזור. לא יכול לשמש יחד עם <var>$1pageid</var>.", "apihelp-rollback-param-pageid": "מזהה הדף לשחזור. לא יכול לשמש יחד עם <var>$1title</var>.", @@ -1263,9 +1274,14 @@ "apihelp-setnotificationtimestamp-example-pagetimestamp": "הגדרת חותם־הזמן להודעה ל־<kbd>Main page</kbd> כך שכל העריכות מאז 1 בינואר 2012 מוגדרות בתור כאלה שלא נצפו.", "apihelp-setnotificationtimestamp-example-allpages": "אתחול מצב ההודעה עבור דפים במרחב השם <kbd>{{ns:user}}</kbd>.", "apihelp-setpagelanguage-description": "שנה את השפה של דף", - "apihelp-setpagelanguage-description-disabled": "שינוי השפה של דף לא מורשה בוויקי זה.\n\nהפעל את <var>[[mw:Manual:$wgPageLanguageUseDB|$wgPageLanguageUseDB]]</var> על מנת להשתמש בפעולה זו", + "apihelp-setpagelanguage-description-disabled": "שינוי השפה של דף לא מורשה בוויקי זה.\n\nהפעל את <var>[[mw:Special:MyLanguage/Manual:$wgPageLanguageUseDB|$wgPageLanguageUseDB]]</var> על מנת להשתמש בפעולה זו", "apihelp-setpagelanguage-param-title": "כותרת הדף שאת שפתו ברצונך לשנות. לא אפשרי להשתמש באפשרות עם <var>$1pageid</var>.", + "apihelp-setpagelanguage-param-pageid": "מזהה הדף שאת שפתו ברצונך לשנות. לא אפשרי להשתמש באפשרות עם <var>$1title</var>.", + "apihelp-setpagelanguage-param-lang": "קוד השפה של השפה שאליה צריך לשנות את הדף. יש להשתמש ב־<kbd>default</kbd> כדי לאתחל את הדף לשפת בררת המחדל של הוויקי.", + "apihelp-setpagelanguage-param-reason": "הסיבה לשינוי.", "apihelp-setpagelanguage-param-tags": "אילו תגי שינוי להחיל על העיול ביומן שמתבצע כתוצאה מהפעולה הזאת.", + "apihelp-setpagelanguage-example-language": "שינוי השפה של <kbd>Main Page</kbd> לבסקית.", + "apihelp-setpagelanguage-example-default": "שינוי השפה של הדף בעל המזהה 123 לשפה הרגילה של הוויקי.", "apihelp-stashedit-description": "הכנת עריכה במטמון משותף.\n\nזה מיועד לשימוש דרך AJAX מתוך ערך כדי לשפר את הביצועים של שמירת הדף.", "apihelp-stashedit-param-title": "כותרת הדף הנערך.", "apihelp-stashedit-param-section": "מספר הפסקה. <kbd>0</kbd> עבור הפסקה הראשונה, <kbd>new</kbd> עבור פסקה חדשה.", @@ -1279,11 +1295,11 @@ "apihelp-tag-description": "הוספת או הסרה של תגים מגרסאות בודדות או עיולי יומן בודדים.", "apihelp-tag-param-rcid": "מזהה שינוי אחרון אחד או יותר שתג יתווסף אליו או יוסר ממנו.", "apihelp-tag-param-revid": "מזהה גרסה אחד או יותר שתג יתווסף אליה או יוסר ממנה.", - "apihelp-tag-param-logid": "מזהה עיול יומן אחד או יותר שתג יתווסף אליו או יוסר ממנו.", + "apihelp-tag-param-logid": "מזהה רשומת יומן אחת או יותר שתג יתווסף אליה או יוסר ממנה.", "apihelp-tag-param-add": "התגים להוספה. אפשר להוסיף רק תגים קיימים.", "apihelp-tag-param-remove": "תגים להסרה. רק תגים שהוגדרו ידנית או שאינם מוגדרים כלל יכולים להיות מוסרים.", "apihelp-tag-param-reason": "סיבה לשינוי.", - "apihelp-tag-param-tags": "אילו תגים להחיל על עיול היומן שייווצר כתוצאה מהפעולה הזאת.", + "apihelp-tag-param-tags": "אילו תגים להחיל על רשומת היומן שתיווצר כתוצאה מהפעולה הזאת.", "apihelp-tag-example-rev": "הוספת התג <kbd>vandalism</kbd> לגרסה עם המזהה 123 בלי לציין סיבה", "apihelp-tag-example-log": "הסרת התג <kbd>spam</kbd> מעיול עם המזהה 123 עם הסיבה <kbd>Wrongly applied</kbd>", "apihelp-tokens-description": "קבלת אסימונים לפעולות שמשנות נתונים.\n\nהיחידה הזאת הוכרזה בתור מיושנת לטובת [[Special:ApiHelp/query+tokens|action=query&meta=tokens]].", @@ -1291,14 +1307,14 @@ "apihelp-tokens-example-edit": "אחזור אסימון עריכה (בררת המחדל).", "apihelp-tokens-example-emailmove": "אחזור אסימון דוא\"ל ואסימון העברה.", "apihelp-unblock-description": "שחרור משתמש מחסימה.", - "apihelp-unblock-param-id": "מזהה החסימה לשחרור (מתקבל דרך <kbd>list=blocks</kbd>). לא יכול לשמש יחד עם <var>$1user</var> או <var>$luserid</var>.", - "apihelp-unblock-param-user": "שם משתמש, כתובת IP או טווח כתובות IP לחסימה. לא יכול לשמש יחד עם <var>$1id</var> או <var>$luserid</var>.", + "apihelp-unblock-param-id": "מזהה החסימה לשחרור (מתקבל דרך <kbd>list=blocks</kbd>). לא יכול לשמש יחד עם <var>$1user</var> או <var>$1userid</var>.", + "apihelp-unblock-param-user": "שם משתמש, כתובת IP או טווח כתובות IP לחסימה. לא יכול לשמש יחד עם <var>$1id</var> או <var>$1userid</var>.", "apihelp-unblock-param-userid": "מזהה המשתמש שישוחרר מחסימה. לא יכול לשמש יחד עם <var>$1id</var> או <var>$1user</var>.", "apihelp-unblock-param-reason": "סיבה להסרת חסימה.", "apihelp-unblock-param-tags": "תגי שינוי שיחולו על העיול ביומן החסימה.", "apihelp-unblock-example-id": "לשחרר את החסימה עם מזהה #<kbd>105</kbd>.", "apihelp-unblock-example-user": "לשחרר את החסימה של המשתמש <kbd>Bob</kbd> עם הסיבה <kbd>Sorry Bob</kbd>.", - "apihelp-undelete-description": "שחזור גרסאות של דף מחוק.\n\nאפשר לאחזר רשימת גרסאות מחוקות (כולל חותמי־זמן) דרך [[Special:ApiHelp/query+deletedrevs|list=deletedrevs]], ואפשר לאחזר רשימת מזהי קבצים מחוקים דרך [[Special:ApiHelp/query+filearchive|list=filearchive]].", + "apihelp-undelete-description": "שחזור גרסאות של דף מחוק.\n\nאפשר לאחזר רשימת גרסאות מחוקות (כולל חותמי־זמן) דרך [[Special:ApiHelp/query+deletedrevisions|prop=deletedrevisions]], ואפשר לאחזר רשימת מזהי קבצים מחוקים דרך [[Special:ApiHelp/query+filearchive|list=filearchive]].", "apihelp-undelete-param-title": "שם הדף לשחזור ממחיקה.", "apihelp-undelete-param-reason": "סיבה לשחזור.", "apihelp-undelete-param-tags": "תגי שינוי שיחולו על העיול ביומן המחיקה.", @@ -1323,7 +1339,7 @@ "apihelp-upload-param-sessionkey": "אותו דבר כמו $1filekey, מושאר לצור תאימות אחורה.", "apihelp-upload-param-stash": "אם זה מוגדר, השרת יסליק זמנית את הקובץ במקום להוסיף אותו למאגר.", "apihelp-upload-param-filesize": "גודל הקובץ של כל ההעלאה.", - "apihelp-upload-param-offset": "היסט החתיכה בבייטים.", + "apihelp-upload-param-offset": "היסט הפלח בבתים.", "apihelp-upload-param-chunk": "תוכן החתיכה.", "apihelp-upload-param-async": "להפוך פעולות קבצים גדולות לאסינכרוניות כשאפשר.", "apihelp-upload-param-checkstatus": "לאחזר רק מצב העלאה עבור מפתח הקובץ שניתן.", @@ -1332,12 +1348,14 @@ "apihelp-userrights-description": "שינוי חברות בקבוצות של המשתמש.", "apihelp-userrights-param-user": "שם משתמש.", "apihelp-userrights-param-userid": "מזהה משתמש.", - "apihelp-userrights-param-add": "הוספת המשתמש לקבוצות האלו.", + "apihelp-userrights-param-add": "הוספת המשתמש לקבוצות האלו, ואם הוא כבר חבר, עדכון זמן התפוגה של החברות בקבוצה הזאת.", + "apihelp-userrights-param-expiry": "חותמי־זמן תפוגה. יכולים להיות יחסיים (למשל <kbd>5 months</kbd> או <kbd>2 weeks</kbd>) או מוחלטים (למשל <kbd>2014-09-18T12:34:56Z</kbd>). אם מוגדר רק חותם־זמן אחד, הוא ישמש לכל הקבוצות שהועברו לפרמטר <var>$1add</var>. יש להשתמש ב־<kbd>infinite</kbd>, <kbd>indefinite</kbd>, <kbd>infinity</kbd>, או <kbd>never</kbd> בשביל קבוצת משתמשים שאינה פגה לעולם.", "apihelp-userrights-param-remove": "הסרת משתמש מהקבוצות האלו.", "apihelp-userrights-param-reason": "סיבה לשינוי.", "apihelp-userrights-param-tags": "לשנות את התגים שיוחלו על העיול ביומן הרשאות המשתמש.", "apihelp-userrights-example-user": "הוספת המשתמש <kbd>FooBot</kbd> לקבוצה <kbd>bot</kbd> והסרתו מהקבוצות <kbd>sysop</kbd> ו־<kbd>bureaucrat</kbd>.", "apihelp-userrights-example-userid": "הוספת המשתמש עם המזהה <kbd>123</kbd> לקבוצה <kbd>bot</kbd> והסרתו מהקבוצות <kbd>sysop</kbd> ו־<kbd>bureaucrat</kbd>.", + "apihelp-userrights-example-expiry": "להוסיף את <kbd>SometimeSysop</kbd> לקבוצה <kbd>sysop</kbd> לחודש אחד.", "apihelp-validatepassword-description": "לבדוק תקינות ססמה אל מול מדיניות הססמאות של הוויקי.\n\nהתקינות מדווחת כ־<samp>Good</samp> אם הססמה קבילה, <samp>Change</samp> אם הססמה יכולה לשמש לכניסה, אבל צריכה להשתנות, או <samp>Invalid</samp> אם הססמה אינה שמישה.", "apihelp-validatepassword-param-password": "ססמה שתקינותה תיבדק.", "apihelp-validatepassword-param-user": "שם משתמש, לשימוש בעת בדיקת יצירת חשבון. המשתמש ששמו ניתן צריך לא להיות קיים.", @@ -1369,8 +1387,8 @@ "apihelp-xml-param-includexmlnamespace": "אם זה צוין, מוסיף מרחב שם של XML.", "apihelp-xmlfm-description": "לפלוט נתונים בתסדיר XML (עם הדפסה יפה ב־HTML).", "api-format-title": "תוצאה של API של מדיה־ויקי", - "api-format-prettyprint-header": "זהו ייצוג ב־HTML של תסדיר $1. תסדיר HTML טוב לתיקון שגיאות, אבל אינו מתאים ליישומים.\n\nיש לציין את הפרמטר <var>format</var> כדי לשנות את תסדיר הפלט. כדי לראות ייצוג של תסדיר $1 לא ב־HTML יש לרשום <kbd>format=$2</kbd>.\n\nר' את [[mw:API|התיעוד המלא]], או את [[Special:ApiHelp/main|העזרה של API]] למידע נוסף.", - "api-format-prettyprint-header-only-html": "זה ייצוג HTML שמיועד לניפוי שגיאות ואינו מתאים לשימוש ביישומים.\n\nר' את [[mw:API|התיעוד המלא]] או את [[Special:ApiHelp/main|העזרה של API]] למידע נוסף.", + "api-format-prettyprint-header": "זהו ייצוג ב־HTML של תסדיר $1. תסדיר HTML טוב לתיקון שגיאות, אבל אינו מתאים ליישומים.\n\nיש לציין את הפרמטר <var>format</var> כדי לשנות את תסדיר הפלט. כדי לראות ייצוג של תסדיר $1 לא ב־HTML יש לרשום <kbd>format=$2</kbd>.\n\nר' את [[mw:Special:MyLanguage/API|התיעוד המלא]], או את [[Special:ApiHelp/main|העזרה של API]] למידע נוסף.", + "api-format-prettyprint-header-only-html": "זה ייצוג HTML שמיועד לניפוי שגיאות ואינו מתאים לשימוש ביישומים.\n\nר' את [[mw:Special:MyLanguage/API|התיעוד המלא]] או את [[Special:ApiHelp/main|העזרה של API]] למידע נוסף.", "api-format-prettyprint-status": "התשובה הזאת הייתה מוחזרת עם סטטוס ה־HTTP מס' $1 עם הטקסט $2.", "api-pageset-param-titles": "רשימת כותרות.", "api-pageset-param-pageids": "רשימת מזהי דף לעבוד עליהם.", @@ -1418,8 +1436,8 @@ "api-help-param-default-empty": "ברירת מחדל: <span class=\"apihelp-empty\">(ריק)</span>", "api-help-param-token": "אסימון \"$1\" שאוחזר מ־[[Special:ApiHelp/query+tokens|action=query&meta=tokens]]", "api-help-param-token-webui": "לשם תאימות, גם האסימון שמשמש בממשק דפדפן מתקבל.", - "api-help-param-disabled-in-miser-mode": "כבוי בשל [[mw:Manual:$wgMiserMode|מצב חיסכון]].", - "api-help-param-limited-in-miser-mode": "<strong>לתשומת לבך:</strong> בשל [[mw:Manual:$wgMiserMode|מצב חיסכון]], שימוש בזה יכול להוביל לפחות מ־<var>$1limit</var> תוצאות לפני המשך; במצבים קיצוניים ייתכן שיחזרו אפס תוצאות.", + "api-help-param-disabled-in-miser-mode": "כבוי בשל [[mw:Special:MyLanguage/Manual:$wgMiserMode|מצב חיסכון]].", + "api-help-param-limited-in-miser-mode": "<strong>לתשומת לבך:</strong> בשל [[mw:Special:MyLanguage/Manual:$wgMiserMode|מצב חיסכון]], שימוש בזה יכול להוביל לפחות מ־<var>$1limit</var> תוצאות לפני המשך; במצבים קיצוניים ייתכן שיחזרו אפס תוצאות.", "api-help-param-direction": "באיזה כיוון למנות:\n;newer:לרשום את הישנים ביותר בהתחלה. לתשומת לבך: $1start חייב להיות לפני $1end.\n;older:לרשום את החדשים ביותר בהתחלה (בררת מחדל). לתשומת לבך: $1start חייב להיות אחרי $1end.", "api-help-param-continue": "כשיש עוד תוצאות, להשתמש בזה בשביל להמשיך.", "api-help-param-no-description": "<span class=\"apihelp-empty\">(ללא תיאור)</span>", @@ -1469,6 +1487,7 @@ "apierror-blockedfrommail": "נחסמת משליחת דוא״ל.", "apierror-blocked": "נחסמת מעריכה.", "apierror-botsnotsupported": "הממשק הזה לא נתמך עבור בוטים.", + "apierror-cannot-async-upload-file": "הפרמטרים <var>async</var> ו־<var>file</var> אינם יכולים להיות משולבים. אם ברצונך לבצע עיבוד אסינכרוני של הקובץ המועלה שלך, יש להעלות אותו תחילה לסליק (באמצעות הפרמטר <var>stash</var>) ואז לפרסם את הקובץ המוסלק באופן אסינכרוני (באמצעות <var>filekey</var> ו־<var>async</var>).", "apierror-cannotreauthenticate": "הפעולה הזאת אינה זמינה, כי הזהות שלך לא יכולה להיות מאומתת.", "apierror-cannotviewtitle": "אין לך הרשאה להציג את $1.", "apierror-cantblock-email": "אין לך הרשאה לחסום משתמשים משליחת דואר אלקטרוני דרך הוויקי.", @@ -1507,35 +1526,163 @@ "apierror-integeroutofrange-abovemax": "<var>$1</var> אינו יכול להיות גדול מ־$2 (עכשיו מוגדר $3) עבור משתמשים.", "apierror-integeroutofrange-belowminimum": "<var>$1</var> אינו יכול להיות גדול מ־$2 (עכשיו מוגדר $3).", "apierror-invalidcategory": "שם הקטגוריה שהזנת אינו תקין.", + "apierror-invalid-chunk": "ההיסט בתוספת הפלח הנוכחי גדולים מגודל הקובץ כפי שנטען.", + "apierror-invalidexpiry": "זמן תפוגה בלתי־תקין \"$1\".", "apierror-invalid-file-key": "לא מפתח קובץ תקין.", + "apierror-invalidlang": "קוד שפה בלתי־תקין לפרמטר <var>$1</var>.", + "apierror-invalidoldimage": "הפרמטר <var>oldimage</var> נשלח בתסדיר בלתי־תקין.", + "apierror-invalidparammix-cannotusewith": "הפרמטר <kbd>$1</kbd> אינו יכול לשמש עם <kbd>$2</kbd>.", + "apierror-invalidparammix-mustusewith": "הפרמטר <kbd>$1</kbd> יכול לשמש רק עם <kbd>$2</kbd>.", + "apierror-invalidparammix-parse-new-section": "לא ניתן לשלב את <kbd>section=new</kbd> עם הפרמטרים <var>oldid</var>, <var>pageid</var> או <var>page</var>. נא להשתמש ב־<var>title</var> ו־<var>text</var>.", + "apierror-invalidparammix": "{{PLURAL:$2|הפרמטרים}} $1 אינם יכולים לשמש יחדיו.", + "apierror-invalidsection": "הפרמטר <var>section</var> להיות מזהה מקטע תקין או <kbd>new</kbd>.", + "apierror-invalidsha1base36hash": "גיבוב ה־SHA1Base36 שסופק אינו תקין.", + "apierror-invalidsha1hash": "גיבוב ה־SHA1 שסופק אינו תקין.", "apierror-invalidtitle": "כותרת רעה \"$1\".", + "apierror-invalidurlparam": "ערך בלתי־תקין עבור <var>$1urlparam</var> (ערך: <kbd>$2=$3</kbd>).", "apierror-invaliduser": "שם משתמש בלתי־תקין \"$1\".", + "apierror-invaliduserid": "מזהה המשתמש <var>$1</var> אינו תקין.", + "apierror-maxlag-generic": "ממתין לשרת מסד נתונים: עיכוב של {{PLURAL:$1|שנייה אחת|$1 שניות}}.", "apierror-maxlag": "ממתין ל־$2: שיהוי של {{PLURAL:$1|שנייה אחת|$1 שניות}}.", + "apierror-mimesearchdisabled": "חיפוש MIME כבוי במצב קמצן.", + "apierror-missingcontent-pageid": "תוכן חסר עבור מזהה הדף $1.", + "apierror-missingparam-at-least-one-of": "דרוש {{PLURAL:$2|הפרמטר|לפחות אחד מהפרמטרים}} $1.", + "apierror-missingparam-one-of": "דרוש {{PLURAL:$2|הפרמטר|אחד מהפרמטרים}} $1.", + "apierror-missingparam": "הפרמטר <var>$1</var> צריך להיות מוגדר.", + "apierror-missingrev-pageid": "אין גרסה נוכחית של דף עם המזהה $1.", + "apierror-missingtitle-createonly": "כותרות חסרות יכולות להיות מוגנות עם <kbd>create</kbd>.", + "apierror-missingtitle": "הדף שנתת אינו קיים.", + "apierror-missingtitle-byname": "הדף $1 אינו קיים.", + "apierror-moduledisabled": "המודול <kbd>$1</kbd> כובה.", + "apierror-multival-only-one-of": "{{PLURAL:$3|רק הערך|רק אחד מתוך הערכים}} $2 מותר עבור הפרמטר <var>$1</var>.", + "apierror-multival-only-one": "רק ערך אחד מותר עבור הפרמטר <var>$1</var>.", + "apierror-multpages": "<var>$1</var> יכול לשמש רק בדף בודד.", + "apierror-mustbeloggedin-changeauth": "יש להיכנס לחשבון כדי לשנות נתוני אימות.", "apierror-mustbeloggedin-generic": "חובה להיכנס.", "apierror-mustbeloggedin-linkaccounts": "חובה להיכנס לחשבון כדי לקשר חשבונות.", "apierror-mustbeloggedin-removeauth": "חובה להיכנס לחשבון כדי להסיר מידע אימות.", "apierror-mustbeloggedin-uploadstash": "סליק ההעלאה זמין רק למשתמשים שנכנסו לחשבון.", "apierror-mustbeloggedin": "חובה להיכנס לחשבון כדי $1.", + "apierror-mustbeposted": "המודול <kbd>$1</kbd> דורש בקשת POST.", + "apierror-mustpostparams": "{{PLURAL:$2|הפרמטר הבא|הפרמטרים הבאים}} נמצאו במחרוזת השאילתה, אבל חייבים להיות ב־POST בגוף: $1.", + "apierror-noapiwrite": "עריכת הוויקי הזה דרך ה־API כובתה. נא לוודא שהמשפט <code dir=\"ltr\">$wgEnableWriteAPI=true;</code> כלול בקובץ <code>LocalSettings.php</code> של הוויקי.", "apierror-nochanges": "לא התבקשו שינויים.", "apierror-nodeleteablefile": "אין גרסה ישנה כזאת של הקובץ.", "apierror-no-direct-editing": "עריכה ישירה דרך ה־API אינה נתמכת עבור דגם התוכן $1 שמשמש ב{{GRAMMAR:תחילית|$2}}.", "apierror-noedit-anon": "משתמשים אלמוניים אינם יכולים לערוך דפים.", - "apierror-nosuchlogid": "אין עיול יומן עם המזהה $1.", + "apierror-noedit": "אין לך הרשאה לערוך דפים.", + "apierror-noimageredirect-anon": "משתמשים אלמוניים אינם יכולים ליצור הפניות לתמונות.", + "apierror-noimageredirect": "אין לך הרשאה ליצור הפניות לתמונות.", + "apierror-nosuchlogid": "אין רשומה ביומן עם המזהה $1.", + "apierror-nosuchpageid": "אין דף עם המזהה $1.", + "apierror-nosuchrcid": "לא נעשה לאחרונה שינוי עם המזהה $1.", + "apierror-nosuchrevid": "אין גרסה עם המזהה $1.", + "apierror-nosuchsection": "לא קיים מקטע $1.", + "apierror-nosuchsection-what": "אין מקטע $1 ב{{GRAMMAR:תחילית|$2}}.", + "apierror-nosuchuserid": "אין משתמש עם המזהה $1.", + "apierror-notarget": "לא נתת יעד תקין לפעולה הזאת.", + "apierror-notpatrollable": "לא ניתן לנטר את הגרסה $1 כי היא ישנה מדי.", + "apierror-nouploadmodule": "לא הוגדר מודול העלאה.", + "apierror-opensearch-json-warnings": "לא ניתן לייצג את האזהרות בתסדיר JSON של OpenSearch.", + "apierror-pagecannotexist": "מרחב השם אינו מתיר דפים אמתיים.", + "apierror-pagedeleted": "הדף הזה נמחק מאז שאחזרת את חותם הזמן שלו.", + "apierror-pagelang-disabled": "שינוי שפת הדף אסור בוויקי הזה.", + "apierror-paramempty": "הפרמטר <var>$1</var> אינו יכול להיות ריק.", + "apierror-parsetree-notwikitext": "<kbd>prop=parsetree</kbd> נתמך רק בתוכן קוד ויקי (wikitext).", + "apierror-parsetree-notwikitext-title": "<kbd>prop=parsetree</kbd> נתמך רק בתוכן קוד ויקי (wikitext). $1 משתמש במודל התוכן $2.", + "apierror-pastexpiry": "זמן התפוגה \"$1\" בעבר.", + "apierror-permissiondenied": "אין לך הרשאה $1.", + "apierror-permissiondenied-generic": "ההרשאה נדחתה.", + "apierror-permissiondenied-patrolflag": "עליך להחזיק בהרשאות <code>patrol</code> או <code>patrolmarks</code> כדי לבקש דגל מנוטר.", + "apierror-permissiondenied-unblock": "אין לך הרשאה לשחרר חסימה של משתמשים.", + "apierror-prefixsearchdisabled": "חיפוש תחילית כבוי במצב קמצן.", + "apierror-promised-nonwrite-api": "כותר <code>Promise-Non-Write-API-Action</code> של HTTP אינו יכול להישלח למודולי API שפועלים במצב כתיבה.", + "apierror-protect-invalidaction": "סוג הגנה בלתי־תקין \"$1\".", + "apierror-protect-invalidlevel": "קמת הגנה בלתי־תקינה \"$1\".", + "apierror-ratelimited": "עברת את מכסת הקצב שלך. נא להמתין זמן־מה ונסות שוב.", + "apierror-readapidenied": "יש צורך בהרשאת קריאה כדי להשתמש במודול הזה.", + "apierror-readonly": "הוויקי הזה במצב לקריאה בלבד עכשיו.", + "apierror-reauthenticate": "לא עברת אימות לאחרונה בשיחה הזאת, נא להתאמת מחדש.", + "apierror-redirect-appendonly": "ניסית לערוך במצב מעבר־אחר־הפניות (redirect-following), שצריך לשמש יחד עם <kbd>section=new</kbd>, <var>prependtext</var>, או <var>appendtext</var>.", + "apierror-revdel-mutuallyexclusive": "אותו השדה אינו יכול לשמש עם <var>hide</var> ועם <var>show</var>.", + "apierror-revdel-needtarget": "כותרת יעד נחוצה בשביל סוג ה־RevDel הזה.", + "apierror-revdel-paramneeded": "לפחות ערך אחד נחוץ בשביל <var>hide</var> או <var>show</var>.", + "apierror-revisions-norevids": "הפרמטר <var>revids</var> אינו יכול לשמש עם אפשרויות הרשימה (<var>$1limit</var>, <var>$1startid</var>, <var>$1endid</var>, <kbd>$1dir=newer</kbd>, <var>$1user</var>, <var>$1excludeuser</var>, <var>$1start</var>, ו־<var>$1end</var>).", + "apierror-revisions-singlepage": "<var>titles</var>, <var>pageids</var> או מחולל שימשו לאספקת דפים מרובים, אבל הפרמטרים <var>$1limit</var>, <var>$1startid</var>, <var>$1endid</var>, <kbd>$1dir=newer</kbd>, <var>$1user</var>, <var>$1excludeuser</var>, <var>$1start</var>, ו־<var>$1end</var> יכולים לשמש רק בדף בודד.", + "apierror-revwrongpage": "הגרסה $1 אינה גרסה של $2.", + "apierror-searchdisabled": "חיפוש <var>$1</var> כבוי.", + "apierror-sectionreplacefailed": "לא היה אפשר למזג את המקטע המעודכן.", + "apierror-sectionsnotsupported": "מקטעים אינם נתמכים במודל התוכן $1.", + "apierror-sectionsnotsupported-what": "מקטעים אינם נתמכים ב־$1.", + "apierror-show": "פרמטר לא נכון – אי־אפשר לספק ערכים שמבטלים זה את זה.", + "apierror-siteinfo-includealldenied": "לא ניתן להציג את המידע של כל השרתים אלא אם <var dir=\"ltr\">$wgShowHostNames</var> מוגדר להיות true.", + "apierror-sizediffdisabled": "ההבדל בגודל כבוי במצב קמצן.", + "apierror-spamdetected": "העריכה שלך סורבה כי הכילה חלק ספאם: <code>$1</code>.", + "apierror-specialpage-cantexecute": "אין לך הרשאה להציג את התוצאות של הדף המיוחד הזה.", + "apierror-stashedfilenotfound": "לא היה אפשר למצור את הקובץ בסליק: $1.", + "apierror-stashedit-missingtext": "לא נמצא טקסט מוסלק עם הגיבוב שניתן.", + "apierror-stashfailed-complete": "העלאה מפולחת הושלמה, יש לבדוק את המצב בשביל לראות פרטים.", + "apierror-stashfailed-nosession": "אין שיחת העלאה מפולחת עם המפתח הזה.", + "apierror-stashfilestorage": "לא היה אפשר לאחסן את ההעלאה בסליק: $1", "apierror-stashinvalidfile": "קובץ מוסלק בלתי־תקין.", "apierror-stashnosuchfilekey": "אין מפתח קובץ כזה: $1.", "apierror-stashpathinvalid": "מפתח קובץ מתסדיר בלתי־הולם או בלתי־תקין באופן אחר: $1.", "apierror-stashwrongowner": "בעלים בלתי־תקין: $1", "apierror-stashzerolength": "קובץ באורך אפס, ואל יכול משוחזר בסליק: $1.", "apierror-systemblocked": "נחסמת אוטומטית על־ידי מדיה־ויקי.", + "apierror-templateexpansion-notwikitext": "הרחבת תבניות נתמכת רק בתוכן קוד ויקי (wikitext). $1 משתמש במודל התוכן $2.", + "apierror-toofewexpiries": "{{PLURAL:$1|ניתן חותם זמן תפוגה אחד|ניתנו $1 חותמי זמן תפוגה}} כאשר {{PLURAL:$2|היה נחוץ אחד|היו נחוצים $1}}.", + "apierror-unknownaction": "הפעולה שניתנה, <kbd>$1</kbd>, אינה מוכרת.", + "apierror-unknownerror-editpage": "שגיאת EditPage בלתי־ידועה: $1.", "apierror-unknownerror-nocode": "שגיאה בלתי־ידועה.", "apierror-unknownerror": "שגיאה בלתי ידועה: \"$1\".", "apierror-unknownformat": "תסדיר בלתי־ידוע \"$1\".", + "apierror-unrecognizedparams": "{{PLURAL:$2|פרמטר בלתי־מוכר|פרמטרים בלתי־מוכרים}}: $1.", + "apierror-unrecognizedvalue": "לפרמטר <var>$1</var> יש ערך בלתי־מוכר: $2.", + "apierror-unsupportedrepo": "מאגר קבצים מקומי אינו תומך בשאילתה לכל התמונות.", "apierror-upload-filekeyneeded": "חובה לספק <var>filekey</var> כאשר <var>offset</var> אינו אפס.", "apierror-upload-filekeynotallowed": "לא ניתן לספק <var>filekey</var> כאשר <var>offset</var> הוא 0.", + "apierror-upload-inprogress": "העלאה מתוך סליק כבר התחילה.", "apierror-upload-missingresult": "אין תוצאות בנתוני מצב.", + "apierror-urlparamnormal": "לא היה אפשר לנרמל את פרמטרי התמונה עבור $1.", + "apierror-writeapidenied": "אין לך הרשאה לערוך את הוויקי הזה דרך ה־API.", + "apiwarn-alldeletedrevisions-performance": "לביצועים טובים יותר בעת יצירת כותרת, יש להשתמש ב־<kbd>$1dir=newer</kbd>.", + "apiwarn-badurlparam": "לא היה אפשר לפענח את <var>$1urlparam</var> עבור $2. משתמשים רק ב־width ו־height.", + "apiwarn-badutf8": "הערך הערך שהועבר ל־<var>$1</var> מכיל נתונים בלתי־תקינים או בלתי־מנורמלים. נתונים טקסט אמורים להיות תקינים, מנורמלי NFC ללא תווי בקרה C0 למעט HT (\\t), LF (\\n), ו־CR (\\r).", + "apiwarn-checktoken-percentencoding": "נא לבדוק שסימנים כמו \"+\" באסימון מקודדים עם אחוזים בצורה נכונה ב־URL.", + "apiwarn-deprecation-deletedrevs": "<kbd>list=deletedrevs</kbd> הוצהר בתור מיושן. נא להשתמש ב־ <kbd>prop=deletedrevisions</kbd> או ב־<kbd>list=alldeletedrevisions</kbd> במקום זה.", + "apiwarn-deprecation-expandtemplates-prop": "מכיוון שלא ניתנו ערכים לפרמטר <var>prop</var>, תסדיר מיושן ישמש לפלט. התסדיר הזה מיושן, ובעתיד יינתן ערך בררת מחדל לפרמטר <var>prop</var>, כך שתמיד ישמש התסדיר החדש.", + "apiwarn-deprecation-httpsexpected": "משמש HTTP כשהיה צפוי HTTPS.", + "apiwarn-deprecation-login-botpw": "כניסה לחשבון עיקרי (main-account) דרך <kbd>action=login</kbd> מיושנת ועלולה להפסיק לעבוד ללא אזהרה נוספת. כדי להמשיך להיכנס עם <kbd>action=login</kbd>, ר' [[Special:BotPasswords]]. כדי להמשיך באופן מאובטח באמצעות חשבון עיקרי, ר' <kbd>action=clientlogin</kbd>.", + "apiwarn-deprecation-login-nobotpw": "כניסה בחשבון ראשי עם <kbd>action=login</kbd> מיושנת ויכולה להפסיק לעבוד ללא אזהרה. כדי להיכנס באופן מאובטח, ר' <kbd>action=clientlogin</kbd>.", + "apiwarn-deprecation-login-token": "אחזור אסימון דרך <kbd>action=login</kbd> מיושן. נא להשתמש ב־<kbd>action=query&meta=tokens&type=login</kbd> במקום זה.", + "apiwarn-deprecation-parameter": "הפרמטר <var>$1</var> מיושן.", + "apiwarn-deprecation-parse-headitems": "<kbd>prop=headitems</kbd> מיושן מאז מדיה־ויקי 1.28. יש להשתמש ב־<kbd>prop=headhtml</kbd> בעת יצירת מסמכי HTML חדשים, או ב־<kbd>prop=modules|jsconfigvars</kbd> בעת עדכון מסמך בצד הלקוח.", + "apiwarn-deprecation-purge-get": "שימוש ב־<kbd>action=purge</kbd> דרך GET מיושן. יש להשתמש ב־POST במקום זה.", + "apiwarn-deprecation-withreplacement": "<kbd>$1</kbd> מיושן. יש להשתמש ב־<kbd>$2</kbd> במקום זה.", + "apiwarn-difftohidden": "לא היה אפשר לעשות השוואה עם גרסה $1: התוכן מוסתר.", + "apiwarn-errorprinterfailed": "מדפיס השגיאות לא עבד. ינסה שוב ללא פרמטרים.", + "apiwarn-errorprinterfailed-ex": "מדפיס השגיאות לא עבד (ינסה שוב ללא פרמטרים): $1", "apiwarn-invalidcategory": "\"$1\" אינה קטגוריה.", "apiwarn-invalidtitle": "\"$1\" אינה כותרת תקינה.", + "apiwarn-invalidxmlstylesheetext": "לגיליון הסגנונות אמור להיות הסיומת <code dir=\"ltr\">.xsl</code>.", + "apiwarn-invalidxmlstylesheet": "ניתן גיליון סגנונות שאינו תקין או אינו קיים.", + "apiwarn-invalidxmlstylesheetns": "גיליון הסגנונות אמור להיות במרחב השם {{ns:MediaWiki}}.", + "apiwarn-moduleswithoutvars": "המאפיין <kbd>modules</kbd> לא הוגדר, אבל לא <kbd>jsconfigvars</kbd> או <kbd>encodedjsconfigvars</kbd>. משתני הגדרות נחוצים בשביל שימוש נכון במודולים.", "apiwarn-notfile": "\"$1\" אינו קובץ.", + "apiwarn-nothumb-noimagehandler": "לא היה אפשר ליצור תמונה ממוזערת כי לקובץ $1 לא משויך מטפל תמונה.", + "apiwarn-parse-nocontentmodel": "לא ניתן <var>title</var> או <var>contentmodel</var>, נניח שזה $1.", + "apiwarn-parse-titlewithouttext": "<var>title</var> שימש ללא <var>text</var>, והתבקשו מאפייני דף מפוענח. האם התכוונת להשתמש ב־<var>page</var> במקום <var>title</var>?", + "apiwarn-redirectsandrevids": "פתרון הפניות לא יכול לשמש יחד עם הפרמטר <var>revids</var>. הפניות ש־<var>revids</var> מצביע אליהן לא נפתרו.", + "apiwarn-tokennotallowed": "הפעולה \"$1\" אינה מותרת למשתמש הנוכחי.", + "apiwarn-tokens-origin": "לא ניתן לקבל אסימונים כשמדיניות המקור הזהה אינה חלה.", + "apiwarn-toomanyvalues": "יותר מדי ערכים סופקו לפרמטר <var>$1</var>. המגבלה היא $2.", + "apiwarn-truncatedresult": "התוצאה נחתכה כי אחרת היא הייתה ארוכה מהמגבלה של $1 בתים.", + "apiwarn-unclearnowtimestamp": "העברת \"$2\" בתור פרמטר חותם־זמן <var>$1</var> הוצהרה בתור מיושנת. אם מסיבה כלשהי אתם צריכים להגדיר במפורש את הזמן הנוכחי ללא חישובו בצד הלקוח, יש להשתמש ב־<kbd>now</kbd>.", + "apiwarn-unrecognizedvalues": "לפרמטר <var>$1</var> היתנ ג{{PLURAL:$3|ניתן ערך בלתי־ידוע|ניתנו ערכים בלתי־ידועים}}: $2.", + "apiwarn-unsupportedarray": "הפרמטר <var>$1</var> משתמש בתחביר מערכים שאינו נתמך ב־PHP.", + "apiwarn-urlparamwidth": "התעלמות מרוחב (width) שהוגדר ב־<var>$1urlparam</var> (ערך: $2) לטובת רוחב שנגזר מ־<var>$1urlwidth</var>/<var>$1urlheight</var> (ערך: $3).", + "apiwarn-validationfailed-badchars": "תווים בלתי־תקינים במפתח (מותרים רק <code>a-z</code>, <code>A-Z</code>, <code>0-9</code>, <code>_</code>, ו־<code>-</code>).", "apiwarn-validationfailed-badpref": "לא העדפה תקינה.", "apiwarn-validationfailed-cannotset": "לא יכולה להיות מוגדרת על־ידי המודול הזה.", "apiwarn-validationfailed-keytoolong": "המפתח ארוך מדי (מותר לכתוב לא יותר מ־$1 בתים).", @@ -1543,6 +1690,7 @@ "apiwarn-wgDebugAPI": "<strong>אזהרת אבטחה</strong>: <var dir=\"ltr\">$wgDebugAPI</var> מופעל.", "api-feed-error-title": "שגיאה ($1)", "api-usage-docref": "ר' $1 לשימוש ב־API.", + "api-usage-mailinglist-ref": "עשו מינוי לרשימת התפוצה mediawiki-api-announce בכתובת <https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce> בשביל הודעות על התיישנות API ושינויים שוברים.", "api-exception-trace": "$1 בקובץ $2 (שורה $3)\n$4", "api-credits-header": "קרדיטים", "api-credits": "מפתחי ה־API:\n* רואן קטאו (מפתח מוביל 2007–2009)\n* ויקטור וסילייב\n* בריאן טונג מין\n* סאם ריד\n* יורי אסטרחן (יוצר, מפתח מוביל מספטמבר 2006 עד ספטמבר 2007)\n* בראד יורש (מפתח מוביל מאז 2013)\n\nאנא שלחו הערות, הצעות ושאלות לכתובת mediawiki-api@lists.wikimedia.org או כתבו דיווח באג באתר https://phabricator.wikimedia.org." diff --git a/includes/api/i18n/it.json b/includes/api/i18n/it.json index b543bb89ce24..22b23f03cf05 100644 --- a/includes/api/i18n/it.json +++ b/includes/api/i18n/it.json @@ -19,7 +19,7 @@ "Margherita.mignanelli" ] }, - "apihelp-main-description": "<div class=\"hlist plainlinks api-main-links\">\n* [[mw:API:Main_page|Documentazione]] (in inglese)\n* [[mw:API:FAQ|FAQ]] (in inglese)\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api Mailing list]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce Annunci sull'API]\n* [https://phabricator.wikimedia.org/maniphest/query/GebfyV4uCaLd/#R Bug & richieste]\n</div>\n<strong>Stato:</strong> tutte le funzioni e caratteristiche mostrate su questa pagina dovrebbero funzionare, ma le API sono ancora in fase attiva di sviluppo, e potrebbero cambiare in qualsiasi momento. Iscriviti alla [https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ la mailing list sugli annunci delle API MediaWiki] per essere informato sugli aggiornamenti.\n\n<strong>Istruzioni sbagliate:</strong> quando vengono impartite alle API delle istruzioni sbagliate, un'intestazione HTTP verrà inviata col messaggio \"MediaWiki-API-Error\" e, sia il valore dell'intestazione, sia il codice d'errore, verranno impostati con lo stesso valore. Per maggiori informazioni leggi [[mw:API:Errors_and_warnings|API:Errori ed avvertimenti]] (in inglese).\n\n<strong>Test:</strong> per testare facilmente le richieste API, vedi [[Special:ApiSandbox]].", + "apihelp-main-description": "<div class=\"hlist plainlinks api-main-links\">\n* [[mw:Special:MyLanguage/API:Main_page|Documentazione]] (in inglese)\n* [[mw:Special:MyLanguage/API:FAQ|FAQ]] (in inglese)\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api Mailing list]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce Annunci sull'API]\n* [https://phabricator.wikimedia.org/maniphest/query/GebfyV4uCaLd/#R Bug & richieste]\n</div>\n<strong>Stato:</strong> tutte le funzioni e caratteristiche mostrate su questa pagina dovrebbero funzionare, ma le API sono ancora in fase attiva di sviluppo, e potrebbero cambiare in qualsiasi momento. Iscriviti alla [https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ la mailing list sugli annunci delle API MediaWiki] per essere informato sugli aggiornamenti.\n\n<strong>Istruzioni sbagliate:</strong> quando vengono impartite alle API delle istruzioni sbagliate, un'intestazione HTTP verrà inviata col messaggio \"MediaWiki-API-Error\" e, sia il valore dell'intestazione, sia il codice d'errore, verranno impostati con lo stesso valore. Per maggiori informazioni leggi [[mw:Special:MyLanguage/API:Errors_and_warnings|API:Errori ed avvertimenti]] (in inglese).\n\n<strong>Test:</strong> per testare facilmente le richieste API, vedi [[Special:ApiSandbox]].", "apihelp-main-param-action": "Azione da compiere.", "apihelp-main-param-format": "Formato dell'output.", "apihelp-main-param-assert": "Verifica che l'utente abbia effettuato l'accesso se si è impostato <kbd>user</kbd>, o che abbia i permessi di bot se si è impostato <kbd>bot</kbd>.", @@ -205,7 +205,7 @@ "apihelp-move-example-move": "Sposta <kbd>Badtitle</kbd> a <kbd>Goodtitle</kbd> senza lasciare redirect.", "apihelp-opensearch-param-search": "Stringa di ricerca.", "apihelp-opensearch-param-limit": "Numero massimo di risultati da restituire.", - "apihelp-opensearch-param-suggest": "Non fare nulla se <var>[[mw:Manual:$wgEnableOpenSearchSuggest|$wgEnableOpenSearchSuggest]]</var> è falso.", + "apihelp-opensearch-param-suggest": "Non fare nulla se <var>[[mw:Special:MyLanguage/Manual:$wgEnableOpenSearchSuggest|$wgEnableOpenSearchSuggest]]</var> è falso.", "apihelp-opensearch-param-format": "Il formato dell'output.", "apihelp-opensearch-example-te": "Trova le pagine che iniziano con <kbd>Te</kbd>.", "apihelp-options-param-optionvalue": "Il valore per l'opzione specificata da <var>$1optionname</var>.", @@ -231,6 +231,7 @@ "apihelp-protect-example-protect": "Proteggi una pagina.", "apihelp-protect-example-unprotect": "Sproteggi una pagina impostando restrizione su <kbd>all</kbd> (cioè a tutti è consentito intraprendere l'azione).", "apihelp-protect-example-unprotect2": "Sproteggi una pagina impostando nessuna restrizione.", + "apihelp-purge-description": "Pulisce la cache per i titoli indicati.", "apihelp-purge-param-forcelinkupdate": "Aggiorna la tabella dei collegamenti.", "apihelp-purge-param-forcerecursivelinkupdate": "Aggiorna la tabella dei collegamenti per questa pagina, e per ogni pagina che usa questa pagina come template.", "apihelp-query-param-list": "Quali elenchi ottenere.", @@ -529,6 +530,7 @@ "apihelp-query+siteinfo-paramvalue-prop-libraries": "Restituisci librerie installate sul wiki.", "apihelp-query+siteinfo-paramvalue-prop-extensions": "Restituisci estensioni installate sul wiki.", "apihelp-query+siteinfo-paramvalue-prop-restrictions": "Restituisce informazioni sui tipi di restrizione (protezione) disponibili.", + "apihelp-query+siteinfo-paramvalue-prop-languagevariants": "Restituisce un'elenco di codici lingua per cui [[mw:Special:MyLanguage/LanguageConverter|LanguageConverter]] è attivo, e le varianti supportate per ognuno di essi.", "apihelp-query+siteinfo-example-simple": "Recupera informazioni sul sito.", "apihelp-query+tags-param-prop": "Quali proprietà ottenere:", "apihelp-query+templates-param-limit": "Quanti template restituire.", @@ -577,7 +579,7 @@ "apihelp-removeauthenticationdata-description": "Rimuove i dati di autenticazione per l'utente corrente.", "apihelp-removeauthenticationdata-example-simple": "Tentativo di rimuovere gli attuali dati utente per <kbd>FooAuthenticationRequest</kbd>.", "apihelp-resetpassword-description": "Invia una mail per reimpostare la password di un utente.", - "apihelp-resetpassword-description-noroutes": "Non sono disponibili rotte per la reimpostazione della password.\n\nAbilita le rotte in <var>[[mw:Manual:$wgPasswordResetRoutes|$wgPasswordResetRoutes]]</var> per usare questo modulo.", + "apihelp-resetpassword-description-noroutes": "Non sono disponibili rotte per la reimpostazione della password.\n\nAbilita le rotte in <var>[[mw:Special:MyLanguage/Manual:$wgPasswordResetRoutes|$wgPasswordResetRoutes]]</var> per usare questo modulo.", "apihelp-resetpassword-param-user": "Utente in corso di ripristino.", "apihelp-resetpassword-param-email": "Indirizzo di posta elettronica dell'utente in corso di ripristino.", "apihelp-resetpassword-example-user": "Invia una mail per reimpostare la password all'utente <kbd>Example</kbd>.", @@ -598,8 +600,8 @@ "apihelp-tokens-param-type": "Tipi di token da richiedere.", "apihelp-tokens-example-edit": "Recupera un token di modifica (il predefinito).", "apihelp-unblock-description": "Sblocca un utente", - "apihelp-unblock-param-user": "Nome utente, indirizzo IP o range di IP da sbloccare. Non può essere usato insieme a <var>$1id</var> o <var>$luserid</var>.", - "apihelp-unblock-param-userid": "ID utente da sbloccare. Non può essere usato insieme a <var>$1id</var> o <var>$luserid</var>.", + "apihelp-unblock-param-user": "Nome utente, indirizzo IP o range di IP da sbloccare. Non può essere usato insieme a <var>$1id</var> o <var>$1userid</var>.", + "apihelp-unblock-param-userid": "ID utente da sbloccare. Non può essere usato insieme a <var>$1id</var> o <var>$1userid</var>.", "apihelp-unblock-param-reason": "Motivo dello sblocco.", "apihelp-unblock-param-tags": "Modifica etichette da applicare all'elemento del registro dei blocchi.", "apihelp-undelete-param-title": "Titolo della pagina da ripristinare.", @@ -612,7 +614,7 @@ "apihelp-upload-example-url": "Carica da un URL.", "apihelp-userrights-param-user": "Nome utente.", "apihelp-userrights-param-userid": "ID utente.", - "apihelp-userrights-param-add": "Aggiungi l'utente a questi gruppi.", + "apihelp-userrights-param-add": "Aggiungere l'utente a questi gruppi, o se sono già membri, aggiornare la scadenza della loro appartenenza a quel gruppo.", "apihelp-userrights-param-remove": "Rimuovi l'utente da questi gruppi.", "apihelp-userrights-param-reason": "Motivo del cambiamento.", "apihelp-validatepassword-description": "Convalida una password seguendo le politiche del wiki sulle password.\n\nLa validità è riportata come <samp>Good</samp> se la password è accettabile, <samp>Change</samp> se la password può essere utilizzata per l'accesso ma deve essere modificata, o <samp>Invalid</samp> se la password non è utilizzabile.", @@ -672,6 +674,7 @@ "api-help-authmanagerhelper-returnurl": "URL di ritorno per i flussi di autenticazione di terze parti, deve essere assoluto. E' necessario fornirlo, oppure va fornito <var>$1continue</var>.\n\nAlla ricezione di una risposta <samp>REDIRECT</samp>, in genere si apre un browser o una vista web all'URL specificato <samp>redirecttarget</samp> per un flusso di autenticazione di terze parti. Quando questo è completato, la terza parte invierà il browser o la vista web a questo URL. Dovresti estrarre qualsiasi parametro POST o della richiesta dall'URL e passarli come un request <var>$1continue</var> a questo modulo API.", "api-help-authmanagerhelper-continue": "Questa richiesta è una continuazione dopo una precedente risposta <samp>UI</samp> o <samp>REDIRECT</samp>. È necessario fornirlo, oppure fornire <var>$1returnurl</var>.", "api-help-authmanagerhelper-additional-params": "Questo modulo accetta parametri aggiuntivi a seconda delle richieste di autenticazione disponibili. Utilizza <kbd>[[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]]</kbd> con <kbd>amirequestsfor=$1</kbd> (o una precedente risposta da questo modulo, se applicabile) per determinare le richieste disponibili e i campi usati da queste.", + "apierror-invalidoldimage": "Il parametro <var>oldimage</var> ha un formato non valido.", "apierror-invaliduserid": "L'ID utente <var>$1</var> non è valido.", "apierror-nosuchuserid": "Non c'è alcun utente con ID $1.", "api-credits-header": "Crediti" diff --git a/includes/api/i18n/ja.json b/includes/api/i18n/ja.json index 49f8de7a8a63..d2f595d4ed99 100644 --- a/includes/api/i18n/ja.json +++ b/includes/api/i18n/ja.json @@ -294,7 +294,7 @@ "apihelp-protect-example-protect": "ページを保護する。", "apihelp-protect-example-unprotect": "制限値を <kbd>all</kbd> にしてページの保護を解除する。", "apihelp-protect-example-unprotect2": "制限を設定されたページ保護を解除します。", - "apihelp-purge-description": "指定されたページのキャッシュをパージします。\n\n利用者がログインしていない場合は、 POST リクエストが必要です。", + "apihelp-purge-description": "指定されたページのキャッシュを破棄します。", "apihelp-purge-param-forcelinkupdate": "リンクテーブルを更新します。", "apihelp-purge-example-simple": "ページ <kbd>Main Page</kbd> および <kbd>API</kbd> をパージする。", "apihelp-purge-example-generator": "標準名前空間にある最初の10ページをパージする。", @@ -773,6 +773,7 @@ "apihelp-query+siteinfo-paramvalue-prop-general": "システム全体の情報。", "apihelp-query+siteinfo-paramvalue-prop-specialpagealiases": "特別ページの別名の一覧。", "apihelp-query+siteinfo-paramvalue-prop-magicwords": "マジックワードとこれらの別名の一覧。", + "apihelp-query+siteinfo-paramvalue-prop-fileextensions": "アップロードが許可されているファイル拡張子の一覧を返します。", "apihelp-query+siteinfo-param-numberingroup": "利用者グループに属する利用者の数を一覧表示します。", "apihelp-query+siteinfo-example-simple": "サイト情報を取得する。", "apihelp-query+tags-description": "変更タグを一覧表示します。", @@ -801,8 +802,9 @@ "apihelp-query+transcludedin-example-generator": "<kbd>Main Page</kbd> をトランスクルードしているページに関する情報を取得する。", "apihelp-query+usercontribs-description": "利用者によるすべての編集を取得します。", "apihelp-query+usercontribs-param-limit": "返す投稿記録の最大数。", - "apihelp-query+usercontribs-param-user": "投稿記録を取得する利用者。", - "apihelp-query+usercontribs-param-userprefix": "この値で始まる名前のすべての利用者の投稿記録を取得します。$1user をオーバーライドします。", + "apihelp-query+usercontribs-param-user": "投稿記録を取得する利用者。<var>$1userids</var> または <var>$1userprefix</var> とは同時に使用できません。", + "apihelp-query+usercontribs-param-userprefix": "この値で始まる名前のすべての利用者の投稿記録を取得します。<var>$1user</var> または <var>$1userids</var> とは同時に使用できません。", + "apihelp-query+usercontribs-param-userids": "投稿記録を取得する利用者のID。<var>$1user</var> または <var>$1userprefix</var> とは同時に使用できません。", "apihelp-query+usercontribs-param-namespace": "この名前空間への投稿記録のみを一覧表示する。", "apihelp-query+usercontribs-paramvalue-prop-ids": "ページIDと版IDを追加します。", "apihelp-query+usercontribs-paramvalue-prop-title": "ページ名と名前空間IDを追加します。", @@ -844,6 +846,7 @@ "apihelp-query+watchlistraw-param-prop": "追加で取得するプロパティ:", "apihelp-query+watchlistraw-param-dir": "一覧表示する方向。", "apihelp-query+watchlistraw-example-generator": "現在の利用者のウォッチリスト上のページに関する情報を取得する。", + "apihelp-resetpassword-example-user": "利用者 <kbd>Example</kbd> にパスワード再設定の電子メールを送信する。", "apihelp-revisiondelete-description": "版の削除および復元を行います。", "apihelp-revisiondelete-param-reason": "削除または復元の理由。", "apihelp-revisiondelete-example-revision": "<kbd>Main Page</kbd> の版 <kbd>12345</kbd> の本文を隠す。", @@ -871,7 +874,7 @@ "apihelp-unblock-param-tags": "ブロック記録の項目に適用する変更タグ。", "apihelp-unblock-example-id": "ブロックID #<kbd>105</kbd> を解除する。", "apihelp-unblock-example-user": "<kbd>Sorry Bob</kbd> という理由で利用者 <kbd>Bob</kbd> のブロックを解除する。", - "apihelp-undelete-description": "削除されたページの版を復元します。\n\n削除された版の一覧 (タイムスタンプを含む) は[[Special:ApiHelp/query+deletedrevs|list=deletedrevs]]に、また削除されたファイルのID一覧は[[Special:ApiHelp/query+filearchive|list=filearchive]]で見つけることができます。", + "apihelp-undelete-description": "削除されたページの版を復元します。\n\n削除された版の一覧 (タイムスタンプを含む) は[[Special:ApiHelp/query+deletedrevisions|prop=deletedrevisions]]に、また削除されたファイルのID一覧は[[Special:ApiHelp/query+filearchive|list=filearchive]]で見つけることができます。", "apihelp-undelete-param-title": "復元するページ名。", "apihelp-undelete-param-reason": "復元の理由。", "apihelp-undelete-param-tags": "削除記録の項目に適用する変更タグ。", @@ -943,6 +946,7 @@ "api-help-permissions": "{{PLURAL:$1|権限}}:", "api-help-permissions-granted-to": "{{PLURAL:$1|権限を持つグループ}}: $2", "api-help-open-in-apisandbox": "<small>[サンドボックスで開く]</small>", + "apierror-missingparam": "パラメーター <var>$1</var> を設定してください。", "api-credits-header": "クレジット", "api-credits": "API の開発者:\n* Roan Kattouw (2007年9月-2009年の主任開発者)\n* Victor Vasiliev\n* Bryan Tong Minh\n* Sam Reed\n* Yuri Astrakhan (作成者、2006年9月-2007年9月の主任開発者)\n* Brad Jorsch (2013年-現在の主任開発者)\n\nコメント、提案、質問は mediawiki-api@lists.wikimedia.org にお送りください。\nバグはこちらへご報告ください: https://phabricator.wikimedia.org/" } diff --git a/includes/api/i18n/ko.json b/includes/api/i18n/ko.json index 5e5e5d5fa048..f171f084a5ff 100644 --- a/includes/api/i18n/ko.json +++ b/includes/api/i18n/ko.json @@ -48,6 +48,7 @@ "apihelp-block-param-allowusertalk": "자신의 토론 문서를 편집할 수 있도록 허용합니다. (<var>[[mw:Manual:$wgBlockAllowsUTEdit|$wgBlockAllowsUTEdit]]</var> 값에 따라 다름)", "apihelp-block-param-reblock": "사용자가 이미 차단된 경우, 기존 차단 설정을 바꿉니다.", "apihelp-block-param-watchuser": "해당 사용자 또는 IP 주소의 사용자 문서 및 토론 문서를 주시합니다.", + "apihelp-block-param-tags": "차단 기록의 항목에 적용할 태그를 변경합니다.", "apihelp-block-example-ip-simple": "IP <kbd>192.0.2.5</kbd>에 대해 <kbd>First strike</kbd>라는 이유로 3일 간 차단하기", "apihelp-block-example-user-complex": "사용자 <kbd>Vandal</kbd>을 <kbd>Vandalism</kbd>이라는 이유로 무기한 차단하며 계정 생성 및 이메일 발송을 막기", "apihelp-changeauthenticationdata-description": "현재 사용자의 인증 데이터를 변경합니다.", @@ -83,6 +84,8 @@ "apihelp-createaccount-param-language": "사용자에게 기본으로 설정할 언어 코드. (선택 사항, 기본값으로는 본문의 언어입니다)", "apihelp-createaccount-example-pass": "사용자 <kbd>testuser</kbd>를 만들고 비밀번호를 <kbd>test123</kbd>으로 설정합니다.", "apihelp-createaccount-example-mail": "사용자 <kbd>testmailuser</kbd>를 만들고 자동 생성된 비밀번호를 이메일로 보냅니다.", + "apihelp-cspreport-description": "브라우저가 콘텐츠 보안 정책의 위반을 보고하기 위해 사용합니다. 이 모듈은 SCP를 준수하는 웹 브라우저에 의해 자동으로 사용될 때를 제외하고는 사용해서는 안 됩니다.", + "apihelp-cspreport-param-reportonly": "강제적 정책이 아닌, 모니터링 정책에서 나온 보고서인 것으로 표시합니다", "apihelp-delete-description": "문서 삭제", "apihelp-delete-param-title": "삭제할 문서의 제목. <var>$1pageid</var>과 함께 사용할 수 없습니다.", "apihelp-delete-param-pageid": "삭제할 문서의 ID. <var>$1title</var>과 함께 사용할 수 없습니다.", @@ -90,8 +93,10 @@ "apihelp-delete-param-watch": "문서를 현재 사용자의 주시문서 목록에 추가합니다.", "apihelp-delete-param-unwatch": "문서를 현재 사용자의 주시문서 목록에서 제거합니다.", "apihelp-delete-example-simple": "<kbd>Main Page</kbd>를 삭제합니다.", + "apihelp-delete-example-reason": "<kbd>Preparing for move</kbd> 라는 이유로 <kbd>Main Page</kbd>를 삭제하기.", "apihelp-disabled-description": "이 모듈은 해제되었습니다.", "apihelp-edit-description": "문서를 만들고 편집합니다.", + "apihelp-edit-param-title": "편집할 문서의 제목. <var>$1pageid</var>과 같이 사용할 수 없습니다.", "apihelp-edit-param-section": "문단 번호입니다. <kbd>0</kbd>은 최상위 문단, <kbd>new</kbd>는 새 문단입니다.", "apihelp-edit-param-sectiontitle": "새 문단을 위한 제목.", "apihelp-edit-param-text": "문서 내용.", @@ -107,6 +112,7 @@ "apihelp-edit-param-redirect": "자동으로 넘겨주기 처리하기.", "apihelp-edit-param-contentmodel": "새 콘텐츠의 콘텐츠 모델.", "apihelp-edit-example-edit": "문서 편집", + "apihelp-edit-example-undo": "자동 편집요약으로 13579판에서 13585판까지 되돌리기.", "apihelp-emailuser-description": "사용자에게 이메일을 보냅니다.", "apihelp-emailuser-param-target": "이메일을 받을 사용자.", "apihelp-emailuser-param-subject": "제목 헤더.", @@ -150,16 +156,24 @@ "apihelp-feedwatchlist-param-feedformat": "피드 포맷.", "apihelp-feedwatchlist-example-default": "주시문서 목록 피드를 보여줍니다.", "apihelp-filerevert-description": "파일을 이전 판으로 되돌립니다.", + "apihelp-filerevert-param-filename": "파일: 접두어가 없는 대상 파일 이름.", "apihelp-filerevert-param-comment": "업로드 댓글입니다.", "apihelp-filerevert-example-revert": "<kbd>Wiki.png</kbd>를 <kbd>2011-03-05T15:27:40Z</kbd> 판으로 되돌립니다.", - "apihelp-help-description": "지정된 모듈의 도움말을 보여줍니다.", + "apihelp-help-description": "지정된 모듈의 도움말을 표시합니다.", + "apihelp-help-param-modules": "(<var>action</var>, <var>format</var> 변수의 값 또는 <kbd>main</kbd>)에 대한 도움말을 표시하는 모듈입니다. <kbd>+</kbd>로 하위 모듈을 지정할 수 있습니다.", + "apihelp-help-param-submodules": "명명된 모듈의 하위 모듈의 도움말을 포함합니다.", + "apihelp-help-param-recursivesubmodules": "하위 모듈의 도움말을 반복하여 포함합니다.", "apihelp-help-param-helpformat": "도움말 출력 포맷.", + "apihelp-help-param-wrap": "표준 API 응답 구조로 출력을 감쌉니다.", + "apihelp-help-param-toc": "HTML 출력에 목차를 포함합니다.", "apihelp-help-example-main": "메인 모듈의 도움말입니다.", "apihelp-help-example-recursive": "모든 도움말을 한 페이지로 모읍니다.", + "apihelp-help-example-help": "도움말 모듈 자체에 대한 도움말입니다.", + "apihelp-help-example-query": "2개의 쿼리 하위 모듈의 도움말입니다.", "apihelp-imagerotate-description": "하나 이상의 그림을 회전합니다.", "apihelp-imagerotate-param-rotation": "시계 방향으로 회전할 그림의 각도.", "apihelp-import-param-xml": "업로드한 XML 파일.", - "apihelp-login-param-name": "계정 이름.", + "apihelp-login-param-name": "사용자 이름.", "apihelp-login-param-password": "비밀번호.", "apihelp-login-param-domain": "도메인 (선택).", "apihelp-login-param-token": "처음 요청에서 로그인 토큰을 취득했습니다.", @@ -177,11 +191,13 @@ "apihelp-move-param-watch": "현재 사용자의 주시 문서에 이 문서와 넘겨주기 문서를 추가하기", "apihelp-move-param-unwatch": "현재 사용자의 주시 문서에 이 문서와 넘겨주기 문서를 제거하기", "apihelp-move-param-ignorewarnings": "모든 경고 무시하기", + "apihelp-move-example-move": "<kbd>기존 제목</kbd>에서 <kbd>대상 제목</kbd>으로 넘겨주기를 만들지 않고 이동하기.", "apihelp-opensearch-description": "OpenSearch 프로토콜을 이용하여 위키 검색하기", "apihelp-opensearch-param-search": "문자열 검색", "apihelp-opensearch-param-limit": "반환할 결과의 최대 수", "apihelp-opensearch-param-namespace": "검색할 이름공간.", "apihelp-opensearch-param-format": "출력 포맷.", + "apihelp-opensearch-example-te": "<kbd>Te</kbd>로 시작하는 문서를 찾기.", "apihelp-options-param-reset": "사이트 기본으로 설정 초기화", "apihelp-options-example-reset": "모든 설정 초기화", "apihelp-paraminfo-description": "API 모듈의 정보를 가져옵니다.", @@ -215,10 +231,16 @@ "apihelp-parse-example-page": "페이지의 구문을 분석합니다.", "apihelp-parse-example-text": "위키텍스트의 구문을 분석합니다.", "apihelp-parse-example-summary": "요약을 구문 분석합니다.", + "apihelp-patrol-param-rcid": "점검할 최근 바뀜 ID입니다.", + "apihelp-patrol-param-revid": "점검할 판 ID입니다.", + "apihelp-patrol-example-rcid": "최근의 변경사항을 점검합니다.", + "apihelp-patrol-example-revid": "판을 점검합니다.", "apihelp-protect-description": "문서의 보호 수준을 변경합니다.", "apihelp-protect-param-reason": "보호 또는 보호 해제의 이유.", "apihelp-protect-example-protect": "문서 보호", + "apihelp-purge-description": "주어진 제목을 위한 캐시를 새로 고침.", "apihelp-purge-param-forcelinkupdate": "링크 테이블을 업데이트합니다.", + "apihelp-purge-example-simple": "<kbd>Main Page</kbd>와 <kbd>API</kbd> 문서를 새로 고침.", "apihelp-query-param-prop": "조회된 페이지에 대해 가져올 속성입니다.", "apihelp-query-param-list": "가져올 목록입니다.", "apihelp-query-param-meta": "가져올 메타데이터입니다.", @@ -264,11 +286,14 @@ "apihelp-query+allusers-description": "등록된 모든 사용자를 열거합니다.", "apihelp-query+allusers-param-dir": "정렬 방향.", "apihelp-query+allusers-param-prop": "포함할 정보:", + "apihelp-query+allusers-paramvalue-prop-blockinfo": "현재 차단된 사용자의 정보를 추가함.", "apihelp-query+allusers-paramvalue-prop-editcount": "사용자의 편집 수를 추가합니다.", "apihelp-query+allusers-param-witheditsonly": "편집을 한 사용자만 나열합니다.", "apihelp-query+allusers-example-Y": "<kbd>Y</kbd>로 시작하는 사용자를 나열합니다.", "apihelp-query+authmanagerinfo-description": "현재의 인증 상태에 대한 정보를 검색합니다.", "apihelp-query+backlinks-param-namespace": "열거할 이름공간.", + "apihelp-query+backlinks-example-simple": "<kbd>Main Page</kbd>를 가리키는 링크를 보이기.", + "apihelp-query+backlinks-example-generator": "<kbd>Main Page</kbd>를 가리키는 문서의 정보를 보기.", "apihelp-query+blocks-description": "차단된 모든 사용자와 IP 주소를 나열합니다.", "apihelp-query+blocks-param-start": "나열을 시작할 타임스탬프", "apihelp-query+blocks-param-end": "나열을 끝낼 타임스탬프", @@ -473,9 +498,15 @@ "apihelp-query+watchlist-paramvalue-prop-flags": "편집에 대한 플래그를 추가합니다.", "apihelp-query+watchlist-paramvalue-prop-loginfo": "적절한 곳에 로그 정보를 추가합니다.", "apihelp-removeauthenticationdata-description": "현재 사용자의 인증 데이터를 제거합니다.", + "apihelp-resetpassword-description": "비밀번호 재설정 이메일을 사용자에게 보냅니다.", + "apihelp-resetpassword-param-user": "재설정할 사용자입니다.", + "apihelp-resetpassword-param-email": "재설정할 사용자의 이메일 주소입니다.", + "apihelp-resetpassword-example-user": "사용자 <kbd>Example</kbd>에게 비밀번호 재설정 이메일을 보냅니다.", + "apihelp-resetpassword-example-email": "<kbd>user@example.com</kbd> 이메일 주소를 가진 모든 사용자에 대해 비밀번호 재설정 이메일을 보냅니다.", "apihelp-revisiondelete-description": "판을 삭제하거나 되살립니다.", "apihelp-revisiondelete-param-reason": "삭제 또는 복구 이유.", "apihelp-rollback-param-tags": "되돌리기를 적용하기 위해 태그합니다.", + "apihelp-rollback-example-simple": "<kbd>Project:대문</kbd> 문서의 <kbd>예시</kbd>의 마지막 판을 되돌리기", "apihelp-setpagelanguage-description": "문서의 언어를 변경합니다.", "apihelp-setpagelanguage-description-disabled": "이 위키에서 문서의 언어 변경은 허용되지 않습니다.\n\n이 동작을 사용하려면 <var>[[mw:Manual:$wgPageLanguageUseDB|$wgPageLanguageUseDB]]</var>을 활성화하십시오.", "apihelp-setpagelanguage-param-reason": "변경 이유.", @@ -486,9 +517,11 @@ "apihelp-tag-param-reason": "변경 이유.", "apihelp-tokens-param-type": "요청할 토큰의 종류.", "apihelp-unblock-description": "사용자를 차단 해제합니다.", - "apihelp-unblock-param-user": "차단을 해제할 사용자 이름, IP 주소, IP 주소 대역입니다. <var>$1id</var> 또는 <var>$luserid</var>와(과) 함께 사용할 수 없습니다.", + "apihelp-unblock-param-user": "차단을 해제할 사용자 이름, IP 주소, IP 주소 대역입니다. <var>$1id</var> 또는 <var>$1userid</var>와(과) 함께 사용할 수 없습니다.", "apihelp-unblock-param-userid": "차단을 해제할 사용자 ID입니다. <var>$1id</var> 또는 <var>$1user</var>와(과) 함께 사용할 수 없습니다.", "apihelp-unblock-param-reason": "차단 해제 이유.", + "apihelp-unblock-param-tags": "차단 기록의 항목에 적용할 태그를 변경합니다.", + "apihelp-upload-param-filename": "대상 파일 이름.", "apihelp-upload-param-ignorewarnings": "모든 경고를 무시합니다.", "apihelp-userrights-param-user": "사용자 이름.", "apihelp-userrights-param-userid": "사용자 ID.", @@ -547,8 +580,13 @@ "api-help-permissions-granted-to": "{{PLURAL:$1|다음 그룹에 부여됨}}: $2", "api-help-right-apihighlimits": "API 쿼리에서 더 높은 제한 사용 (느린 쿼리: $1, 빠른 쿼리: $2) 느린 쿼리에 대한 제한은 다중값 매개변수에도 적용됩니다.", "api-help-open-in-apisandbox": "<small>[연습장에서 열기]</small>", + "api-help-authmanager-general-usage": "이 모듈을 사용하는 일반적인 절차는 다음과 같습니다:\n# <kbd>amirequestsfor=$4</kbd>와 함께 <kbd>[[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]]</kbd>에서 사용할 수 있는 필드와 <kbd>[[Special:ApiHelp/query+tokens|action=query&meta=tokens]]</kbd>의 토큰을 가져옵니다.\n# 사용자에게 필드를 제시하고 사용자의 제출 사항을 취득합니다.\n# <var>$1returnurl</var> 및 관련된 모든 필드를 제공, 이 모듈에 전달합니다.\n# 응답 시 <samp>status</samp>를 확인합니다.\n#* <samp>PASS</samp> 또는 <samp>FAIL</samp>을 수신한 경우 작업은 끝난 것입니다. 동작은 성공하였거나 그렇지 않은 경우입니다.\n#* <samp>UI</samp>를 수신한 경우 사용자에게 새로운 필드를 제시하고 사용자의 제출 사항을 취득합니다. 그 뒤 <var>$1continue</var> 및 관련된 모든 필드 집합과 함께 이 모듈에 전달하고 단계 4를 반복합니다.\n#* <samp>REDIRECT</samp>를 수신한 경우, 사용자를 <samp>redirecttarget</samp>으로 넘겨준 다음 <var>$1returnurl</var>로 반환될 때까지 기다립니다. 그 뒤 <var>$1continue</var> 및 반환 URL에 전달되는, 모든 관련 필드와 함께 이 모듈에 전달하고 단계 4를 반복합니다.\n#* <samp>RESTART</samp>를 수실한 경우 인증은 동작했으나 연결된 사용자 계정이 없다는 것을 의미합니다. <samp>UI</samp>나 <samp>FAIL</samp>로 간주할 수 있습니다.", + "api-help-authmanagerhelper-requests": "<kbd>amirequestsfor=$1</kbd>와(과) 함께 <kbd>[[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]]</kbd>에서 반환된 <samp>id</samp>를 통해, 또는 이 모듈의 과거 응답으로부터 이 인증 요청만을 사용합니다.", "api-help-authmanagerhelper-request": "<kbd>amirequestsfor=$1</kbd>을(를) 지정하여 <kbd>[[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]]</kbd>가 반환한 <samp>id</samp>를 통해 이 인증 요청을 사용합니다.", "api-help-authmanagerhelper-messageformat": "반환 메시지에 사용할 형식.", + "api-help-authmanagerhelper-mergerequestfields": "모든 인증 요청에 대한 필드 정보를 하나의 배열로 합칩니다.", + "api-help-authmanagerhelper-preservestate": "가능하면 과거에 실패한 로그인 시도의 상태를 보존합니다.", + "api-help-authmanagerhelper-continue": "이 요청은 초기 <samp>UI</samp> 또는 <samp>REDIRECT</samp> 응답 이후에 계속됩니다. 이것 또는 <var>$1returnurl</var> 중 하나가 필요합니다.", "api-help-authmanagerhelper-additional-params": "이 모듈은 사용 가능한 인증 요청에 따라 추가 변수를 허용합니다. 사용 가능한 요청 및 사용되는 필드를 결정하려면 <kbd>amirequestsfor=$1</kbd>(또는 해당되는 경우 이 모듈의 과거 응답)과 함께 <kbd>[[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]]</kbd>을(를) 사용하십시오.", "apierror-articleexists": "작성하려는 문서가 이미 만들어져 있습니다.", "apierror-autoblocked": "사용자의 IP 주소는 차단된 사용자에 의해 사용되었으므로 자동으로 차단된 상태입니다.", @@ -576,9 +614,11 @@ "apierror-filenopath": "로컬 파일 경로를 가져올 수 없습니다.", "apierror-import-unknownerror": "알 수 없는 가져오기 오류: $1.", "apierror-invalidcategory": "입력한 분류 이름이 올바르지 않습니다.", + "apierror-invalidexpiry": "잘못된 만료 기한 \"$1\".", "apierror-invalid-file-key": "유효한 파일 키가 아닙니다.", - "apierror-invalidoldimage": "oldimage 변수에 유효하지 않은 형식이 있습니다.", + "apierror-invalidoldimage": "<var>oldimage</var> 변수에 유효하지 않은 형식이 있습니다.", "apierror-invalidparammix-cannotusewith": "<kbd>$1</kbd> 변수는 <kbd>$2</kbd>와(과) 함께 사용할 수 없습니다.", + "apierror-invalidsection": "<var>section</var> 변수는 유효한 섹션 ID 또는 <kbd>new</kbd>이어야 합니다.", "apierror-invalidsha1base36hash": "제공된 SHA1Base36 해시가 유효하지 않습니다.", "apierror-invalidsha1hash": "제공된 SHA1 해시가 유효하지 않습니다.", "apierror-invalidtitle": "잘못된 제목 \"$1\".", diff --git a/includes/api/i18n/lb.json b/includes/api/i18n/lb.json index a803645fb8cf..262f903f0114 100644 --- a/includes/api/i18n/lb.json +++ b/includes/api/i18n/lb.json @@ -159,6 +159,7 @@ "apihelp-revisiondelete-param-reason": "Grond fir ze Läschen oder ze Restauréieren.", "apihelp-rsd-example-simple": "Den RSD-Schema exportéieren", "apihelp-setpagelanguage-description": "D'Sprooch vun enger Säit änneren", + "apihelp-setpagelanguage-description-disabled": "Aschalten\n<var>[[mw:Special:MyLanguage/Manual:$wgPageLanguageUseDB|$wgPageLanguageUseDB]]</var> fir dëse Aktioun ze benotzen", "apihelp-setpagelanguage-param-reason": "Grond fir d'Ännerung.", "apihelp-setpagelanguage-example-language": "Ännert d'Sprooch vun der <kbd>Main Page</kbd> op baskesch.", "apihelp-stashedit-param-title": "Titel vun der Säit déi geännert gëtt.", diff --git a/includes/api/i18n/lt.json b/includes/api/i18n/lt.json index 79c3e6349758..0935c59ce77c 100644 --- a/includes/api/i18n/lt.json +++ b/includes/api/i18n/lt.json @@ -304,6 +304,8 @@ "apihelp-query+watchlist-paramvalue-type-log": "Žurnalo įrašai.", "apihelp-resetpassword-param-user": "Iš naujo nustatomas vartotojas.", "apihelp-resetpassword-param-email": "Iš naujo nustatomo vartotojo el. pašto adresas.", + "apihelp-setpagelanguage-description": "Keisti puslapio kalbą.", + "apihelp-setpagelanguage-param-reason": "Keitimo priežastis.", "apihelp-stashedit-param-title": "Puslapio pavadinimas buvo redaguotas.", "apihelp-stashedit-param-sectiontitle": "Naujo skyriaus pavadinimas.", "apihelp-stashedit-param-text": "Puslapio turinys.", @@ -376,6 +378,7 @@ "apierror-invalidparammix": "{{PLURAL:$2|parametrai}} $1 negali būti naudojami kartu.", "apierror-invalidtitle": "Blogas pavadinimas „$1“.", "apierror-invaliduser": "Negalimas vartotojo vardas „$1“.", + "apierror-invaliduserid": "Vartotojo ID <var>$1</var> nėra galimas.", "apierror-missingtitle": "Puslapis, kurį nurodėte, neegzistuoja.", "apierror-missingtitle-byname": "Puslapis $1 neegzistuoja", "apierror-multpages": "<var>$1</var> gali būti naudojamas tik su vienu puslapiu.", @@ -390,6 +393,7 @@ "apierror-nosuchsection": "Nėra skyriaus $1.", "apierror-nosuchsection-what": "$2 nėra sekcijos $1.", "apierror-nosuchuserid": "Nėra vartotojo su ID $1.", + "apierror-pagelang-disabled": "Puslapio kalbos keitimas nėra leidžiamas šioje viki.", "apierror-paramempty": "Parametras <var>$1</var> negali būti tusčiau.", "apierror-permissiondenied": "Neturite leidimo $1.", "apierror-permissiondenied-generic": "Teisė nesuteikta.", diff --git a/includes/api/i18n/mk.json b/includes/api/i18n/mk.json index 3abfdeb96844..3045332f3b8f 100644 --- a/includes/api/i18n/mk.json +++ b/includes/api/i18n/mk.json @@ -13,8 +13,8 @@ "apihelp-main-param-maxage": "Задајте му олку секунди на заглавието за контрола HTTP-меѓускладот <code>s-maxage</code>. Грешките никогаш не се чуваат во меѓускладот.", "apihelp-main-param-assert": "Провери дали корисникот е најавен ако е зададено <kbd>user</kbd> или дали го има корисничкото право на бот, ако е зададено <kbd>bot</kbd>.", "apihelp-main-param-requestid": "Тука внесената вредност ќе биде вклучена во извештајот. Може да се користи за разликување на барањата.", - "apihelp-main-param-servedby": "Вклучи го домаќинското име што го услужило барањето во резултатите.", - "apihelp-main-param-curtimestamp": "Бклучи тековно време и време и датум во резултатот.", + "apihelp-main-param-servedby": "Вклучи го домаќинското име што го услужило барањето во исходот.", + "apihelp-main-param-curtimestamp": "Вклучи тековно време и време и датум во исходот.", "apihelp-main-param-origin": "Кога му пристапувате на Пирлогот користејќи повеќедоменско AJAX-барање (CORS), задајте му го на ова изворниот домен. Ова мора да се вклучи во секое подготвително барање и затоа мора да биде дел од URI на барањето (не главната содржина во POST). Ова мора точно да се совпаѓа со еден од изворниците на заглавието Origin:, така што мора да е зададен на нешто како <kbd>https://mk.wikipedia.org</kbd> or <kbd>https://meta.wikimedia.org</kbd>. Ако овој параметар не се совпаѓа со заглавието <code>Origin</code>:, ќе се појави одговор 403. Ако се совпаѓа, а изворникот е на бел список (на допуштени), тогаш ќе се зададе заглавието <code>Access-Control-Allow-Origin</code>.", "apihelp-main-param-uselang": "Јазик за преведување на пораките. Список на јазични кодови ќе најдете на <kbd>[[Special:ApiHelp/query+siteinfo|action=query&meta=siteinfo]]</kbd> со <kbd>siprop=languages</kbd> или укажете <kbd>user</kbd> за да го користите тековно зададениот јазик корисникот, или пак укажете <kbd>content</kbd> за да го користите јазикот на содржината на ова вики.", "apihelp-block-description": "Блокирај корисник.", @@ -79,7 +79,7 @@ "apihelp-edit-param-tags": "Ознаки за измена што се однесуваат на преработката.", "apihelp-edit-param-minor": "Ситно уредување.", "apihelp-edit-param-notminor": "Неситно уредување.", - "apihelp-edit-param-bot": "Означи го уредувањево како ботско.", + "apihelp-edit-param-bot": "Означи го уредувањево како ботовско.", "apihelp-edit-param-basetimestamp": "Датум и време на преработката на базата, кои се користат за утврдување на спротиставености во уредувањето. Може да се добие преку [[Special:ApiHelp/query+revisions|action=query&prop=revisions&rvprop=timestamp]].", "apihelp-edit-param-starttimestamp": "Датум и време кога сте почнало уредувањето, кои се користат за утврдување на спротиставености во уредувањата. Соодветната вредност се добива користејќи <var>[[Special:ApiHelp/main|curtimestamp]]</var> кога ќе почнете со уредување (на пр. кога ќе се вчита содржината што ќе ја уредувате).", "apihelp-edit-param-recreate": "Занемари ги грешките што се појавуваат во врска со страницата што е избришана во меѓувреме.", @@ -110,7 +110,7 @@ "apihelp-expandtemplates-param-title": "Наслов на страница.", "apihelp-expandtemplates-param-text": "Викитекст за претворање.", "apihelp-expandtemplates-param-revid": "Назнака на преработката, за <nowiki>{{REVISIONID}}</nowiki> и слични променливи.", - "apihelp-expandtemplates-param-prop": "Кои информации треба да ги добиете:\n\nИмајте на ум дека ако не изберете никаква вредност, резултатот ќе го содржи викитекстот, но изводот ќе биде во застарен формат.", + "apihelp-expandtemplates-param-prop": "Кои информации треба да ги добиете:\n\nИмајте на ум дека ако не изберете никаква вредност, исходот ќе го содржи викитекстот, но изводот ќе биде во застарен формат.", "apihelp-expandtemplates-paramvalue-prop-wikitext": "Проширениот викитекст.", "apihelp-expandtemplates-param-includecomments": "Дали во изводот да се вклучени HTML-коментари.", "apihelp-expandtemplates-param-generatexml": "Создај XML-дрво на расчленување (заменето со $1prop=parsetree).", @@ -125,14 +125,15 @@ "apihelp-feedcontributions-param-deletedonly": "Прикажувај само избришани придонеси.", "apihelp-feedcontributions-param-toponly": "Прикажувај само последни преработки.", "apihelp-feedcontributions-param-newonly": "Прикажувај само новосоздадени страници", + "apihelp-feedcontributions-param-hideminor": "Сокриј ситни уредувања.", "apihelp-feedcontributions-param-showsizediff": "Покажувај ја големинската разлика меѓу преработките.", "apihelp-feedcontributions-example-simple": "Покажувај придонеси на <kbd>Пример</kbd>.", "apihelp-feedrecentchanges-description": "Дава канал со скорешни промени.", "apihelp-feedrecentchanges-param-feedformat": "Форматот на каналот.", - "apihelp-feedrecentchanges-param-namespace": "На кој именски простор да се ограничат резултатите.", + "apihelp-feedrecentchanges-param-namespace": "На кој именски простор да се ограничи исходот.", "apihelp-feedrecentchanges-param-invert": "Сите именски простори освен избраниот.", "apihelp-feedrecentchanges-param-associated": "Вклучи придружни именски простори (разговор или главен).", - "apihelp-feedrecentchanges-param-days": "На кои денови да се ограничат резултатите.", + "apihelp-feedrecentchanges-param-days": "На кои денови да се ограничат ставките.", "apihelp-feedrecentchanges-param-limit": "Највеќе ставки во исходот за прикажување.", "apihelp-feedrecentchanges-param-from": "Прикажи ги промените оттогаш.", "apihelp-feedrecentchanges-param-hideminor": "Скриј ги ситните промени.", @@ -141,9 +142,12 @@ "apihelp-feedrecentchanges-param-hideliu": "Скриј ги промените направени од регистрирани корисници.", "apihelp-feedrecentchanges-param-hidepatrolled": "Скриј ги испатролираните промени.", "apihelp-feedrecentchanges-param-hidemyself": "Скриј ги промените на тековниот корисник.", + "apihelp-feedrecentchanges-param-hidecategorization": "Сокриј префрлања во други категории.", "apihelp-feedrecentchanges-param-tagfilter": "Филтрирање по ознака.", "apihelp-feedrecentchanges-param-target": "Прикажи само промени на страници што водат од оваа.", "apihelp-feedrecentchanges-param-showlinkedto": "Наместо тоа, прикажи ги промените на страниците поврзани со избраната страница.", + "apihelp-feedrecentchanges-param-categories": "Прикажи само промени на страниците во сите овие категории.", + "apihelp-feedrecentchanges-param-categories_any": "Прикажи само промени на страниците во било која од категориите.", "apihelp-feedrecentchanges-example-simple": "Прикажи скорешни промени", "apihelp-feedrecentchanges-example-30days": "Прикажувај скорешни промени 30 дена", "apihelp-feedwatchlist-description": "Дава канал од набљудуваните.", @@ -165,15 +169,17 @@ "apihelp-help-param-wrap": "Обвиткај го изводот како станрадна одѕивна структура од прилотот.", "apihelp-help-param-toc": "Вклучи табела со содржина во HTML-изводот.", "apihelp-help-example-main": "Помош за главниот модул", + "apihelp-help-example-submodules": "Помош за <kbd>action=query</kbd> и сите негови подмодули.", "apihelp-help-example-recursive": "Сета помош на една страница", "apihelp-help-example-help": "Помош за самиот помошен модул", "apihelp-help-example-query": "Помош за два подмодула за барања", "apihelp-imagerotate-description": "Сврти една или повеќе слики.", "apihelp-imagerotate-param-rotation": "За колку степени да се сврти надесно.", + "apihelp-imagerotate-param-tags": "Ознаки за примена врз ставката во дневникот на подигања.", "apihelp-imagerotate-example-simple": "Сврти ја <kbd>Податотека:Пример.png</kbd> за <kbd>90</kbd> степени.", "apihelp-imagerotate-example-generator": "Сврти ги сите слики во <kbd>Категорија:Некоја</kbd> за <kbd>180</kbd> степени.", "apihelp-import-description": "Увези страница од друго вики или од XML-податотека.\n\nИмајте на ум дека POST на HTTP мора да се изведе како подигање на податотеката (т.е. користејќи повеќеделни податоци/податоци од образец) кога ја испраќате податотеката за параметарот <var>xml</var>.", - "apihelp-import-param-summary": "Увези опис.", + "apihelp-import-param-summary": "Опис на увозот на дневнички запис.", "apihelp-import-param-xml": "Подигната XML-податотека.", "apihelp-import-param-interwikisource": "За меѓујазични увози: од кое вики да се увезе.", "apihelp-import-param-interwikipage": "За меѓујазични увози: страница за увоз.", @@ -206,10 +212,10 @@ "apihelp-move-example-move": "Премести го <kbd>Badtitle</kbd> на <kbd>Goodtitle</kbd>, неоставајќи пренасочување", "apihelp-opensearch-description": "Пребарување на викито со протоколот OpenSearch.", "apihelp-opensearch-param-search": "Низа за пребарување.", - "apihelp-opensearch-param-limit": "Највеќе резултати за прикажување.", + "apihelp-opensearch-param-limit": "Највеќе ставки за прикажување.", "apihelp-opensearch-param-namespace": "Именски простори за пребарување.", "apihelp-opensearch-param-suggest": "Не прави ништо ако <var>[[mw:Manual:$wgEnableOpenSearchSuggest|$wgEnableOpenSearchSuggest]]</var> е неточно.", - "apihelp-opensearch-param-redirects": "Како да се работи со пренасочувања:\n;return: Дај го самото пренасочување.\n;resolve: Дај ја целната страница. Може да даде помалку од $1limit резултати.\nОд историски причини, по основно е „return“ за $1format=json и „resolve“ за други формати.", + "apihelp-opensearch-param-redirects": "Како да се работи со пренасочувања:\n;return: Дај го самото пренасочување.\n;resolve: Дај ја целната страница. Може да даде помалку од $1limit ставки.\nОд историски причини, по основно е „return“ за $1format=json и „resolve“ за други формати.", "apihelp-opensearch-param-format": "Формат на изводот.", "apihelp-opensearch-example-te": "Најди страници што почнуваат со <kbd>Те</kbd>.", "apihelp-options-description": "Смени ги нагодувањата на тековниот корисник.\n\nМожат да се зададат само можностите заведени во јадрото или во едно од воспоставените додатоци, или пак можности со клуч кој ја има претставката <code>userjs-</code> (предвиден за употреба од кориснички скрипти).", @@ -313,7 +319,7 @@ "apihelp-query+revisions-example-first5-user": "Дај ги првите 5 преработки на <kbd>Главна страница</kbd> кои се направени од корисникот „зададен од МедијаВики“ (<kbd>MediaWiki default</kbd>)", "apihelp-query+search-example-simple": "Побарај <kbd>meaning</kbd>.", "apihelp-query+search-example-text": "Побарај го <kbd>meaning</kbd> по текстовите.", - "apihelp-query+search-example-generator": "Дај информации за страниците што излегуваат во резултатите од пребарувањето на <kbd>meaning</kbd>.", + "apihelp-query+search-example-generator": "Дај информации за страниците што излегуваат во исходот од пребарувањето на <kbd>meaning</kbd>.", "apihelp-query+siteinfo-description": "Дај општи информации за мрежното место.", "apihelp-upload-param-filename": "Целно име на податотеката.", "apihelp-upload-param-comment": "Коментар при подигање. Се користи и како првичен текст на страницата за нови податотеки ако не е укажано <var>$1text</var>.", @@ -361,7 +367,7 @@ "apihelp-xml-param-xslt": "Ако е укажано, ја додава именуваната страница како XSL-стилска страница. Вредноста мора да биде наслов во именскиот простор „{{ns:MediaWiki}}“ што ќе завршува со <code>.xsl</code>.", "apihelp-xml-param-includexmlnamespace": "Ако е укажано, додава именски простор XML.", "apihelp-xmlfm-description": "Давај го изводот во XML-формат (подобрен испис во HTML).", - "api-format-title": "Резултат од Извршникот на МедијаВики", + "api-format-title": "Исход од Извршникот на МедијаВики", "api-format-prettyprint-header": "Ова е HTML-претстава на форматот $1. HTML е добар за отстранување на грешки, но не е погоден за употреба во извршник.\n\nУкажете го параметарот <var>format</var> за да го смените изводниот формат. За да ги видите претставите на форматот $1 вон HTML, задајте <kbd>format=$2</kbd>.\n\nПовеќе информации ќе најдете на [[mw:API|целосната документација]], или пак [[Special:ApiHelp/main|помош со извршникот]].", "api-pageset-param-titles": "Список на наслови на кои ќе се работи", "api-pageset-param-pageids": "Список на назнаки за страници на кои ќе се работи", @@ -408,7 +414,7 @@ "api-help-param-token": "Шифра „$1“ добиена од [[Special:ApiHelp/query+tokens|action=query&meta=tokens]]", "api-help-param-token-webui": "За складност, се прифаќа и шифрата што се користи за обичниот кориснички посредник.", "api-help-param-disabled-in-miser-mode": "Исклучено поради [[mw:Manual:$wgMiserMode|скржавиот режим]].", - "api-help-param-limited-in-miser-mode": "<strong>Напомена:</strong> Бидејќи сте во [[mw:Manual:$wgMiserMode|скржав режим]], користејќи го ова може да добиете помалку од <var>$1limit</var> резултати пред да продолжите; во крајни случаи може да не добиете ниеден резултат.", + "api-help-param-limited-in-miser-mode": "<strong>Напомена:</strong> Бидејќи сте во [[mw:Manual:$wgMiserMode|скржав режим]], користејќи го ова може да добиете помалку од <var>$1limit</var> исходни ставки пред да продолжите; во крајни случаи може да не добиете ниедна ставка.", "api-help-param-direction": "Во која насока да се набројува:\n;понови:Прво најстарите. Напомена: $1start мора да биде пред $1end.\n;постари:Прво најновите (по основно). Напомена: $1start мора да биде подоцна од $1end.", "api-help-param-continue": "Употребете го ова за да продолжите кога има повеќе расположиви ставки.", "api-help-param-no-description": "<span class=\"apihelp-empty\">(нема опис)</span>", diff --git a/includes/api/i18n/nl.json b/includes/api/i18n/nl.json index 732991aedd83..a5291c795656 100644 --- a/includes/api/i18n/nl.json +++ b/includes/api/i18n/nl.json @@ -18,28 +18,47 @@ "Mainframe98" ] }, - "apihelp-main-description": "<div class=\"hlist plainlinks api-main-links\">\n* [[mw:API:Main_page|Documentatie]]\n* [[mw:API:FAQ|FAQ]]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api E-maillijst]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce API-aankondigingen]\n* [https://phabricator.wikimedia.org/maniphest/query/GebfyV4uCaLd/#R Bugs & verzoeken]\n</div>\n<strong>Status:</strong> Alle functies die op deze pagina worden weergegeven horen te werken. Aan de API wordt actief gewerkt, en deze kan gewijzigd worden. Abonneer u op de [https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ e-maillijst mediawiki-api-announce] voor meldingen over aanpassingen.\n\n<strong>Foutieve verzoeken:</strong> als de API foutieve verzoeken ontvangt, wordt er geantwoord met een HTTP-header met de sleutel \"MediaWiki-API-Error\" en daarna worden de waarde van de header en de foutcode op dezelfde waarde ingesteld. Zie [[mw:API:Errors_and_warnings|API: Errors and warnings]] voor meer informatie.\n\n<strong>Testen:</strong> u kunt [[Special:ApiSandbox|eenvoudig API-verzoeken testen]].", + "apihelp-main-description": "<div class=\"hlist plainlinks api-main-links\">\n* [[mw:Special:MyLanguage/API:Main_page|Documentatie]]\n* [[mw:Special:MyLanguage/API:FAQ|FAQ]]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api E-maillijst]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce API-aankondigingen]\n* [https://phabricator.wikimedia.org/maniphest/query/GebfyV4uCaLd/#R Bugs & verzoeken]\n</div>\n<strong>Status:</strong> Alle functies die op deze pagina worden weergegeven horen te werken. Aan de API wordt actief gewerkt, en deze kan gewijzigd worden. Abonneer u op de [https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ e-maillijst mediawiki-api-announce] voor meldingen over aanpassingen.\n\n<strong>Foutieve verzoeken:</strong> als de API foutieve verzoeken ontvangt, wordt er geantwoord met een HTTP-header met de sleutel \"MediaWiki-API-Error\" en daarna worden de waarde van de header en de foutcode op dezelfde waarde ingesteld. Zie [[mw:Special:MyLanguage/API:Errors_and_warnings|API: Foutmeldingen en waarschuwingen]] voor meer informatie.\n\n<strong>Testen:</strong> u kunt [[Special:ApiSandbox|eenvoudig API-verzoeken testen]].", "apihelp-main-param-action": "Welke handeling uit te voeren.", "apihelp-main-param-format": "De opmaak van de uitvoer.", - "apihelp-main-param-maxlag": "De maximale vertraging kan gebruikt worden als MediaWiki is geïnstalleerd op een databasecluster die gebruik maakt van replicatie. Om te voorkomen dat handelingen nog meer databasereplicatievertraging veroorzaken, kan deze parameter er voor zorgen dat de client wacht totdat de replicatievertraging lager is dan de aangegeven waarde. In het geval van buitensporige vertraging, wordt de foutcode <samp>maxlag</samp> teruggegeven met een bericht als <samp>Waiting for $host: $lag seconds lagged</samp>.<br />Zie [[mw:Manual:Maxlag_parameter|Handboek: Maxlag parameter]] voor mee informatie.", + "apihelp-main-param-maxlag": "De maximale vertraging kan gebruikt worden als MediaWiki is geïnstalleerd op een databasecluster die gebruik maakt van replicatie. Om te voorkomen dat handelingen nog meer databasereplicatievertraging veroorzaken, kan deze parameter er voor zorgen dat de client wacht totdat de replicatievertraging lager is dan de aangegeven waarde. In het geval van buitensporige vertraging, wordt de foutcode <samp>maxlag</samp> teruggegeven met een bericht als <samp>Waiting for $host: $lag seconds lagged</samp>.<br />Zie [[mw:Special:MyLanguage/Manual:Maxlag_parameter|Handleiding:Maxlag parameter]] voor meer informatie.", "apihelp-main-param-smaxage": "Stelt de <code>s-maxage</code> HTTP cache controle header in op het aangegeven aantal seconden. Foutmeldingen komen nooit in de cache.", "apihelp-main-param-maxage": "Stelt de <code>max-age</code> HTTP cache controle header in op het aangegeven aantal seconden. Foutmeldingen komen nooit in de cache.", "apihelp-main-param-assert": "Controleer of de gebruiker is aangemeld als <kbd>user</kbd> is meegegeven, en of de gebruiker het robotgebruikersrecht heeft als <kbd>bot</kbd> is meegegeven.", + "apihelp-main-param-assertuser": "Bevestig dat de huidige gebruiker de genoemde gebruiker is.", "apihelp-main-param-requestid": "Elke waarde die hier gegeven wordt, wordt aan het antwoord toegevoegd. Dit kan gebruikt worden om verzoeken te onderscheiden.", "apihelp-main-param-servedby": "Voeg de hostnaam van de server die de aanvraag heeft afgehandeld toe aan het antwoord.", "apihelp-main-param-curtimestamp": "Huidige tijd aan het antwoord toevoegen.", + "apihelp-main-param-responselanginfo": "Toon de talen gebruikt voor <var>uselang</var> en <var>errorlang</var> in het resultaat.", + "apihelp-main-param-errorlang": "De taal om te gebruiken voor waarschuwingen en fouten. <kbd>[[Special:ApiHelp/query+siteinfo|action=query&meta=siteinfo]]</kbd> met <kbd>siprop=languages</kbd> toont een lijst van taalcodes, of stel <kbd>inhoud</kbd> in om gebruik te maken van de inhoudstaal van deze wiki, of stel <kbd>uselang</kbd> in om gebruik te maken van dezelfde waarde als de <var>uselang</var> parameter.", + "apihelp-main-param-errorsuselocal": "Indien ingesteld maken foutmeldingen gebruik van lokaal-aangepaste berichten in de {{ns:MediaWiki}} naamruimte.", "apihelp-block-description": "Gebruiker blokkeren.", "apihelp-block-param-user": "Gebruikersnaam, IP-adres of IP-range om te blokkeren. Kan niet samen worden gebruikt me <var>$1userid</var>", "apihelp-block-param-userid": "Gebruikers-ID om te blokkeren. Kan niet worden gebruikt in combinatie met <var>$1user</var>.", + "apihelp-block-param-expiry": "Vervaldatum. Kan relatief zijn (bijv. <kbd>5 months</kbd> of <kbd>2 weeks</kbd>) of absoluut (<kbd>2014-09-18T12:34:56Z</kbd>). Indien ingesteld op <kbd>infinite</kbd>, <kbd>indefinite</kbd>, of <kbd>never</kbd> verloopt de blokkade nooit.", "apihelp-block-param-reason": "Reden voor blokkade.", "apihelp-block-param-anononly": "Alleen anonieme gebruikers blokkeren (uitschakelen van anonieme bewerkingen via dit IP-adres)", "apihelp-block-param-nocreate": "Voorkom registeren van accounts.", "apihelp-block-param-autoblock": "Blokkeer automatisch het laatst gebruikte IP-adres en ieder volgend IP-adres van waaruit ze proberen aan te melden.", + "apihelp-block-param-noemail": "Gebruiker weerhouden van het sturen van e-mail. (Vereist het <code>blockemail</code> recht).", "apihelp-block-param-hidename": "Verberg de gebruikersnaam uit het blokkeerlogboek. (Vereist het <code>hideuser</code> recht).", + "apihelp-block-param-allowusertalk": "De gebruiker toestaan om hun eigen overlegpagina te bewerken (afhankelijk van <var>[[mw:Special:MyLanguage/Manual:$wgBlockAllowsUTEdit|$wgBlockAllowsUTEdit]]</var>).", "apihelp-block-param-reblock": "De huidige blokkade aanpassen als de gebruiker al geblokkeerd is.", "apihelp-block-param-watchuser": "De gebruikerspagina en overlegpagina van de gebruiker of het IP-adres volgen.", + "apihelp-block-param-tags": "Wijzigingslabels om toe te passen op de regel in het blokkeerlogboek.", "apihelp-block-example-ip-simple": "Het IP-adres <kbd>192.0.2.5</kbd> voor drie dagen blokkeren met <kbd>First strike</kbd> als opgegeven reden.", + "apihelp-block-example-user-complex": "Blokkeer gebruiker<kbd>Vandal</kbd> voor altijd met reden <kbd>Vandalism</kbd> en voorkom het aanmaken van nieuwe accounts en het versturen van email", + "apihelp-changeauthenticationdata-example-password": "Poging tot het wachtwoord van de huidige gebruiker te veranderen naar <kbd>ExamplePassword</kbd>.", + "apihelp-checktoken-description": "Controleer de geldigheid van een token van <kbd>[[Special:ApiHelp/query+tokens|action=query&meta=tokens]]</kbd>.", "apihelp-checktoken-param-type": "Tokentype wordt getest.", + "apihelp-checktoken-param-token": "Token om te controleren.", + "apihelp-checktoken-param-maxtokenage": "Maximum levensduur van de token, in seconden.", + "apihelp-checktoken-example-simple": "Test de geldigheid van een <kbd>csrf</kbd> token.", + "apihelp-clearhasmsg-description": "Wist de <code>hasmsg</code> vlag voor de huidige gebruiker.", + "apihelp-clearhasmsg-example-1": "Wis de <code>hasmsg</code> vlag voor de huidige gebruiker.", + "apihelp-clientlogin-description": "Log in op de wiki met behulp van de interactieve flow.", + "apihelp-clientlogin-example-login": "Start het inlogproces op de wiki als gebruiker <kbd>Example</kbd> met wachtwoord <kbd>ExamplePassword</kbd>.", + "apihelp-compare-description": "Toon het verschil tussen 2 pagina's.\n\nEen versienummer, een paginatitel of een pagina-ID is vereist voor zowel de \"from\" en \"to\" parameter.", "apihelp-compare-param-fromtitle": "Eerste paginanaam om te vergelijken.", "apihelp-compare-param-fromid": "Eerste pagina-ID om te vergelijken.", "apihelp-compare-param-fromrev": "Eerste versie om te vergelijken.", @@ -47,15 +66,21 @@ "apihelp-compare-param-toid": "Tweede pagina-ID om te vergelijken.", "apihelp-compare-param-torev": "Tweede versie om te vergelijken.", "apihelp-createaccount-description": "Nieuwe gebruikersaccount aanmaken.", + "apihelp-createaccount-example-create": "Start het proces voor het aanmaken van de gebruiker <kbd>Example</kbd> met het wachtwoord <kbd>ExamplePassword</kbd>.", "apihelp-createaccount-param-name": "Gebruikersnaam.", + "apihelp-createaccount-param-password": "Wachtwoord (genegeerd als <var>$1mailpassword</var> is ingesteld).", + "apihelp-createaccount-param-domain": "Domein voor externe authentificatie (optioneel).", "apihelp-createaccount-param-email": "E-mailadres van de gebruiker (optioneel).", "apihelp-createaccount-param-realname": "Echte naam van de gebruiker (optioneel).", + "apihelp-createaccount-param-reason": "Optionele reden voor het aanmaken van het account voor in het logboek.", "apihelp-createaccount-param-language": "Taalcode om als standaard in te stellen voor de gebruiker (optioneel, standaard de inhoudstaal).", "apihelp-createaccount-example-pass": "Maak gebruiker <kbd>testuser</kbd> aan met wachtwoord <kbd>test123</kbd>.", "apihelp-createaccount-example-mail": "Maak gebruiker <kbd>testmailuser</kbd> aan en e-mail een willekeurig gegenereerd wachtwoord.", "apihelp-delete-description": "Een pagina verwijderen.", + "apihelp-delete-param-title": "Titel van de pagina om te verwijderen. Kan niet samen worden gebruikt met <var>$1pageid</var>.", "apihelp-delete-param-pageid": "ID van de pagina om te verwijderen. Kan niet samen worden gebruikt met <var>$1title</var>.", "apihelp-delete-param-reason": "Reden voor verwijdering. Wanneer dit niet is opgegeven wordt een automatisch gegenereerde reden gebruikt.", + "apihelp-delete-param-tags": "Wijzigingslabels om toe te passen op de regel in het verwijderlogboek.", "apihelp-delete-param-watch": "De pagina aan de volglijst van de huidige gebruiker toevoegen.", "apihelp-delete-param-unwatch": "De pagina van de volglijst van de huidige gebruiker verwijderen.", "apihelp-delete-example-simple": "Verwijder <kbd>Main Page</kbd>.", @@ -74,14 +99,23 @@ "apihelp-edit-param-nocreate": "Een foutmelding geven als de pagina niet bestaat.", "apihelp-edit-param-watch": "Voeg de pagina toe aan de volglijst van de huidige gebruiker.", "apihelp-edit-param-unwatch": "Verwijder de pagina van de volglijst van de huidige gebruiker.", + "apihelp-edit-param-md5": "De MD5-hash van de $1text parameter, of de $1prependtext en $1appendtext parameters samengevoegd. Indien ingesteld, wordt de bewerking niet gemaakt, tenzij de hash juist is.", + "apihelp-edit-param-prependtext": "Voeg deze tekst toe aan het begin van de pagina. Overschrijft $1text.", + "apihelp-edit-param-appendtext": "Voeg deze tekst toe aan het begin van de pagina. Overschrijft $1text.\n\nGebruik $1section=new in plaats van deze parameter om een nieuw kopje toe te voegen.", + "apihelp-edit-param-undo": "Maak deze versie ongedaan. Overschrijft $1text, $1prependtext en $1appendtext.", + "apihelp-edit-param-undoafter": "Maak alle versies vanaf $1undo to deze ongedaan maken. Indien niet ingesteld wordt slechts één versie ongedaan gemaakt.", "apihelp-edit-param-redirect": "Doorverwijzingen automatisch oplossen.", + "apihelp-edit-param-contentmodel": "Inhoudsmodel van de nieuwe inhoud.", + "apihelp-edit-param-token": "Het token moet altijd worden verzonden als de laatste parameter, of tenminste na de $1text parameter.", "apihelp-edit-example-edit": "Een pagina bewerken.", + "apihelp-edit-example-prepend": "Voeg <kbd>__NOTOC__</kbd> toe aan het begin van een pagina.", "apihelp-edit-example-undo": "Versies 13579 tot 13585 ongedaan maken met automatische beschrijving.", "apihelp-emailuser-description": "Gebruiker e-mailen.", "apihelp-emailuser-param-target": "Gebruiker naar wie de e-mail moet worden gestuurd.", "apihelp-emailuser-param-subject": "Onderwerpkoptekst.", "apihelp-emailuser-param-text": "E-mailtekst.", "apihelp-emailuser-param-ccme": "Mij een kopie sturen van deze e-mail.", + "apihelp-emailuser-example-email": "Stuur een e-mail naar de gebruiker <kbd>WikiSysop</kbd> met de tekst <kbd>Inhoud</kbd>.", "apihelp-expandtemplates-param-title": "Paginanaam.", "apihelp-expandtemplates-param-text": "Wikitekst om om te zetten.", "apihelp-expandtemplates-paramvalue-prop-wikitext": "De uitgevulde wikitekst.", @@ -92,8 +126,15 @@ "apihelp-feedcontributions-param-year": "Van jaar (en eerder).", "apihelp-feedcontributions-param-month": "Van maand (en eerder).", "apihelp-feedcontributions-param-deletedonly": "Alleen verwijderde bijdragen weergeven.", + "apihelp-feedcontributions-param-toponly": "Alleen bewerkingen die de nieuwste versies zijn weergeven.", + "apihelp-feedcontributions-param-newonly": "Alleen bewerkingen die nieuwe pagina's aanmaken weergeven.", "apihelp-feedcontributions-param-hideminor": "Verberg kleine bewerkingen.", + "apihelp-feedcontributions-param-showsizediff": "Toon het verschil in grootte tussen versies.", + "apihelp-feedcontributions-example-simple": "Toon bijdragen voor gebruiker <kbd>Example</kbd>.", + "apihelp-feedrecentchanges-param-feedformat": "De indeling van de feed.", + "apihelp-feedrecentchanges-param-namespace": "Naamruimte om de resultaten tot te beperken.", "apihelp-feedrecentchanges-param-invert": "Alle naamruimten behalve de geselecteerde.", + "apihelp-feedrecentchanges-param-days": "Aantal dagen om de resultaten tot te beperken.", "apihelp-feedrecentchanges-param-limit": "Het maximaal aantal weer te geven resultaten.", "apihelp-feedrecentchanges-param-hideminor": "Kleine wijzigingen verbergen.", "apihelp-feedrecentchanges-param-hidebots": "Wijzigingen gedaan door bots verbergen.", @@ -105,13 +146,29 @@ "apihelp-feedrecentchanges-param-tagfilter": "Filteren op label.", "apihelp-feedrecentchanges-example-simple": "Recente wijzigingen weergeven.", "apihelp-feedrecentchanges-example-30days": "Recente wijzigingen van de afgelopen 30 dagen weergeven.", + "apihelp-feedwatchlist-param-feedformat": "De indeling van de feed.", "apihelp-filerevert-description": "Een oude versie van een bestand terugplaatsen.", + "apihelp-filerevert-param-filename": "Doel bestandsnaam, zonder het Bestand: voorvoegsel.", + "apihelp-filerevert-param-comment": "Opmerking voor het uploaden.", + "apihelp-filerevert-example-revert": "Zet <kbd>Wiki.png</kbd> terug naar de versie van <kbd>2011-03-05T15:27:40Z</kbd>.", + "apihelp-help-description": "Toon help voor de opgegeven modules.", + "apihelp-help-param-helpformat": "Indeling van de help uitvoer.", + "apihelp-help-example-main": "Hulp voor de hoofdmodule.", + "apihelp-help-example-submodules": "Hulp voor <kbd>action=query</kbd> en alle submodules.", "apihelp-help-example-recursive": "Alle hulp op een pagina.", "apihelp-help-example-help": "Help voor de help-module zelf.", "apihelp-imagerotate-description": "Een of meerdere afbeeldingen draaien.", + "apihelp-imagerotate-param-rotation": "Aantal graden om de afbeelding met de klok mee te draaien.", + "apihelp-imagerotate-param-tags": "Labels om toe te voegen aan de regel in het uploadlogboek.", + "apihelp-imagerotate-example-simple": "Roteer <kbd>File:Example.png</kbd> met <kbd>90</kbd> graden.", + "apihelp-imagerotate-example-generator": "Roteer alle afbeeldingen in <kbd>Category:Flip</kbd> met <kbd>180</kbd> graden.", + "apihelp-import-description": "Importeer een pagina van een andere wiki, of van een XML bestand.\n\nMerk op dat de HTTP POST moet worden uitgevoerd als bestandsupload (bijv. door middel van multipart/form-data) wanneer een bestand wordt verstuurd voor de <var>xml</var> parameter.", + "apihelp-import-param-summary": "Importsamenvatting voor het logboek.", "apihelp-import-param-xml": "Geüpload XML-bestand.", + "apihelp-import-param-interwikisource": "Voor interwiki imports: wiki om van te importeren.", "apihelp-import-param-namespace": "Importeren in deze naamruimte. Can niet samen gebruikt worden met <var>$1rootpage</var>.", "apihelp-import-param-rootpage": "Importeren als subpagina van deze pagina. Kan niet samen met <var>$1namespace</var> gebruikt worden.", + "apihelp-import-example-import": "Importeer [[meta:Help:ParserFunctions]] in naamruimte 100 met de volledige geschiedenis.", "apihelp-login-param-name": "Gebruikersnaam.", "apihelp-login-param-password": "Wachtwoord.", "apihelp-login-param-domain": "Domein (optioneel).", @@ -119,9 +176,15 @@ "apihelp-logout-description": "Afmelden en sessiegegevens wissen.", "apihelp-logout-example-logout": "Meldt de huidige gebruiker af.", "apihelp-managetags-param-tag": "Label om aan te maken, te activeren of te deactiveren. Voor het aanmaken van een label, mag het niet bestaan. Voor het verwijderen van een label, moet het bestaan. Voor het activeren van een label, moet het bestaan en mag het niet gebruikt worden door een uitbreiding. Voor het deactiveren van een label, moet het gebruikt worden en handmatig gedefinieerd zijn.", + "apihelp-managetags-example-create": "Maak een label met de naam <kbd>spam</kbd> aan met als reden <kbd>For use in edit patrolling</kbd>", + "apihelp-managetags-example-delete": "Verwijder het <kbd>vandlaism</kbd> label met de reden <kbd>Misspelt</kbd>", + "apihelp-mergehistory-description": "Geschiedenis van pagina's samenvoegen.", + "apihelp-mergehistory-param-reason": "Reden voor samenvoegen van de geschiedenis.", + "apihelp-mergehistory-example-merge": "Voeg de hele geschiedenis van <kbd>Oldpage</kbd> samen met <kbd>Newpage</kbd>.", "apihelp-move-description": "Pagina hernoemen.", "apihelp-move-param-to": "Nieuwe paginanaam.", "apihelp-move-param-reason": "Reden voor de naamswijziging.", + "apihelp-move-param-movetalk": "Hernoem de overlegpagina, indien deze bestaat.", "apihelp-move-param-noredirect": "Geen doorverwijzing achterlaten.", "apihelp-move-param-watch": "Pagina en de omleiding toevoegen aan de volglijst van de huidige gebruiker.", "apihelp-move-param-unwatch": "Verwijder de pagina en de doorverwijzing van de volglijst van de huidige gebruiker.", @@ -145,6 +208,7 @@ "apihelp-options-param-optionvalue": "De waarde voor de optie opgegeven door <var>$1optionname</var>.", "apihelp-options-example-reset": "Alle voorkeuren opnieuw instellen.", "apihelp-options-example-change": "Voorkeuren wijzigen voor <kbd>skin</kbd> en <kbd>hideminor</kbd>.", + "apihelp-paraminfo-description": "Verkrijg informatie over API-modules.", "apihelp-parse-paramvalue-prop-categorieshtml": "Vraagt een HTML-versie van de categorieën op.", "apihelp-parse-example-page": "Een pagina verwerken.", "apihelp-parse-example-text": "Wikitext verwerken.", @@ -154,28 +218,124 @@ "apihelp-patrol-example-revid": "Een versie markeren als gecontroleerd.", "apihelp-protect-param-reason": "Reden voor opheffen van de beveiliging.", "apihelp-protect-example-protect": "Een pagina beveiligen", + "apihelp-purge-param-forcelinkupdate": "Werk de koppelingstabellen bij.", + "apihelp-purge-param-forcerecursivelinkupdate": "Werk de koppelingentabel bij, en werk de koppelingstabellen bij voor alle pagina's die gebruik maken van deze pagina als sjabloon.", + "apihelp-query+allcategories-param-dir": "Richting om in te sorteren.", + "apihelp-query+allcategories-param-limit": "Hoeveel categorieën te tonen.", + "apihelp-query+allcategories-paramvalue-prop-size": "Voegt het aantal pagina's in de categorie toe.", + "apihelp-query+allcategories-paramvalue-prop-hidden": "Markeert categorieën die verborgen zijn met <code>__HIDDENCAT__</code>", "apihelp-query+alldeletedrevisions-param-tag": "Alleen versies weergeven met dit label.", + "apihelp-query+alldeletedrevisions-param-excludeuser": "Toon geen versies door deze gebruiker.", + "apihelp-query+alldeletedrevisions-param-namespace": "Toon alleen pagina's in deze naamruimte.", + "apihelp-query+allfileusages-paramvalue-prop-title": "Voegt de titel van het bestand toe.", + "apihelp-query+allfileusages-param-limit": "Hoeveel items er in totaal moeten worden getoond.", + "apihelp-query+allimages-example-recent": "Toon een lijst van recentlijk geüploade bestanden, vergelijkbaar met [[Special:NewFiles]].", + "apihelp-query+alllinks-param-namespace": "De naamruimte om door te lopen.", + "apihelp-query+alllinks-param-limit": "Hoeveel items er in totaal moeten worden getoond.", "apihelp-query+allmessages-param-enableparser": "Stel in om de parser in te schakelen, zorgt voor het voorverwerken van de wikitekst van een bericht (vervangen van magische woorden, de afhandeling van sjablonen, enzovoort).", + "apihelp-query+allmessages-param-lang": "Toon berichten in deze taal.", + "apihelp-query+allmessages-param-from": "Toon berichten vanaf dit bericht.", + "apihelp-query+allmessages-param-to": "Toon berichten tot aan dit bericht.", + "apihelp-query+allredirects-description": "Toon alle doorverwijzingen naar een naamruimte.", + "apihelp-query+allrevisions-example-user": "Toon de laatste 50 bijdragen van de gebruiker <kbd>Example</kbd>.", "apihelp-query+mystashedfiles-paramvalue-prop-type": "Vraag het MIME- en mediatype van het bestand op.", + "apihelp-query+mystashedfiles-param-limit": "Hoeveel bestanden te tonen.", + "apihelp-query+allusers-param-excludegroup": "Sluit gebruikers in de gegeven groepen uit.", + "apihelp-query+allusers-paramvalue-prop-blockinfo": "Voegt informatie over een actuale blokkade van de gebruiker toe.", + "apihelp-query+allusers-paramvalue-prop-groups": "Toont de groepen waar de gebruiker in zit. Dit gebruikt meer serverbronnen en kan minder resultaten teruggeven dat de opgegeven limiet.", + "apihelp-query+allusers-paramvalue-prop-implicitgroups": "Toon alle groepen de gebruiker automatisch in zit.", + "apihelp-query+allusers-paramvalue-prop-rights": "Toon de rechten die de gebruiker heeft.", + "apihelp-query+allusers-paramvalue-prop-editcount": "Voegt het aantal bewerkingen van de gebruiker toe.", + "apihelp-query+allusers-paramvalue-prop-registration": "Voegt de registratiedatum van de gebruiker toe, indien beschikbaar (kan leeg zijn).", + "apihelp-query+allusers-param-witheditsonly": "Toon alleen gebruikers die bewerkingen hebben gemaakt.", + "apihelp-query+allusers-param-activeusers": "Toon alleen gebruikers die actief zijn geweest in de laatste $1 {{PLURAL:$1|dag|dagen}}.", + "apihelp-query+allusers-example-Y": "Toon gebruikers vanaf <kbd>Y</kbd>.", + "apihelp-query+authmanagerinfo-description": "Haal informatie op over de huidige authentificatie status.", + "apihelp-query+backlinks-description": "Vind alle pagina's die verwijzen naar de gegeven pagina.", + "apihelp-query+backlinks-param-title": "Titel om op te zoeken. Kan niet worden gebruikt in combinatie met<var>$1pageid</var>.", + "apihelp-query+backlinks-param-pageid": "Pagina ID om op te zoeken. Kan niet worden gebruikt in combinatie met <var>$1title</var>.", + "apihelp-query+backlinks-param-namespace": "De naamruimte om door te lopen.", + "apihelp-query+backlinks-example-simple": "Toon verwijzingen naar de <kbd>Hoofdpagina</kbd>.", + "apihelp-query+blocks-description": "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-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.", + "apihelp-query+blocks-example-users": "Toon blokkades van gebruikers <kbd>Alice</kbd> en <kbd>Bob</kbd>.", + "apihelp-query+categories-description": "Toon alle categorieën waar de pagina in zit.", + "apihelp-query+categories-paramvalue-prop-hidden": "Markeert categorieën die verborgen zijn met <code>__HIDDENCAT__</code>", + "apihelp-query+categories-param-show": "Welke soort categorieën te tonen.", + "apihelp-query+categories-param-limit": "Hoeveel categorieën te tonen.", + "apihelp-query+categorymembers-paramvalue-prop-ids": "Voegt de pagina-ID toe.", + "apihelp-query+categorymembers-paramvalue-prop-title": "Voegt de titel en de naamruimte-ID van de pagina toe.", + "apihelp-query+categorymembers-param-dir": "Richting om in te sorteren.", "apihelp-query+deletedrevisions-param-tag": "Alleen versies weergeven met dit label.", "apihelp-query+deletedrevs-param-tag": "Alleen versies weergeven met dit label.", + "apihelp-query+embeddedin-param-namespace": "De naamruimte om door te lopen.", + "apihelp-query+fileusage-paramvalue-prop-pageid": "Pagina ID van elke pagina.", + "apihelp-query+fileusage-paramvalue-prop-title": "Titel van elke pagina.", + "apihelp-query+imageusage-param-namespace": "De naamruimte om door te lopen.", + "apihelp-query+imageusage-example-simple": "Toon pagina's die gebruik maken van [[:File:Albert Einstein Head.jpg]].", + "apihelp-query+imageusage-example-generator": "Toon informatie over pagina's die gebruik maken van [[:File:Albert Einstein Head.jpg]].", + "apihelp-query+iwbacklinks-param-prefix": "Voorvoegsel voor de interwiki.", "apihelp-query+logevents-param-type": "Logboekregels alleen voor dit type filteren.", "apihelp-query+logevents-param-tag": "Alleen logboekregels weergeven met dit label.", "apihelp-query+logevents-example-simple": "Recente logboekregels weergeven.", + "apihelp-query+protectedtitles-paramvalue-prop-level": "Voegt het beveiligingsniveau toe.", + "apihelp-query+protectedtitles-example-simple": "Toon beveiligde titels.", + "apihelp-query+querypage-param-limit": "Aantal resultaten om te tonen.", + "apihelp-query+querypage-example-ancientpages": "Toon resultaten van [[Special:Ancientpages]].", + "apihelp-query+random-param-namespace": "Toon alleen pagina's in deze naamruimten.", + "apihelp-query+random-param-limit": "Beperk het aantal aan willekeurige pagina's dat wordt getoond.", + "apihelp-query+random-example-simple": "Toon twee willekeurige pagina's uit de hoofdnaamruimte.", + "apihelp-query+random-example-generator": "Toon pagina informatie over twee willekeurige pagina's uit de hoofdnaamruimte.", + "apihelp-query+recentchanges-param-user": "Toon alleen wijzigingen door deze gebruiker.", + "apihelp-query+recentchanges-param-excludeuser": "Toon geen wijzigingen door deze gebruiker", "apihelp-query+recentchanges-param-tag": "Alleen versies weergeven met dit label.", + "apihelp-query+recentchanges-paramvalue-prop-comment": "Voegt de bewerkingssamenvatting voor de bewerking toe.", "apihelp-query+recentchanges-paramvalue-prop-loginfo": "Voegt logboekgegevens toe aan logboekregels (logboek-ID, logboektype, enzovoort).", + "apihelp-query+recentchanges-example-simple": "Toon recente wijzigingen.", + "apihelp-query+redirects-paramvalue-prop-pageid": "Pagina ID van elke doorverwijzing.", + "apihelp-query+redirects-paramvalue-prop-title": "Titel van elke doorverwijzing.", + "apihelp-query+redirects-param-namespace": "Toon alleen pagina's in deze naamruimten.", + "apihelp-query+redirects-param-limit": "Hoeveel doorverwijzingen te tonen.", + "apihelp-query+redirects-example-simple": "Toon een lijst van doorverwijzingen naar [[Main Page]].", + "apihelp-query+redirects-example-generator": "Toon informatie over alle doorverwijzingen naar [[Main Page]].", "apihelp-query+revisions-param-tag": "Alleen versies weergeven met dit label.", + "apihelp-query+revisions+base-paramvalue-prop-content": "Versietekst.", "apihelp-query+revisions+base-paramvalue-prop-tags": "Labels voor de versie.", "apihelp-query+revisions+base-param-difftotextpst": "\"pre-save\"-transformatie uitvoeren op de tekst alvorens de verschillen te bepalen. Alleen geldig als dit wordt gebruikt met <var>$1difftotext</var>.", + "apihelp-query+search-description": "Voer een volledige tekst zoekopdracht uit.", + "apihelp-query+search-param-limit": "Hoeveel pagina's te tonen.", + "apihelp-query+search-example-simple": "Zoeken naar <kbd>betekenis</kbd>.", + "apihelp-query+siteinfo-paramvalue-prop-namespacealiases": "Toon geregistreerde naamruimte aliassen.", + "apihelp-query+siteinfo-paramvalue-prop-specialpagealiases": "Toon speciale pagina aliassen.", + "apihelp-query+siteinfo-paramvalue-prop-magicwords": "Toon magische woorden en hun aliassen.", + "apihelp-query+siteinfo-paramvalue-prop-statistics": "Toon site statistieken.", + "apihelp-query+siteinfo-paramvalue-prop-libraries": "Toont bibliotheken die op de wiki zijn geïnstalleerd.", + "apihelp-query+siteinfo-paramvalue-prop-extensions": "Toont uitbreidingen die op de wiki zijn geïnstalleerd.", "apihelp-query+siteinfo-paramvalue-prop-fileextensions": "Geeft een lijst met bestandsextensies (bestandstypen) die geüpload mogen worden.", + "apihelp-query+siteinfo-paramvalue-prop-rightsinfo": "Toont wiki rechten (licentie) informatie als deze beschikbaar is.", "apihelp-query+tags-description": "Wijzigingslabels weergeven.", "apihelp-query+tags-paramvalue-prop-name": "Voegt de naam van het label toe.", "apihelp-query+tags-paramvalue-prop-displayname": "Voegt het systeembericht toe voor het label.", "apihelp-query+tags-paramvalue-prop-description": "Voegt beschrijving van het label toe.", "apihelp-query+tags-paramvalue-prop-defined": "Geeft aan of het label is gedefinieerd.", "apihelp-query+tags-paramvalue-prop-active": "Of het label nog steeds wordt toegepast.", + "apihelp-query+tags-example-simple": "Toon beschikbare labels.", + "apihelp-query+templates-description": "Toon alle pagina's ingesloten op de gegeven pagina's.", + "apihelp-query+templates-param-limit": "Het aantal sjablonen om te tonen.", + "apihelp-query+transcludedin-paramvalue-prop-pageid": "Pagina ID van elke pagina.", + "apihelp-query+transcludedin-paramvalue-prop-title": "Titel van elke pagina.", + "apihelp-query+usercontribs-description": "Toon alle bewerkingen door een gebruiker.", + "apihelp-query+usercontribs-param-limit": "Het maximum aantal bewerkingen om te tonen.", + "apihelp-query+usercontribs-param-namespace": "Toon alleen bijdragen in deze naamruimten.", "apihelp-query+usercontribs-param-tag": "Alleen versies weergeven met dit label.", + "apihelp-query+usercontribs-example-ipprefix": "Toon bijdragen van alle IP-adressen met het voorvoegsel <kbd>192.0.2.</kbd>.", + "apihelp-query+userinfo-description": "Toon informatie over de huidige gebruiker.", + "apihelp-query+userinfo-paramvalue-prop-realname": "Toon de gebruikers echte naam.", "apihelp-query+watchlist-paramvalue-prop-loginfo": "Voegt logboekgegevens toe waar van toepassing.", "apihelp-query+watchlist-param-type": "Welke typen wijzigingen weer te geven:", "apihelp-query+watchlist-paramvalue-type-edit": "Gewone paginabewerkingen.", @@ -184,7 +344,7 @@ "apihelp-query+watchlist-paramvalue-type-log": "Logboekregels.", "apihelp-query+watchlist-paramvalue-type-categorize": "Wijzigingen in categorielidmaatschap.", "apihelp-stashedit-param-text": "Pagina-inhoud.", - "apihelp-unblock-param-user": "Gebruikersnaam, IP-adres of IP-range om te deblokkeren. Kan niet samen worden gebruikt met <var>$1id</var> of <var>$luserid</var>.", + "apihelp-unblock-param-user": "Gebruikersnaam, IP-adres of IP-range om te deblokkeren. Kan niet samen worden gebruikt met <var>$1id</var> of <var>$1userid</var>.", "apihelp-unblock-param-userid": "Gebruikers-ID om te deblokkeren. Kan niet worden gebruikt in combinatie met <var>$1id</var> of <var>$1user</var>.", "apihelp-json-param-formatversion": "Uitvoeropmaak:\n;1:Achterwaarts compatibele opmaak (XML-stijl booleans, <samp>*</samp>-sleutels voor contentnodes, enzovoort).\n;2:Experimentele moderne opmaak. Details kunnen wijzigen!\n;latest:Gebruik de meest recente opmaak (op het moment <kbd>2</kbd>), kan zonder waarschuwing wijzigen.", "apihelp-php-param-formatversion": "Uitvoeropmaak:\n;1:Achterwaarts compatibele opmaak (XML-stijl booleans, <samp>*</samp>-sleutels voor contentnodes, enzovoort).\n;2:Experimentele moderne opmaak. Details kunnen wijzigen!\n;latest:Gebruik de meest recente opmaak (op het moment <kbd>2</kbd>), kan zonder waarschuwing wijzigen.", diff --git a/includes/api/i18n/oc.json b/includes/api/i18n/oc.json index f03af9027adb..8d1f4a52a6f9 100644 --- a/includes/api/i18n/oc.json +++ b/includes/api/i18n/oc.json @@ -119,7 +119,7 @@ "apierror-noimageredirect-anon": "Leis utilizaires anonims pòdon pas crear de redireccions d'imatge.", "apierror-noimageredirect": "Avètz pas lei drechs necessaris per crear de redireccions d'imatge.", "apierror-nosuchsection": "I a ges seccion $1", - "apierror-nosuchsection-what": "I a ges seccion $1 dins $1", + "apierror-nosuchsection-what": "I a pas de seccion $1 dins $2.", "apierror-permissiondenied-generic": "Autorizacion refusada.", "apierror-unknownerror-nocode": "Error desconeguda.", "apierror-unknownerror": "Error desconeguda : $1", diff --git a/includes/api/i18n/pl.json b/includes/api/i18n/pl.json index 7369ee159837..a33945924926 100644 --- a/includes/api/i18n/pl.json +++ b/includes/api/i18n/pl.json @@ -37,7 +37,7 @@ "apihelp-block-param-autoblock": "Zablokuj ostatni adres IP tego użytkownika i automatycznie wszystkie kolejne, z których będzie się logował.", "apihelp-block-param-noemail": "Uniemożliwia użytkownikowi wysyłanie wiadomości e-mail za pośrednictwem interfejsu wiki. (Wymagane uprawnienie <code>blockemail</code>).", "apihelp-block-param-hidename": "Ukryj nazwę użytkownika z rejestru blokad. (Wymagane uprawnienia <code>hideuser</code>)", - "apihelp-block-param-allowusertalk": "Pozwala użytkownikowi edytować własną stronę dyskusji (zależy od <var>[[mw:Manual:$wgBlockAllowsUTEdit|$wgBlockAllowsUTEdit]]</var>).", + "apihelp-block-param-allowusertalk": "Pozwala użytkownikowi edytować własną stronę dyskusji (zależy od <var>[[mw:Special:MyLanguage/Manual:$wgBlockAllowsUTEdit|$wgBlockAllowsUTEdit]]</var>).", "apihelp-block-param-reblock": "Jeżeli ten użytkownik jest już zablokowany, nadpisz blokadę.", "apihelp-block-param-watchuser": "Obserwuj stronę użytkownika i jego IP oraz ich strony dyskusji.", "apihelp-block-example-ip-simple": "Zablokuj IP <kbd>192.0.2.5</kbd> na 3 dni z powodem <kbd>First strike</kbd>.", @@ -207,6 +207,7 @@ "apihelp-mergehistory-param-reason": "Powód łączenia historii.", "apihelp-mergehistory-example-merge": "Połącz całą historię strony <kbd>Oldpage</kbd> ze stroną <kbd>Newpage</kbd>.", "apihelp-move-description": "Przenieś stronę.", + "apihelp-move-param-from": "Tytuł strony do zmiany nazwy. Nie można używać razem z <var>$1fromid</var>.", "apihelp-move-param-to": "Tytuł na jaki zmienić nazwę strony.", "apihelp-move-param-reason": "Powód zmiany nazwy.", "apihelp-move-param-movetalk": "Zmień nazwę strony dyskusji, jeśli istnieje.", @@ -220,7 +221,7 @@ "apihelp-opensearch-param-search": "Wyszukaj tekst.", "apihelp-opensearch-param-limit": "Maksymalna liczba zwracanych wyników.", "apihelp-opensearch-param-namespace": "Przestrzenie nazw do przeszukania.", - "apihelp-opensearch-param-suggest": "Nie działa jeżeli <var>[[mw:Manual:$wgEnableOpenSearchSuggest|$wgEnableOpenSearchSuggest]]</var> ustawiono na false.", + "apihelp-opensearch-param-suggest": "Nic nie rób, jeżeli <var>[[mw:Special:MyLanguage/Manual:$wgEnableOpenSearchSuggest|$wgEnableOpenSearchSuggest]]</var> ustawiono na false.", "apihelp-opensearch-param-redirects": "Jak obsługiwać przekierowania:\n;return:Zwróć samo przekierowanie.\n;resolve:Zwróć stronę docelową. Może zwrócić mniej niż wyników określonych w $1limit.\nZ powodów historycznych, domyślnie jest to \"return\" dla $1format=json, a \"resolve\" dla innych formatów.", "apihelp-opensearch-param-format": "Format danych wyjściowych.", "apihelp-opensearch-param-warningsaserror": "Jeżeli pojawią się ostrzeżenia związane z <kbd>format=json</kbd>, zwróć błąd API zamiast ignorowania ich.", @@ -249,6 +250,7 @@ "apihelp-parse-paramvalue-prop-images": "Zdjęcia z przetworzonego wikitekstu.", "apihelp-parse-paramvalue-prop-externallinks": "Linki zewnętrzne z przetworzonego wikitekstu.", "apihelp-parse-paramvalue-prop-sections": "Sekcje z przetworzonego wikitekstu.", + "apihelp-parse-paramvalue-prop-displaytitle": "Dodaje tytuł parsowanego wikitekstu.", "apihelp-parse-paramvalue-prop-wikitext": "Zwróć oryginalny wikitext, który został przeanalizowany.", "apihelp-parse-param-preview": "Analizuj w trybie podglądu.", "apihelp-parse-param-disabletoc": "Pomiń spis treści na wyjściu.", @@ -266,7 +268,7 @@ "apihelp-protect-example-protect": "Zabezpiecz stronę", "apihelp-protect-example-unprotect": "Odbezpiecz stronę ustawiając ograniczenia na <kbd>all</kbd> (czyli każdy może wykonać działanie).", "apihelp-protect-example-unprotect2": "Odbezpiecz stronę ustawiając brak ograniczeń.", - "apihelp-purge-description": "Wyczyść pamięć podręczną dla stron o podanych tytułach.\n\nWymaga wysłania jako żądanie POST jeżeli użytkownik jest niezalogowany.", + "apihelp-purge-description": "Wyczyść pamięć podręczną dla stron o podanych tytułach.", "apihelp-purge-param-forcelinkupdate": "Uaktualnij tabele linków.", "apihelp-purge-param-forcerecursivelinkupdate": "Uaktualnij tabele linków włącznie z linkami dotyczącymi każdej strony wykorzystywanej jako szablon na tej stronie.", "apihelp-purge-example-simple": "Wyczyść strony <kbd>Main Page</kbd> i <kbd>API</kbd>.", @@ -344,6 +346,7 @@ "apihelp-query+allrevisions-example-ns-main": "Wyświetl pierwsze 50 wersji w przestrzeni głównej.", "apihelp-query+mystashedfiles-param-limit": "Liczba plików do pobrania.", "apihelp-query+alltransclusions-param-prop": "Jakie informacje dołączyć:", + "apihelp-query+alltransclusions-paramvalue-prop-title": "Dodaje tytuł osadzenia.", "apihelp-query+alltransclusions-param-namespace": "Przestrzeń nazw do emulacji.", "apihelp-query+alltransclusions-param-limit": "Łączna liczba elementów do zwrócenia.", "apihelp-query+allusers-param-from": "Nazwa użytkownika, od którego rozpocząć wyliczanie.", @@ -360,14 +363,23 @@ "apihelp-query+backlinks-description": "Znajdź wszystkie strony, które linkują do danej strony.", "apihelp-query+backlinks-param-namespace": "Przestrzeń nazw do emulacji.", "apihelp-query+backlinks-example-simple": "Pokazuj linki do <kbd>Main page</kbd>.", + "apihelp-query+blocks-description": "Lista wszystkich zablokowanych użytkowników i adresów IP.", "apihelp-query+blocks-param-start": "Znacznik czasu, od którego rozpocząć wyliczanie.", "apihelp-query+blocks-param-end": "Znacznik czasu, na którym zakończyć wyliczanie.", "apihelp-query+blocks-param-ids": "Lista zablokowanych ID do wylistowania (opcjonalne).", "apihelp-query+blocks-param-users": "Lista użytkowników do wyszukania (opcjonalne).", "apihelp-query+blocks-param-limit": "Maksymalna liczba blokad do wylistowania.", + "apihelp-query+blocks-paramvalue-prop-id": "Dodaje identyfikator blokady.", + "apihelp-query+blocks-paramvalue-prop-user": "Dodaje nazwę zablokowanego użytkownika.", + "apihelp-query+blocks-paramvalue-prop-userid": "Dodaje identyfikator zablokowanego użytkownika.", + "apihelp-query+blocks-paramvalue-prop-timestamp": "Dodaje znacznik czasu założenia blokady.", + "apihelp-query+blocks-paramvalue-prop-expiry": "Dodaje znacznik czasu wygaśnięcia blokady.", "apihelp-query+blocks-paramvalue-prop-reason": "Dodaje powód zablokowania.", + "apihelp-query+blocks-paramvalue-prop-range": "Dodaje zakres adresów IP, na który zastosowano blokadę.", "apihelp-query+blocks-example-simple": "Listuj blokady.", + "apihelp-query+categories-paramvalue-prop-timestamp": "Dodaje znacznik czasu dodania kategorii.", "apihelp-query+categories-param-limit": "Liczba kategorii do zwrócenia.", + "apihelp-query+categoryinfo-description": "Zwraca informacje o danych kategoriach.", "apihelp-query+categorymembers-description": "Wszystkie strony w danej kategorii.", "apihelp-query+categorymembers-param-title": "Kategoria, której zawartość wymienić (wymagane). Musi zawierać prefiks <kbd>{{ns:category}}:</kbd>. Nie może być używany równocześnie z <var>$1pageid</var>.", "apihelp-query+categorymembers-param-pageid": "ID strony kategorii, z której wymienić strony. Nie może być użyty równocześnie z <var>$1title</var>.", @@ -404,6 +416,7 @@ "apihelp-query+exturlusage-param-limit": "Liczba stron do zwrócenia.", "apihelp-query+filearchive-paramvalue-prop-dimensions": "Alias rozmiaru.", "apihelp-query+filearchive-paramvalue-prop-description": "Dodaje opis wersji obrazka.", + "apihelp-query+filearchive-paramvalue-prop-mime": "Dodaje typ MIME obrazka.", "apihelp-query+filearchive-example-simple": "Pokaż listę wszystkich usuniętych plików.", "apihelp-query+filerepoinfo-example-simple": "Uzyskaj informacje na temat repozytoriów plików.", "apihelp-query+fileusage-description": "Znajdź wszystkie strony, które używają danych plików.", @@ -425,12 +438,15 @@ "apihelp-query+iwbacklinks-param-limit": "Łączna liczba stron do zwrócenia.", "apihelp-query+iwbacklinks-paramvalue-prop-iwprefix": "Dodaje prefiks interwiki.", "apihelp-query+iwbacklinks-paramvalue-prop-iwtitle": "Dodaje tytuł interwiki.", + "apihelp-query+iwlinks-description": "Wyświetla wszystkie liki interwiki z danych stron.", "apihelp-query+iwlinks-paramvalue-prop-url": "Dodaje pełny adres URL.", "apihelp-query+iwlinks-param-limit": "Łączna liczba linków interwiki do zwrócenia.", "apihelp-query+langbacklinks-param-limit": "Łączna liczba stron do zwrócenia.", "apihelp-query+langbacklinks-paramvalue-prop-lllang": "Dodaje kod języka linku językowego.", "apihelp-query+langbacklinks-paramvalue-prop-lltitle": "Dodaje tytuł linku językowego.", "apihelp-query+langlinks-paramvalue-prop-url": "Dodaje pełny adres URL.", + "apihelp-query+links-description": "Zwraca wszystkie linki z danych stron.", + "apihelp-query+links-param-namespace": "Pokaż linki tylko w tych przestrzeniach nazw.", "apihelp-query+links-param-limit": "Liczba linków do zwrócenia.", "apihelp-query+linkshere-description": "Znajdź wszystkie strony, które linkują do danych stron.", "apihelp-query+linkshere-paramvalue-prop-title": "Nazwa każdej strony.", @@ -438,8 +454,12 @@ "apihelp-query+linkshere-param-limit": "Liczba do zwrócenia.", "apihelp-query+logevents-description": "Pobierz eventy z logu.", "apihelp-query+logevents-example-simple": "Lista ostatnich zarejestrowanych zdarzeń.", + "apihelp-query+pagepropnames-param-limit": "Maksymalna liczba zwracanych nazw.", "apihelp-query+pageswithprop-param-prop": "Jakie informacje dołączyć:", "apihelp-query+pageswithprop-paramvalue-prop-ids": "Doda ID strony.", + "apihelp-query+pageswithprop-paramvalue-prop-value": "Dodaje wartość właściwości strony.", + "apihelp-query+pageswithprop-param-limit": "Maksymalna liczba zwracanych stron.", + "apihelp-query+pageswithprop-param-dir": "W jakim kierunku sortować.", "apihelp-query+pageswithprop-example-generator": "Pobierz dodatkowe informacje o pierwszych 10 stronach wykorzystując <code>__NOTOC__</code>.", "apihelp-query+prefixsearch-param-search": "Wyszukaj tekst.", "apihelp-query+prefixsearch-param-namespace": "Przestrzenie nazw do przeszukania.", @@ -448,12 +468,17 @@ "apihelp-query+protectedtitles-description": "Lista wszystkich tytułów zabezpieczonych przed tworzeniem.", "apihelp-query+protectedtitles-param-namespace": "Listuj tylko strony z tych przestrzeni nazw.", "apihelp-query+protectedtitles-param-limit": "Łączna liczba stron do zwrócenia.", + "apihelp-query+protectedtitles-paramvalue-prop-level": "Dodaje poziom zabezpieczeń.", "apihelp-query+protectedtitles-example-simple": "Lista chronionych nagłówków", + "apihelp-query+querypage-param-page": "Nazwa strony specjalnej. Należy pamiętać o wielkości liter.", "apihelp-query+querypage-param-limit": "Liczba zwracanych wyników.", "apihelp-query+random-param-namespace": "Zwraca strony tylko w tych przestrzeniach nazw.", + "apihelp-query+random-param-filterredir": "Jaki filtrować przekierowania.", + "apihelp-query+random-example-simple": "Zwraca dwie losowe strony z głównej przestrzeni nazw.", "apihelp-query+recentchanges-param-user": "Listuj tylko zmiany dokonane przez tego użytkownika.", "apihelp-query+recentchanges-param-excludeuser": "Nie listuj zmian dokonanych przez tego użytkownika.", "apihelp-query+recentchanges-param-tag": "Pokazuj tylko zmiany oznaczone tym tagiem.", + "apihelp-query+recentchanges-paramvalue-prop-comment": "Dodaje komentarz do edycji.", "apihelp-query+recentchanges-example-simple": "Lista ostatnich zmian.", "apihelp-query+redirects-description": "Zwraca wszystkie przekierowania do danej strony.", "apihelp-query+redirects-paramvalue-prop-title": "Nazwa każdego przekierowania.", @@ -461,6 +486,7 @@ "apihelp-query+revisions+base-paramvalue-prop-ids": "Identyfikator wersji.", "apihelp-query+revisions+base-paramvalue-prop-flags": "Znaczniki wersji (drobne).", "apihelp-query+revisions+base-paramvalue-prop-timestamp": "Znacznik czasu wersji.", + "apihelp-query+revisions+base-paramvalue-prop-user": "Użytkownik, który utworzył wersję.", "apihelp-query+revisions+base-paramvalue-prop-size": "Długość wersji (w bajtach).", "apihelp-query+revisions+base-paramvalue-prop-sha1": "SHA-1 (base 16) wersji.", "apihelp-query+revisions+base-paramvalue-prop-content": "Tekst wersji.", @@ -477,6 +503,7 @@ "apihelp-query+siteinfo-paramvalue-prop-general": "Ogólne informacje o systemie.", "apihelp-query+siteinfo-paramvalue-prop-namespaces": "Lista zarejestrowanych przestrzeni nazw i ich nazwy kanoniczne.", "apihelp-query+siteinfo-paramvalue-prop-namespacealiases": "Lista zarejestrowanych aliasów przestrzeni nazw.", + "apihelp-query+siteinfo-paramvalue-prop-magicwords": "Lista słów magicznych i ich aliasów.", "apihelp-query+siteinfo-param-numberingroup": "Wyświetla liczbę użytkowników w grupach użytkowników.", "apihelp-query+siteinfo-example-simple": "Pobierz informacje o stronie.", "apihelp-query+stashimageinfo-param-sessionkey": "Alias dla $1filekey, dla kompatybilności wstecznej.", @@ -488,14 +515,19 @@ "apihelp-query+tags-paramvalue-prop-active": "Czy znacznik jest nadal stosowany.", "apihelp-query+tags-example-simple": "Lista dostęnych tagów.", "apihelp-query+templates-description": "Zwraca wszystkie strony osadzone w danych stronach.", + "apihelp-query+templates-param-namespace": "Pokaż szablony tylko w tych przestrzeniach nazw.", "apihelp-query+templates-param-limit": "Ile szablonów zwrócić?", "apihelp-query+transcludedin-paramvalue-prop-title": "Nazwa każdej strony.", "apihelp-query+transcludedin-paramvalue-prop-redirect": "Oznacz, jeśli strona jest przekierowaniem.", "apihelp-query+transcludedin-param-limit": "Ile zwrócić.", + "apihelp-query+usercontribs-paramvalue-prop-comment": "Dodaje komentarz edycji.", + "apihelp-query+usercontribs-paramvalue-prop-parsedcomment": "Dodaje sparsowany komentarz edycji.", "apihelp-query+userinfo-description": "Pobierz informacje o aktualnym użytkowniku.", "apihelp-query+userinfo-param-prop": "Jakie informacje dołączyć:", "apihelp-query+userinfo-paramvalue-prop-groups": "Wyświetla wszystkie grupy, do których należy bieżący użytkownik.", "apihelp-query+userinfo-paramvalue-prop-rights": "Wyświetla wszystkie uprawnienia, które ma bieżący użytkownik.", + "apihelp-query+userinfo-paramvalue-prop-editcount": "Dodaje liczbę edycji bieżącego użytkownika.", + "apihelp-query+userinfo-paramvalue-prop-email": "Dodaje adres e-mail użytkownika i datę jego potwierdzenia.", "apihelp-query+userinfo-paramvalue-prop-registrationdate": "Dodaje datę rejestracji użytkownika.", "apihelp-query+userinfo-example-simple": "Pobierz informacje o aktualnym użytkowniku.", "apihelp-query+userinfo-example-data": "Pobierz dodatkowe informacje o aktualnym użytkowniku.", @@ -513,27 +545,39 @@ "apihelp-query+watchlist-paramvalue-type-external": "Zmiany zewnętrzne.", "apihelp-resetpassword-description": "Wyślij użytkownikowi e-mail do resetowania hasła.", "apihelp-resetpassword-example-email": "Wyślij e-mail do resetowania hasła do wszystkich użytkowników posiadających adres <kbd>user@example.com</kbd>.", + "apihelp-revisiondelete-param-ids": "Identyfikatory wersji do usunięcia.", + "apihelp-revisiondelete-param-hide": "Co ukryć w każdej z wersji.", + "apihelp-revisiondelete-param-show": "Co pokazać w każdej z wersji.", + "apihelp-revisiondelete-param-reason": "Powód usunięcia lub przywrócenia.", + "apihelp-setpagelanguage-description": "Zmień język strony.", + "apihelp-setpagelanguage-param-reason": "Powód zmiany.", "apihelp-stashedit-param-title": "Tytuł edytowanej strony.", "apihelp-stashedit-param-sectiontitle": "Tytuł nowej sekcji.", "apihelp-stashedit-param-text": "Zawartość strony.", + "apihelp-stashedit-param-summary": "Opis zmian.", "apihelp-tag-param-reason": "Powód zmiany.", "apihelp-unblock-description": "Odblokuj użytkownika.", - "apihelp-unblock-param-user": "Nazwa użytkownika, adres IP albo zakres adresów IP, które chcesz odblokować. Nie można używać jednocześnie z <var>$1id</var> lub <var>$luserid</var>.", + "apihelp-unblock-param-user": "Nazwa użytkownika, adres IP albo zakres adresów IP, które chcesz odblokować. Nie można używać jednocześnie z <var>$1id</var> lub <var>$1userid</var>.", "apihelp-unblock-param-reason": "Powód odblokowania.", + "apihelp-undelete-param-title": "Tytuł strony do przywrócenia.", "apihelp-undelete-param-reason": "Powód przywracania.", "apihelp-upload-param-filename": "Nazwa pliku docelowego.", "apihelp-upload-param-watch": "Obserwuj stronę.", "apihelp-upload-param-ignorewarnings": "Ignoruj wszystkie ostrzeżenia.", "apihelp-upload-param-file": "Zawartość pliku.", "apihelp-userrights-param-user": "Nazwa użytkownika.", + "apihelp-userrights-param-userid": "Identyfikator użytkownika.", "apihelp-userrights-param-add": "Dodaj użytkownika do tych grup.", + "apihelp-userrights-param-remove": "Usuń użytkownika z tych grup.", "apihelp-userrights-param-reason": "Powód zmiany.", + "apihelp-validatepassword-param-password": "Hasło do walidacji.", "apihelp-json-description": "Dane wyjściowe w formacie JSON.", "apihelp-jsonfm-description": "Dane wyjściowe w formacie JSON (prawidłowo wyświetlane w HTML).", "apihelp-php-description": "Dane wyjściowe w serializowany formacie PHP.", "apihelp-phpfm-description": "Dane wyjściowe w serializowanym formacie PHP (prawidłowo wyświetlane w HTML).", "apihelp-xml-description": "Dane wyjściowe w formacie XML.", "apihelp-xml-param-xslt": "Jeśli określony, dodaje podaną stronę jako arkusz styli XSL. Powinna to być strona wiki w przestrzeni nazw MediaWiki, której nazwa kończy się na <code>.xsl</code>.", + "apihelp-xml-param-includexmlnamespace": "Jeśli zaznaczono, dodaje przestrzeń nazw XML.", "apihelp-xmlfm-description": "Dane wyjściowe w formacie XML (prawidłowo wyświetlane w HTML).", "api-format-title": "Wynik MediaWiki API", "api-pageset-param-titles": "Lista tytułów, z którymi pracować.", @@ -583,35 +627,60 @@ "api-help-permissions-granted-to": "{{PLURAL:$1|Przydzielone dla}}: $2", "api-help-right-apihighlimits": "Użyj wyższych limitów w zapytaniach API (dla zapytań powolnych: $1; dla zapytań szbkich: $2). Limity zapytań powolnych są także stosowane dla parametrów z podanymi wieloma wartościami.", "api-help-open-in-apisandbox": "<small>[otwórz w brudnopisie]</small>", + "apierror-articleexists": "Artykuł, który próbowałeś utworzyć, już został utworzony.", "apierror-baddiff": "Różnicy wersji nie można odtworzyć. Jedna lub obie wersje nie istnieją, lub nie masz uprawnień do ich wyświetlenia.", "apierror-badgenerator-unknown": "Nieznany <kbd>generator=$1</kbd>.", "apierror-badip": "Parametr IP nie jest prawidłowy.", + "apierror-badparameter": "Nieprawidłowa wartość parametru <var>$1</var>.", "apierror-badquery": "Nieprawidłowe zapytanie.", + "apierror-badtoken": "Nieprawidłowy token CSRF.", "apierror-blockedfrommail": "Została Ci zablokowana możliwość wysyłania e-maili.", "apierror-blocked": "Została Ci zablokowana możliwość edycji.", + "apierror-botsnotsupported": "Interfejs nie jest obsługiwany dla botów.", + "apierror-cannotviewtitle": "Nie masz uprawnień do oglądania $1.", "apierror-cantblock": "Nie masz uprawnień do blokowania użytkowników.", + "apierror-cantimport-upload": "Nie masz uprawnień do importowania przesłanych stron.", "apierror-cantimport": "Nie masz uprawnień do importowania stron.", "apierror-cantsend": "Nie jesteś zalogowany, nie masz potwierdzonego adresu e-mail, albo nie masz prawa wysyłać e-maili do innych użytkowników, więc nie możesz wysłać wiadomości e-mail.", + "apierror-cantundelete": "Nie można przywrócić: dana wersja nie istnieje albo została już przywrócona.", + "apierror-databaseerror": "[$1] Błąd zapytania do bazy danych.", + "apierror-exceptioncaught": "[$1] Stwierdzono wyjątek: $2", "apierror-filedoesnotexist": "Plik nie istnieje.", + "apierror-import-unknownerror": "Nieznany błąd podczas importowania: $1.", "apierror-integeroutofrange-abovebotmax": "Wartość <var>$1</var> dla botów i administratorów nie może przekraczać $2 (ustawiono $3).", "apierror-integeroutofrange-abovemax": "Wartość <var>$1</var> dla użytkowników nie może przekraczać $2 (ustawiono $3).", "apierror-integeroutofrange-belowminimum": "Wartość <var>$1</var> nie może być mniejsza niż $2 (ustawiono $3).", + "apierror-invalidcategory": "Wprowadzona nazwa kategorii jest nieprawidłowa.", "apierror-invalidlang": "Nieprawidłowy kod języka dla parametru <var>$1</var>.", + "apierror-invalidoldimage": "Parametr <var>oldimage</var> ma nieprawidłowy format.", "apierror-invalidparammix": "{{PLURAL:$2|Parametry}} $1 nie mogą być używane razem.", "apierror-invalidtitle": "Zły tytuł „$1”.", "apierror-invalidurlparam": "Nieprawidłowa wartość <var>$1urlparam</var> (<kbd>$2=$3</kbd>).", + "apierror-invaliduser": "Niepoprawna nazwa użytkownika „$1”.", + "apierror-invaliduserid": "Identyfikator użytkownika <var>$1</var> jest nieprawidłowy.", + "apierror-maxlag-generic": "Oczekiwania na serwer bazy danych: opóźnienie $1 {{PLURAL:$1|sekunda|sekundy|sekund}}.", "apierror-missingparam": "Parametr <var>$1</var> musi być podany.", "apierror-missingtitle": "Wybrana przez ciebie strona nie istnieje.", "apierror-missingtitle-byname": "Strona $1 nie istnieje.", "apierror-moduledisabled": "Moduł <kbd>$1</kbd> został wyłączony.", "apierror-mustbeloggedin-generic": "Musisz być zalogowany.", + "apierror-mustbeloggedin": "Musisz się zalogować, aby mieć możliwość $1.", + "apierror-nodeleteablefile": "Nie ma takiej starej wersji pliku.", "apierror-noedit-anon": "Niezarejestrowani użytkownicy nie mogą edytować stron.", "apierror-noedit": "Nie masz uprawnień do edytowania stron.", + "apierror-noimageredirect-anon": "Anonimowi użytkownicy nie mogą tworzyć przekierowań plików.", + "apierror-noimageredirect": "Nie masz uprawnień do tworzenia przekierowań plików.", + "apierror-nosuchpageid": "Nie ma strony z identyfikatorem $1.", + "apierror-nosuchrevid": "Nie ma wersji z identyfikatorem $1.", + "apierror-nosuchsection": "Nie ma sekcji $1.", "apierror-permissiondenied": "Nie masz uprawnień do $1.", "apierror-permissiondenied-generic": "Brak dostępu.", "apierror-permissiondenied-unblock": "Nie masz uprawnień do odblokowania użytkowników.", "apierror-protect-invalidaction": "Nieprawidłowy rodzaj zabezpieczenia „$1”.", "apierror-protect-invalidlevel": "Nieprawidłowy poziom zabezpieczeń „$1”.", + "apierror-readonly": "Wiki jest teraz w trybie tylko do odczytu.", + "apierror-revwrongpage": "r$1 nie jest wersją strony $2.", + "apierror-sectionsnotsupported-what": "Sekcje nie są obsługiwane przez $1.", "apierror-specialpage-cantexecute": "Nie masz uprawnień, aby zobaczyć wyniki tej strony specjalnej.", "apierror-stashwrongowner": "Nieprawidłowy właściciel: $1", "apierror-unknownerror-nocode": "Nieznany błąd.", @@ -621,6 +690,8 @@ "apiwarn-invalidcategory": "„$1” nie jest kategorią.", "apiwarn-invalidtitle": "„$1” nie jest poprawnym tytułem.", "apiwarn-notfile": "„$1” nie jest plikiem.", + "apiwarn-toomanyvalues": "Podano zbyt wiele wartości dla parametru <var>$1</var>. Ograniczenie do $2.", + "apiwarn-validationfailed": "Błąd walidacji dla <kbd>$1</kbd>: $2", "api-feed-error-title": "Błąd ($1)", "api-exception-trace": "$1 w $2($3)\n$4", "api-credits-header": "Twórcy", diff --git a/includes/api/i18n/pt-br.json b/includes/api/i18n/pt-br.json index 9899945f1612..7d5f7ead9a1b 100644 --- a/includes/api/i18n/pt-br.json +++ b/includes/api/i18n/pt-br.json @@ -10,16 +10,20 @@ "Raphaelras", "Caçador de Palavras", "LucyDiniz", - "Eduardo Addad de Oliveira" + "Eduardo Addad de Oliveira", + "Warley Felipe C.", + "TheEduGobi" ] }, "apihelp-main-param-action": "Qual ação executar.", "apihelp-main-param-format": "O formato da saída.", "apihelp-main-param-smaxage": "Define o cabeçalho <code>s-maxage</code> para esta quantidade de segundos. Os erros não são armazenados em cache.", "apihelp-main-param-maxage": "Define o cabeçalho <code>max-age</code> para esta quantidade de segundos. Os erros não são armazenados em cache.", + "apihelp-main-param-assertuser": "Verificar que o utilizador atual é o utilizador nomeado.", "apihelp-main-param-requestid": "Qualquer valor dado aqui será incluído na resposta. Pode ser usado para distinguir requisições.", "apihelp-main-param-servedby": "Inclua o nome de host que atendeu a solicitação nos resultados.", "apihelp-main-param-curtimestamp": "Inclui a data atual no resultado.", + "apihelp-main-param-origin": "Ao acessar a API usando uma solicitação AJAX por domínio cruzado (CORS), defina isto como o domínio de origem. Isto deve estar incluso em toda solicitação ''pre-flight'', sendo portanto parte do URI da solicitação (ao invés do corpo do POST).\n\nPara solicitações autenticadas, isto deve corresponder a uma das origens no cabeçalho <code>Origin</code>, para que seja algo como <kbd>https://pt.wikipedia.org</kbd> ou <kbd>https://meta.wikimedia.org</kbd>. Se este parâmetro não corresponder ao cabeçalho <code>Origin</code>, uma resposta 403 será retornada. Se este parâmetro corresponder ao cabeçalho <code>Origin</code> e a origem for permitida (''whitelisted''), os cabeçalhos <code>Access-Control-Allow-Origin</code> e <code>Access-Control-Allow-Credentials</code> serão definidos.\n\nPara solicitações não autenticadas, especifique o valor <kbd>*</kbd>. Isto fará com que o cabeçalho <code>Access-Control-Allow-Origin</code> seja definido, porém o <code>Access-Control-Allow-Credentials</code> será <code>false</code> e todos os dados específicos para usuários tornar-se-ão restritos.", "apihelp-block-description": "Bloquear um usuário", "apihelp-block-param-user": "Nome de usuário, endereço IP ou faixa de IP para bloquear.", "apihelp-block-param-reason": "Razão do bloqueio.", @@ -28,6 +32,7 @@ "apihelp-block-param-autoblock": "Bloquear automaticamente o endereço IP usado e quaisquer endereços IPs subseqüentes que tentarem acessar a partir deles.", "apihelp-block-param-hidename": "Oculta o nome do usuário do ''log'' de bloqueio. (Requer o direito <code>hideuser</code>).", "apihelp-block-param-reblock": "Se o usuário já estiver bloqueado, sobrescrever o bloqueio existente.", + "apihelp-block-param-watchuser": "Vigiar as páginas de utilizador e de discussão, do utilizador ou do endereço IP.", "apihelp-block-example-ip-simple": "Bloquear endereço IP <kbd>192.0.2.5</kbd> por três dias com razão <kbd>Primeira medida</kbd>.", "apihelp-block-example-user-complex": "Bloquear usuário <kbd>Vandal</kbd> indefinidamente com razão <kbd>Vandalism</kbd> e o impede de criar nova conta e envio de emails.", "apihelp-compare-param-fromtitle": "Primeiro título para comparar.", @@ -86,8 +91,9 @@ "apihelp-feedcontributions-param-deletedonly": "Mostrar apenas contribuições excluídas.", "apihelp-feedcontributions-param-toponly": "Mostrar somente as edições que sejam a última revisão.", "apihelp-feedcontributions-param-newonly": "Mostrar somente as edições que são criação de páginas.", + "apihelp-feedcontributions-param-hideminor": "Ocultar edições menores.", "apihelp-feedcontributions-param-showsizediff": "Mostrar a diferença de tamanho entre as revisões.", - "apihelp-feedrecentchanges-description": "Retorna um feed de alterações recentes.", + "apihelp-feedrecentchanges-description": "Retorna um ''feed'' de mudanças recentes.", "apihelp-feedrecentchanges-param-feedformat": "O formato do feed.", "apihelp-feedrecentchanges-param-namespace": "Espaço nominal a partir do qual limitar resultados.", "apihelp-feedrecentchanges-param-invert": "Todos os espaços nominais, exceto o selecionado.", @@ -95,10 +101,12 @@ "apihelp-feedrecentchanges-param-from": "Mostra modificações desde então.", "apihelp-feedrecentchanges-param-hideminor": "Ocultar modificações menores.", "apihelp-feedrecentchanges-param-hidebots": "Ocultar modificações menores feitas por bots.", + "apihelp-feedrecentchanges-param-hidepatrolled": "Ocultar mudanças patrulhadas.", "apihelp-feedrecentchanges-param-hidemyself": "Ocultar alterações feitas pelo usuário atual.", + "apihelp-feedrecentchanges-param-hidecategorization": "Alterações de membros pertencentes à uma categoria.", "apihelp-feedrecentchanges-param-tagfilter": "Filtrar por tag.", "apihelp-feedrecentchanges-example-simple": "Mostrar as mudanças recentes.", - "apihelp-feedrecentchanges-example-30days": "Mostrar as alterações recentes por 30 dias.", + "apihelp-feedrecentchanges-example-30days": "Mostrar as mudanças recentes por 30 dias.", "apihelp-feedwatchlist-description": "Retornar um feed da lista de vigiados.", "apihelp-feedwatchlist-param-feedformat": "O formato do feed.", "apihelp-feedwatchlist-param-hours": "Lista páginas modificadas dentro dessa quantia de horas a partir de agora.", @@ -236,6 +244,7 @@ "apihelp-query+alllinks-param-namespace": "O espaço nominal a se enumerar.", "apihelp-query+alllinks-param-limit": "Quantos itens retornar.", "apihelp-query+alllinks-example-generator": "Obtém páginas contendo os links.", + "apihelp-query+allmessages-description": "Devolver as mensagens deste site.", "apihelp-query+allmessages-param-prop": "Quais propriedades obter.", "apihelp-query+allmessages-param-customised": "Retornar apenas mensagens neste estado personalização.", "apihelp-query+allmessages-param-lang": "Retornar mensagens neste idioma.", @@ -252,6 +261,7 @@ "apihelp-query+allredirects-param-from": "O título do redirecionamento a partir do qual começar a enumerar.", "apihelp-query+allredirects-param-to": "O título do redirecionamento onde parar de enumerar.", "apihelp-query+allredirects-param-namespace": "O espaço nominal a se enumerar.", + "apihelp-query+allrevisions-description": "Listar todas as revisões.", "apihelp-query+alltransclusions-param-namespace": "O espaço nominal a se enumerar.", "apihelp-query+alltransclusions-param-limit": "Quantos itens retornar.", "apihelp-query+backlinks-param-title": "Título a se pesquisar. Não pode ser usado em conjunto com <var>$1pageid</var>.", @@ -288,14 +298,17 @@ "apihelp-query+iwbacklinks-description": "Encontra todas as páginas que apontam para o determinado link interwiki.\n\nPode ser usado para encontrar todos os links com um prefixo, ou todos os links para um título (com um determinado prefixo). Usar nenhum parâmetro é efetivamente \"todos os links interwiki\".", "apihelp-query+iwbacklinks-param-prefix": "Prefixo para o interwiki.", "apihelp-query+iwbacklinks-param-limit": "Quantas páginas retornar.", + "apihelp-query+iwlinks-paramvalue-prop-url": "Adiciona o URL completo.", "apihelp-query+langbacklinks-param-limit": "Quantas páginas retornar.", "apihelp-query+langlinks-param-limit": "Quantos links de idioma retornar.", "apihelp-query+links-param-limit": "Quantos links retornar.", "apihelp-query+linkshere-param-limit": "Quantos retornar.", + "apihelp-query+logevents-example-simple": "Listar os eventos recentes do registo.", "apihelp-query+prefixsearch-param-limit": "O número máximo a se retornar.", "apihelp-query+protectedtitles-param-limit": "Quantas páginas retornar.", "apihelp-query+protectedtitles-paramvalue-prop-level": "Adicionar o nível de proteção", "apihelp-query+protectedtitles-example-simple": "Listar títulos protegidos", + "apihelp-query+querypage-param-limit": "O número máximo a se retornar.", "apihelp-query+random-param-filterredir": "Como filtrar por redirecionamentos.", "apihelp-query+recentchanges-param-user": "Listar apenas alterações de usuário.", "apihelp-query+recentchanges-param-excludeuser": "Não listar as alterações deste usuário.", @@ -363,5 +376,14 @@ "apihelp-userrights-param-add": "Adicionar o usuário para estes grupos.", "apihelp-userrights-param-remove": "Remover o usuário destes grupos.", "apihelp-userrights-param-reason": "Motivo para a mudança.", - "apihelp-none-description": "Nenhuma saída." + "apihelp-none-description": "Nenhuma saída.", + "api-help-flag-deprecated": "Este módulo é obsoleto.", + "api-help-source": "Fonte: $1", + "api-help-source-unknown": "Fonte: <span class=\"apihelp-unknown\">desconhecida</span>", + "api-help-license": "Licença: [[$1|$2]]", + "api-help-license-noname": "Licença: [[$1|Ver ligação]]", + "api-help-license-unknown": "Fonte: <span class=\"apihelp-unknown\">desconhecida</span>", + "api-help-parameters": "{{PLURAL:$1|Parâmetro|Parâmetros}}:", + "api-help-param-deprecated": "Obsoleto", + "api-help-param-required": "Este parâmetro é obrigatório." } diff --git a/includes/api/i18n/pt.json b/includes/api/i18n/pt.json index 0c5b72073e50..cb5997b25f66 100644 --- a/includes/api/i18n/pt.json +++ b/includes/api/i18n/pt.json @@ -361,7 +361,7 @@ "apihelp-protect-example-protect": "Proteger uma página.", "apihelp-protect-example-unprotect": "Desproteger uma página definindo a restrição <kbd>all</kbd> (isto é, todos podem executar a operação).", "apihelp-protect-example-unprotect2": "Desproteger uma página definindo que não há restrições.", - "apihelp-purge-description": "Limpar a ''cache'' para os títulos especificados.\n\nRequer um pedido POST se o utilizador não tiver iniciado uma sessão.", + "apihelp-purge-description": "Limpar a ''cache'' para os títulos especificados.", "apihelp-purge-param-forcelinkupdate": "Atualizar as tabelas de ligações.", "apihelp-purge-param-forcerecursivelinkupdate": "Atualizar a tabela de ligações, e atualizar as tabelas de ligações de qualquer página que usa esta página como modelo.", "apihelp-purge-example-simple": "Purgar as páginas <kbd>Main Page</kbd> e <kbd>API</kbd>.", @@ -1057,6 +1057,7 @@ "apihelp-query+siteinfo-paramvalue-prop-rightsinfo": "Devolve informação sobre os direitos (a licença) da wiki, se disponível.", "apihelp-query+siteinfo-paramvalue-prop-restrictions": "Devolve informação sobre os tipos de restrição (proteção) disponíveis.", "apihelp-query+siteinfo-paramvalue-prop-languages": "Devolve uma lista das línguas que o MediaWiki suporta (opcionalmente localizada, usando <var>$1inlanguagecode</var>).", + "apihelp-query+siteinfo-paramvalue-prop-languagevariants": "Devolve uma lista dos códigos de língua para os quais o [[mw:LanguageConverter|LanguageConverter]] está ativado, e as variantes suportadas para cada código.", "apihelp-query+siteinfo-paramvalue-prop-skins": "Devolve uma lista de todos os temas ativados (opcionalmente localizada, usando <var>$1inlanguagecode</var>, ou então na língua do conteúdo).", "apihelp-query+siteinfo-paramvalue-prop-extensiontags": "Devolve uma lista dos elementos de extensões do analisador sintático.", "apihelp-query+siteinfo-paramvalue-prop-functionhooks": "Devolve uma lista dos ''hooks'' de funções do analisador sintático.", @@ -1295,14 +1296,14 @@ "apihelp-tokens-example-edit": "Obter uma chave de edição (padrão).", "apihelp-tokens-example-emailmove": "Obter uma chave de correio eletrónico e uma chave de movimentação.", "apihelp-unblock-description": "Desbloquear um utilizador.", - "apihelp-unblock-param-id": "Identificador do bloqueio a desfazer (obtido com <kbd>list=blocks</kbd>). Não pode ser usado em conjunto com <var>$1user</var> ou <var>$luserid</var>.", - "apihelp-unblock-param-user": "O nome de utilizador, endereço IP ou gama de endereços IP a ser desbloqueado. Não pode ser usado em conjunto com <var>$1id</var> ou <var>$luserid</var>.", + "apihelp-unblock-param-id": "Identificador do bloqueio a desfazer (obtido com <kbd>list=blocks</kbd>). Não pode ser usado em conjunto com <var>$1user</var> ou <var>$1userid</var>.", + "apihelp-unblock-param-user": "O nome de utilizador, endereço IP ou gama de endereços IP a ser desbloqueado. Não pode ser usado em conjunto com <var>$1id</var> ou <var>$1userid</var>.", "apihelp-unblock-param-userid": "O identificador do utilizador a ser desbloqueado. Não pode ser usado em conjunto com <var>$1id</var> ou <var>$1user</var>.", "apihelp-unblock-param-reason": "Motivo para o desbloqueio.", "apihelp-unblock-param-tags": "As etiquetas de modificação a aplicar à entrada no registo de bloqueios.", "apihelp-unblock-example-id": "Desfazer o bloqueio com o identificador #<kbd>105</kbd>.", "apihelp-unblock-example-user": "Desbloquear o utilizador <kbd>Bob</kbd> com o motivo <kbd>Sorry Bob</kbd>.", - "apihelp-undelete-description": "Restaurar revisões de uma página eliminada.\n\nPode obter-se uma lista de revisões eliminadas (incluindo as datas e horas de eliminação) com [[Special:ApiHelp/query+deletedrevs|list=deletedrevs]] e uma lista de identificadores de ficheiros eliminados com [[Special:ApiHelp/query+filearchive|list=filearchive]].", + "apihelp-undelete-description": "Restaurar revisões de uma página eliminada.\n\nPode obter-se uma lista de revisões eliminadas (incluindo as datas e horas de eliminação) com [[Special:ApiHelp/query+deletedrevisions|prop=deletedrevisions]] e uma lista de identificadores de ficheiros eliminados com [[Special:ApiHelp/query+filearchive|list=filearchive]].", "apihelp-undelete-param-title": "Título da página a restaurar.", "apihelp-undelete-param-reason": "Motivo para restaurar a página.", "apihelp-undelete-param-tags": "Etiquetas de modificação a aplicar à entrada no registo de eliminações.", @@ -1333,10 +1334,10 @@ "apihelp-upload-param-checkstatus": "Obter só o estado de carregamento para a chave de ficheiro indicada.", "apihelp-upload-example-url": "Carregar de um URL.", "apihelp-upload-example-filekey": "Prosseguir um carregamento que falhou devido a avisos.", - "apihelp-userrights-description": "Alterar os membros de um grupo de utilizadores.", + "apihelp-userrights-description": "Alterar os grupos a que um utilizador pertence.", "apihelp-userrights-param-user": "O nome de utilizador.", "apihelp-userrights-param-userid": "O identificador de utilizador.", - "apihelp-userrights-param-add": "Adicionar o utilizador a estes grupos.", + "apihelp-userrights-param-add": "Adicionar o utilizador a estes grupos ou, se já for membro de um grupo, atualizar a data de expiração da sua pertença a esse grupo.", "apihelp-userrights-param-remove": "Remover o utilizador destes grupos.", "apihelp-userrights-param-reason": "O motivo da alteração.", "apihelp-userrights-param-tags": "Etiquetas de modificação a aplicar à entrada no registo de privilégios de utilizadores.", diff --git a/includes/api/i18n/qqq.json b/includes/api/i18n/qqq.json index ef2ba8fde6b8..6e7065346309 100644 --- a/includes/api/i18n/qqq.json +++ b/includes/api/i18n/qqq.json @@ -330,6 +330,7 @@ "apihelp-parse-paramvalue-prop-limitreportdata": "{{doc-apihelp-paramvalue|parse|prop|limitreportdata}}", "apihelp-parse-paramvalue-prop-limitreporthtml": "{{doc-apihelp-paramvalue|parse|prop|limitreporthtml}}", "apihelp-parse-paramvalue-prop-parsetree": "{{doc-apihelp-paramvalue|parse|prop|parsetree|params=* $1 - Value of the constant CONTENT_MODEL_WIKITEXT|paramstart=2}}", + "apihelp-parse-paramvalue-prop-parsewarnings": "{{doc-apihelp-paramvalue|parse|prop|parsewarnings}}", "apihelp-parse-param-pst": "{{doc-apihelp-param|parse|pst}}", "apihelp-parse-param-onlypst": "{{doc-apihelp-param|parse|onlypst}}", "apihelp-parse-param-effectivelanglinks": "{{doc-apihelp-param|parse|effectivelanglinks}}", @@ -667,7 +668,7 @@ "apihelp-query+deletedrevs-param-excludeuser": "{{doc-apihelp-param|query+deletedrevs|excludeuser}}", "apihelp-query+deletedrevs-param-namespace": "{{doc-apihelp-param|query+deletedrevs|namespace}}", "apihelp-query+deletedrevs-param-limit": "{{doc-apihelp-param|query+deletedrevs|limit}}", - "apihelp-query+deletedrevs-param-prop": "{{doc-apihelp-param|query+deletedrevs|prop}}", + "apihelp-query+deletedrevs-param-prop": "{{doc-apihelp-param|query+deletedrevs|prop}}\n{{doc-important|You can translate the word \"Deprecated\", but please do not alter the <code><nowiki>class=\"apihelp-deprecated\"</nowiki></code> attribute}}", "apihelp-query+deletedrevs-example-mode1": "{{doc-apihelp-example|query+deletedrevs}}", "apihelp-query+deletedrevs-example-mode2": "{{doc-apihelp-example|query+deletedrevs}}", "apihelp-query+deletedrevs-example-mode3-main": "{{doc-apihelp-example|query+deletedrevs}}", @@ -1038,8 +1039,8 @@ "apihelp-query+search-paramvalue-prop-sectiontitle": "{{doc-apihelp-paramvalue|query+search|prop|sectiontitle}}", "apihelp-query+search-paramvalue-prop-categorysnippet": "{{doc-apihelp-paramvalue|query+search|prop|categorysnippet}}", "apihelp-query+search-paramvalue-prop-isfilematch": "{{doc-apihelp-paramvalue|query+search|prop|isfilematch}}", - "apihelp-query+search-paramvalue-prop-score": "{{doc-apihelp-paramvalue|query+search|prop|score}}", - "apihelp-query+search-paramvalue-prop-hasrelated": "{{doc-apihelp-paramvalue|query+search|prop|hasrelated}}", + "apihelp-query+search-paramvalue-prop-score": "{{doc-apihelp-paramvalue|query+search|prop|score}}\n{{doc-important|Please do not alter the <code><nowiki>class=\"apihelp-deprecated\"</nowiki></code> attribute}}", + "apihelp-query+search-paramvalue-prop-hasrelated": "{{doc-apihelp-paramvalue|query+search|prop|hasrelated}}\n{{doc-important|Please do not alter the <code><nowiki>class=\"apihelp-deprecated\"</nowiki></code> attribute}}", "apihelp-query+search-param-limit": "{{doc-apihelp-param|query+search|limit}}", "apihelp-query+search-param-interwiki": "{{doc-apihelp-param|query+search|interwiki}}", "apihelp-query+search-param-backend": "{{doc-apihelp-param|query+search|backend}}", @@ -1064,6 +1065,7 @@ "apihelp-query+siteinfo-paramvalue-prop-rightsinfo": "{{doc-apihelp-paramvalue|query+siteinfo|prop|rightsinfo}}", "apihelp-query+siteinfo-paramvalue-prop-restrictions": "{{doc-apihelp-paramvalue|query+siteinfo|prop|restrictions}}", "apihelp-query+siteinfo-paramvalue-prop-languages": "{{doc-apihelp-paramvalue|query+siteinfo|prop|languages}}", + "apihelp-query+siteinfo-paramvalue-prop-languagevariants": "{{doc-apihelp-paramvalue|query+siteinfo|prop|languagevariants}}", "apihelp-query+siteinfo-paramvalue-prop-skins": "{{doc-apihelp-paramvalue|query+siteinfo|prop|skins}}", "apihelp-query+siteinfo-paramvalue-prop-extensiontags": "{{doc-apihelp-paramvalue|query+siteinfo|prop|extensiontags}}", "apihelp-query+siteinfo-paramvalue-prop-functionhooks": "{{doc-apihelp-paramvalue|query+siteinfo|prop|functionhooks}}", @@ -1146,11 +1148,12 @@ "apihelp-query+userinfo-paramvalue-prop-blockinfo": "{{doc-apihelp-paramvalue|query+userinfo|prop|blockinfo}}", "apihelp-query+userinfo-paramvalue-prop-hasmsg": "{{doc-apihelp-paramvalue|query+userinfo|prop|hasmsg}}", "apihelp-query+userinfo-paramvalue-prop-groups": "{{doc-apihelp-paramvalue|query+userinfo|prop|groups}}", + "apihelp-query+userinfo-paramvalue-prop-groupmemberships": "{{doc-apihelp-paramvalue|query+userinfo|prop|groupmemberships}}", "apihelp-query+userinfo-paramvalue-prop-implicitgroups": "{{doc-apihelp-paramvalue|query+userinfo|prop|implicitgroups}}", "apihelp-query+userinfo-paramvalue-prop-rights": "{{doc-apihelp-paramvalue|query+userinfo|prop|rights}}", "apihelp-query+userinfo-paramvalue-prop-changeablegroups": "{{doc-apihelp-paramvalue|query+userinfo|prop|changeablegroups}}", "apihelp-query+userinfo-paramvalue-prop-options": "{{doc-apihelp-paramvalue|query+userinfo|prop|options}}", - "apihelp-query+userinfo-paramvalue-prop-preferencestoken": "{{doc-apihelp-paramvalue|query+userinfo|prop|preferencestoken}}", + "apihelp-query+userinfo-paramvalue-prop-preferencestoken": "{{doc-apihelp-paramvalue|query+userinfo|prop|preferencestoken}}\n{{doc-important|You can translate the word \"Deprecated\", but please do not alter the <code><nowiki>class=\"apihelp-deprecated\"</nowiki></code> attribute}}", "apihelp-query+userinfo-paramvalue-prop-editcount": "{{doc-apihelp-paramvalue|query+userinfo|prop|editcount}}", "apihelp-query+userinfo-paramvalue-prop-ratelimits": "{{doc-apihelp-paramvalue|query+userinfo|prop|ratelimits}}", "apihelp-query+userinfo-paramvalue-prop-realname": "{{doc-apihelp-paramvalue|query+userinfo|prop|realname}}", @@ -1166,6 +1169,7 @@ "apihelp-query+users-param-prop": "{{doc-apihelp-param|query+users|prop|paramvalues=1}}", "apihelp-query+users-paramvalue-prop-blockinfo": "{{doc-apihelp-paramvalue|query+users|prop|blockinfo}}", "apihelp-query+users-paramvalue-prop-groups": "{{doc-apihelp-paramvalue|query+users|prop|groups}}", + "apihelp-query+users-paramvalue-prop-groupmemberships": "{{doc-apihelp-paramvalue|query+users|prop|groupmemberships}}", "apihelp-query+users-paramvalue-prop-implicitgroups": "{{doc-apihelp-paramvalue|query+users|prop|implicitgroups}}", "apihelp-query+users-paramvalue-prop-rights": "{{doc-apihelp-paramvalue|query+users|prop|rights}}", "apihelp-query+users-paramvalue-prop-editcount": "{{doc-apihelp-paramvalue|query+users|prop|editcount}}", @@ -1344,11 +1348,13 @@ "apihelp-userrights-param-user": "{{doc-apihelp-param|userrights|user}}\n{{Identical|Username}}", "apihelp-userrights-param-userid": "{{doc-apihelp-param|userrights|userid}}\n{{Identical|User ID}}", "apihelp-userrights-param-add": "{{doc-apihelp-param|userrights|add}}", + "apihelp-userrights-param-expiry": "{{doc-apihelp-param|userrights|expiry}}", "apihelp-userrights-param-remove": "{{doc-apihelp-param|userrights|remove}}", "apihelp-userrights-param-reason": "{{doc-apihelp-param|userrights|reason}}", "apihelp-userrights-param-tags": "{{doc-apihelp-param|userrights|tags}}", "apihelp-userrights-example-user": "{{doc-apihelp-example|userrights}}", "apihelp-userrights-example-userid": "{{doc-apihelp-example|userrights}}", + "apihelp-userrights-example-expiry": "{{doc-apihelp-example|userrights}}", "apihelp-validatepassword-description": "{{doc-apihelp-description|validatepassword}}", "apihelp-validatepassword-param-password": "{{doc-apihelp-param|validatepassword|password}}", "apihelp-validatepassword-param-user": "{{doc-apihelp-param|validatepassword|user}}", @@ -1445,7 +1451,7 @@ "api-help-permissions-granted-to": "Used to introduce the list of groups each permission is assigned to.\n\nParameters:\n* $1 - Number of groups\n* $2 - List of group names, comma-separated", "api-help-right-apihighlimits": "{{technical}}{{doc-right|apihighlimits|prefix=api-help}}\nThis message is used instead of {{msg-mw|right-apihighlimits}} in the API help to display the actual limits.\n\nParameters:\n* $1 - Limit for slow queries\n* $2 - Limit for fast queries", "api-help-open-in-apisandbox": "Text for the link to open an API example in [[Special:ApiSandbox]].", - "api-help-authmanager-general-usage": "Text giving a brief overview of how to use an AuthManager-using API module. Parameters:\n* $1 - Module parameter prefix, e.g. \"login\"\n* $2 - Module name, e.g. \"clientlogin\"\n* $3 - Module path, e.g. \"clientlogin\"\n* $4 - AuthManager action to use with this module.\n* $5 - Token type needed by the module.", + "api-help-authmanager-general-usage": "{{doc-important|Do not translate text that either quoted, or inside <nowiki><var></var></nowiki>, <nowiki><kbd></kbd></nowiki>, <nowiki><samp></samp></nowiki>, or <nowiki><code></code></nowiki> in this message.}}\nText giving a brief overview of how to use an AuthManager-using API module. Parameters:\n* $1 - Module parameter prefix, e.g. \"login\"\n* $2 - Module name, e.g. \"clientlogin\"\n* $3 - Module path, e.g. \"clientlogin\"\n* $4 - AuthManager action to use with this module.\n* $5 - Token type needed by the module.", "api-help-authmanagerhelper-requests": "{{doc-apihelp-param|description=the \"requests\" parameter for AuthManager-using API modules|params=* $1 - AuthManager action used by this module|paramstart=2|noseealso=1}}", "api-help-authmanagerhelper-request": "{{doc-apihelp-param|description=the \"request\" parameter for AuthManager-using API modules|params=* $1 - AuthManager action used by this module|paramstart=2|noseealso=1}}", "api-help-authmanagerhelper-messageformat": "{{doc-apihelp-param|description=the \"messageformat\" parameter for AuthManager-using API modules|noseealso=1}}", @@ -1486,6 +1492,7 @@ "apierror-blockedfrommail": "{{doc-apierror}}", "apierror-blocked": "{{doc-apierror}}", "apierror-botsnotsupported": "{{doc-apierror}}", + "apierror-cannot-async-upload-file": "{{doc-apierror}}", "apierror-cannotreauthenticate": "{{doc-apierror}}", "apierror-cannotviewtitle": "{{doc-apierror}}\n\nParameters:\n* $1 - Title.", "apierror-cantblock-email": "{{doc-apierror}}", @@ -1659,7 +1666,7 @@ "apiwarn-deprecation-parse-headitems": "{{doc-apierror}}", "apiwarn-deprecation-purge-get": "{{doc-apierror}}", "apiwarn-deprecation-withreplacement": "{{doc-apierror}}\n\nParameters:\n* $1 - Query string fragment that is deprecated, e.g. \"action=tokens\".\n* $2 - Query string fragment to use instead, e.g. \"action=tokens\".", - "apiwarn-difftohidden": "{{doc-apierror}}\n\nParameters:\n* $1 - Revision ID number.", + "apiwarn-difftohidden": "{{doc-apierror}}\n\nParameters:\n* $1 - Revision ID number.\n\n\"r\" is short for \"revision\". You may translate it.", "apiwarn-errorprinterfailed": "{{doc-apierror}}", "apiwarn-errorprinterfailed-ex": "{{doc-apierror}}\n\nParameters:\n* $1 - Exception message, which may already end in punctuation. Probably in English.", "apiwarn-invalidcategory": "{{doc-apierror}}\n\nParameters:\n* $1 - Supplied category name.", @@ -1689,6 +1696,7 @@ "apiwarn-wgDebugAPI": "{{doc-apierror}}", "api-feed-error-title": "Used as a feed item title when an error occurs in <kbd>action=feedwatchlist</kbd>.\n\nParameters:\n* $1 - API error code\n{{Identical|Error}}", "api-usage-docref": "\n\nParameters:\n* $1 - URL of the API auto-generated documentation.", + "api-usage-mailinglist-ref": "{{doc-apierror}} Also used in the error response.", "api-exception-trace": "\n\nParameters:\n* $1 - Exception class.\n* $2 - File from which the exception was thrown.\n* $3 - Line number from which the exception was thrown.\n* $4 - Exception backtrace.", "api-credits-header": "Header for the API credits section in the API help output\n{{Identical|Credit}}", "api-credits": "API credits text, displayed in the API help output" diff --git a/includes/api/i18n/ru.json b/includes/api/i18n/ru.json index a078f1bd91ac..671ac987fd83 100644 --- a/includes/api/i18n/ru.json +++ b/includes/api/i18n/ru.json @@ -22,13 +22,16 @@ "MaxBioHazard", "Kareyac", "Mailman", - "Ping08" + "Ping08", + "Ivan-r", + "Redredsonia", + "Alexey zakharenkov" ] }, - "apihelp-main-description": "<div class=\"hlist plainlinks api-main-links\">\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</div>\n<strong>Статус:</strong> Все отображаемые на этой странице функции должны работать, однако API находится в статусе активной разработки и может измениться в любой момент. Подпишитесь на [https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ почтовую рассылку mediawiki-api-announce], чтобы быть в курсе обновлений.\n\n<strong>Ошибочные запросы:</strong> Если API получает запрос с ошибкой, вернётся заголовок HTTP с ключом «MediaWiki-API-Error», после чего значение заголовка и код ошибки будут отправлены обратно и установлены в то же значение. Более подробную информацию см. [[mw:API:Errors_and_warnings|API: Ошибки и предупреждения]].\n\n<strong>Тестирование:</strong> для удобства тестирования API-запросов, см. [[Special:ApiSandbox]].", + "apihelp-main-description": "<div class=\"hlist plainlinks api-main-links\">\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</div>\n<strong>Статус:</strong> Все отображаемые на этой странице функции должны работать, однако API находится в статусе активной разработки и может измениться в любой момент. Подпишитесь на [https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ почтовую рассылку mediawiki-api-announce], чтобы быть в курсе обновлений.\n\n<strong>Ошибочные запросы:</strong> Если API получает запрос с ошибкой, вернётся заголовок HTTP с ключом «MediaWiki-API-Error», после чего значение заголовка и код ошибки будут отправлены обратно и установлены в то же значение. Более подробную информацию см. [[mw:Special:MyLanguage/API:Errors_and_warnings|API: Ошибки и предупреждения]].\n\n<strong>Тестирование:</strong> для удобства тестирования API-запросов, см. [[Special:ApiSandbox]].", "apihelp-main-param-action": "Действие, которое следует выполнить.", "apihelp-main-param-format": "Формат вывода.", - "apihelp-main-param-maxlag": "Значение максимального отставания может использоваться, когда MediaWiki устанавливается на кластер из реплицируемых баз данных. Чтобы избежать ухудшения ситуации с отставанием репликации сайта, этот параметр может заставить клиента ждать, когда задержка репликации станет ниже указанного значения. В случае чрезмерной задержки возвращается код ошибки «<samp>maxlag</samp>» с сообщением «<samp>Waiting for $host: $lag seconds lagged</samp>».<br />См. подробнее на странице с описанием [[mw:Manual:Maxlag_parameter|параметра maxlag]].", + "apihelp-main-param-maxlag": "Значение максимального отставания может использоваться, когда MediaWiki устанавливается на кластер из реплицируемых баз данных. Чтобы избежать ухудшения ситуации с отставанием репликации сайта, этот параметр может заставить клиента ждать, когда задержка репликации станет ниже указанного значения. В случае чрезмерной задержки возвращается код ошибки «<samp>maxlag</samp>» с сообщением «<samp>Waiting for $host: $lag seconds lagged</samp>».<br />См. подробнее на странице с описанием [[mw:Special:MyLanguage/Manual:Maxlag_parameter|Manual: параметра Maxlag ]].", "apihelp-main-param-smaxage": "Устанавливает значение HTTP-заголовка Cache-Control <code>s-maxage</code> в заданное число секунд. Ошибки никогда не кэшируются.", "apihelp-main-param-maxage": "Устанавливает значение HTTP-заголовка Cache-Control <code>s-maxage</code> в заданное число секунд. Ошибки никогда не кэшируются.", "apihelp-main-param-assert": "Удостовериться, что пользователь авторизован, если задано <kbd>user</kbd>, или что имеет права бота, если задано <kbd>bot</kbd>.", @@ -49,7 +52,7 @@ "apihelp-block-param-autoblock": "Автоматически блокировать последний использованный IP-адрес и все последующие, с которых будут совершаться попытки авторизации.", "apihelp-block-param-noemail": "Запретить участнику отправлять электронную почту через интерфейс вики. (Требуется право <code>blockemail</code>).", "apihelp-block-param-hidename": "Скрыть имя участника из журнала блокировок. (Требуется право <code>hideuser</code>).", - "apihelp-block-param-allowusertalk": "Позволяет участникам редактировать их собственные страницы обсуждения (зависит от <var>[[mw:Manual:$wgBlockAllowsUTEdit|$wgBlockAllowsUTEdit]]</var>).", + "apihelp-block-param-allowusertalk": "Позволяет участникам редактировать их собственные страницы обсуждения (зависит от <var>[[mw:Special:MyLanguage/Manual:$wgBlockAllowsUTEdit|$wgBlockAllowsUTEdit]]</var>).", "apihelp-block-param-reblock": "Если участник уже заблокирован, перезаписать существующую блокировку.", "apihelp-block-param-watchuser": "Следить за страницей пользователя или IP-участника и страницей обсуждения.", "apihelp-block-example-ip-simple": "Заблокировать IP-адрес <kbd>192.0.2.5</kbd> в течение трех дней с причиной <kbd>первого удара</kbd>.", @@ -74,7 +77,7 @@ "apihelp-compare-param-toid": "Второй идентификатор страницы для сравнения", "apihelp-compare-param-torev": "Вторая версия для сравнения", "apihelp-compare-example-1": "Создание различий между версиями 1 и 2.", - "apihelp-createaccount-description": "Создайте новую учетную запись Пользователя.", + "apihelp-createaccount-description": "Создайте новую учётную запись.", "apihelp-createaccount-param-preservestate": "Если <kbd>[[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]]</kbd> возвращается True для <samp>hasprimarypreservedstate</samp>, просит отмечен как <samp>основной-обязательно</samp> должен быть опущен. Если он возвращает непустое значение для <samp>preservedusername</samp>, что имя пользователя должно быть использовано для <var>пользователя</var> параметр.", "apihelp-createaccount-example-create": "Запустить процесс создания пользователя <kbd>пример</kbd> с паролем <kbd>ExamplePassword</kbd>.", "apihelp-createaccount-param-name": "Имя участника.", @@ -197,6 +200,7 @@ "apihelp-login-param-name": "Имя участника.", "apihelp-login-param-password": "Пароль.", "apihelp-login-param-domain": "Домен (необязательно).", + "apihelp-login-example-gettoken": "Получить токен входа.", "apihelp-login-example-login": "Войти", "apihelp-logout-description": "Выйти и очистить данные сессии.", "apihelp-mergehistory-description": "Объединение историй правок", @@ -231,8 +235,8 @@ "apihelp-parse-example-summary": "Һығымтаны тикшереү.", "apihelp-patrol-param-rcid": "Яңы үҙгәртеүҙәрҙе ҡарау идентификаторы.", "apihelp-patrol-param-revid": "Ҡарау версияһы идентификаторы.", - "apihelp-patrol-example-rcid": "Һуңғы үҙгәрештәрҙе ҡарау.", - "apihelp-patrol-example-revid": "Яңынан ҡарау.", + "apihelp-patrol-example-rcid": "Патрулировать недавние изменения.", + "apihelp-patrol-example-revid": "Патрулировать версию.", "apihelp-protect-description": "Изменить уровень защиты страницы.", "apihelp-protect-param-title": "Бит атамаһы. $1pageid менән бергә ҡулланылмай.", "apihelp-protect-param-reason": "(ООН) һағы сәбәптәре.", @@ -240,7 +244,7 @@ "apihelp-protect-example-unprotect": "Снять защиту страницы, установив ограничения <kbd>all</kbd> (т. е. любой желающий может принять меры).", "apihelp-protect-example-unprotect2": "Бер ниндәй сикләүҙәр ҡуймай биттән һаҡлауҙы алырға.", "apihelp-purge-param-forcelinkupdate": "Обновление связей таблиц.", - "apihelp-purge-param-forcerecursivelinkupdate": "Һылтанманы һәм таблицаны яңыртығыҙ һәм был битте шаблон итеп ҡулланған башҡа биттәр өсөн һылтанмаларҙы ла яңыртығыҙ.", + "apihelp-purge-param-forcerecursivelinkupdate": "Обновить таблицу ссылок для данной страницы, а также всех страниц, использующих данную как шаблон.", "apihelp-purge-example-generator": "Продувка первые 10 страниц в основном пространстве имен.", "apihelp-query-param-list": "Какие списки использовать", "apihelp-query-param-meta": "Какие метаданные использовать", @@ -328,6 +332,7 @@ "apihelp-query+revisions+base-param-limit": "Ограничение на количество версий которое будут вовзращено", "apihelp-query+search-description": "Выполнить полнотекстовый поиск.", "apihelp-query+siteinfo-paramvalue-prop-fileextensions": "Возвращает список расширений (типы файлов), которые доступны к загрузке", + "apihelp-query+siteinfo-paramvalue-prop-languagevariants": "Возвращает список кодов языков, для которых включён [[mw:Special:MyLanguage/LanguageConverter|LanguageConverter]], а также варианты,поддерживаемые для каждого языка.", "apihelp-query+tags-description": "Список изменерий тегов.", "apihelp-query+tags-example-simple": "Лист доступных тегов", "apihelp-query+templates-param-namespace": "Показывать шаблоны только из данного списка имен", @@ -336,8 +341,8 @@ "apihelp-query+usercontribs-description": "Получить все правки пользователя", "apihelp-revisiondelete-description": "удалить и восстановить редакции", "apihelp-stashedit-param-sectiontitle": "Заголовок нового раздела.", - "apihelp-unblock-description": "Разблокировать пользователя.", - "apihelp-unblock-param-user": "Имя участника, IP-адрес или диапазон IP-адресов, которые вы хотите разблокировать. Нельзя использовать одновременно с <var>$1id</var> или <var>$luserid</var>.", + "apihelp-unblock-description": "Разблокировать участника.", + "apihelp-unblock-param-user": "Имя участника, IP-адрес или диапазон IP-адресов, которые вы хотите разблокировать. Нельзя использовать одновременно с <var>$1id</var> или <var>$1userid</var>.", "apihelp-unblock-param-userid": "ID участника, которого вы хотите разблокировать. Нельзя использовать одновременно с <var>$1id</var> или <var>$1user</var>.", "apihelp-unblock-param-reason": "Причина разблокировки", "apihelp-unblock-example-id": "Разблокировать блок с идентификатором #<kbd>105</kbd>.", @@ -356,7 +361,7 @@ "apihelp-upload-param-chunk": "Кусок содержимого.", "apihelp-upload-example-url": "Загрузить через URL", "apihelp-userrights-description": "Изменить членство в группе пользователей.", - "apihelp-userrights-param-user": "Имя пользователя", + "apihelp-userrights-param-user": "Имя учётной записи.", "apihelp-userrights-param-userid": "Идентификатор пользователя.", "apihelp-userrights-param-add": "Добавить пользователя в эти группы.", "apihelp-userrights-param-remove": "Удалить пользователя из этих групп.", @@ -409,9 +414,10 @@ "api-help-param-continue": "Когда доступно больше результатов, использовать этот чтобы продолжить.", "api-help-param-no-description": "<span class=\"apihelp-empty\">(описание отсутствует)</span>", "api-help-examples": "Пример{{PLURAL:$1||ы}}:", - "api-help-permissions": "{{PLURAL:$1|Permission|Permissions}}:", + "api-help-permissions": "{{PLURAL:$1|Разрешение|Разрешения}}:", "api-help-permissions-granted-to": "{{PLURAL:$1|Granted to}}: $2", "apierror-integeroutofrange-abovemax": "<var>$1</var> не может быть более $2 (на $3) для пользователей.", + "apierror-invalidoldimage": "Параметр <var>oldimage</var> имеет недопустимый формат.", "apierror-nosuchuserid": "Нет пользователя с ID $1.", "apierror-pagelang-disabled": "Меняется язык страницы не допускается в этой Вики.", "apierror-protect-invalidaction": "Недопустимый тип защиты \"$1\".", diff --git a/includes/api/i18n/sv.json b/includes/api/i18n/sv.json index b71c88e0b6bb..fb6698f0a875 100644 --- a/includes/api/i18n/sv.json +++ b/includes/api/i18n/sv.json @@ -14,7 +14,8 @@ "VickyC", "Josve05a", "Rockyfelle", - "Macofe" + "Macofe", + "Magol" ] }, "apihelp-main-description": "<div class=\"hlist plainlinks api-main-links\">\n* [[mw:API:Main_page|Dokumentation]]\n* [[mw:API:FAQ|FAQ]]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api E-postlista]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce API-aviseringar]\n* [https://phabricator.wikimedia.org/maniphest/query/GebfyV4uCaLd/#R|Buggar & förslag]\n</div>\n<strong>Status:</strong> Alla funktioner som visas på denna sida borde fungera. API:et är dock fortfarande under aktiv utveckling och kan ändras när som helst. Prenumerera på [https://lists.wikimedia.org/pipermail/mediawiki-api-announce/mediawiki-api-announce e-postlistan] för att få aviseringar om uppdateringar.\n\n<strong>Felaktiga förfrågningar:</strong> När felaktiga förfrågningar skickas till API:et skickas en HTTP-header med nyckeln \"MediaWiki-API-Error\" och sedan sätts både värdet på headern och den felkoden som returneras till samma värde. För mer information läs [[mw:API:Errors_and_warnings|API: Fel och varningar]].", @@ -23,9 +24,11 @@ "apihelp-main-param-smaxage": "Ange headervärdet <code>s-maxage</code> till så här många sekunder. Fel cachelagras aldrig.", "apihelp-main-param-maxage": "Ange headervärdet <code>max-age</code> till så här många sekunder. Fel cachelagras aldrig.", "apihelp-main-param-assert": "Bekräfta att användaren är inloggad om satt till <kbd>user</kbd>, eller har bot-användarrättigheter om satt till <kbd>bot</kbd>.", + "apihelp-main-param-assertuser": "Verifiera att den nuvarande användaren är den namngivne användaren.", "apihelp-main-param-requestid": "Alla värde som anges här kommer att inkluderas i svaret. Kan användas för att särskilja förfrågningar.", "apihelp-main-param-servedby": "Inkludera det värdnamn som besvarade förfrågan i resultatet.", "apihelp-main-param-curtimestamp": "Inkludera den aktuella tidsstämpeln i resultatet.", + "apihelp-main-param-responselanginfo": "Inkluderar de språk som används för <var>uselang</var> och <var>errorlang</var> i resultatet.", "apihelp-main-param-origin": "När API:et används genom en cross-domain AJAX-begäran (CORS), ange detta till den ursprungliga domänen. Detta måste inkluderas i alla pre-flight-begäran, och mpste därför vara en del av den begärda URI:n (inte i POST-datat). Detta måste överensstämma med en av källorna i headern <code>Origin</code> exakt, så den måste sättas till något i stil med <kbd>http://en.wikipedia.org</kbd> eller <kbd>https://meta.wikimedia.org</kbd>. Om denna parameter inte överensstämmer med headern <code>Origin</code>, returneras ett 403-svar. Om denna parameter överensstämmer med headern <code>Origin</code> och källan är vitlistad, sätts en <code>Access-Control-Allow-Origin</code>-header.", "apihelp-main-param-uselang": "Språk som ska användas för meddelandeöversättningar. En lista med koder kan hämtas från <kbd>[[Special:ApiHelp/query+siteinfo|action=query&meta=siteinfo]]</kbd> med <kbd>siprop=languages</kbd>, eller ange <kbd>user</kbd> för att använda den aktuella användarens språkpreferenser, eller ange <kbd>content</kbd> för att använda innehållsspråket.", "apihelp-block-description": "Blockera en användare.", @@ -254,6 +257,7 @@ "apihelp-patrol-example-revid": "Patrullera en sidversion", "apihelp-protect-description": "Ändra skyddsnivån för en sida.", "apihelp-protect-example-protect": "Skydda en sida", + "apihelp-purge-description": "Rensa cachen för angivna titlar.", "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 <kbd>API/</kbd>.", @@ -411,6 +415,7 @@ "apihelp-query+protectedtitles-example-simple": "Lista skyddade titlar.", "apihelp-query+recentchanges-example-simple": "Lista de senaste ändringarna.", "apihelp-query+revisions-example-first5-not-localhost": "Hämta första 5 revideringarna av \"huvudsidan\" och som inte gjorts av anonym användare \"127.0.0.1\"", + "apihelp-query+siteinfo-paramvalue-prop-languagevariants": "Returnerar en lista över språkkoder som [[mw:LanguageConverter|LanguageConverter]] har aktiverat och de varianter som varje stöder.", "apihelp-query+siteinfo-example-simple": "Hämta information om webbplatsen.", "apihelp-query+stashimageinfo-description": "Returnerar filinformation för temporära filer.", "apihelp-query+stashimageinfo-param-filekey": "Nyckel som identifierar en tidigare uppladdning som lagrats temporärt.", @@ -426,8 +431,8 @@ "apihelp-query+watchlistraw-example-simple": "Lista sidor på den aktuella användarens bevakningslista.", "apihelp-setnotificationtimestamp-example-all": "Återställ meddelandestatus för hela bevakningslistan.", "apihelp-stashedit-param-summary": "Ändra sammanfattning.", - "apihelp-unblock-param-id": "ID för blockeringen att häva (hämtas genom <kbd>list=blocks</kbd>). Kan inte användas tillsammans med <var>$1user</var> eller <var>$luserid</var>.", - "apihelp-unblock-param-user": "Användarnamn, IP-adresser eller IP-adressintervall att häva blockering för. Kan inte användas tillsammans med <var>$1id</var> eller <var>$luserid</var>.", + "apihelp-unblock-param-id": "ID för blockeringen att häva (hämtas genom <kbd>list=blocks</kbd>). Kan inte användas tillsammans med <var>$1user</var> eller <var>$1userid</var>.", + "apihelp-unblock-param-user": "Användarnamn, IP-adresser eller IP-adressintervall att häva blockering för. Kan inte användas tillsammans med <var>$1id</var> eller <var>$1userid</var>.", "apihelp-unblock-param-userid": "Användar-ID att häva blockering för. Kan inte användas tillsammans med <var>$1id</var> eller <var>$1user</var>.", "apihelp-upload-param-filekey": "Nyckel som identifierar en tidigare uppladdning som lagrats temporärt.", "apihelp-upload-param-stash": "Om angiven, kommer servern att temporärt lagra filen istället för att lägga till den i centralförvaret.", @@ -450,6 +455,8 @@ "api-help-param-multi-separate": "Separera värden med <kbd>|</kbd> eller [[Special:ApiHelp/main#main/datatypes|alternativ]].", "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 <var>oldimage</var> har ett ogiltigt format.", + "apierror-invalidsection": "Parametern <var>section</var> måste vara ett giltigt avsnitts-ID eller <kbd>new</kbd>.", "apierror-nosuchuserid": "Det finns ingen användare med ID $1.", "apierror-protect-invalidaction": "Ogiltig skyddstyp \"$1\".", "apierror-systemblocked": "Du har blockerats automatiskt av MediaWiki.", diff --git a/includes/api/i18n/uk.json b/includes/api/i18n/uk.json index 01a0d3354615..78f16d3298be 100644 --- a/includes/api/i18n/uk.json +++ b/includes/api/i18n/uk.json @@ -329,6 +329,7 @@ "apihelp-parse-paramvalue-prop-limitreportdata": "Дає звіт по обмеженнях у структурованому вигляді. Не видає даних, якщо встановлено <var>$1disablelimitreport</var>.", "apihelp-parse-paramvalue-prop-limitreporthtml": "Дає HTML-версію звіту по обмеженнях. Не видає даних, якщо встановлено <var>$1disablelimitreport</var>.", "apihelp-parse-paramvalue-prop-parsetree": "Синтаксичне дерево XML вмісту версії (передбачає модель вмісту <code>$1</code>)", + "apihelp-parse-paramvalue-prop-parsewarnings": "Виводить попередження, які з'явилися при обробці контенту.", "apihelp-parse-param-pst": "Зробіть трансформацію вхідних даних перед збереженням і аналізом. Дійсне лише при використанні з текстом.", "apihelp-parse-param-onlypst": "Зробіть трансформацію вхідних даних перед збереженням (PST), але не аналізуйте. Видає той самий вікітекст, після застосування PST. Дійсне лише у разі використання з <var>$1text</var>.", "apihelp-parse-param-effectivelanglinks": "Включає мовні посилання, додані розширеннями (для використання з <kbd>$1prop=langlinks</kbd>).", @@ -367,7 +368,7 @@ "apihelp-protect-example-protect": "Захистити сторінку.", "apihelp-protect-example-unprotect": "Зняти захист зі сторінки, встановивши обмеження для <kbd>all</kbd> (тобто будь-хто зможе робити дії).", "apihelp-protect-example-unprotect2": "Зняти захист з сторінки, встановивши відсутність обмежень.", - "apihelp-purge-description": "Очистити кеш для вказаних заголовків.\n\nВимагає запиту POST, якщо користувач не ввійшов у систему.", + "apihelp-purge-description": "Очистити кеш для вказаних заголовків.", "apihelp-purge-param-forcelinkupdate": "Оновити таблиці посилань.", "apihelp-purge-param-forcerecursivelinkupdate": "Оновити таблицю посилань, і оновити таблиці посилань для кожної сторінки, що використовує цю сторінку як шаблон.", "apihelp-purge-example-simple": "Очистити кеш <kbd>Main Page</kbd> і сторінки <kbd>API</kbd>.", @@ -1063,6 +1064,7 @@ "apihelp-query+siteinfo-paramvalue-prop-rightsinfo": "Видає інформацію щодо прав (ліцензії) вікі, якщо наявна.", "apihelp-query+siteinfo-paramvalue-prop-restrictions": "Видає інформацію про наявні типи обмежень (захисту).", "apihelp-query+siteinfo-paramvalue-prop-languages": "Видає список мов, які підтримує MediaWiki (за бажанням локалізовані через <var>$1inlanguagecode</var>).", + "apihelp-query+siteinfo-paramvalue-prop-languagevariants": "Виводить список кодів мов, для яких увімкнено [[mw:LanguageConverter|LanguageConverter]], а також варіанти, підтримувані кожною з цих мов.", "apihelp-query+siteinfo-paramvalue-prop-skins": "Видає список усіх доступних тем оформлення (опціонально локалізовані з використанням <var>$1inlanguagecode</var>, в іншому разі — мовою вмісту).", "apihelp-query+siteinfo-paramvalue-prop-extensiontags": "Видає список теґів розширення парсеру.", "apihelp-query+siteinfo-paramvalue-prop-functionhooks": "Видає список гуків парсерних функцій.", @@ -1145,6 +1147,7 @@ "apihelp-query+userinfo-paramvalue-prop-blockinfo": "Позначає, чи поточний користувач заблокований, ким, з якої причини.", "apihelp-query+userinfo-paramvalue-prop-hasmsg": "Додає мітку <samp>messages</samp>, якщо у користувача є непроглянуті повідомлення.", "apihelp-query+userinfo-paramvalue-prop-groups": "Перелічує усі групи, до яких належить поточний користувач.", + "apihelp-query+userinfo-paramvalue-prop-groupmemberships": "Перелічити групи, в які поточний користувач безпосередньо входить, а також термін дії членств.", "apihelp-query+userinfo-paramvalue-prop-implicitgroups": "Перелічує усі групи, до яких поточний користувач належить автоматично.", "apihelp-query+userinfo-paramvalue-prop-rights": "Перелічує усі права, які має поточний користувач.", "apihelp-query+userinfo-paramvalue-prop-changeablegroups": "Перелічує групи, у які користувач може додавати і з яких вилучати.", @@ -1165,6 +1168,7 @@ "apihelp-query+users-param-prop": "Яку інформацію включити:", "apihelp-query+users-paramvalue-prop-blockinfo": "Мітки про те чи є користувач заблокованим, ким, і з якою причиною.", "apihelp-query+users-paramvalue-prop-groups": "Перелічує всі групи, до яких належить кожен з користувачів.", + "apihelp-query+users-paramvalue-prop-groupmemberships": "Перелічити групи, в які користувачі безпосередньо входять, а також термін дії членств.", "apihelp-query+users-paramvalue-prop-implicitgroups": "Перелічує всі групи, членом яких користувач є автоматично.", "apihelp-query+users-paramvalue-prop-rights": "Перелічує всі права, які має кожен з користувачів.", "apihelp-query+users-paramvalue-prop-editcount": "Додає лічильник редагувань користувача.", @@ -1301,14 +1305,14 @@ "apihelp-tokens-example-edit": "Отримати жетон редагування (за замовчуванням).", "apihelp-tokens-example-emailmove": "Отримати жетон електронної пошти та жетон перейменування.", "apihelp-unblock-description": "Розблокувати користувача.", - "apihelp-unblock-param-id": "Ідентифікатор блоку чи розблокування (отриманий через <kbd>list=blocks</kbd>). Не може бути використано разом із <var>$1user</var> або <var>$luserid</var>.", - "apihelp-unblock-param-user": "Ім'я користувача, IP-адреса чи IP-діапазон до розблокування. Не може бути використано разом із <var>$1id</var> або <var>$luserid</var>.", + "apihelp-unblock-param-id": "Ідентифікатор блоку чи розблокування (отриманий через <kbd>list=blocks</kbd>). Не може бути використано разом із <var>$1user</var> або <var>$1userid</var>.", + "apihelp-unblock-param-user": "Ім'я користувача, IP-адреса чи IP-діапазон до розблокування. Не може бути використано разом із <var>$1id</var> або <var>$1userid</var>.", "apihelp-unblock-param-userid": "Ідентифікатор користувача до розблокування. Не може бути використано разом із <var>$1id</var> або <var>$1user</var>.", "apihelp-unblock-param-reason": "Причина розблокування.", "apihelp-unblock-param-tags": "Змінити теги, що мають бути застосовані до запису в журналі блокувань.", "apihelp-unblock-example-id": "Зняти блокування з ідентифікатором #<kbd>105</kbd>.", "apihelp-unblock-example-user": "Розблокувати користувача <kbd>Bob</kbd> з причиною <kbd>Sorry Bob</kbd>.", - "apihelp-undelete-description": "Відновити версії вилученої сторінки.\n\nСписок вилучених версій (включено з часовими мітками) може бути отримано через [[Special:ApiHelp/query+deletedrevs|list=deletedrevs]], а список ідентифікаторів вилучених файлів може бути отримано через [[Special:ApiHelp/query+filearchive|list=filearchive]].", + "apihelp-undelete-description": "Відновити версії вилученої сторінки.\n\nСписок вилучених версій (включено з часовими мітками) може бути отримано через [[Special:ApiHelp/query+deletedrevisions|prop=deletedrevisions]], а список ідентифікаторів вилучених файлів може бути отримано через [[Special:ApiHelp/query+filearchive|list=filearchive]].", "apihelp-undelete-param-title": "Назва сторінки, яку слід відновити.", "apihelp-undelete-param-reason": "Причина відновлення.", "apihelp-undelete-param-tags": "Змінити теги, що мають бути застосовані до запису в журналі вилучень.", @@ -1342,12 +1346,14 @@ "apihelp-userrights-description": "Змінити членство користувача у групах.", "apihelp-userrights-param-user": "Ім'я користувача.", "apihelp-userrights-param-userid": "Ідентифікатор користувача.", - "apihelp-userrights-param-add": "Додати користувача до цих груп.", + "apihelp-userrights-param-add": "Додати користувача до цих груп. Якщо він вже є членом групи, оновити термін дії членства.", + "apihelp-userrights-param-expiry": "Часові мітки, коли завершується членство. Можуть бути відносними (наприклад, <kbd>5 months</kbd> або <kbd>2 weeks</kbd>) або абсолютними (як <kbd>2014-09-18T12:34:56Z</kbd>). Якщо задано тільки оду часову мітку, вона буде стосуватися всіх груп, переданих параметром <var>$1add</var>. Використовуйте <kbd>infinite</kbd>, <kbd>indefinite</kbd>, <kbd>infinity</kbd> або <kbd>never</kbd>, щоб задати безстрокове членство.", "apihelp-userrights-param-remove": "Вилучити користувача із цих груп.", "apihelp-userrights-param-reason": "Причина зміни.", "apihelp-userrights-param-tags": "Змінити теги для застосування до запису в журналі зміни прав користувача.", "apihelp-userrights-example-user": "Додати користувача <kbd>FooBot</kbd> до групи <kbd>bot</kbd> та вилучити із груп <kbd>sysop</kbd> та <kbd>bureaucrat</kbd>.", "apihelp-userrights-example-userid": "Додати користувача з ідентифікатором <kbd>123</kbd> до групи <kbd>bot</kbd> та вилучити із груп <kbd>sysop</kbd> та <kbd>bureaucrat</kbd>.", + "apihelp-userrights-example-expiry": "Додати користувача <kbd>SometimeSysop</kbd> в групу <kbd>sysop</kbd> на 1 місяць.", "apihelp-validatepassword-description": "Перевірити пароль на предмет відповідності політикам вікі щодо паролів.\n\nРезультати перевірки вказуються як <samp>Good</samp> якщо пароль прийнятний, <samp>Change</samp> якщо пароль може використовуватись для входу, але його треба змінити, і <samp>Invalid</samp> — якщо пароль використовувати не можна.", "apihelp-validatepassword-param-password": "Пароль до перевірки.", "apihelp-validatepassword-param-user": "Ім'я користувача, для використання при тестуванні створення облікового запису. Вказаний користувач не повинен існувати.", @@ -1479,6 +1485,7 @@ "apierror-blockedfrommail": "Вам заблоковано можливість надсилання електронної пошти.", "apierror-blocked": "Вам заблоковано можливість редагування.", "apierror-botsnotsupported": "Інтерфейс не підтримується для ботів.", + "apierror-cannot-async-upload-file": "Параметри <var>async</var> та <var>file</var> не можна поєднувати. Якщо Ви хочете, щоб завантажений Вами файл був опрацьований асинхронно, спершу завантажте його у сховок (використавши параметр <var>stash</var>), а тоді опублікуйте цей підготовлений файл (використавши <var>filekey</var> та <var>async</var>).", "apierror-cannotreauthenticate": "Ця діє недоступна, оскільки Вашу ідентичність неможливо перевірити.", "apierror-cannotviewtitle": "Ви не маєте дозволу на перегляд $1.", "apierror-cantblock-email": "Ви не маєте прав на блокування користувачам можливості надсилання електронної пошти через вікі.", @@ -1521,12 +1528,12 @@ "apierror-invalidexpiry": "Недійсний час завершення «$1».", "apierror-invalid-file-key": "Недійсний ключ файлу.", "apierror-invalidlang": "Недійсний код мови для параметра <var>$1</var>.", - "apierror-invalidoldimage": "Параметр «oldimage» має недійсний формат.", + "apierror-invalidoldimage": "Параметр <var>oldimage</var> має недійсний формат.", "apierror-invalidparammix-cannotusewith": "Параметр <kbd>$1</kbd> не можна використовувати з <kbd>$2</kbd>.", "apierror-invalidparammix-mustusewith": "Параметр <kbd>$1</kbd> можна використовувати тільки з <kbd>$2</kbd>.", "apierror-invalidparammix-parse-new-section": "<kbd>section=new</kbd> не можна поєднувати з параметрами <var>oldid</var>, <var>pageid</var> чи <var>page</var>. Будь ласка, використовуйте <var>title</var> і <var>text</var>.", "apierror-invalidparammix": "{{PLURAL:$2|Ці параметри}} $1 не можна використовувати водночас.", - "apierror-invalidsection": "Параметр «section» має бути дійсним ідентифікатором розділу або <kbd>new</kbd>.", + "apierror-invalidsection": "Параметр <var>section</var> має бути дійсним ідентифікатором розділу або <kbd>new</kbd>.", "apierror-invalidsha1base36hash": "Поданий хеш SHA1Base36 недійсний.", "apierror-invalidsha1hash": "Поданий хеш SHA1 недійсний.", "apierror-invalidtitle": "Погана назва «$1».", @@ -1667,9 +1674,9 @@ "apiwarn-redirectsandrevids": "Вирішення перенаправлень не може використовуватись разом з параметром <var>revids</var>. Усі перенаправлення, на які вказує <var>revids</var>, не було вирішено.", "apiwarn-tokennotallowed": "Дія «$1» недозволена для поточного користувача.", "apiwarn-tokens-origin": "Токени не можна отримати, поки не застосована політика одного походження.", - "apiwarn-toomanyvalues": "Надто багато значень задано для параметра <var>$1</var>: ліміт становить $2.", + "apiwarn-toomanyvalues": "Надто багато значень задано для параметра <var>$1</var>. Ліміт становить $2.", "apiwarn-truncatedresult": "Цей результат було скорочено, оскільки інакше він перевищив би ліміт у $1 байтів.", - "apiwarn-unclearnowtimestamp": "Вказування «$2» для параметра мітки часу <var>$1</var> є застарілим. Якщо з якоїсь причини Вам треба чітко вказати поточний час без вираховування його з боку клієнта, використайте <kbd>now<kbd>.", + "apiwarn-unclearnowtimestamp": "Вказування «$2» для параметра мітки часу <var>$1</var> є застарілим. Якщо з якоїсь причини Вам треба чітко вказати поточний час без вираховування його з боку клієнта, використайте <kbd>now</kbd>.", "apiwarn-unrecognizedvalues": "{{PLURAL:$3|Нерозпізнане|Нерозпізнані}} значення для параметра <var>$1</var>: $2.", "apiwarn-unsupportedarray": "Параметр <var>$1</var> використовує непідтримуваний синтаксис PHP-масиву.", "apiwarn-urlparamwidth": "Ігнорування значення ширини, встановленого в <var>$1urlparam</var> ($2) на користь значення ширини, запозиченого із <var>$1urlwidth</var>/<var>$1urlheight</var> ($3).", @@ -1681,6 +1688,7 @@ "apiwarn-wgDebugAPI": "<strong>Попередження щодо безпеки</strong>: увімкнено <var>$wgDebugAPI</var>.", "api-feed-error-title": "Помилка ($1)", "api-usage-docref": "Див. $1 щодо використання API.", + "api-usage-mailinglist-ref": "Щоб взнавати про заплановані і остаточні критичні зміни API, підпишіться на розсилку mediawiki-api-announce тут: <https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce>.", "api-exception-trace": "$1 у $2($3)\n$4", "api-credits-header": "Автор(и)", "api-credits": "Розробники API:\n* Roan Kattouw (головний розробник вер. 2007–2009)\n* Victor Vasiliev\n* Bryan Tong Minh\n* Sam Reed\n* Yuri Astrakhan (творець, головний розробник вер. 2006 – вер. 2007)\n* Brad Jorsch (головний розробник 2013 – тепер)\n\nБудь ласка, надсилайте свої коментарі, пропозиції та запитання на mediawiki-api@lists.wikimedia.org\nабо зафайліть звіт про баґ на https://phabricator.wikimedia.org/." diff --git a/includes/api/i18n/zh-hans.json b/includes/api/i18n/zh-hans.json index c376573dccb2..152f1df2643b 100644 --- a/includes/api/i18n/zh-hans.json +++ b/includes/api/i18n/zh-hans.json @@ -25,10 +25,10 @@ "D41D8CD98F" ] }, - "apihelp-main-description": "<div class=\"hlist plainlinks api-main-links\">\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</div>\n<strong>状态信息:</strong>本页所展示的所有特性都应正常工作,但是API仍在开发当中,将会随时变化。请订阅[https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ mediawiki-api-announce 邮件列表]以便获得更新通知。\n\n<strong>错误请求:</strong>当API收到错误请求时,HTTP header将会返回一个包含\"MediaWiki-API-Error\"的值,随后header的值与error code将会送回并设置为相同的值。详细信息请参阅[[mw:API:Errors_and_warnings|API: 错误与警告]]。\n\n<strong>测试中:</strong>测试API请求的易用性,请参见[[Special:ApiSandbox]]。", + "apihelp-main-description": "<div class=\"hlist plainlinks api-main-links\">\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</div>\n<strong>状态信息:</strong>本页所展示的所有特性都应正常工作,但是API仍在开发当中,将会随时变化。请订阅[https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ mediawiki-api-announce 邮件列表]以便获得更新通知。\n\n<strong>错误请求:</strong>当API收到错误请求时,HTTP header将会返回一个包含\"MediaWiki-API-Error\"的值,随后header的值与error code将会送回并设置为相同的值。详细信息请参阅[[mw:Special:MyLanguage/API:Errors_and_warnings|API: 错误与警告]]。\n\n<strong>测试中:</strong>测试API请求的易用性,请参见[[Special:ApiSandbox]]。", "apihelp-main-param-action": "要执行的操作。", "apihelp-main-param-format": "输出的格式。", - "apihelp-main-param-maxlag": "最大延迟可被用于MediaWiki安装于数据库复制集中。要保存导致更多网站复制延迟的操作,此参数可使客户端等待直到复制延迟少于指定值时。万一发生过多延迟,错误代码<samp>maxlag</samp>会返回消息,例如<samp>等待$host中:延迟$lag秒</samp>。<br />参见[[mw:Manual:Maxlag_parameter|Manual: Maxlag parameter]]以获取更多信息。", + "apihelp-main-param-maxlag": "最大延迟可被用于MediaWiki安装于数据库复制集中。要保存导致更多网站复制延迟的操作,此参数可使客户端等待直到复制延迟少于指定值时。万一发生过多延迟,错误代码<samp>maxlag</samp>会返回消息,例如<samp>等待$host中:延迟$lag秒</samp>。<br />参见[[mw:Special:MyLanguage/Manual:Maxlag_parameter|Manual: Maxlag parameter]]以获取更多信息。", "apihelp-main-param-smaxage": "设置<code>s-maxage</code> HTTP缓存控制头至这些秒。错误不会缓存。", "apihelp-main-param-maxage": "设置<code>max-age</code> HTTP缓存控制头至这些秒。错误不会缓存。", "apihelp-main-param-assert": "如果设置为<kbd>user</kbd>就验证用户是否登录,或如果设置为<kbd>bot</kbd>就验证是否有机器人用户权限。", @@ -52,7 +52,7 @@ "apihelp-block-param-autoblock": "自动封禁最近使用的IP地址,以及以后他们尝试登陆使用的IP地址。", "apihelp-block-param-noemail": "阻止用户通过wiki发送电子邮件。(需要<code>blockemail</code>权限)。", "apihelp-block-param-hidename": "从封禁日志中隐藏用户名。(需要<code>hideuser</code>权限)。", - "apihelp-block-param-allowusertalk": "允许用户编辑自己的讨论页(取决于<var>[[mw:Manual:$wgBlockAllowsUTEdit|$wgBlockAllowsUTEdit]]</var>)。", + "apihelp-block-param-allowusertalk": "允许用户编辑自己的讨论页(取决于<var>[[mw:Special:MyLanguage/Manual:$wgBlockAllowsUTEdit|$wgBlockAllowsUTEdit]]</var>)。", "apihelp-block-param-reblock": "如果该用户已被封禁,则覆盖已有的封禁。", "apihelp-block-param-watchuser": "监视用户或该 IP 的用户页和讨论页。", "apihelp-block-param-tags": "要在封禁日志中应用到实体的更改标签。", @@ -284,7 +284,7 @@ "apihelp-opensearch-param-search": "搜索字符串。", "apihelp-opensearch-param-limit": "要返回的结果最大数。", "apihelp-opensearch-param-namespace": "搜索的名字空间。", - "apihelp-opensearch-param-suggest": "如果<var>[[mw:Manual:$wgEnableOpenSearchSuggest|$wgEnableOpenSearchSuggest]]</var>设置为false则不做任何事情。", + "apihelp-opensearch-param-suggest": "如果<var>[[mw:Special:MyLanguage/Manual:$wgEnableOpenSearchSuggest|$wgEnableOpenSearchSuggest]]</var>设置为false则不做任何事情。", "apihelp-opensearch-param-redirects": "如何处理重定向:\n;return:返回重定向本身。\n;resolve:返回目标页面。可能返回少于$1limit个结果。\n由于历史原因,$1format=json默认为\"return\",其他格式默认为\"resolve\"。", "apihelp-opensearch-param-format": "输出格式。", "apihelp-opensearch-param-warningsaserror": "如果警告通过<kbd>format=json</kbd>提升,返回一个API错误而不是忽略它们。", @@ -339,6 +339,7 @@ "apihelp-parse-paramvalue-prop-limitreportdata": "以结构化的方式提供限制报告。如果<var>$1disablelimitreport</var>被设定则不提供数据。", "apihelp-parse-paramvalue-prop-limitreporthtml": "提供限制报告的HTML版本。当<var>$1disablelimitreport</var>被设置时不会提供数据。", "apihelp-parse-paramvalue-prop-parsetree": "修订内容的XML解析树(需要内容模型<code>$1</code>)", + "apihelp-parse-paramvalue-prop-parsewarnings": "在解析内容时提供发生的警告", "apihelp-parse-param-pst": "在解析输入前,对输入做一次保存前变换处理。仅当使用文本时有效。", "apihelp-parse-param-onlypst": "在输入内容中执行预保存转换(PST),但不解析它。在PST被应用后返回相同的wiki文本。只当与<var>$1text</var>一起使用时有效。", "apihelp-parse-param-effectivelanglinks": "包含由扩展提供的语言链接(用于与<kbd>$1prop=langlinks</kbd>一起使用)。", @@ -377,7 +378,7 @@ "apihelp-protect-example-protect": "保护一个页面。", "apihelp-protect-example-unprotect": "通过设置限制为<kbd>all</kbd>解除保护一个页面(就是说任何人都可以执行操作)。", "apihelp-protect-example-unprotect2": "通过设置没有限制解除保护一个页面。", - "apihelp-purge-description": "为指定标题刷新缓存。\n\n如果用户尚未登录的话,就需要POST请求。", + "apihelp-purge-description": "为指定标题刷新缓存。", "apihelp-purge-param-forcelinkupdate": "更新链接表。", "apihelp-purge-param-forcerecursivelinkupdate": "更新链接表中,并更新任何使用此页作为模板的页面的链接表。", "apihelp-purge-example-simple": "刷新<kbd>Main Page</kbd>和<kbd>API</kbd>页面。", @@ -418,7 +419,7 @@ "apihelp-query+alldeletedrevisions-param-user": "只列出此用户做出的修订。", "apihelp-query+alldeletedrevisions-param-excludeuser": "不要列出此用户做出的修订。", "apihelp-query+alldeletedrevisions-param-namespace": "只列出此名字空间的页面。", - "apihelp-query+alldeletedrevisions-param-miser-user-namespace": "<strong>注意:</strong>由于[[mw:Manual:$wgMiserMode|miser模式]],同时使用<var>$1user</var>和<var>$1namespace</var>将导致继续前返回少于<var>$1limit</var>个结果,在极端条件下可能不返回任何结果。", + "apihelp-query+alldeletedrevisions-param-miser-user-namespace": "<strong>注意:</strong>由于[[mw:Special:MyLanguage/Manual:$wgMiserMode|miser模式]],同时使用<var>$1user</var>和<var>$1namespace</var>将导致继续前返回少于<var>$1limit</var>个结果,在极端条件下可能不返回任何结果。", "apihelp-query+alldeletedrevisions-param-generatetitles": "当作为生成器使用时,生成标题而不是修订ID。", "apihelp-query+alldeletedrevisions-example-user": "列出由<kbd>Example</kbd>作出的最近50次已删除贡献。", "apihelp-query+alldeletedrevisions-example-ns-main": "列出前50次已删除的主名字空间修订。", @@ -737,7 +738,7 @@ "apihelp-query+filearchive-paramvalue-prop-archivename": "添加用于非最新版本的存档版本的文件名。", "apihelp-query+filearchive-example-simple": "显示已删除文件列表。", "apihelp-query+filerepoinfo-description": "返回有关wiki配置的图片存储库的元信息。", - "apihelp-query+filerepoinfo-param-prop": "要获取的存储库属性(这在一些wiki上可能有更多可用选项):\n;apiurl:链接至API的URL - 对从主机获取图片信息有用。\n;name:存储库关键词 - 用于例如<var>[[mw:Manual:$wgForeignFileRepos|$wgForeignFileRepos]]</var>,并且[[Special:ApiHelp/query+imageinfo|imageinfo]]会返回值。\n;displayname:人类可读的存储库wiki名称。\n;rooturl:图片路径的根URL。\n;local:存储库是否在本地。", + "apihelp-query+filerepoinfo-param-prop": "要获取的存储库属性(这在一些wiki上可能有更多可用选项):\n;apiurl:链接至API的URL - 对从主机获取图片信息有用。\n;name:存储库关键词 - 用于例如<var>[[mw:Special:MyLanguage/Manual:$wgForeignFileRepos|$wgForeignFileRepos]]</var>,并且[[Special:ApiHelp/query+imageinfo|imageinfo]]会返回值。\n;displayname:人类可读的存储库wiki名称。\n;rooturl:图片路径的根URL。\n;local:存储库是否在本地。", "apihelp-query+filerepoinfo-example-simple": "获得有关文件存储库的信息。", "apihelp-query+fileusage-description": "查找所有使用指定文件的页面。", "apihelp-query+fileusage-param-prop": "要获取的属性:", @@ -1073,10 +1074,11 @@ "apihelp-query+siteinfo-paramvalue-prop-rightsinfo": "当可用时返回wiki的版权(许可协议)信息。", "apihelp-query+siteinfo-paramvalue-prop-restrictions": "返回可用的编辑限制(保护)类型信息。", "apihelp-query+siteinfo-paramvalue-prop-languages": "返回MediaWiki支持的语言列表(可选择使用<var>$1inlanguagecode</var>本地化)。", + "apihelp-query+siteinfo-paramvalue-prop-languagevariants": "当启用了[[mw:Special:MyLanguage/LanguageConverter|语言转换器]],并且每个语言变体都受支持时,返回语言代码列表。", "apihelp-query+siteinfo-paramvalue-prop-skins": "返回所有启用的皮肤列表(可选择使用<var>$1inlanguagecode</var>本地化,否则是内容语言)。", "apihelp-query+siteinfo-paramvalue-prop-extensiontags": "返回解析器扩展标签列表。", "apihelp-query+siteinfo-paramvalue-prop-functionhooks": "返回解析器函数钩列表。", - "apihelp-query+siteinfo-paramvalue-prop-showhooks": "返回所有订阅的钩列表(<var>[[mw:Manual:$wgHooks|$wgHooks]]</var>的内容)。", + "apihelp-query+siteinfo-paramvalue-prop-showhooks": "返回所有订阅的钩列表(<var>[[mw:Special:MyLanguage/Manual:$wgHooks|$wgHooks]]</var>的内容)。", "apihelp-query+siteinfo-paramvalue-prop-variables": "返回变量ID列表。", "apihelp-query+siteinfo-paramvalue-prop-protocols": "返回外部链接中允许的协议列表。", "apihelp-query+siteinfo-paramvalue-prop-defaultoptions": "返回用户设置的默认值。", @@ -1145,7 +1147,7 @@ "apihelp-query+usercontribs-paramvalue-prop-flags": "添加编辑标记。", "apihelp-query+usercontribs-paramvalue-prop-patrolled": "标记已巡查编辑。", "apihelp-query+usercontribs-paramvalue-prop-tags": "列举用于编辑的标签。", - "apihelp-query+usercontribs-param-show": "只显示符合这些标准的项目,例如只显示不是小编辑的编辑:<kbd>$2show=!minor</kbd>。\n\n如果<kbd>$2show=patrolled</kbd>或<kbd>$2show=!patrolled</kbd>被设定,早于<var>[[mw:Manual:$wgRCMaxAge|$wgRCMaxAge]]</var>($1秒)的修订不会被显示。", + "apihelp-query+usercontribs-param-show": "只显示符合这些标准的项目,例如只显示不是小编辑的编辑:<kbd>$2show=!minor</kbd>。\n\n如果<kbd>$2show=patrolled</kbd>或<kbd>$2show=!patrolled</kbd>被设定,早于<var>[[mw:Special:MyLanguage/Manual:$wgRCMaxAge|$wgRCMaxAge]]</var>($1秒)的修订不会被显示。", "apihelp-query+usercontribs-param-tag": "只列出被此标签标记的修订。", "apihelp-query+usercontribs-param-toponly": "只列举作为最新修订的更改。", "apihelp-query+usercontribs-example-user": "显示用户<kbd>Example</kbd>的贡献。", @@ -1155,6 +1157,7 @@ "apihelp-query+userinfo-paramvalue-prop-blockinfo": "如果当前用户被封禁就标记,并注明是谁封禁,以何种原因封禁的。", "apihelp-query+userinfo-paramvalue-prop-hasmsg": "如果当前用户有等待中的消息的话,添加标签<samp>messages</samp>。", "apihelp-query+userinfo-paramvalue-prop-groups": "列举当前用户隶属的所有群组。", + "apihelp-query+userinfo-paramvalue-prop-groupmemberships": "列举明确分配给当前用户的用户组,包括每个用户组成员的过期时间。", "apihelp-query+userinfo-paramvalue-prop-implicitgroups": "列举当前用户的所有自动成为成员的用户组。", "apihelp-query+userinfo-paramvalue-prop-rights": "列举当前用户拥有的所有权限。", "apihelp-query+userinfo-paramvalue-prop-changeablegroups": "列举当前用户可以添加并移除的用户组。", @@ -1175,6 +1178,7 @@ "apihelp-query+users-param-prop": "要包含的信息束:", "apihelp-query+users-paramvalue-prop-blockinfo": "如果用户被封禁就标记,并注明是谁封禁,以何种原因封禁的。", "apihelp-query+users-paramvalue-prop-groups": "列举每位用户属于的所有组。", + "apihelp-query+users-paramvalue-prop-groupmemberships": "列举明确分配给每位用户的用户组,包括每个用户组成员的过期时间。", "apihelp-query+users-paramvalue-prop-implicitgroups": "列举用户自动作为成员之一的所有组。", "apihelp-query+users-paramvalue-prop-rights": "列举每位用户拥有的所有权限。", "apihelp-query+users-paramvalue-prop-editcount": "添加用户的编辑计数。", @@ -1240,7 +1244,7 @@ "apihelp-removeauthenticationdata-description": "从当前用户移除身份验证数据。", "apihelp-removeauthenticationdata-example-simple": "尝试移除当前用户的<kbd>FooAuthenticationRequest</kbd>数据。", "apihelp-resetpassword-description": "向用户发送密码重置邮件。", - "apihelp-resetpassword-description-noroutes": "没有密码重置路由可用。\n\n在<var>[[mw:Manual:$wgPasswordResetRoutes|$wgPasswordResetRoutes]]</var>中启用路由以使用此模块。", + "apihelp-resetpassword-description-noroutes": "没有密码重置路由可用。\n\n在<var>[[mw:Special:MyLanguage/Manual:$wgPasswordResetRoutes|$wgPasswordResetRoutes]]</var>中启用路由以使用此模块。", "apihelp-resetpassword-param-user": "正在重置的用户。", "apihelp-resetpassword-param-email": "正在重置用户的电子邮件地址。", "apihelp-resetpassword-example-user": "向用户<kbd>Example</kbd>发送密码重置邮件。", @@ -1278,7 +1282,7 @@ "apihelp-setnotificationtimestamp-example-pagetimestamp": "设置<kbd>Main page</kbd>的通知时间戳,这样所有从2012年1月1日起的编辑都会是未复核的。", "apihelp-setnotificationtimestamp-example-allpages": "重置在<kbd>{{ns:user}}</kbd>名字空间中的页面的通知状态。", "apihelp-setpagelanguage-description": "更改页面的语言。", - "apihelp-setpagelanguage-description-disabled": "此wiki不允许更改页面的语言。\n\n启用<var>[[mw:Manual:$wgPageLanguageUseDB|$wgPageLanguageUseDB]]</var>以使用此操作。", + "apihelp-setpagelanguage-description-disabled": "此wiki不允许更改页面的语言。\n\n启用<var>[[mw:Special:MyLanguage/Manual:$wgPageLanguageUseDB|$wgPageLanguageUseDB]]</var>以使用此操作。", "apihelp-setpagelanguage-param-title": "您希望更改语言的页面标题。不能与<var>$1pageid</var>一起使用。", "apihelp-setpagelanguage-param-pageid": "您希望更改语言的页面ID。不能与<var>$1title</var>一起使用。", "apihelp-setpagelanguage-param-lang": "更改页面的目标语言的语言代码。使用<kbd>default</kbd>以重置页面为wiki的默认内容语言。", @@ -1311,14 +1315,14 @@ "apihelp-tokens-example-edit": "检索一个编辑令牌(默认)。", "apihelp-tokens-example-emailmove": "检索一个电子邮件令牌和一个移动令牌。", "apihelp-unblock-description": "解封一位用户。", - "apihelp-unblock-param-id": "解封时需要的封禁ID(通过<kbd>list=blocks</kbd>获得)。不能与<var>$1user</var>或<var>$luserid</var>一起使用。", - "apihelp-unblock-param-user": "要解封的用户名、IP地址或IP地址段。不能与<var>$1id</var>或<var>$luserid</var>一起使用。", + "apihelp-unblock-param-id": "解封时需要的封禁ID(通过<kbd>list=blocks</kbd>获得)。不能与<var>$1user</var>或<var>$1userid</var>一起使用。", + "apihelp-unblock-param-user": "要解封的用户名、IP地址或IP地址段。不能与<var>$1id</var>或<var>$1userid</var>一起使用。", "apihelp-unblock-param-userid": "要封禁的用户ID。不能与<var>$1id</var>或<var>$1user</var>一起使用。", "apihelp-unblock-param-reason": "解封的原因。", "apihelp-unblock-param-tags": "要在封禁日志中应用到实体的更改标签。", "apihelp-unblock-example-id": "解封封禁ID #<kbd>105</kbd>。", "apihelp-unblock-example-user": "解封用户<kbd>Bob</kbd>,原因<kbd>Sorry Bob</kbd>。", - "apihelp-undelete-description": "恢复一个被删除页面的修订。\n\n被删除修订的列表(包括时间戳)可通过[[Special:ApiHelp/query+deletedrevs|list=deletedrevs]]检索到,并且被删除的文件ID列表可通过[[Special:ApiHelp/query+filearchive|list=filearchive]]检索到。", + "apihelp-undelete-description": "恢复一个被删除页面的修订。\n\n被删除修订的列表(包括时间戳)可通过[[Special:ApiHelp/query+deletedrevisions|prop=deletedrevisions]]检索到,并且被删除的文件ID列表可通过[[Special:ApiHelp/query+filearchive|list=filearchive]]检索到。", "apihelp-undelete-param-title": "要恢复的页面标题。", "apihelp-undelete-param-reason": "恢复的原因。", "apihelp-undelete-param-tags": "要在删除日志中应用到实体的更改标签。", @@ -1352,12 +1356,14 @@ "apihelp-userrights-description": "更改一位用户的组成员。", "apihelp-userrights-param-user": "用户名。", "apihelp-userrights-param-userid": "用户ID。", - "apihelp-userrights-param-add": "将用户加入至这些组中。", + "apihelp-userrights-param-add": "将用户加入至这些组中,或如果其已作为成员,更新其所在用户组成员资格的终止时间。", + "apihelp-userrights-param-expiry": "到期时间戳。可以是相对值(例如<kbd>5 months</kbd>或<kbd>2 weeks</kbd>)或绝对值(例如<kbd>2014-09-18T12:34:56Z</kbd>)。如果只设置一个时间戳,它将被用于所有传递给<var>$1add</var>参数的组。对于永不过时的用户组,使用<kbd>infinite</kbd>、<kbd>indefinite</kbd>、<kbd>infinity</kbd>或<kbd>never</kbd>。", "apihelp-userrights-param-remove": "将用户从这些组中移除。", "apihelp-userrights-param-reason": "更改原因。", "apihelp-userrights-param-tags": "要在用户权限日志中应用到实体的更改标签。", "apihelp-userrights-example-user": "将用户<kbd>FooBot</kbd>添加至<kbd>bot</kbd>用户组,并从<kbd>sysop</kbd>和<kbd>bureaucrat</kbd>组移除。", "apihelp-userrights-example-userid": "将ID为<kbd>123</kbd>的用户加入至<kbd>机器人</kbd>组,并将其从<kbd>管理员</kbd>和<kbd>行政员</kbd>组移除。", + "apihelp-userrights-example-expiry": "添加用户<kbd>SometimeSysop</kbd>至用户组<kbd>sysop</kbd>,为期1个月。", "apihelp-validatepassword-description": "验证密码是否符合wiki的密码方针。\n\n如果密码可以接受,就报告有效性为<samp>Good</samp>,如果密码可用于登录但必须更改,则报告为<samp>Change</samp>,或如果密码不可使用,则报告为<samp>Invalid</samp>。", "apihelp-validatepassword-param-password": "要验证的密码。", "apihelp-validatepassword-param-user": "用户名,供测试账户创建时使用。命名的用户必须不存在。", @@ -1389,8 +1395,8 @@ "apihelp-xml-param-includexmlnamespace": "如果指定,添加一个XML名字空间。", "apihelp-xmlfm-description": "输出数据为XML格式(HTML优质打印效果)。", "api-format-title": "MediaWiki API 结果", - "api-format-prettyprint-header": "这是$1格式的HTML表示。HTML对调试很有用,但不适合应用程序使用。\n\n指定<var>format</var>参数以更改输出格式。要查看$1格式的非HTML表示,设置<kbd>format=$2</kbd>。\n\n参见[[mw:API|完整文档]],或[[Special:ApiHelp/main|API 帮助]]以获取更多信息。", - "api-format-prettyprint-header-only-html": "这是用来调试的HTML表现,不适合实际使用。\n\n参见[[mw:API|完整文档]],或[[Special:ApiHelp/main|API帮助]]以获取更多信息。", + "api-format-prettyprint-header": "这是$1格式的HTML实现。HTML对调试很有用,但不适合应用程序使用。\n\n指定<var>format</var>参数以更改输出格式。要查看$1格式的非HTML实现,设置<kbd>format=$2</kbd>。\n\n参见[[mw:Special:MyLanguage/API|完整文档]],或[[Special:ApiHelp/main|API帮助]]以获取更多信息。", + "api-format-prettyprint-header-only-html": "这是用来调试的HTML实现,不适合实际使用。\n\n参见[[mw:Special:MyLanguage/API|完整文档]],或[[Special:ApiHelp/main|API帮助]]以获取更多信息。", "api-format-prettyprint-status": "此响应将会返回HTTP状态$1 $2。", "api-pageset-param-titles": "要工作的标题列表。", "api-pageset-param-pageids": "要工作的页面ID列表。", @@ -1438,8 +1444,8 @@ "api-help-param-default-empty": "默认:<span class=\"apihelp-empty\">(空)</span>", "api-help-param-token": "从[[Special:ApiHelp/query+tokens|action=query&meta=tokens]]取回的“$1”令牌", "api-help-param-token-webui": "出于兼容性考虑,web UI中使用的令牌也被接受。", - "api-help-param-disabled-in-miser-mode": "由于[[mw:Manual:$wgMiserMode|miser模式]]而禁用。", - "api-help-param-limited-in-miser-mode": "<strong>注意:</strong>由于[[mw:Manual:$wgMiserMode|miser模式]],使用这个可能导致继续前返回少于<var>$1limit</var>个结果;极端情况下可能不会返回任何结果。", + "api-help-param-disabled-in-miser-mode": "由于[[mw:Special:MyLanguage/Manual:$wgMiserMode|miser模式]]而禁用。", + "api-help-param-limited-in-miser-mode": "<strong>注意:</strong>由于[[mw:Special:MyLanguage/Manual:$wgMiserMode|miser模式]],使用这个可能导致继续前返回少于<var>$1limit</var>个结果;极端情况下可能不会返回任何结果。", "api-help-param-direction": "列举的方向:\n;newer:最早的优先。注意:$1start应早于$1end。\n;older:最新的优先(默认)。注意:$1start应晚于$1end。", "api-help-param-continue": "当更多结果可用时,使用这个继续。", "api-help-param-no-description": "<span class=\"apihelp-empty\">(没有说明)</span>", @@ -1448,7 +1454,7 @@ "api-help-permissions-granted-to": "{{PLURAL:$1|授予}}:$2", "api-help-right-apihighlimits": "在API查询中使用更高的上限(慢查询:$1;快查询:$2)。慢查询的限制也适用于多值参数。", "api-help-open-in-apisandbox": "<small>[在沙盒中打开]</small>", - "api-help-authmanager-general-usage": "使用此模块的一般程序是:\n# 通过<kbd>amirequestsfor=$4</kbd>取得来自<kbd>[[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]]</kbd>的可用字段,和来自<kbd>[[Special:ApiHelp/query+tokens|action=query&meta=tokens]]</kbd>的<kbd>$5</kbd>令牌。\n# 向用户显示字段,并获得其提交内容。\n# 发送至此模块,提供<var>$1returnurl</var>及任何相关字段。\n# 在响应中检查<samp>status</samp>。\n#* 如果您收到了<samp>PASS</samp>或<samp>FAIL</samp>,您已经完成。操作要么成功,要么不成功。\n#* 如果您收到了<samp>UI</samp>,present the new fields to the user and obtain their submission. Then post to this module with <var>$1continue</var> and the relevant fields set, and repeat step 4.\n#* 如果您收到了<samp>REDIRECT</samp>,direct the user to the <samp>redirecttarget</samp> and wait for the return to <var>$1returnurl</var>. Then post to this module with <var>$1continue</var> and any fields passed to the return URL, and repeat step 4.\n#* 如果您收到了<samp>RESTART</samp>,这意味着身份验证可以工作,但我们没有链接的用户账户。您可以将此看做<samp>UI</samp>或<samp>FAIL</samp>。", + "api-help-authmanager-general-usage": "使用此模块的一般程序是:\n# 通过<kbd>amirequestsfor=$4</kbd>取得来自<kbd>[[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]]</kbd>的可用字段,和来自<kbd>[[Special:ApiHelp/query+tokens|action=query&meta=tokens]]</kbd>的<kbd>$5</kbd>令牌。\n# 向用户显示字段,并获得其提交的内容。\n# 发送(POST)至此模块,提供<var>$1returnurl</var>及任何相关字段。\n# 在响应中检查<samp>status</samp>。\n#* 如果您收到了<samp>PASS</samp>(成功)或<samp>FAIL</samp>(失败),则认为操作结束。成功与否如上句所示。\n#* 如果您收到了<samp>UI</samp>,向用户显示新字段,并再次获取其提交的内容。然后再次使用<var>$1continue</var>,向本模块提交相关字段,并重复第四步。\n#* 如果您收到了<samp>REDIRECT</samp>,将用户指向<samp>redirecttarget</samp>中的目标,等待其返回<var>$1returnurl</var>。然后再次使用<var>$1continue</var>,向本模块提交返回URL中提供的一切字段,并重复第四步。\n#* 如果您收到了<samp>RESTART</samp>,这意味着身份验证正常运作,但我们没有链接的用户账户。您可以将此看做<samp>UI</samp>或<samp>FAIL</samp>。", "api-help-authmanagerhelper-requests": "只使用这些身份验证请求,通过返回自<kbd>[[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]]</kbd>的<samp>id</samp>与<kbd>amirequestsfor=$1</kbd>,或来自此模块之前的响应。", "api-help-authmanagerhelper-request": "使用此身份验证请求,通过返回自<kbd>[[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]]</kbd>的<samp>id</samp>与<kbd>amirequestsfor=$1</kbd>。", "api-help-authmanagerhelper-messageformat": "返回消息使用的格式。", @@ -1489,6 +1495,7 @@ "apierror-blockedfrommail": "您已被封禁,不能发送电子邮件。", "apierror-blocked": "您已被封禁,不能编辑。", "apierror-botsnotsupported": "此界面不支持机器人。", + "apierror-cannot-async-upload-file": "参数<var>async</var>和<var>file</var>不能结合。如果您希望对您上传的文件进行不同处理,请将其首先上传至暂存处(使用<var>stash</var>参数),然后异步发布暂存文件(使用<var>filekey</var>和<var>async</var>)。", "apierror-cannotreauthenticate": "由于您的身份不能被验证,此操作不可用。", "apierror-cannotviewtitle": "您不被允许查看$1。", "apierror-cantblock-email": "您没有权限封禁用户通过wiki发送电子邮件。", @@ -1531,12 +1538,12 @@ "apierror-invalidexpiry": "无效的过期时间“$1”。", "apierror-invalid-file-key": "不是有效的文件关键词。", "apierror-invalidlang": "用于参数<var>$1</var>的语言值无效。", - "apierror-invalidoldimage": "旧图片参数有无效格式。", + "apierror-invalidoldimage": "<var>oldimage</var>参数有无效格式。", "apierror-invalidparammix-cannotusewith": "<kbd>$1</kbd>参数不能与<kbd>$2</kbd>一起使用。", "apierror-invalidparammix-mustusewith": "<kbd>$1</kbd>参数只能与<kbd>$2</kbd>一起使用。", "apierror-invalidparammix-parse-new-section": "<kbd>section=new</kbd>不能连同<var>oldid</var>、<var>pageid</var>或<var>page</var>参数使用。请使用<var>title</var>和<var>text</var>。", "apierror-invalidparammix": "{{PLURAL:$2|参数}}$1不能一起使用。", - "apierror-invalidsection": "章节参数必须为有效的章节ID或<kbd>new</kbd>。", + "apierror-invalidsection": "<var>section</var>参数必须为有效的章节ID或<kbd>new</kbd>。", "apierror-invalidsha1base36hash": "提供的SHA1Base36哈希无效。", "apierror-invalidsha1hash": "提供的SHA1哈希无效。", "apierror-invalidtitle": "错误标题“$1”。", @@ -1604,6 +1611,7 @@ "apierror-readapidenied": "您需要读取权限以使用此模块。", "apierror-readonly": "此wiki目前为只读模式。", "apierror-reauthenticate": "您在该会话中尚未经过验证,请重新验证。", + "apierror-redirect-appendonly": "您试图使用重定向跟随模式编辑,而这必须与<kbd>section=new</kbd>、<var>prependtext</var>或<var>appendtext</var>共同使用。", "apierror-revdel-mutuallyexclusive": "同一字段不能同时用于<var>hide</var>和<var>show</var>。", "apierror-revdel-needtarget": "此修订版本删除类型需要目标标题。", "apierror-revdel-paramneeded": "需要<var>hide</var>和/或<var>show</var>的至少一个值。", @@ -1617,9 +1625,10 @@ "apierror-show": "不正确的参数——不可提供互斥值。", "apierror-siteinfo-includealldenied": "除非<var>$wgShowHostNames</var>为真,否则不能查看所有服务器的信息。", "apierror-sizediffdisabled": "大小差异在Miser模式中被禁用。", - "apierror-spamdetected": "您的编辑被拒绝,因为它包含破坏性碎片:<code>$1</code>。", + "apierror-spamdetected": "您的编辑被拒绝,因为它包含垃圾部分:<code>$1</code>。", "apierror-specialpage-cantexecute": "您没有权限查看此特殊页面的结果。", "apierror-stashedfilenotfound": "无法在暂存处找到文件:$1。", + "apierror-stashedit-missingtext": "提供的哈希中找不到暂存文本。", "apierror-stashfailed-complete": "大块上传已经完成,检查状态以获取详情。", "apierror-stashfailed-nosession": "没有带此关键词的大块上传会话。", "apierror-stashfilestorage": "不能在暂存处存储上传:$1", @@ -1629,6 +1638,7 @@ "apierror-stashzerolength": "文件长度为0,并且不能在暂存库中储存:$1。", "apierror-systemblocked": "您已被MediaWiki自动封禁。", "apierror-templateexpansion-notwikitext": "模板展开只支持wiki文本内容。$1使用内容模型$2。", + "apierror-toofewexpiries": "提供了$1个逾期{{PLURAL:$1|时间戳}},实际则需要$2个。", "apierror-unknownaction": "指定的操作<kbd>$1</kbd>不被承认。", "apierror-unknownerror-editpage": "未知的编辑页面错误:$1。", "apierror-unknownerror-nocode": "未知错误。", @@ -1647,6 +1657,7 @@ "apiwarn-badurlparam": "不能为$2解析<var>$1urlparam</var>。请只使用宽和高。", "apiwarn-badutf8": "<var>$1</var>通过的值包含无效或非标准化数据。正文数据应为有效的NFC标准化Unicode,没有除HT(\\t)、LF(\\n)和CR(\\r)以外的C0控制字符。", "apiwarn-deprecation-deletedrevs": "<kbd>list=deletedrevs</kbd>已被弃用。请改用<kbd>prop=deletedrevisions</kbd>或<kbd>list=alldeletedrevisions</kbd>。", + "apiwarn-deprecation-expandtemplates-prop": "因为没有为<var>prop</var>参数指定值,所以在输出上使用了遗留格式。这种格式已弃用,并且将来会为<var>prop</var>参数设置默认值,这会导致新格式总会被使用。", "apiwarn-deprecation-httpsexpected": "当应为HTTPS时,HTTP被使用。", "apiwarn-deprecation-login-botpw": "通过<kbd>action=login</kbd>的主账户登录已被弃用,并可能在未事先警告的情况下停止工作。要继续通过<kbd>action=login</kbd>登录,请参见[[Special:BotPasswords]]。要安全继续使用主账户登录,请参见<kbd>action=clientlogin</kbd>。", "apiwarn-deprecation-login-nobotpw": "通过<kbd>action=login</kbd>的主账户登录已被弃用,并可能在未事先警告的情况下停止工作。要安全登录,请参见<kbd>action=clientlogin</kbd>。", @@ -1668,11 +1679,14 @@ "apiwarn-nothumb-noimagehandler": "不能创建缩略图,因为$1没有关联的图片处理器。", "apiwarn-parse-nocontentmodel": "<var>title</var>或<var>contentmodel</var>未提供,假设$1。", "apiwarn-parse-titlewithouttext": "<var>title</var>在没有<var>text</var>的情况下被使用,并且请求了已解析页面的属性。您是想用<var>page</var>而不是<var>title</var>么?", + "apiwarn-redirectsandrevids": "重定向解决方案不能与<var>revids</var>参数一起使用。任何<var>revids</var>所指向的重定向都未被解决。", "apiwarn-tokennotallowed": "操作“$1”不允许当前用户使用。", - "apiwarn-toomanyvalues": "参数<var>$1</var>指定了太多的值:上限为$2。", - "apiwarn-unclearnowtimestamp": "为时间戳参数<var>$1</var>传递“$2”已被弃用。如因某些原因您需要明确指定当前时间而不计算客户端,请使用<kbd>now<kbd>。", + "apiwarn-toomanyvalues": "参数<var>$1</var>指定了太多的值。上限为$2。", + "apiwarn-truncatedresult": "此结果被缩短,否则其将大于$1字节的限制。", + "apiwarn-unclearnowtimestamp": "为时间戳参数<var>$1</var>传递“$2”已被弃用。如因某些原因您需要明确指定当前时间而不计算客户端,请使用<kbd>now</kbd>。", "apiwarn-unrecognizedvalues": "参数<var>$1</var>有无法识别的{{PLURAL:$3|值}}:$2。", "apiwarn-unsupportedarray": "参数<var>$1</var>使用未受支持的PHP数组语法。", + "apiwarn-urlparamwidth": "为了获得衍生自<var>$1urlwidth</var>/<var>$1urlheight</var>的宽度值($3),正在忽略<var>$1urlparam</var>的宽度值集($2)。", "apiwarn-validationfailed-badchars": "关键词中的字符无效(只允许<code>a-z</code>、<code>A-Z</code>、<code>0-9</code>、<code>_</code>和<code>-</code>)。", "apiwarn-validationfailed-badpref": "不是有效的偏好。", "apiwarn-validationfailed-cannotset": "不能通过此模块设置。", @@ -1681,6 +1695,7 @@ "apiwarn-wgDebugAPI": "<strong>安全警告</strong>:<var>$wgDebugAPI</var>已启用。", "api-feed-error-title": "错误($1)", "api-usage-docref": "参见$1以获取API用法。", + "api-usage-mailinglist-ref": "在<https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce>订阅mediawiki-api-announce列表以获取API弃用和重大更新的通知。", "api-exception-trace": "$1在$2($3)\n$4", "api-credits-header": "制作人员", "api-credits": "API 开发人员:\n* Yuri Astrakhan(创建者,2006年9月~2007年9月的开发组领导)\n* Roan Kattouw(2007年9月~2009年的开发组领导)\n* Victor Vasiliev\n* Bryan Tong Minh\n* Sam Reed\n* Brad Jorsch(2013年至今的开发组领导)\n\n请将您的评论、建议和问题发送至mediawiki-api@lists.wikimedia.org,或提交错误请求至https://phabricator.wikimedia.org/。" diff --git a/includes/api/i18n/zh-hant.json b/includes/api/i18n/zh-hant.json index 54e476f63889..6f2bcd61dcff 100644 --- a/includes/api/i18n/zh-hant.json +++ b/includes/api/i18n/zh-hant.json @@ -7,12 +7,23 @@ "EagerLin", "Zhxy 519", "Macofe", - "Jasonzhuocn" + "Jasonzhuocn", + "Winstonyin", + "Arthur2e5", + "烈羽" ] }, - "apihelp-main-description": "<div class=\"hlist plainlinks api-main-links\">\n* [[mw:API:Main_page|文件]]\n* [[mw:API:FAQ|FAQ]]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api 郵件清單]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce API公告]\n* [https://phabricator.wikimedia.org/maniphest/query/GebfyV4uCaLd/#R Bug與請求]\n</div>\n<strong>狀態資訊:</strong>本頁所展示的所有功能都應正常工作,但是 API 仍在開發當中,將會隨時變化。請訂閱[https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ mediawiki-api-announce 郵件清單]以便得到更新通知。\n\n<strong>錯誤請求:</strong>當 API 收到錯誤請求時, HTTP header 將會返回一個包含「MediaWiki-API-Error」的值,隨後 header 的值與錯誤碼將會送回並設定為相同的值。詳細資訊請參閱[[mw:API:Errors_and_warnings|API: 錯誤與警告]]。", + "apihelp-main-description": "<div class=\"hlist plainlinks api-main-links\">\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</div>\n<strong>狀態資訊:</strong>本頁所展示的所有功能都應正常工作,但是API仍在開發當中,將會隨時變化。請訂閱[https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ mediawiki-api-announce 郵件清單]以便得到更新通知。\n\n<strong>錯誤的請求:</strong>當API收到錯誤的請求時,會發出以「MediaWiki-API-Error」為鍵的HTTP頭欄位,隨後頭欄位的值與錯誤碼將會被設為相同的值。詳細資訊請參閱[[mw:API:Errors_and_warnings|API: 錯誤與警告]]。\n\n<strong>測試:</strong>要簡化API請求的測試過程,請見[[Special:ApiSandbox]]。", "apihelp-main-param-action": "要執行的動作。", "apihelp-main-param-format": "輸出的格式。", + "apihelp-main-param-smaxage": "將HTTP緩存控制頭欄位設為<code>s-maxage</code>秒。錯誤不會做緩存。", + "apihelp-main-param-maxage": "將HTTP緩存控制頭欄位設為<code>max-age</code>秒。錯誤不會做緩存。", + "apihelp-main-param-assert": "若設為<kbd>user</kbd>,會確認使用者是否已登入;若設為<kbd>bot</kbd>,會確認是否擁有機械人權限。", + "apihelp-main-param-assertuser": "確認目前使用者就是指定的使用者。", + "apihelp-main-param-requestid": "在此處提供的任何值都將包括在響應之中。可用於區分請求。", + "apihelp-main-param-servedby": "在結果中包括提出請求的主機名。", + "apihelp-main-param-curtimestamp": "在結果中包括目前的時間戳。", + "apihelp-main-param-responselanginfo": "在結果中包括<var>uselang</var>和<var>errorlang</var>所用的語言。", "apihelp-block-description": "封鎖使用者。", "apihelp-block-param-user": "您要封鎖的使用者名稱、IP 位址或 IP 範圍。", "apihelp-block-param-reason": "封鎖原因。", @@ -91,6 +102,7 @@ "apihelp-expandtemplates-param-text": "要轉換的 Wikitext。", "apihelp-feedcontributions-description": "回傳使用者貢獻 Feed。", "apihelp-feedcontributions-param-feedformat": "Feed 的格式。", + "apihelp-feedcontributions-param-hideminor": "隱藏小修改。", "apihelp-feedcontributions-param-showsizediff": "顯示修訂版本之間的差異大小。", "apihelp-feedcontributions-example-simple": "返回使用者<kbd>Example</kbd>的貢獻。", "apihelp-feedrecentchanges-description": "返回最近變更摘要。", @@ -126,18 +138,29 @@ "apihelp-login-example-login": "登入", "apihelp-logout-description": "登出並清除 session 資料。", "apihelp-logout-example-logout": "登出當前使用者", + "apihelp-mergehistory-description": "合併頁面歷史", + "apihelp-mergehistory-param-reason": "合併歷史的原因。", + "apihelp-mergehistory-example-merge": "將<kbd>Oldpage</kbd>的整個歷史合併至<kbd>Newpage</kbd>。", + "apihelp-mergehistory-example-merge-timestamp": "將<kbd>Oldpage</kbd>直至<kbd>2015-12-31T04:37:41Z</kbd>的頁面修訂版本合併至<kbd>Newpage</kbd>。", "apihelp-move-description": "移動頁面。", "apihelp-move-param-from": "重新命名本頁面的標題。不能與 <var>$1fromid</var> 一起出現。", "apihelp-move-param-fromid": "重新命名本頁面的 ID 。不能與 <var>$1fromid</var> 一起出現。", "apihelp-move-param-to": "將本頁面的標題重新命名為", "apihelp-move-param-reason": "重新命名的原因。", + "apihelp-move-param-movetalk": "如果討論頁存在,變更討論頁名稱。", "apihelp-move-param-movesubpages": "如果適用,則重新命名子頁面。", "apihelp-move-param-noredirect": "不要建立重新導向。", + "apihelp-move-param-watch": "將頁面和重定向加入目前使用者的監視清單。", + "apihelp-move-param-unwatch": "從目前使用者的監視清單中移除頁面和重定向。", + "apihelp-move-param-watchlist": "在目前使用者的監視清單中無條件地加入或移除頁面,或使用設定,或不變更監視清單。", "apihelp-move-param-ignorewarnings": "忽略所有警告。", + "apihelp-move-example-move": "將<kbd>Badtitle</kbd>移動至<kbd>Goodtitle</kbd>,不留下重定向。", "apihelp-opensearch-description": "使用 OpenSearch 協定搜尋本 wiki。", "apihelp-opensearch-param-search": "搜尋字串。", "apihelp-opensearch-param-limit": "回傳的結果數量上限。", "apihelp-opensearch-param-namespace": "搜尋的命名空間。", + "apihelp-opensearch-param-suggest": "若<var>[[mw:Manual:$wgEnableOpenSearchSuggest|$wgEnableOpenSearchSuggest]]</var>設定為false,則不做任何事。", + "apihelp-opensearch-param-redirects": "如何處理重定向:\n;return:傳回重定向本身。\n;resolve:傳回目標頁面,傳回的結果數目可能少於$1limit。\n由於歷史原因,$1format=json的預設值為「return」,其他格式則為「resolve」。", "apihelp-opensearch-param-format": "輸出的格式。", "apihelp-options-param-reset": "重設偏好設定為網站預設值。", "apihelp-options-example-reset": "重設所有偏好設定", @@ -262,6 +285,7 @@ "api-help-examples": "{{PLURAL:$1|範例}}:", "api-help-permissions": "{{PLURAL:$1|權限}}:", "api-help-permissions-granted-to": "{{PLURAL:$1|已授權給}}: $2", + "api-help-authmanager-general-usage": "使用此模組的一般程式是:\n# 通過<kbd>amirequestsfor=$4</kbd>取得來自<kbd>[[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]]</kbd>的可用欄位,和來自<kbd>[[Special:ApiHelp/query+tokens|action=query&meta=tokens]]</kbd>的<kbd>$5</kbd>令牌。\n# 向用戶顯示欄位,並獲得其提交的內容。\n# 提交(POST)至此模組,提供<var>$1returnurl</var>及任何相關欄位。\n# 在回应中檢查<samp>status</samp>。\n#* 如果您收到了<samp>PASS</samp>(成功)或<samp>FAIL</samp>(失敗),則認為操作結束。成功與否如上句所示。\n#* 如果您收到了<samp>UI</samp>,向用戶顯示新欄位,並再次獲取其提交的內容。然後再次使用<var>$1continue</var>,向本模組提交相關欄位,並重復第四步。\n#* 如果您收到了<samp>REDIRECT</samp>,將使用者指向<samp>redirecttarget</samp>中的目標,等待其返回<var>$1returnurl</var>。然後再次使用<var>$1continue</var>,向本模組提交返回URL中提供的一切欄位,並重復第四步。\n#* 如果您收到了<samp>RESTART</samp>,這意味著身份驗證正常運作,但我們沒有連結的使用者賬戶。您可以將此看做<samp>UI</samp>或<samp>FAIL</samp>。", "api-credits-header": "製作群", "api-credits": "API 開發人員:\n* Roan Kattouw (首席開發者 Sep 2007–2009)\n* Victor Vasiliev\n* Bryan Tong Minh\n* Sam Reed\n* Yuri Astrakhan (創立者,首席開發者 Sep 2006–Sep 2007)\n* Brad Jorsch (首席開發者 2013–present)\n\n請傳送您的評論、建議以及問題至 mediawiki-api@lists.wikimedia.org\n或者回報問題至 https://phabricator.wikimedia.org/。" } diff --git a/includes/auth/ThrottlePreAuthenticationProvider.php b/includes/auth/ThrottlePreAuthenticationProvider.php index 3f6a47d2d371..ae0bc6bb7763 100644 --- a/includes/auth/ThrottlePreAuthenticationProvider.php +++ b/includes/auth/ThrottlePreAuthenticationProvider.php @@ -167,7 +167,9 @@ class ThrottlePreAuthenticationProvider extends AbstractPreAuthenticationProvide $data = $this->manager->getAuthenticationSessionData( 'LoginThrottle' ); if ( !$data ) { - $this->logger->error( 'throttler data not found for {user}', [ 'user' => $user->getName() ] ); + // this can occur when login is happening via AuthenticationRequest::$loginRequest + // so testForAuthentication is skipped + $this->logger->info( 'throttler data not found for {user}', [ 'user' => $user->getName() ] ); return; } diff --git a/includes/cache/BacklinkCache.php b/includes/cache/BacklinkCache.php index 9e6cf1ef4c27..72156063ec6d 100644 --- a/includes/cache/BacklinkCache.php +++ b/includes/cache/BacklinkCache.php @@ -26,6 +26,10 @@ * @copyright © 2011, Antoine Musso */ +use Wikimedia\Rdbms\ResultWrapper; +use Wikimedia\Rdbms\FakeResultWrapper; +use Wikimedia\Rdbms\IDatabase; + /** * Class for fetching backlink lists, approximate backlink counts and * partitions. This is a shared cache. @@ -407,7 +411,7 @@ class BacklinkCache { // 4) ... finally fetch from the slow database :( $cacheEntry = [ 'numRows' => 0, 'batches' => [] ]; // final result - // Do the selects in batches to avoid client-side OOMs (bug 43452). + // Do the selects in batches to avoid client-side OOMs (T45452). // Use a LIMIT that plays well with $batchSize to keep equal sized partitions. $selectSize = max( $batchSize, 200000 - ( 200000 % $batchSize ) ); $start = false; diff --git a/includes/cache/FileCacheBase.php b/includes/cache/FileCacheBase.php index 6d5f8c3e55a5..0a302b6ec156 100644 --- a/includes/cache/FileCacheBase.php +++ b/includes/cache/FileCacheBase.php @@ -242,7 +242,7 @@ abstract class FileCacheBase { : IP::sanitizeRange( "$ip/16" ); # Bail out if a request already came from this range... - $key = wfMemcKey( get_class( $this ), 'attempt', $this->mType, $this->mKey, $ip ); + $key = wfMemcKey( static::class, 'attempt', $this->mType, $this->mKey, $ip ); if ( $cache->get( $key ) ) { return; // possibly the same user } @@ -272,6 +272,6 @@ abstract class FileCacheBase { * @return string */ protected function cacheMissKey() { - return wfMemcKey( get_class( $this ), 'misses', $this->mType, $this->mKey ); + return wfMemcKey( static::class, 'misses', $this->mType, $this->mKey ); } } diff --git a/includes/cache/HTMLFileCache.php b/includes/cache/HTMLFileCache.php index ae8efa9c82ba..b0a3a1c262d1 100644 --- a/includes/cache/HTMLFileCache.php +++ b/includes/cache/HTMLFileCache.php @@ -36,19 +36,6 @@ class HTMLFileCache extends FileCacheBase { const MODE_REBUILD = 2; // background cache rebuild mode /** - * Construct an HTMLFileCache object from a Title and an action - * - * @deprecated since 1.24, instantiate this class directly - * @param Title|string $title Title object or prefixed DB key string - * @param string $action - * @throws MWException - * @return HTMLFileCache - */ - public static function newFromTitle( $title, $action ) { - return new self( $title, $action ); - } - - /** * @param Title|string $title Title object or prefixed DB key string * @param string $action * @throws MWException diff --git a/includes/cache/LinkBatch.php b/includes/cache/LinkBatch.php index d773fff3b7c0..57d4581a5538 100644 --- a/includes/cache/LinkBatch.php +++ b/includes/cache/LinkBatch.php @@ -22,6 +22,8 @@ */ use MediaWiki\Linker\LinkTarget; use MediaWiki\MediaWikiServices; +use Wikimedia\Rdbms\ResultWrapper; +use Wikimedia\Rdbms\IDatabase; /** * Class representing a list of titles diff --git a/includes/cache/LinkCache.php b/includes/cache/LinkCache.php index 23cc26d5e23b..57f66f1f892c 100644 --- a/includes/cache/LinkCache.php +++ b/includes/cache/LinkCache.php @@ -20,6 +20,7 @@ * @file * @ingroup Cache */ +use Wikimedia\Rdbms\IDatabase; use MediaWiki\Linker\LinkTarget; use MediaWiki\MediaWikiServices; @@ -278,6 +279,20 @@ class LinkCache { return $id; } + /** + * @param WANObjectCache $cache + * @param TitleValue $t + * @return string[] + * @since 1.28 + */ + public function getMutableCacheKeys( WANObjectCache $cache, TitleValue $t ) { + if ( $this->isCacheable( $t ) ) { + return [ $cache->makeKey( 'page', $t->getNamespace(), sha1( $t->getDBkey() ) ) ]; + } + + return []; + } + private function isCacheable( LinkTarget $title ) { return ( $title->inNamespace( NS_TEMPLATE ) || $title->inNamespace( NS_FILE ) ); } diff --git a/includes/cache/MessageBlobStore.php b/includes/cache/MessageBlobStore.php index 90ad24192db3..14baeeb22b49 100644 --- a/includes/cache/MessageBlobStore.php +++ b/includes/cache/MessageBlobStore.php @@ -110,9 +110,6 @@ class MessageBlobStore implements LoggerAwareInterface { foreach ( $modules as $name => $module ) { $key = $cacheKeys[$name]; if ( !isset( $result[$key] ) || $curTTLs[$key] === null || $curTTLs[$key] < 0 ) { - $this->logger->info( 'Message blob cache-miss for {module}', - [ 'module' => $name, 'cacheKey' => $key ] - ); $blobs[$name] = $this->recacheMessageBlob( $key, $module, $lang ); } else { // Use unexpired cache @@ -241,6 +238,7 @@ class MessageBlobStore implements LoggerAwareInterface { } $json = FormatJson::encode( (object)$messages ); + // @codeCoverageIgnoreStart if ( $json === false ) { $this->logger->warning( 'Failed to encode message blob for {module} ({lang})', [ 'module' => $module->getName(), @@ -248,6 +246,7 @@ class MessageBlobStore implements LoggerAwareInterface { ] ); $json = '{}'; } + // codeCoverageIgnoreEnd return $json; } } diff --git a/includes/cache/MessageCache.php b/includes/cache/MessageCache.php index 0aca213a00b0..8a42a9a72d27 100644 --- a/includes/cache/MessageCache.php +++ b/includes/cache/MessageCache.php @@ -89,10 +89,12 @@ class MessageCache { */ protected $mInParser = false; - /** @var BagOStuff */ - protected $mMemc; /** @var WANObjectCache */ protected $wanCache; + /** @var BagOStuff */ + protected $clusterCache; + /** @var BagOStuff */ + protected $srvCache; /** * Singleton instance @@ -109,9 +111,13 @@ class MessageCache { */ public static function singleton() { if ( self::$instance === null ) { - global $wgUseDatabaseMessages, $wgMsgCacheExpiry; + global $wgUseDatabaseMessages, $wgMsgCacheExpiry, $wgUseLocalMessageCache; self::$instance = new self( + MediaWikiServices::getInstance()->getMainWANObjectCache(), wfGetMessageCacheStorage(), + $wgUseLocalMessageCache + ? MediaWikiServices::getInstance()->getLocalServerObjectCache() + : new EmptyBagOStuff(), $wgUseDatabaseMessages, $wgMsgCacheExpiry ); @@ -149,24 +155,25 @@ class MessageCache { } /** - * @param BagOStuff $memCached A cache instance. If none, fall back to CACHE_NONE. - * @param bool $useDB + * @param WANObjectCache $wanCache WAN cache instance + * @param BagOStuff $clusterCache Cluster cache instance + * @param BagOStuff $srvCache Server cache instance + * @param bool $useDB Whether to look for message overrides (e.g. MediaWiki: pages) * @param int $expiry Lifetime for cache. @see $mExpiry. */ - function __construct( BagOStuff $memCached, $useDB, $expiry ) { - global $wgUseLocalMessageCache; + public function __construct( + WANObjectCache $wanCache, + BagOStuff $clusterCache, + BagOStuff $srvCache, + $useDB, + $expiry + ) { + $this->wanCache = $wanCache; + $this->clusterCache = $clusterCache; + $this->srvCache = $srvCache; - $this->mMemc = $memCached; $this->mDisable = !$useDB; $this->mExpiry = $expiry; - - if ( $wgUseLocalMessageCache ) { - $this->localCache = MediaWikiServices::getInstance()->getLocalServerObjectCache(); - } else { - $this->localCache = new EmptyBagOStuff(); - } - - $this->wanCache = ObjectCache::getMainWANInstance(); } /** @@ -184,11 +191,16 @@ class MessageCache { // either. $po = ParserOptions::newFromAnon(); $po->setEditSection( false ); + $po->setAllowUnsafeRawHtml( false ); return $po; } $this->mParserOptions = new ParserOptions; $this->mParserOptions->setEditSection( false ); + // Messages may take parameters that could come + // from malicious sources. As a precaution, disable + // the <html> parser tag when parsing messages. + $this->mParserOptions->setAllowUnsafeRawHtml( false ); } return $this->mParserOptions; @@ -203,7 +215,7 @@ class MessageCache { protected function getLocalCache( $code ) { $cacheKey = wfMemcKey( __CLASS__, $code ); - return $this->localCache->get( $cacheKey ); + return $this->srvCache->get( $cacheKey ); } /** @@ -214,7 +226,7 @@ class MessageCache { */ protected function saveToLocalCache( $code, $cache ) { $cacheKey = wfMemcKey( __CLASS__, $code ); - $this->localCache->set( $cacheKey, $cache ); + $this->srvCache->set( $cacheKey, $cache ); } /** @@ -300,7 +312,7 @@ class MessageCache { # below, and use the local stale value if it was not acquired. $where[] = 'global cache is presumed expired'; } else { - $cache = $this->mMemc->get( $cacheKey ); + $cache = $this->clusterCache->get( $cacheKey ); if ( !$cache ) { $where[] = 'global cache is empty'; } elseif ( $this->isCacheExpired( $cache ) ) { @@ -381,12 +393,10 @@ class MessageCache { * @return bool|string True on success or one of ("cantacquire", "disabled") */ protected function loadFromDBWithLock( $code, array &$where, $mode = null ) { - global $wgUseLocalMessageCache; - # If cache updates on all levels fail, give up on message overrides. # This is to avoid easy site outages; see $saveSuccess comments below. $statusKey = wfMemcKey( 'messages', $code, 'status' ); - $status = $this->mMemc->get( $statusKey ); + $status = $this->clusterCache->get( $statusKey ); if ( $status === 'error' ) { $where[] = "could not load; method is still globally disabled"; return 'disabled'; @@ -424,8 +434,8 @@ class MessageCache { * incurring a loadFromDB() overhead on every request, and thus saves the * wiki from complete downtime under moderate traffic conditions. */ - if ( !$wgUseLocalMessageCache ) { - $this->mMemc->set( $statusKey, 'error', 60 * 5 ); + if ( $this->srvCache instanceof EmptyBagOStuff ) { + $this->clusterCache->set( $statusKey, 'error', 60 * 5 ); $where[] = 'could not save cache, disabled globally for 5 minutes'; } else { $where[] = "could not save global cache"; @@ -444,7 +454,7 @@ class MessageCache { * @param integer $mode Use MessageCache::FOR_UPDATE to skip process cache * @return array Loaded messages for storing in caches */ - function loadFromDB( $code, $mode = null ) { + protected function loadFromDB( $code, $mode = null ) { global $wgMaxMsgCacheEntrySize, $wgLanguageCode, $wgAdaptiveMessageCache; $dbr = wfGetDB( ( $mode == self::FOR_UPDATE ) ? DB_MASTER : DB_REPLICA ); @@ -475,7 +485,8 @@ class MessageCache { } else { # Effectively disallows use of '/' character in NS_MEDIAWIKI for uses # other than language code. - $conds[] = 'page_title NOT' . $dbr->buildLike( $dbr->anyString(), '/', $dbr->anyString() ); + $conds[] = 'page_title NOT' . + $dbr->buildLike( $dbr->anyString(), '/', $dbr->anyString() ); } # Conditions to fetch oversized pages to ignore them @@ -503,7 +514,7 @@ class MessageCache { $res = $dbr->select( [ 'page', 'revision', 'text' ], - [ 'page_title', 'old_text', 'old_flags' ], + [ 'page_title', 'old_id', 'old_text', 'old_flags' ], $smallConds, __METHOD__ . "($code)-small" ); @@ -518,7 +529,7 @@ class MessageCache { wfDebugLog( 'MessageCache', __METHOD__ - . ": failed to load message page text for {$row->page_title} ($code)" + . ": failed to load message page text for {$row->page_title} ($code)" ); } else { $entry = ' ' . $text; @@ -541,11 +552,11 @@ class MessageCache { /** * Updates cache as necessary when message page is changed * - * @param string|bool $title Name of the page changed (false if deleted) + * @param string $title Message cache key with initial uppercase letter * @param string|bool $text New contents of the page (false if deleted) */ public function replace( $title, $text ) { - global $wgMaxMsgCacheEntrySize, $wgContLang, $wgLanguageCode; + global $wgLanguageCode; if ( $this->mDisable ) { return; @@ -557,63 +568,75 @@ class MessageCache { return; } - // Note that if the cache is volatile, load() may trigger a DB fetch. - // In that case we reenter/reuse the existing cache key lock to avoid - // a self-deadlock. This is safe as no reads happen *directly* in this - // method between getReentrantScopedLock() and load() below. There is - // no risk of data "changing under our feet" for replace(). - $scopedLock = $this->getReentrantScopedLock( wfMemcKey( 'messages', $code ) ); - // Load the messages from the master DB to avoid race conditions - $this->load( $code, self::FOR_UPDATE ); - - // Load the new value into the process cache... + // (a) Update the process cache with the new message text if ( $text === false ) { + // Page deleted $this->mCache[$code][$title] = '!NONEXISTENT'; - } elseif ( strlen( $text ) > $wgMaxMsgCacheEntrySize ) { - $this->mCache[$code][$title] = '!TOO BIG'; - // Pre-fill the individual key cache with the known latest message text - $key = $this->wanCache->makeKey( 'messages-big', $this->mCache[$code]['HASH'], $title ); - $this->wanCache->set( $key, " $text", $this->mExpiry ); } else { + // Ignore $wgMaxMsgCacheEntrySize so the process cache is up to date $this->mCache[$code][$title] = ' ' . $text; } - // Mark this cache as definitely being "latest" (non-volatile) so - // load() calls do not try to refresh the cache with replica DB data - $this->mCache[$code]['LATEST'] = time(); - - // Update caches if the lock was acquired - if ( $scopedLock ) { - $this->saveToCaches( $this->mCache[$code], 'all', $code ); - } else { - LoggerFactory::getInstance( 'MessageCache' )->error( - __METHOD__ . ': could not acquire lock to update {title} ({code})', - [ 'title' => $title, 'code' => $code ] ); - } - - ScopedCallback::consume( $scopedLock ); - // Relay the purge. Touching this check key expires cache contents - // and local cache (APC) validation hash across all datacenters. - $this->wanCache->touchCheckKey( wfMemcKey( 'messages', $code ) ); - - // Also delete cached sidebar... just in case it is affected - $codes = [ $code ]; - if ( $code === 'en' ) { - // Delete all sidebars, like for example on action=purge on the - // sidebar messages - $codes = array_keys( Language::fetchLanguageNames() ); - } - foreach ( $codes as $code ) { - $sidebarKey = wfMemcKey( 'sidebar', $code ); - $this->wanCache->delete( $sidebarKey ); - } + // (b) Update the shared caches in a deferred update with a fresh DB snapshot + DeferredUpdates::addCallableUpdate( + function () use ( $title, $msg, $code ) { + global $wgContLang, $wgMaxMsgCacheEntrySize; + // Allow one caller at a time to avoid race conditions + $scopedLock = $this->getReentrantScopedLock( wfMemcKey( 'messages', $code ) ); + if ( !$scopedLock ) { + LoggerFactory::getInstance( 'MessageCache' )->error( + __METHOD__ . ': could not acquire lock to update {title} ({code})', + [ 'title' => $title, 'code' => $code ] ); + return; + } + // Load the messages from the master DB to avoid race conditions + $cache = $this->loadFromDB( $code, self::FOR_UPDATE ); + $this->mCache[$code] = $cache; + // Load the process cache values and set the per-title cache keys + $page = WikiPage::factory( Title::makeTitle( NS_MEDIAWIKI, $title ) ); + $page->loadPageData( $page::READ_LATEST ); + $text = $this->getMessageTextFromContent( $page->getContent() ); + // Check if an individual cache key should exist and update cache accordingly + if ( is_string( $text ) && strlen( $text ) > $wgMaxMsgCacheEntrySize ) { + $titleKey = $this->bigMessageCacheKey( $this->mCache[$code]['HASH'], $title ); + $this->wanCache->set( $titleKey, ' ' . $text, $this->mExpiry ); + } + // Mark this cache as definitely being "latest" (non-volatile) so + // load() calls do try to refresh the cache with replica DB data + $this->mCache[$code]['LATEST'] = time(); + // Pre-emptively update the local datacenter cache so things like edit filter and + // blacklist changes are reflect immediately, as these often use MediaWiki: pages. + // The datacenter handling replace() calls should be the same one handling edits + // as they require HTTP POST. + $this->saveToCaches( $this->mCache[$code], 'all', $code ); + // Release the lock now that the cache is saved + ScopedCallback::consume( $scopedLock ); + + // Relay the purge. Touching this check key expires cache contents + // and local cache (APC) validation hash across all datacenters. + $this->wanCache->touchCheckKey( wfMemcKey( 'messages', $code ) ); + // Also delete cached sidebar... just in case it is affected + // @TODO: shouldn't this be $code === $wgLanguageCode? + if ( $code === 'en' ) { + // Purge all language sidebars, e.g. on ?action=purge to the sidebar messages + $codes = array_keys( Language::fetchLanguageNames() ); + } else { + // Purge only the sidebar for this language + $codes = [ $code ]; + } + foreach ( $codes as $code ) { + $this->wanCache->delete( wfMemcKey( 'sidebar', $code ) ); + } - // Update the message in the message blob store - $resourceloader = RequestContext::getMain()->getOutput()->getResourceLoader(); - $blobStore = $resourceloader->getMessageBlobStore(); - $blobStore->updateMessage( $wgContLang->lcfirst( $msg ) ); + // Purge the message in the message blob store + $resourceloader = RequestContext::getMain()->getOutput()->getResourceLoader(); + $blobStore = $resourceloader->getMessageBlobStore(); + $blobStore->updateMessage( $wgContLang->lcfirst( $msg ) ); - Hooks::run( 'MessageCacheReplace', [ $title, $text ] ); + Hooks::run( 'MessageCacheReplace', [ $title, $text ] ); + }, + DeferredUpdates::PRESEND + ); } /** @@ -648,7 +671,7 @@ class MessageCache { protected function saveToCaches( array $cache, $dest, $code = false ) { if ( $dest === 'all' ) { $cacheKey = wfMemcKey( 'messages', $code ); - $success = $this->mMemc->set( $cacheKey, $cache ); + $success = $this->clusterCache->set( $cacheKey, $cache ); $this->setValidationHash( $code, $cache ); } else { $success = true; @@ -720,7 +743,7 @@ class MessageCache { * @return null|ScopedCallback */ protected function getReentrantScopedLock( $key, $timeout = self::WAIT_SEC ) { - return $this->mMemc->getScopedLock( $key, $timeout, self::LOCK_TTL, __METHOD__ ); + return $this->clusterCache->getScopedLock( $key, $timeout, self::LOCK_TTL, __METHOD__ ); } /** @@ -845,7 +868,7 @@ class MessageCache { $alreadyTried = []; - // First try the requested language. + // First try the requested language. $message = $this->getMessageForLang( $lang, $lckey, $useDB, $alreadyTried ); if ( $message !== false ) { return $message; @@ -944,12 +967,13 @@ class MessageCache { * some callers require this behavior. LanguageConverter::parseCachedTable() * and self::get() are some examples in core. * - * @param string $title Message cache key with initial uppercase letter. - * @param string $code Code denoting the language to try. + * @param string $title Message cache key with initial uppercase letter + * @param string $code Code denoting the language to try * @return string|bool The message, or false if it does not exist or on error */ public function getMsgFromNamespace( $title, $code ) { $this->load( $code ); + if ( isset( $this->mCache[$code][$title] ) ) { $entry = $this->mCache[$code][$title]; if ( substr( $entry, 0, 1 ) === ' ' ) { @@ -963,7 +987,7 @@ class MessageCache { } else { // XXX: This is not cached in process cache, should it? $message = false; - Hooks::run( 'MessagesPreLoad', [ $title, &$message ] ); + Hooks::run( 'MessagesPreLoad', [ $title, &$message, $code ] ); if ( $message !== false ) { return $message; } @@ -971,8 +995,8 @@ class MessageCache { return false; } - // Try the individual message cache - $titleKey = $this->wanCache->makeKey( 'messages-big', $this->mCache[$code]['HASH'], $title ); + // Individual message cache key + $titleKey = $this->bigMessageCacheKey( $this->mCache[$code]['HASH'], $title ); if ( $this->mCacheVolatile[$code] ) { $entry = false; @@ -981,6 +1005,7 @@ class MessageCache { __METHOD__ . ': loading volatile key \'{titleKey}\'', [ 'titleKey' => $titleKey, 'code' => $code ] ); } else { + // Try the individual message cache $entry = $this->wanCache->get( $titleKey ); } @@ -1033,7 +1058,8 @@ class MessageCache { $message = false; // negative caching } - if ( $message === false ) { // negative caching + if ( $message === false ) { + // Negative caching in case a "too big" message is no longer available (deleted) $this->mCache[$code][$title] = '!NONEXISTENT'; $this->wanCache->set( $titleKey, '!NONEXISTENT', $this->mExpiry, $cacheOpts ); } @@ -1277,4 +1303,13 @@ class MessageCache { return $msgText; } + + /** + * @param string $hash Hash for this version of the entire key/value overrides map + * @param string $title Message cache key with initial uppercase letter + * @return string + */ + private function bigMessageCacheKey( $hash, $title ) { + return $this->wanCache->makeKey( 'messages-big', $hash, $title ); + } } diff --git a/includes/cache/localisation/LCStoreDB.php b/includes/cache/localisation/LCStoreDB.php index e7e2d10308f8..52611ec508c3 100644 --- a/includes/cache/localisation/LCStoreDB.php +++ b/includes/cache/localisation/LCStoreDB.php @@ -18,6 +18,8 @@ * @file */ +use Wikimedia\Rdbms\IDatabase; + /** * LCStore implementation which uses the standard DB functions to store data. * This will work on any MediaWiki installation. diff --git a/includes/cache/localisation/LocalisationCache.php b/includes/cache/localisation/LocalisationCache.php index 90b3de1ac1b9..d499340d0e5c 100644 --- a/includes/cache/localisation/LocalisationCache.php +++ b/includes/cache/localisation/LocalisationCache.php @@ -212,23 +212,21 @@ class LocalisationCache { case 'detect': if ( !empty( $conf['storeDirectory'] ) ) { $storeClass = 'LCStoreCDB'; + } elseif ( $wgCacheDirectory ) { + $storeConf['directory'] = $wgCacheDirectory; + $storeClass = 'LCStoreCDB'; } else { - $cacheDir = $wgCacheDirectory ?: wfTempDir(); - if ( $cacheDir ) { - $storeConf['directory'] = $cacheDir; - $storeClass = 'LCStoreCDB'; - } else { - $storeClass = 'LCStoreDB'; - } + $storeClass = 'LCStoreDB'; } break; default: throw new MWException( - 'Please set $wgLocalisationCacheConf[\'store\'] to something sensible.' ); + 'Please set $wgLocalisationCacheConf[\'store\'] to something sensible.' + ); } } - wfDebugLog( 'caches', get_class( $this ) . ": using store $storeClass" ); + wfDebugLog( 'caches', static::class . ": using store $storeClass" ); if ( !empty( $conf['storeDirectory'] ) ) { $storeConf['directory'] = $conf['storeDirectory']; } diff --git a/includes/changes/ChangesFeed.php b/includes/changes/ChangesFeed.php index 15a00c70365e..cffb59a4a82c 100644 --- a/includes/changes/ChangesFeed.php +++ b/includes/changes/ChangesFeed.php @@ -20,6 +20,8 @@ * @file */ +use Wikimedia\Rdbms\ResultWrapper; + /** * Feed to Special:RecentChanges and Special:RecentChangesLiked * @@ -152,7 +154,7 @@ class ChangesFeed { if ( $feedAge < $wgFeedCacheTimeout || $feedLastmodUnix > $lastmodUnix ) { wfDebug( "RC: loading feed from cache ($key; $feedLastmod; $lastmod)...\n" ); if ( $feedLastmodUnix < $lastmodUnix ) { - $wgOut->setLastModified( $feedLastmod ); // bug 21916 + $wgOut->setLastModified( $feedLastmod ); // T23916 } return $cache->get( $key ); } else { diff --git a/includes/changes/ChangesList.php b/includes/changes/ChangesList.php index 77038edd7d70..92a3d3f2e22b 100644 --- a/includes/changes/ChangesList.php +++ b/includes/changes/ChangesList.php @@ -23,8 +23,11 @@ */ use MediaWiki\Linker\LinkRenderer; use MediaWiki\MediaWikiServices; +use Wikimedia\Rdbms\ResultWrapper; class ChangesList extends ContextSource { + const CSS_CLASS_PREFIX = 'mw-changeslist-'; + /** * @var Skin */ @@ -47,11 +50,17 @@ class ChangesList extends ContextSource { protected $linkRenderer; /** + * @var array + */ + protected $filterGroups; + + /** * Changeslist constructor * * @param Skin|IContextSource $obj + * @param array $filterGroups Array of ChangesListFilterGroup objects (currently optional) */ - public function __construct( $obj ) { + public function __construct( $obj, array $filterGroups = [] ) { if ( $obj instanceof IContextSource ) { $this->setContext( $obj ); $this->skin = $obj->getSkin(); @@ -62,6 +71,7 @@ class ChangesList extends ContextSource { $this->preCacheMessages(); $this->watchMsgCache = new HashBagOStuff( [ 'maxKeys' => 50 ] ); $this->linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer(); + $this->filterGroups = $filterGroups; } /** @@ -69,16 +79,19 @@ class ChangesList extends ContextSource { * Some users might want to use an enhanced list format, for instance * * @param IContextSource $context + * @param array $groups Array of ChangesListFilterGroup objects (currently optional) * @return ChangesList */ - public static function newFromContext( IContextSource $context ) { + public static function newFromContext( IContextSource $context, array $groups = [] ) { $user = $context->getUser(); $sk = $context->getSkin(); $list = null; if ( Hooks::run( 'FetchChangesList', [ $user, &$sk, &$list ] ) ) { $new = $context->getRequest()->getBool( 'enhanced', $user->getOption( 'usenewrc' ) ); - return $new ? new EnhancedChangesList( $context ) : new OldChangesList( $context ); + return $new ? + new EnhancedChangesList( $context, $groups ) : + new OldChangesList( $context, $groups ); } else { return $list; } @@ -160,17 +173,39 @@ class ChangesList extends ContextSource { $logType = $rc->mAttribs['rc_log_type']; if ( $logType ) { - $classes[] = Sanitizer::escapeClass( 'mw-changeslist-log-' . $logType ); + $classes[] = Sanitizer::escapeClass( self::CSS_CLASS_PREFIX . 'log-' . $logType ); } else { - $classes[] = Sanitizer::escapeClass( 'mw-changeslist-ns' . + $classes[] = Sanitizer::escapeClass( self::CSS_CLASS_PREFIX . 'ns' . $rc->mAttribs['rc_namespace'] . '-' . $rc->mAttribs['rc_title'] ); } // Indicate watched status on the line to allow for more // comprehensive styling. $classes[] = $watched && $rc->mAttribs['rc_timestamp'] >= $watched - ? 'mw-changeslist-line-watched' - : 'mw-changeslist-line-not-watched'; + ? self::CSS_CLASS_PREFIX . 'line-watched' + : self::CSS_CLASS_PREFIX . 'line-not-watched'; + + $classes = array_merge( $classes, $this->getHTMLClassesForFilters( $rc ) ); + + return $classes; + } + + /** + * Get an array of CSS classes attributed to filters for this row + * + * @param RecentChange $rc + * @return array Array of CSS classes + */ + protected function getHTMLClassesForFilters( $rc ) { + $classes = []; + + if ( $this->filterGroups !== null ) { + foreach ( $this->filterGroups as $filterGroup ) { + foreach ( $filterGroup->getFilters() as $filter ) { + $filter->applyCssClassIfNeeded( $this, $rc, $classes ); + } + } + } return $classes; } @@ -379,7 +414,7 @@ class ChangesList extends ContextSource { $diffLink = $this->linkRenderer->makeKnownLink( $rc->getTitle(), new HtmlArmor( $this->message['diff'] ), - [], + [ 'class' => 'mw-changeslist-diff' ], $query ); } @@ -391,7 +426,7 @@ class ChangesList extends ContextSource { $diffhist .= $this->linkRenderer->makeKnownLink( $rc->getTitle(), new HtmlArmor( $this->message['hist'] ), - [], + [ 'class' => 'mw-changeslist-history' ], [ 'curid' => $rc->mAttribs['rc_cur_id'], 'action' => 'history' @@ -444,8 +479,10 @@ class ChangesList extends ContextSource { # TODO: Deprecate the $s argument, it seems happily unused. $s = ''; + # Avoid PHP 7.1 warning from passing $this by reference + $changesList = $this; Hooks::run( 'ChangesListInsertArticleLink', - [ &$this, &$articlelink, &$s, &$rc, $unpatrolled, $watched ] ); + [ &$changesList, &$articlelink, &$s, &$rc, $unpatrolled, $watched ] ); return "{$s} {$articlelink}"; } diff --git a/includes/changes/ChangesListBooleanFilter.php b/includes/changes/ChangesListBooleanFilter.php new file mode 100644 index 000000000000..4117a110cfce --- /dev/null +++ b/includes/changes/ChangesListBooleanFilter.php @@ -0,0 +1,221 @@ +<?php +/** + * Represents a hide-based boolean filter (used on ChangesListSpecialPage and descendants) + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @license GPL 2+ + * @author Matthew Flaschen + */ + +use Wikimedia\Rdbms\IDatabase; + +/** + * An individual filter in a boolean group + * + * @since 1.29 + */ +class ChangesListBooleanFilter extends ChangesListFilter { + /** + * Name. Used as URL parameter + * + * @var string $name + */ + + // This can sometimes be different on Special:RecentChanges + // and Special:Watchlist, due to the double-legacy hooks + // (SpecialRecentChangesFilters and SpecialWatchlistFilters) + + // but there will be separate sets of ChangesListFilterGroup and ChangesListFilter instances + // for those pages (it should work even if they're both loaded + // at once, but that can't happen). + /** + * Main unstructured UI i18n key + * + * @var string $showHide + */ + protected $showHide; + + /** + * Whether there is a feature designed to replace this filter available on the + * structured UI + * + * @var bool $isReplacedInStructuredUi + */ + protected $isReplacedInStructuredUi; + + /** + * Default + * + * @var bool $defaultValue + */ + protected $defaultValue; + + /** + * Callable used to do the actual query modification; see constructor + * + * @var callable $queryCallable + */ + protected $queryCallable; + + /** + * Create a new filter with the specified configuration. + * + * It infers which UI (it can be either or both) to display the filter on based on + * which messages are provided. + * + * If 'label' is provided, it will be displayed on the structured UI. If + * 'showHide' is provided, it will be displayed on the unstructured UI. Thus, + * 'label', 'description', and 'showHide' are optional depending on which UI + * it's for. + * + * @param array $filterDefinition ChangesListFilter definition + * + * $filterDefinition['name'] string Name. Used as URL parameter. + * $filterDefinition['group'] ChangesListFilterGroup Group. Filter group this + * belongs to. + * $filterDefinition['label'] string i18n key of label for structured UI. + * $filterDefinition['description'] string i18n key of description for structured + * UI. + * $filterDefinition['showHide'] string Main i18n key used for unstructured UI. + * $filterDefinition['isReplacedInStructuredUi'] bool Whether there is an + * equivalent feature available in the structured UI; this is optional, defaulting + * to true. It does not need to be set if the exact same filter is simply visible + * on both. + * $filterDefinition['default'] bool Default + * $filterDefinition['priority'] int Priority integer. Higher value means higher + * up in the group's filter list. + * $filterDefinition['queryCallable'] callable Callable accepting parameters, used + * to implement filter's DB query modification. Callback parameters: + * string $specialPageClassName Class name of current special page + * IContextSource $context Context, for e.g. user + * IDatabase $dbr Database, for addQuotes, makeList, and similar + * array &$tables Array of tables; see IDatabase::select $table + * array &$fields Array of fields; see IDatabase::select $vars + * array &$conds Array of conditions; see IDatabase::select $conds + * array &$query_options Array of query options; see IDatabase::select $options + * array &$join_conds Array of join conditions; see IDatabase::select $join_conds + * Optional only for legacy filters that still use the query hooks directly + */ + public function __construct( $filterDefinition ) { + parent::__construct( $filterDefinition ); + + if ( isset( $filterDefinition['showHide'] ) ) { + $this->showHide = $filterDefinition['showHide']; + } + + if ( isset( $filterDefinition['isReplacedInStructuredUi'] ) ) { + $this->isReplacedInStructuredUi = $filterDefinition['isReplacedInStructuredUi']; + } else { + $this->isReplacedInStructuredUi = false; + } + + if ( isset( $filterDefinition['default'] ) ) { + $this->defaultValue = $filterDefinition['default']; + } else { + throw new MWException( 'You must set a default' ); + } + + if ( isset( $filterDefinition['queryCallable'] ) ) { + $this->queryCallable = $filterDefinition['queryCallable']; + } + } + + /** + * @return bool|null Default value + */ + public function getDefault() { + return $this->defaultValue; + } + + /** + * Sets default + * + * @param bool Default value + */ + public function setDefault( $defaultValue ) { + $this->defaultValue = $defaultValue; + } + + /** + * @return string Main i18n key for unstructured UI + */ + public function getShowHide() { + return $this->showHide; + } + + /** + * @inheritdoc + */ + public function displaysOnUnstructuredUi() { + return !!$this->showHide; + } + + /** + * @inheritdoc + */ + public function isFeatureAvailableOnStructuredUi() { + return $this->isReplacedInStructuredUi || + parent::isFeatureAvailableOnStructuredUi(); + } + + /** + * Modifies the query to include the filter. This is only called if the filter is + * in effect (taking into account the default). + * + * @param IDatabase $dbr Database, for addQuotes, makeList, and similar + * @param ChangesListSpecialPage $specialPage Current special page + * @param array &$tables Array of tables; see IDatabase::select $table + * @param array &$fields Array of fields; see IDatabase::select $vars + * @param array &$conds Array of conditions; see IDatabase::select $conds + * @param array &$query_options Array of query options; see IDatabase::select $options + * @param array &$join_conds Array of join conditions; see IDatabase::select $join_conds + */ + public function modifyQuery( IDatabase $dbr, ChangesListSpecialPage $specialPage, + &$tables, &$fields, &$conds, &$query_options, &$join_conds ) { + + if ( $this->queryCallable === null ) { + return; + } + + call_user_func_array( + $this->queryCallable, + [ + get_class( $specialPage ), + $specialPage->getContext(), + $dbr, + &$tables, + &$fields, + &$conds, + &$query_options, + &$join_conds + ] + ); + } + + /** + * @inheritdoc + */ + public function getJsData() { + $output = parent::getJsData(); + + $output['default'] = $this->defaultValue; + + return $output; + } + +} diff --git a/includes/changes/ChangesListBooleanFilterGroup.php b/includes/changes/ChangesListBooleanFilterGroup.php new file mode 100644 index 000000000000..1fdcd0021e45 --- /dev/null +++ b/includes/changes/ChangesListBooleanFilterGroup.php @@ -0,0 +1,59 @@ +<?php + +/** + * If the group is active, any unchecked filters will + * translate to hide parameters in the URL. E.g. if 'Human (not bot)' is checked, + * but 'Bot' is unchecked, hidebots=1 will be sent. + * + * @since 1.29 + */ +class ChangesListBooleanFilterGroup extends ChangesListFilterGroup { + /** + * Type marker, used by JavaScript + */ + const TYPE = 'send_unselected_if_any'; + + /** + * Create a new filter group with the specified configuration + * + * @param array $groupDefinition Configuration of group + * * $groupDefinition['name'] string Group name + * * $groupDefinition['title'] string i18n key for title (optional, can be omitted + * * only if none of the filters in the group display in the structured UI) + * * $groupDefinition['priority'] int Priority integer. Higher means higher in the + * * group list. + * * $groupDefinition['filters'] array Numeric array of filter definitions, each of which + * * is an associative array to be passed to the filter constructor. However, + * * 'priority' is optional for the filters. Any filter that has priority unset + * * will be put to the bottom, in the order given. + */ + public function __construct( array $groupDefinition ) { + $groupDefinition['isFullCoverage'] = true; + $groupDefinition['type'] = self::TYPE; + + parent::__construct( $groupDefinition ); + } + + /** + * @inheritdoc + */ + protected function createFilter( array $filterDefinition ) { + return new ChangesListBooleanFilter( $filterDefinition ); + } + + /** + * Registers a filter in this group + * + * @param ChangesListBooleanFilter $filter ChangesListBooleanFilter + */ + public function registerFilter( ChangesListBooleanFilter $filter ) { + $this->filters[$filter->getName()] = $filter; + } + + /** + * @inheritdoc + */ + public function isPerGroupRequestParameter() { + return false; + } +} diff --git a/includes/changes/ChangesListFilter.php b/includes/changes/ChangesListFilter.php new file mode 100644 index 000000000000..b3a16a81b36f --- /dev/null +++ b/includes/changes/ChangesListFilter.php @@ -0,0 +1,398 @@ +<?php +/** + * Represents a filter (used on ChangesListSpecialPage and descendants) + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @license GPL 2+ + * @author Matthew Flaschen + */ + +/** + * Represents a filter (used on ChangesListSpecialPage and descendants) + * + * @since 1.29 + */ +abstract class ChangesListFilter { + /** + * Filter name + * + * @var string $name + */ + protected $name; + + /** + * CSS class suffix used for attribution, e.g. 'bot'. + * + * In this example, if bot actions are included in the result set, this CSS class + * will then be included in all bot-flagged actions. + * + * @var string|null $cssClassSuffix + */ + protected $cssClassSuffix; + + /** + * Callable that returns true if and only if a row is attributed to this filter + * + * @var callable $isRowApplicableCallable + */ + protected $isRowApplicableCallable; + + /** + * Group. ChangesListFilterGroup this belongs to + * + * @var ChangesListFilterGroup $group + */ + protected $group; + + /** + * i18n key of label for structured UI + * + * @var string $label + */ + protected $label; + + /** + * i18n key of description for structured UI + * + * @var string $description + */ + protected $description; + + /** + * List of conflicting groups + * + * @var array $conflictingGroups Array of associative arrays with conflict + * information. See setUnidirectionalConflict + */ + protected $conflictingGroups = []; + + /** + * List of conflicting filters + * + * @var array $conflictingFilters Array of associative arrays with conflict + * information. See setUnidirectionalConflict + */ + protected $conflictingFilters = []; + + /** + * List of filters that are a subset of the current filter + * + * @var array $subsetFilters Array of associative arrays with subset information + */ + protected $subsetFilters = []; + + /** + * Priority integer. Higher value means higher up in the group's filter list. + * + * @var string $priority + */ + protected $priority; + + const RESERVED_NAME_CHAR = '_'; + + /** + * Creates a new filter with the specified configuration, and registers it to the + * specified group. + * + * It infers which UI (it can be either or both) to display the filter on based on + * which messages are provided. + * + * If 'label' is provided, it will be displayed on the structured UI. Thus, + * 'label', 'description', and sub-class parameters are optional depending on which + * UI it's for. + * + * @param array $filterDefinition ChangesListFilter definition + * + * $filterDefinition['name'] string Name of filter; use lowercase with no + * punctuation + * $filterDefinition['cssClassSuffix'] string CSS class suffix, used to mark + * that a particular row belongs to this filter (when a row is included by the + * filter) (optional) + * $filterDefinition['isRowApplicableCallable'] Callable taking two parameters, the + * IContextSource, and the RecentChange object for the row, and returning true if + * the row is attributed to this filter. The above CSS class will then be + * automatically added (optional, required if cssClassSuffix is used). + * $filterDefinition['group'] ChangesListFilterGroup Group. Filter group this + * belongs to. + * $filterDefinition['label'] string i18n key of label for structured UI. + * $filterDefinition['description'] string i18n key of description for structured + * UI. + * $filterDefinition['priority'] int Priority integer. Higher value means higher + * up in the group's filter list. + */ + public function __construct( array $filterDefinition ) { + if ( isset( $filterDefinition['group'] ) ) { + $this->group = $filterDefinition['group']; + } else { + throw new MWException( 'You must use \'group\' to specify the ' . + 'ChangesListFilterGroup this filter belongs to' ); + } + + if ( strpos( $filterDefinition['name'], self::RESERVED_NAME_CHAR ) !== false ) { + throw new MWException( 'Filter names may not contain \'' . + self::RESERVED_NAME_CHAR . + '\'. Use the naming convention: \'lowercase\'' + ); + } + + if ( $this->group->getFilter( $filterDefinition['name'] ) ) { + throw new MWException( 'Two filters in a group cannot have the ' . + "same name: '{$filterDefinition['name']}'" ); + } + + $this->name = $filterDefinition['name']; + + if ( isset( $filterDefinition['cssClassSuffix'] ) ) { + $this->cssClassSuffix = $filterDefinition['cssClassSuffix']; + $this->isRowApplicableCallable = $filterDefinition['isRowApplicableCallable']; + } + + if ( isset( $filterDefinition['label'] ) ) { + $this->label = $filterDefinition['label']; + $this->description = $filterDefinition['description']; + } + + $this->priority = $filterDefinition['priority']; + + $this->group->registerFilter( $this ); + } + + /** + * Marks that the given ChangesListFilterGroup or ChangesListFilter conflicts with this object. + * + * WARNING: This means there is a conflict when both things are *shown* + * (not filtered out), even for the hide-based filters. So e.g. conflicting with + * 'hideanons' means there is a conflict if only anonymous users are *shown*. + * + * @param ChangesListFilterGroup|ChangesListFilter $other Other + * ChangesListFilterGroup or ChangesListFilter + * @param string $globalKey i18n key for top-level conflict message + * @param string $forwardKey i18n key for conflict message in this + * direction (when in UI context of $this object) + * @param string $backwardKey i18n key for conflict message in reverse + * direction (when in UI context of $other object) + */ + public function conflictsWith( $other, $globalKey, $forwardKey, + $backwardKey ) { + + if ( $globalKey === null || $forwardKey === null || + $backwardKey === null ) { + + throw new MWException( 'All messages must be specified' ); + } + + $this->setUnidirectionalConflict( + $other, + $globalKey, + $forwardKey + ); + + $other->setUnidirectionalConflict( + $this, + $globalKey, + $backwardKey + ); + } + + /** + * Marks that the given ChangesListFilterGroup or ChangesListFilter conflicts with + * this object. + * + * Internal use ONLY. + * + * @param ChangesListFilterGroup|ChangesListFilter $other Other + * ChangesListFilterGroup or ChangesListFilter + * @param string $globalDescription i18n key for top-level conflict message + * @param string $contextDescription i18n key for conflict message in this + * direction (when in UI context of $this object) + */ + public function setUnidirectionalConflict( $other, $globalDescription, + $contextDescription ) { + + if ( $other instanceof ChangesListFilterGroup ) { + $this->conflictingGroups[] = [ + 'group' => $other->getName(), + 'globalDescription' => $globalDescription, + 'contextDescription' => $contextDescription, + ]; + } elseif ( $other instanceof ChangesListFilter ) { + $this->conflictingFilters[] = [ + 'group' => $other->getGroup()->getName(), + 'filter' => $other->getName(), + 'globalDescription' => $globalDescription, + 'contextDescription' => $contextDescription, + ]; + } else { + throw new MWException( 'You can only pass in a ChangesListFilterGroup or a ChangesListFilter' ); + } + } + + /** + * Marks that the current instance is (also) a superset of the filter passed in. + * This can be called more than once. + * + * This means that anything in the results for the other filter is also in the + * results for this one. + * + * @param ChangesListFilter The filter the current instance is a superset of + */ + public function setAsSupersetOf( ChangesListFilter $other ) { + if ( $other->getGroup() !== $this->getGroup() ) { + throw new MWException( 'Supersets can only be defined for filters in the same group' ); + } + + $this->subsetFilters[] = [ + // It's always the same group, but this makes the representation + // more consistent with conflicts. + 'group' => $other->getGroup()->getName(), + 'filter' => $other->getName(), + ]; + } + + /** + * @return string Name, e.g. hideanons + */ + public function getName() { + return $this->name; + } + + /** + * @return ChangesListFilterGroup Group this belongs to + */ + public function getGroup() { + return $this->group; + } + + /** + * @return string i18n key of label for structured UI + */ + public function getLabel() { + return $this->label; + } + + /** + * @return string i18n key of description for structured UI + */ + public function getDescription() { + return $this->description; + } + + /** + * Checks whether the filter should display on the unstructured UI + * + * @return bool Whether to display + */ + abstract public function displaysOnUnstructuredUi(); + + /** + * Checks whether the filter should display on the structured UI + * This refers to the exact filter. See also isFeatureAvailableOnStructuredUi. + * + * @return bool Whether to display + */ + public function displaysOnStructuredUi() { + return $this->label !== null; + } + + /** + * Checks whether an equivalent feature for this filter is available on the + * structured UI. + * + * This can either be the exact filter, or a new filter that replaces it. + */ + public function isFeatureAvailableOnStructuredUi() { + return $this->displaysOnStructuredUi(); + } + + /** + * @return int Priority. Higher value means higher up in the group list + */ + public function getPriority() { + return $this->priority; + } + + /** + * Gets the CSS class + * + * @return string|null CSS class, or null if not defined + */ + protected function getCssClass() { + if ( $this->cssClassSuffix !== null ) { + return ChangesList::CSS_CLASS_PREFIX . $this->cssClassSuffix; + } else { + return null; + } + } + + /** + * Add CSS class if needed + * + * @param IContextSource $ctx Context source + * @param RecentChange $rc Recent changes object + * @param Non-associative array of CSS class names; appended to if needed + */ + public function applyCssClassIfNeeded( IContextSource $ctx, RecentChange $rc, array &$classes ) { + if ( $this->isRowApplicableCallable === null ) { + return; + } + + if ( call_user_func( $this->isRowApplicableCallable, $ctx, $rc ) ) { + $classes[] = $this->getCssClass(); + } + } + + /** + * Gets the JS data required by the front-end of the structured UI + * + * @return array Associative array Data required by the front-end. messageKeys is + * a special top-level value, with the value being an array of the message keys to + * send to the client. + */ + public function getJsData() { + $output = [ + 'name' => $this->getName(), + 'label' => $this->getLabel(), + 'description' => $this->getDescription(), + 'cssClass' => $this->getCssClass(), + 'priority' => $this->priority, + 'subset' => $this->subsetFilters, + 'conflicts' => [], + ]; + + $output['messageKeys'] = [ + $this->getLabel(), + $this->getDescription(), + ]; + + $conflicts = array_merge( + $this->conflictingGroups, + $this->conflictingFilters + ); + + foreach ( $conflicts as $conflictInfo ) { + $output['conflicts'][] = $conflictInfo; + array_push( + $output['messageKeys'], + $conflictInfo['globalDescription'], + $conflictInfo['contextDescription'] + ); + } + + return $output; + } +} diff --git a/includes/changes/ChangesListFilterGroup.php b/includes/changes/ChangesListFilterGroup.php new file mode 100644 index 000000000000..4ff55201cc81 --- /dev/null +++ b/includes/changes/ChangesListFilterGroup.php @@ -0,0 +1,402 @@ +<?php +/** + * Represents a filter group (used on ChangesListSpecialPage and descendants) + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @license GPL 2+ + * @author Matthew Flaschen + */ + +// TODO: Might want to make a super-class or trait to share behavior (especially re +// conflicts) between ChangesListFilter and ChangesListFilterGroup. +// What to call it. FilterStructure? That would also let me make +// setUnidirectionalConflict protected. + +/** + * Represents a filter group (used on ChangesListSpecialPage and descendants) + * + * @since 1.29 + */ +abstract class ChangesListFilterGroup { + /** + * Name (internal identifier) + * + * @var string $name + */ + protected $name; + + /** + * i18n key for title + * + * @var string $title + */ + protected $title; + + /** + * i18n key for header of What's This? + * + * @var string|null $whatsThisHeader + */ + protected $whatsThisHeader; + + /** + * i18n key for body of What's This? + * + * @var string|null $whatsThisBody + */ + protected $whatsThisBody; + + /** + * URL of What's This? link + * + * @var string|null $whatsThisUrl + */ + protected $whatsThisUrl; + + /** + * i18n key for What's This? link + * + * @var string|null $whatsThisLinkText + */ + protected $whatsThisLinkText; + + /** + * Type, from a TYPE constant of a subclass + * + * @var string $type + */ + protected $type; + + /** + * Priority integer. Higher values means higher up in the + * group list. + * + * @var string $priority + */ + protected $priority; + + /** + * Associative array of filters, as ChangesListFilter objects, with filter name as key + * + * @var array $filters + */ + protected $filters; + + /** + * Whether this group is full coverage. This means that checking every item in the + * group means no changes list (e.g. RecentChanges) entries are filtered out. + * + * @var bool $isFullCoverage + */ + protected $isFullCoverage; + + /** + * List of conflicting groups + * + * @var array $conflictingGroups Array of associative arrays with conflict + * information. See setUnidirectionalConflict + */ + protected $conflictingGroups = []; + + /** + * List of conflicting filters + * + * @var array $conflictingFilters Array of associative arrays with conflict + * information. See setUnidirectionalConflict + */ + protected $conflictingFilters = []; + + const DEFAULT_PRIORITY = -100; + + const RESERVED_NAME_CHAR = '_'; + + /** + * Create a new filter group with the specified configuration + * + * @param array $groupDefinition Configuration of group + * * $groupDefinition['name'] string Group name; use camelCase with no punctuation + * * $groupDefinition['title'] string i18n key for title (optional, can be omitted + * * only if none of the filters in the group display in the structured UI) + * * $groupDefinition['type'] string A type constant from a subclass of this one + * * $groupDefinition['priority'] int Priority integer. Higher value means higher + * * up in the group list (optional, defaults to -100). + * * $groupDefinition['filters'] array Numeric array of filter definitions, each of which + * * is an associative array to be passed to the filter constructor. However, + * * 'priority' is optional for the filters. Any filter that has priority unset + * * will be put to the bottom, in the order given. + * * $groupDefinition['isFullCoverage'] bool Whether the group is full coverage; + * * if true, this means that checking every item in the group means no + * * changes list entries are filtered out. + */ + public function __construct( array $groupDefinition ) { + if ( strpos( $groupDefinition['name'], self::RESERVED_NAME_CHAR ) !== false ) { + throw new MWException( 'Group names may not contain \'' . + self::RESERVED_NAME_CHAR . + '\'. Use the naming convention: \'camelCase\'' + ); + } + + $this->name = $groupDefinition['name']; + + if ( isset( $groupDefinition['title'] ) ) { + $this->title = $groupDefinition['title']; + } + + if ( isset ( $groupDefinition['whatsThisHeader'] ) ) { + $this->whatsThisHeader = $groupDefinition['whatsThisHeader']; + $this->whatsThisBody = $groupDefinition['whatsThisBody']; + $this->whatsThisUrl = $groupDefinition['whatsThisUrl']; + $this->whatsThisLinkText = $groupDefinition['whatsThisLinkText']; + } + + $this->type = $groupDefinition['type']; + if ( isset( $groupDefinition['priority'] ) ) { + $this->priority = $groupDefinition['priority']; + } else { + $this->priority = self::DEFAULT_PRIORITY; + } + + $this->isFullCoverage = $groupDefinition['isFullCoverage']; + + $this->filters = []; + $lowestSpecifiedPriority = -1; + foreach ( $groupDefinition['filters'] as $filterDefinition ) { + if ( isset( $filterDefinition['priority'] ) ) { + $lowestSpecifiedPriority = min( $lowestSpecifiedPriority, $filterDefinition['priority'] ); + } + } + + // Convenience feature: If you specify a group (and its filters) all in + // one place, you don't have to specify priority. You can just put them + // in order. However, if you later add one (e.g. an extension adds a filter + // to a core-defined group), you need to specify it. + $autoFillPriority = $lowestSpecifiedPriority - 1; + foreach ( $groupDefinition['filters'] as $filterDefinition ) { + if ( !isset( $filterDefinition['priority'] ) ) { + $filterDefinition['priority'] = $autoFillPriority; + $autoFillPriority--; + } + $filterDefinition['group'] = $this; + + $filter = $this->createFilter( $filterDefinition ); + $this->registerFilter( $filter ); + } + } + + /** + * Creates a filter of the appropriate type for this group, from the definition + * + * @param array $filterDefinition Filter definition + * @return ChangesListFilter Filter + */ + abstract protected function createFilter( array $filterDefinition ); + + /** + * Marks that the given ChangesListFilterGroup or ChangesListFilter conflicts with this object. + * + * WARNING: This means there is a conflict when both things are *shown* + * (not filtered out), even for the hide-based filters. So e.g. conflicting with + * 'hideanons' means there is a conflict if only anonymous users are *shown*. + * + * @param ChangesListFilterGroup|ChangesListFilter $other Other + * ChangesListFilterGroup or ChangesListFilter + * @param string $globalKey i18n key for top-level conflict message + * @param string $forwardKey i18n key for conflict message in this + * direction (when in UI context of $this object) + * @param string $backwardKey i18n key for conflict message in reverse + * direction (when in UI context of $other object) + */ + public function conflictsWith( $other, $globalKey, $forwardKey, + $backwardKey ) { + + if ( $globalKey === null || $forwardKey === null || + $backwardKey === null ) { + + throw new MWException( 'All messages must be specified' ); + } + + $this->setUnidirectionalConflict( + $other, + $globalKey, + $forwardKey + ); + + $other->setUnidirectionalConflict( + $this, + $globalKey, + $backwardKey + ); + } + + /** + * Marks that the given ChangesListFilterGroup or ChangesListFilter conflicts with + * this object. + * + * Internal use ONLY. + * + * @param ChangesListFilterGroup|ChangesListFilter $other Other + * ChangesListFilterGroup or ChangesListFilter + * @param string $globalDescription i18n key for top-level conflict message + * @param string $contextDescription i18n key for conflict message in this + * direction (when in UI context of $this object) + */ + public function setUnidirectionalConflict( $other, $globalDescription, + $contextDescription ) { + + if ( $other instanceof ChangesListFilterGroup ) { + $this->conflictingGroups[] = [ + 'group' => $other->getName(), + 'globalDescription' => $globalDescription, + 'contextDescription' => $contextDescription, + ]; + } elseif ( $other instanceof ChangesListFilter ) { + $this->conflictingFilters[] = [ + 'group' => $other->getGroup()->getName(), + 'filter' => $other->getName(), + 'globalDescription' => $globalDescription, + 'contextDescription' => $contextDescription, + ]; + } else { + throw new MWException( 'You can only pass in a ChangesListFilterGroup or a ChangesListFilter' ); + } + } + + /** + * @return string Internal name + */ + public function getName() { + return $this->name; + } + + /** + * @return string i18n key for title + */ + public function getTitle() { + return $this->title; + } + + /** + * @return string Type (TYPE constant from a subclass) + */ + public function getType() { + return $this->type; + } + + /** + * @return int Priority. Higher means higher in the group list + */ + public function getPriority() { + return $this->priority; + } + + /** + * @return array Associative array of ChangesListFilter objects, with filter name as key + */ + public function getFilters() { + return $this->filters; + } + + /** + * Get filter by name + * + * @param string $name Filter name + * @return ChangesListFilter|null Specified filter, or null if it is not registered + */ + public function getFilter( $name ) { + return isset( $this->filters[$name] ) ? $this->filters[$name] : null; + } + + /** + * Check whether the URL parameter is for the group, or for individual filters. + * Defaults can also be defined on the group if and only if this is true. + * + * @return bool True if and only if the URL parameter is per-group + */ + abstract public function isPerGroupRequestParameter(); + + /** + * Gets the JS data in the format required by the front-end of the structured UI + * + * @return array|null Associative array, or null if there are no filters that + * display in the structured UI. messageKeys is a special top-level value, with + * the value being an array of the message keys to send to the client. + */ + public function getJsData() { + $output = [ + 'name' => $this->name, + 'type' => $this->type, + 'fullCoverage' => $this->isFullCoverage, + 'filters' => [], + 'priority' => $this->priority, + 'conflicts' => [], + 'messageKeys' => [ $this->title ] + ]; + + if ( isset ( $this->whatsThisHeader ) ) { + $output['whatsThisHeader'] = $this->whatsThisHeader; + $output['whatsThisBody'] = $this->whatsThisBody; + $output['whatsThisUrl'] = $this->whatsThisUrl; + $output['whatsThisLinkText'] = $this->whatsThisLinkText; + + array_push( + $output['messageKeys'], + $output['whatsThisHeader'], + $output['whatsThisBody'], + $output['whatsThisLinkText'] + ); + } + + usort( $this->filters, function ( $a, $b ) { + return $b->getPriority() - $a->getPriority(); + } ); + + foreach ( $this->filters as $filterName => $filter ) { + if ( $filter->displaysOnStructuredUi() ) { + $filterData = $filter->getJsData(); + $output['messageKeys'] = array_merge( + $output['messageKeys'], + $filterData['messageKeys'] + ); + unset( $filterData['messageKeys'] ); + $output['filters'][] = $filterData; + } + } + + if ( count( $output['filters'] ) === 0 ) { + return null; + } + + $output['title'] = $this->title; + + $conflicts = array_merge( + $this->conflictingGroups, + $this->conflictingFilters + ); + + foreach ( $conflicts as $conflictInfo ) { + $output['conflicts'][] = $conflictInfo; + array_push( + $output['messageKeys'], + $conflictInfo['globalDescription'], + $conflictInfo['contextDescription'] + ); + } + + return $output; + } +} diff --git a/includes/changes/ChangesListStringOptionsFilter.php b/includes/changes/ChangesListStringOptionsFilter.php new file mode 100644 index 000000000000..1c977b9d4ef6 --- /dev/null +++ b/includes/changes/ChangesListStringOptionsFilter.php @@ -0,0 +1,17 @@ +<?php + +/** + * An individual filter in a ChangesListStringOptionsFilterGroup. + * + * This filter type will only be displayed on the structured UI currently. + * + * @since 1.29 + */ +class ChangesListStringOptionsFilter extends ChangesListFilter { + /** + * @inheritdoc + */ + public function displaysOnUnstructuredUi() { + return false; + } +} diff --git a/includes/changes/ChangesListStringOptionsFilterGroup.php b/includes/changes/ChangesListStringOptionsFilterGroup.php new file mode 100644 index 000000000000..723ef3986d5f --- /dev/null +++ b/includes/changes/ChangesListStringOptionsFilterGroup.php @@ -0,0 +1,243 @@ +<?php +/** + * Represents a filter group (used on ChangesListSpecialPage and descendants) + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @license GPL 2+ + * @author Matthew Flaschen + */ + +use Wikimedia\Rdbms\IDatabase; + +/** + * Represents a filter group with multiple string options. They are passed to the server as + * a single form parameter separated by a delimiter. The parameter name is the + * group name. E.g. groupname=opt1;opt2 . + * + * If all options are selected they are replaced by the term "all". + * + * There is also a single DB query modification for the whole group. + * + * @since 1.29 + */ + +class ChangesListStringOptionsFilterGroup extends ChangesListFilterGroup { + /** + * Type marker, used by JavaScript + */ + const TYPE = 'string_options'; + + /** + * Delimiter + */ + const SEPARATOR = ';'; + + /** + * Signifies that all options in the group are selected. + */ + const ALL = 'all'; + + /** + * Signifies that no options in the group are selected, meaning the group has no effect. + * + * For full-coverage groups, this is the same as ALL if all filters are allowed. + * For others, it is not. + */ + const NONE = ''; + + /** + * Group name; used as form parameter. + * + * @var string $name + */ + + /** + * Defaul parameter value + * + * @var string $defaultValue + */ + protected $defaultValue; + + /** + * Callable used to do the actual query modification; see constructor + * + * @var callable $queryCallable + */ + protected $queryCallable; + + /** + * Create a new filter group with the specified configuration + * + * @param array $groupDefinition Configuration of group + * * $groupDefinition['name'] string Group name + * * $groupDefinition['title'] string i18n key for title (optional, can be omitted + * * only if none of the filters in the group display in the structured UI) + * * $groupDefinition['priority'] int Priority integer. Higher means higher in the + * * group list. + * * $groupDefinition['filters'] array Numeric array of filter definitions, each of which + * * is an associative array to be passed to the filter constructor. However, + * * 'priority' is optional for the filters. Any filter that has priority unset + * * will be put to the bottom, in the order given. + * * $groupDefinition['default'] string Default for group. + * * $groupDefinition['isFullCoverage'] bool Whether the group is full coverage; + * * if true, this means that checking every item in the group means no + * * changes list entries are filtered out. + * * $groupDefinition['queryCallable'] callable Callable accepting parameters: + * * string $specialPageClassName Class name of current special page + * * IContextSource $context Context, for e.g. user + * * IDatabase $dbr Database, for addQuotes, makeList, and similar + * * array &$tables Array of tables; see IDatabase::select $table + * * array &$fields Array of fields; see IDatabase::select $vars + * * array &$conds Array of conditions; see IDatabase::select $conds + * * array &$query_options Array of query options; see IDatabase::select $options + * * array &$join_conds Array of join conditions; see IDatabase::select $join_conds + * * array $selectedValues The allowed and requested values, lower-cased and sorted + */ + public function __construct( array $groupDefinition ) { + if ( !isset( $groupDefinition['isFullCoverage'] ) ) { + throw new MWException( 'You must specify isFullCoverage' ); + } + + $groupDefinition['type'] = self::TYPE; + + parent::__construct( $groupDefinition ); + + $this->queryCallable = $groupDefinition['queryCallable']; + + if ( isset( $groupDefinition['default'] ) ) { + $this->setDefault( $groupDefinition['default'] ); + } else { + throw new MWException( 'You must specify a default' ); + } + } + + /** + * @inheritdoc + */ + public function isPerGroupRequestParameter() { + return true; + } + + /** + * Sets default of filter group. + * + * @param string $defaultValue + */ + public function setDefault( $defaultValue ) { + $this->defaultValue = $defaultValue; + } + + /** + * Gets default of filter group + * + * @return string $defaultValue + */ + public function getDefault() { + return $this->defaultValue; + } + + /** + * @inheritdoc + */ + protected function createFilter( array $filterDefinition ) { + return new ChangesListStringOptionsFilter( $filterDefinition ); + } + + /** + * Registers a filter in this group + * + * @param ChangesListStringOptionsFilter $filter ChangesListStringOptionsFilter + */ + public function registerFilter( ChangesListStringOptionsFilter $filter ) { + $this->filters[$filter->getName()] = $filter; + } + + /** + * Modifies the query to include the filter group. + * + * The modification is only done if the filter group is in effect. This means that + * one or more valid and allowed filters were selected. + * + * @param IDatabase $dbr Database, for addQuotes, makeList, and similar + * @param ChangesListSpecialPage $specialPage Current special page + * @param array &$tables Array of tables; see IDatabase::select $table + * @param array &$fields Array of fields; see IDatabase::select $vars + * @param array &$conds Array of conditions; see IDatabase::select $conds + * @param array &$query_options Array of query options; see IDatabase::select $options + * @param array &$join_conds Array of join conditions; see IDatabase::select $join_conds + * @param string $value URL parameter value + */ + public function modifyQuery( IDatabase $dbr, ChangesListSpecialPage $specialPage, + &$tables, &$fields, &$conds, &$query_options, &$join_conds, $value ) { + + $allowedFilterNames = []; + foreach ( $this->filters as $filter ) { + $allowedFilterNames[] = $filter->getName(); + } + + if ( $value === self::ALL ) { + $selectedValues = $allowedFilterNames; + } else { + $selectedValues = explode( self::SEPARATOR, strtolower( $value ) ); + + // remove values that are not recognized or not currently allowed + $selectedValues = array_intersect( + $selectedValues, + $allowedFilterNames + ); + } + + // If there are now no values, because all are disallowed or invalid (also, + // the user may not have selected any), this is a no-op. + + // If everything is unchecked, the group always has no effect, regardless + // of full-coverage. + if ( count( $selectedValues ) === 0 ) { + return; + } + + sort( $selectedValues ); + + call_user_func_array( + $this->queryCallable, + [ + get_class( $specialPage ), + $specialPage->getContext(), + $dbr, + &$tables, + &$fields, + &$conds, + &$query_options, + &$join_conds, + $selectedValues + ] + ); + } + + /** + * @inheritdoc + */ + public function getJsData() { + $output = parent::getJsData(); + + $output['separator'] = self::SEPARATOR; + $output['default'] = $this->getDefault(); + + return $output; + } +} diff --git a/includes/changes/EnhancedChangesList.php b/includes/changes/EnhancedChangesList.php index d3a414b5beae..b8a2ac857551 100644 --- a/includes/changes/EnhancedChangesList.php +++ b/includes/changes/EnhancedChangesList.php @@ -34,9 +34,10 @@ class EnhancedChangesList extends ChangesList { /** * @param IContextSource|Skin $obj + * @param array $filterGroups Array of ChangesListFilterGroup objects (currently optional) * @throws MWException */ - public function __construct( $obj ) { + public function __construct( $obj, array $filterGroups = [] ) { if ( $obj instanceof Skin ) { // @todo: deprecate constructing with Skin $context = $obj->getContext(); @@ -49,7 +50,7 @@ class EnhancedChangesList extends ChangesList { $context = $obj; } - parent::__construct( $context ); + parent::__construct( $context, $filterGroups ); // message is set by the parent ChangesList class $this->cacheEntryFactory = new RCCacheEntryFactory( @@ -358,16 +359,17 @@ class EnhancedChangesList extends ChangesList { protected function getLineData( array $block, RCCacheEntry $rcObj, array $queryParams = [] ) { $RCShowChangedSize = $this->getConfig()->get( 'RCShowChangedSize' ); - $classes = [ 'mw-enhanced-rc' ]; $type = $rcObj->mAttribs['rc_type']; $data = []; $lineParams = []; + $classes = [ 'mw-enhanced-rc' ]; if ( $rcObj->watched && $rcObj->mAttribs['rc_timestamp'] >= $rcObj->watched ) { - $classes = [ 'mw-enhanced-watched' ]; + $classes[] = 'mw-enhanced-watched'; } + $classes = array_merge( $classes, $this->getHTMLClassesForFilters( $rcObj ) ); $separator = ' <span class="mw-changeslist-separator">. .</span> '; @@ -530,7 +532,7 @@ class EnhancedChangesList extends ChangesList { $links['total-changes'] = $this->linkRenderer->makeKnownLink( $block0->getTitle(), new HtmlArmor( $nchanges[$n] ), - [], + [ 'class' => 'mw-changeslist-groupdiff' ], $queryParams + [ 'diff' => $currentRevision, 'oldid' => $last->mAttribs['rc_last_oldid'], @@ -540,7 +542,7 @@ class EnhancedChangesList extends ChangesList { $links['total-changes-since-last'] = $this->linkRenderer->makeKnownLink( $block0->getTitle(), new HtmlArmor( $sinceLastVisitMsg[$sinceLast] ), - [], + [ 'class' => 'mw-changeslist-groupdiff' ], $queryParams + [ 'diff' => $currentRevision, 'oldid' => $unvisitedOldid, @@ -562,7 +564,7 @@ class EnhancedChangesList extends ChangesList { $links['history'] = $this->linkRenderer->makeKnownLink( $block0->getTitle(), new HtmlArmor( $this->message['enhancedrc-history'] ), - [], + [ 'class' => 'mw-changeslist-history' ], $params ); } @@ -717,7 +719,7 @@ class EnhancedChangesList extends ChangesList { . $this->linkRenderer->makeKnownLink( $pageTitle, new HtmlArmor( $this->message['hist'] ), - [], + [ 'class' => 'mw-changeslist-history' ], $query ) )->escaped(); return $retVal; diff --git a/includes/changes/OldChangesList.php b/includes/changes/OldChangesList.php index 8eb06ced0322..a5d5191da812 100644 --- a/includes/changes/OldChangesList.php +++ b/includes/changes/OldChangesList.php @@ -34,7 +34,7 @@ class OldChangesList extends ChangesList { public function recentChangesLine( &$rc, $watched = false, $linenumber = null ) { $classes = $this->getHTMLClasses( $rc, $watched ); - // use mw-line-even/mw-line-odd class only if linenumber is given (feature from bug 14468) + // use mw-line-even/mw-line-odd class only if linenumber is given (feature from T16468) if ( $linenumber ) { if ( $linenumber & 1 ) { $classes[] = 'mw-line-odd'; @@ -50,7 +50,9 @@ class OldChangesList extends ChangesList { $rc->mAttribs['rc_namespace'] . '-' . $rc->mAttribs['rc_title'] ); } - if ( !Hooks::run( 'OldChangesListRecentChangesLine', [ &$this, &$html, $rc, &$classes ] ) ) { + // Avoid PHP 7.1 warning from passing $this by reference + $list = $this; + if ( !Hooks::run( 'OldChangesListRecentChangesLine', [ &$list, &$html, $rc, &$classes ] ) ) { return false; } diff --git a/includes/changes/RCCacheEntryFactory.php b/includes/changes/RCCacheEntryFactory.php index 2c5c8b128c9e..8ce21f5f77a3 100644 --- a/includes/changes/RCCacheEntryFactory.php +++ b/includes/changes/RCCacheEntryFactory.php @@ -186,7 +186,7 @@ class RCCacheEntryFactory { $curLink = $curMessage; } else { $curUrl = htmlspecialchars( $cacheEntry->getTitle()->getLinkURL( $queryParams ) ); - $curLink = "<a href=\"$curUrl\">$curMessage</a>"; + $curLink = "<a class=\"mw-changeslist-diff-cur\" href=\"$curUrl\">$curMessage</a>"; } return $curLink; @@ -229,16 +229,18 @@ class RCCacheEntryFactory { return $diffMessage; } $diffUrl = htmlspecialchars( $pageTitle->getLinkURL( $queryParams ) ); - $diffLink = "<a href=\"$diffUrl\">$diffMessage</a>"; + $diffLink = "<a class=\"mw-changeslist-diff\" href=\"$diffUrl\">$diffMessage</a>"; } else { $diffUrl = htmlspecialchars( $cacheEntry->getTitle()->getLinkURL( $queryParams ) ); - $diffLink = "<a href=\"$diffUrl\">$diffMessage</a>"; + $diffLink = "<a class=\"mw-changeslist-diff\" href=\"$diffUrl\">$diffMessage</a>"; } return $diffLink; } /** + * Builds the link to the previous version + * * @param RecentChange $cacheEntry * @param bool $showDiffLinks * @@ -257,7 +259,7 @@ class RCCacheEntryFactory { $lastLink = $this->linkRenderer->makeKnownLink( $cacheEntry->getTitle(), new HtmlArmor( $lastMessage ), - [], + [ 'class' => 'mw-changeslist-diff' ], $this->buildDiffQueryParams( $cacheEntry ) ); } diff --git a/includes/changes/RecentChange.php b/includes/changes/RecentChange.php index 81f64a8176a9..dcab158ddb1f 100644 --- a/includes/changes/RecentChange.php +++ b/includes/changes/RecentChange.php @@ -329,7 +329,9 @@ class RecentChange { $this->mAttribs['rc_id'] = $dbw->insertId(); # Notify extensions - Hooks::run( 'RecentChange_save', [ &$this ] ); + // Avoid PHP 7.1 warning from passing $this by reference + $rc = $this; + Hooks::run( 'RecentChange_save', [ &$rc ] ); if ( count( $this->tags ) ) { ChangeTags::addTags( $this->tags, $this->mAttribs['rc_id'], @@ -389,8 +391,8 @@ class RecentChange { $performer = $this->getPerformer(); - foreach ( $feeds as $feed ) { - $feed += [ + foreach ( $feeds as $params ) { + $params += [ 'omit_bots' => false, 'omit_anon' => false, 'omit_user' => false, @@ -399,59 +401,48 @@ class RecentChange { ]; if ( - ( $feed['omit_bots'] && $this->mAttribs['rc_bot'] ) || - ( $feed['omit_anon'] && $performer->isAnon() ) || - ( $feed['omit_user'] && !$performer->isAnon() ) || - ( $feed['omit_minor'] && $this->mAttribs['rc_minor'] ) || - ( $feed['omit_patrolled'] && $this->mAttribs['rc_patrolled'] ) || + ( $params['omit_bots'] && $this->mAttribs['rc_bot'] ) || + ( $params['omit_anon'] && $performer->isAnon() ) || + ( $params['omit_user'] && !$performer->isAnon() ) || + ( $params['omit_minor'] && $this->mAttribs['rc_minor'] ) || + ( $params['omit_patrolled'] && $this->mAttribs['rc_patrolled'] ) || $this->mAttribs['rc_type'] == RC_EXTERNAL ) { continue; } - $engine = self::getEngine( $feed['uri'] ); - if ( isset( $this->mExtra['actionCommentIRC'] ) ) { $actionComment = $this->mExtra['actionCommentIRC']; } else { $actionComment = null; } - /** @var $formatter RCFeedFormatter */ - $formatter = is_object( $feed['formatter'] ) ? $feed['formatter'] : new $feed['formatter'](); - $line = $formatter->getLine( $feed, $this, $actionComment ); - if ( !$line ) { - // T109544 - // If a feed formatter returns null, this will otherwise cause an - // error in at least RedisPubSubFeedEngine. - // Not sure where/how this should best be handled. - continue; - } - - $engine->send( $feed, $line ); + $feed = RCFeed::factory( $params ); + $feed->notify( $this, $actionComment ); } } /** - * Gets the stream engine object for a given URI from $wgRCEngines - * + * @since 1.22 + * @deprecated since 1.29 Use RCFeed::factory() instead * @param string $uri URI to get the engine object for - * @throws MWException * @return RCFeedEngine The engine object + * @throws MWException */ - public static function getEngine( $uri ) { + public static function getEngine( $uri, $params = [] ) { + // TODO: Merge into RCFeed::factory(). global $wgRCEngines; - $scheme = parse_url( $uri, PHP_URL_SCHEME ); if ( !$scheme ) { - throw new MWException( __FUNCTION__ . ": Invalid stream logger URI: '$uri'" ); + throw new MWException( "Invalid RCFeed uri: '$uri'" ); } - if ( !isset( $wgRCEngines[$scheme] ) ) { - throw new MWException( __FUNCTION__ . ": Unknown stream logger URI scheme: $scheme" ); + throw new MWException( "Unknown RCFeedEngine scheme: '$scheme'" ); } - - return new $wgRCEngines[$scheme]; + if ( defined( 'MW_PHPUNIT_TEST' ) && is_object( $wgRCEngines[$scheme] ) ) { + return $wgRCEngines[$scheme]; + } + return new $wgRCEngines[$scheme]( $params ); } /** diff --git a/includes/changetags/ChangeTagsList.php b/includes/changetags/ChangeTagsList.php index dd8bab987888..a37f5f2c11b2 100644 --- a/includes/changetags/ChangeTagsList.php +++ b/includes/changetags/ChangeTagsList.php @@ -49,8 +49,9 @@ abstract class ChangeTagsList extends RevisionListBase { $className = 'ChangeTagsLogList'; break; default: - throw new Exception( "Class $className requested, but does not exist" ); + throw new Exception( "Class $typeName requested, but does not exist" ); } + return new $className( $context, $title, $ids ); } diff --git a/includes/changetags/ChangeTagsLogList.php b/includes/changetags/ChangeTagsLogList.php index 480aaced7db8..271005f465ce 100644 --- a/includes/changetags/ChangeTagsLogList.php +++ b/includes/changetags/ChangeTagsLogList.php @@ -19,6 +19,8 @@ * @ingroup Change tagging */ +use Wikimedia\Rdbms\IDatabase; + /** * Stores a list of taggable log entries. * @since 1.25 diff --git a/includes/changetags/ChangeTagsRevisionList.php b/includes/changetags/ChangeTagsRevisionList.php index 8eae23844436..a0248c617b2b 100644 --- a/includes/changetags/ChangeTagsRevisionList.php +++ b/includes/changetags/ChangeTagsRevisionList.php @@ -19,6 +19,8 @@ * @ingroup Change tagging */ +use Wikimedia\Rdbms\IDatabase; + /** * Stores a list of taggable revisions. * @since 1.25 diff --git a/includes/collation/Collation.php b/includes/collation/Collation.php index 7659d6c4d500..d67bc7eecdce 100644 --- a/includes/collation/Collation.php +++ b/includes/collation/Collation.php @@ -67,7 +67,7 @@ abstract class Collation { return new CollationFa; default: $match = []; - if ( preg_match( '/^uca-([a-z@=-]+)$/', $collationName, $match ) ) { + if ( preg_match( '/^uca-([A-Za-z@=-]+)$/', $collationName, $match ) ) { return new IcuCollation( $match[1] ); } diff --git a/includes/collation/CollationEt.php b/includes/collation/CollationEt.php index 5dc9fa29ec8d..ca7b76531403 100644 --- a/includes/collation/CollationEt.php +++ b/includes/collation/CollationEt.php @@ -19,7 +19,7 @@ */ /** - * Workaround for incorrect collation of Estonian language ('et') in ICU (bug 54168). + * Workaround for incorrect collation of Estonian language ('et') in ICU (T56168). * * 'W' and 'V' should not be considered the same letter for the purposes of collation in modern * Estonian. We work around this by replacing 'W' and 'w' with 'ᴡ' U+1D21 'LATIN LETTER SMALL diff --git a/includes/collation/IcuCollation.php b/includes/collation/IcuCollation.php index bc5a20967a67..e0eb1c13c83e 100644 --- a/includes/collation/IcuCollation.php +++ b/includes/collation/IcuCollation.php @@ -330,7 +330,7 @@ class IcuCollation extends Collation { $cache = ObjectCache::getLocalServerInstance( CACHE_ANYTHING ); $cacheKey = $cache->makeKey( 'first-letters', - get_class( $this ), + static::class, $this->locale, $this->digitTransformLanguage->getCode(), self::getICUVersion(), @@ -513,8 +513,12 @@ class IcuCollation extends Collation { * can't be determined. * * The constant INTL_ICU_VERSION this function refers to isn't really - * documented. It is available since PHP 5.3.7 (see PHP bug 54561). - * This function will return false on older PHPs. + * documented. It is available since PHP 5.3.7 (see PHP 54561 + * https://bugs.php.net/bug.php?id=54561). This function will return + * false on older PHPs. + * + * TODO: Remove the backwards-compatibility as MediaWiki now requires + * higher levels of PHP. * * @since 1.21 * @return string|bool diff --git a/includes/compat/Timestamp.php b/includes/compat/Timestamp.php new file mode 100644 index 000000000000..bd254327e212 --- /dev/null +++ b/includes/compat/Timestamp.php @@ -0,0 +1,18 @@ +<?php +// This file is loaded by composer.json#autoload.files instead of autoload.php, +// because PHP's class loader does not support autoloading an alias for a class that +// isn't already loaded. See also AutoLoaderTest and ClassCollector. + +// By using an autoload file, this will trigger directly at runtime outside any class +// loading context. This file will then register the alias and, as class_alias() does +// by default, it will trigger a plain autoload for the destination class. + +// The below uses string concatenation for the alias to avoid being seen by ClassCollector, +// which would insist on adding it to autoload.php, after which AutoLoaderTest will +// complain about class_alias() not being in the target class file. + +/** + * @deprecated since 1.29 + * @since 1.20 + */ +class_alias( Wikimedia\Timestamp\TimestampException::class, 'Timestamp' . 'Exception' ); diff --git a/includes/config/EtcdConfig.php b/includes/config/EtcdConfig.php new file mode 100644 index 000000000000..0f2f64103a3f --- /dev/null +++ b/includes/config/EtcdConfig.php @@ -0,0 +1,275 @@ +<?php +/** + * Copyright 2017 + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @author Aaron Schulz + */ + +use Psr\Log\LoggerAwareInterface; +use Psr\Log\LoggerInterface; +use Wikimedia\WaitConditionLoop; + +/** + * Interface for configuration instances + * + * @since 1.29 + */ +class EtcdConfig implements Config, LoggerAwareInterface { + /** @var MultiHttpClient */ + private $http; + /** @var BagOStuff */ + private $srvCache; + /** @var array */ + private $procCache; + /** @var LoggerInterface */ + private $logger; + + /** @var string */ + private $host; + /** @var string */ + private $protocol; + /** @var string */ + private $directory; + /** @var string */ + private $encoding; + /** @var integer */ + private $baseCacheTTL; + /** @var integer */ + private $skewCacheTTL; + /** @var integer */ + private $timeout; + /** @var string */ + private $directoryHash; + + /** + * @param array $params Parameter map: + * - host: the host address and port + * - protocol: either http or https + * - directory: the etc "directory" were MediaWiki specific variables are located + * - encoding: one of ("JSON", "YAML") + * - cache: BagOStuff instance or ObjectFactory spec thereof for a server cache. + * The cache will also be used as a fallback if etcd is down. + * - cacheTTL: logical cache TTL in seconds + * - skewTTL: maximum seconds to randomly lower the assigned TTL on cache save + * - timeout: seconds to wait for etcd before throwing an error + */ + public function __construct( array $params ) { + $params += [ + 'protocol' => 'http', + 'encoding' => 'JSON', + 'cacheTTL' => 10, + 'skewTTL' => 1, + 'timeout' => 10 + ]; + + $this->host = $params['host']; + $this->protocol = $params['protocol']; + $this->directory = trim( $params['directory'], '/' ); + $this->directoryHash = sha1( $this->directory ); + $this->encoding = $params['encoding']; + $this->skewCacheTTL = $params['skewTTL']; + $this->baseCacheTTL = max( $params['cacheTTL'] - $this->skewCacheTTL, 0 ); + $this->timeout = $params['timeout']; + + if ( !isset( $params['cache'] ) ) { + $this->srvCache = new HashBagOStuff( [] ); + } elseif ( $params['cache'] instanceof BagOStuff ) { + $this->srvCache = $params['cache']; + } else { + $this->srvCache = ObjectFactory::getObjectFromSpec( $params['cache'] ); + } + + $this->logger = new Psr\Log\NullLogger(); + $this->http = new MultiHttpClient( [ + 'connTimeout' => $this->timeout, + 'reqTimeout' => $this->timeout + ] ); + } + + public function setLogger( LoggerInterface $logger ) { + $this->logger = $logger; + } + + public function has( $name ) { + $this->load(); + + return array_key_exists( $name, $this->procCache['config'] ); + } + + public function get( $name ) { + $this->load(); + + if ( !array_key_exists( $name, $this->procCache['config'] ) ) { + throw new ConfigException( "No entry found for '$name'." ); + } + + return $this->procCache['config'][$name]; + } + + private function load() { + if ( $this->procCache !== null ) { + return; // already loaded + } + + $now = microtime( true ); + $key = $this->srvCache->makeKey( 'variable', $this->directoryHash ); + + // Get the cached value or block until it is regenerated (by this or another thread)... + $data = null; // latest config info + $error = null; // last error message + $loop = new WaitConditionLoop( + function () use ( $key, $now, &$data, &$error ) { + // Check if the values are in cache yet... + $data = $this->srvCache->get( $key ); + if ( is_array( $data ) && $data['expires'] > $now ) { + $this->logger->debug( "Found up-to-date etcd configuration cache." ); + + return WaitConditionLoop::CONDITION_REACHED; + } + + // Cache is either empty or stale; + // refresh the cache from etcd, using a mutex to reduce stampedes... + if ( $this->srvCache->lock( $key, 0, $this->baseCacheTTL ) ) { + try { + list( $config, $error, $retry ) = $this->fetchAllFromEtcd(); + if ( $config === null ) { + $this->logger->error( "Failed to fetch configuration: $error" ); + // Fail fast if the error is likely to just keep happening + return $retry + ? WaitConditionLoop::CONDITION_CONTINUE + : WaitConditionLoop::CONDITION_FAILED; + } + + // Avoid having all servers expire cache keys at the same time + $expiry = microtime( true ) + $this->baseCacheTTL; + $expiry += mt_rand( 0, 1e6 ) / 1e6 * $this->skewCacheTTL; + + $data = [ 'config' => $config, 'expires' => $expiry ]; + $this->srvCache->set( $key, $data, BagOStuff::TTL_INDEFINITE ); + + $this->logger->info( "Refreshed stale etcd configuration cache." ); + + return WaitConditionLoop::CONDITION_REACHED; + } finally { + $this->srvCache->unlock( $key ); // release mutex + } + } + + if ( is_array( $data ) ) { + $this->logger->info( "Using stale etcd configuration cache." ); + + return WaitConditionLoop::CONDITION_REACHED; + } + + return WaitConditionLoop::CONDITION_CONTINUE; + }, + $this->timeout + ); + + if ( $loop->invoke() !== WaitConditionLoop::CONDITION_REACHED ) { + // No cached value exists and etcd query failed; throw an error + throw new ConfigException( "Failed to load configuration from etcd: $error" ); + } + + $this->procCache = $data; + } + + /** + * @return array (config array or null, error string, allow retries) + */ + public function fetchAllFromEtcd() { + $dsd = new DnsSrvDiscoverer( $this->host ); + $servers = $dsd->getServers(); + if ( !$servers ) { + return $this->fetchAllFromEtcdServer( $this->host ); + } + + do { + // Pick a random etcd server from dns + $server = $dsd->pickServer( $servers ); + $host = IP::combineHostAndPort( $server['target'], $server['port'] ); + // Try to load the config from this particular server + list( $config, $error, $retry ) = $this->fetchAllFromEtcdServer( $host ); + if ( is_array( $config ) || !$retry ) { + break; + } + + // Avoid the server next time if that failed + $dsd->removeServer( $server, $servers ); + } while ( $servers ); + + return [ $config, $error, $retry ]; + } + + /** + * @param string $address Host and port + * @return array (config array or null, error string, whether to allow retries) + */ + protected function fetchAllFromEtcdServer( $address ) { + // Retrieve all the values under the MediaWiki config directory + list( $rcode, $rdesc, /* $rhdrs */, $rbody, $rerr ) = $this->http->run( [ + 'method' => 'GET', + 'url' => "{$this->protocol}://{$address}/v2/keys/{$this->directory}/", + 'headers' => [ 'content-type' => 'application/json' ] + ] ); + + static $terminalCodes = [ 404 => true ]; + if ( $rcode < 200 || $rcode > 399 ) { + return [ + null, + strlen( $rerr ) ? $rerr : "HTTP $rcode ($rdesc)", + empty( $terminalCodes[$rcode] ) + ]; + } + + $info = json_decode( $rbody, true ); + if ( $info === null || !isset( $info['node']['nodes'] ) ) { + return [ null, $rcode, "Unexpected JSON response; missing 'nodes' list.", false ]; + } + + $config = []; + foreach ( $info['node']['nodes'] as $node ) { + if ( !empty( $node['dir'] ) ) { + continue; // skip directories + } + + $name = basename( $node['key'] ); + $value = $this->unserialize( $node['value'] ); + if ( !is_array( $value ) || !isset( $value['val'] ) ) { + return [ null, "Failed to parse value for '$name'.", false ]; + } + + $config[$name] = $value['val']; + } + + return [ $config, null, false ]; + } + + /** + * @param string $string + * @return mixed + */ + private function unserialize( $string ) { + if ( $this->encoding === 'YAML' ) { + return yaml_parse( $string ); + } else { // JSON + return json_decode( $string, true ); + } + } +} diff --git a/includes/content/ContentHandler.php b/includes/content/ContentHandler.php index db20f514d217..bccb147e5ba4 100644 --- a/includes/content/ContentHandler.php +++ b/includes/content/ContentHandler.php @@ -198,9 +198,6 @@ abstract class ContentHandler { $ext = $m[1]; } - // Hook can force JS/CSS - Hooks::run( 'TitleIsCssOrJsPage', [ $title, &$isCodePage ], '1.21' ); - // Is this a user subpage containing code? $isCodeSubpage = NS_USER == $ns && !$isCodePage @@ -213,9 +210,6 @@ abstract class ContentHandler { $isWikitext = is_null( $model ) || $model == CONTENT_MODEL_WIKITEXT; $isWikitext = $isWikitext && !$isCodePage && !$isCodeSubpage; - // Hook can override $isWikitext - Hooks::run( 'TitleIsWikitextPage', [ $title, &$isWikitext ], '1.21' ); - if ( !$isWikitext ) { switch ( $ext ) { case 'js': @@ -367,7 +361,9 @@ abstract class ContentHandler { public static function getContentModels() { global $wgContentHandlers; - return array_keys( $wgContentHandlers ); + $models = array_keys( $wgContentHandlers ); + Hooks::run( 'GetContentModels', [ &$models ] ); + return $models; } public static function getAllContentFormats() { @@ -903,7 +899,7 @@ abstract class ContentHandler { $onlyAuthor = $row->rev_user_text; // Try to find a second contributor foreach ( $res as $row ) { - if ( $row->rev_user_text != $onlyAuthor ) { // Bug 22999 + if ( $row->rev_user_text != $onlyAuthor ) { // T24999 $onlyAuthor = false; break; } @@ -1096,65 +1092,6 @@ abstract class ContentHandler { } /** - * Call a legacy hook that uses text instead of Content objects. - * Will log a warning when a matching hook function is registered. - * If the textual representation of the content is changed by the - * hook function, a new Content object is constructed from the new - * text. - * - * @param string $event Event name - * @param array $args Parameters passed to hook functions - * @param string|null $deprecatedVersion Emit a deprecation notice - * when the hook is run for the provided version - * - * @return bool True if no handler aborted the hook - */ - public static function runLegacyHooks( $event, $args = [], - $deprecatedVersion = null - ) { - - if ( !Hooks::isRegistered( $event ) ) { - return true; // nothing to do here - } - - // convert Content objects to text - $contentObjects = []; - $contentTexts = []; - - foreach ( $args as $k => $v ) { - if ( $v instanceof Content ) { - /* @var Content $v */ - - $contentObjects[$k] = $v; - - $v = $v->serialize(); - $contentTexts[$k] = $v; - $args[$k] = $v; - } - } - - // call the hook functions - $ok = Hooks::run( $event, $args, $deprecatedVersion ); - - // see if the hook changed the text - foreach ( $contentTexts as $k => $orig ) { - /* @var Content $content */ - - $modified = $args[$k]; - $content = $contentObjects[$k]; - - if ( $modified !== $orig ) { - // text was changed, create updated Content object - $content = $content->getContentHandler()->unserializeContent( $modified ); - } - - $args[$k] = $content; - } - - return $ok; - } - - /** * Get fields definition for search index * * @todo Expose title, redirect, namespace, text, source_text, text_bytes @@ -1169,7 +1106,6 @@ abstract class ContentHandler { 'category', SearchIndexField::INDEX_TYPE_TEXT ); - $fields['category']->setFlag( SearchIndexField::FLAG_CASEFOLD ); $fields['external_link'] = $engine->makeSearchFieldMapping( @@ -1186,9 +1122,13 @@ abstract class ContentHandler { 'template', SearchIndexField::INDEX_TYPE_KEYWORD ); - $fields['template']->setFlag( SearchIndexField::FLAG_CASEFOLD ); + $fields['content_model'] = $engine->makeSearchFieldMapping( + 'content_model', + SearchIndexField::INDEX_TYPE_KEYWORD + ); + return $fields; } @@ -1217,8 +1157,11 @@ abstract class ContentHandler { * @return array Map of name=>value for fields * @since 1.28 */ - public function getDataForSearchIndex( WikiPage $page, ParserOutput $output, - SearchEngine $engine ) { + public function getDataForSearchIndex( + WikiPage $page, + ParserOutput $output, + SearchEngine $engine + ) { $fieldData = []; $content = $page->getContent(); @@ -1235,6 +1178,7 @@ abstract class ContentHandler { $fieldData['text'] = $text; $fieldData['source_text'] = $text; $fieldData['text_bytes'] = $content->getSize(); + $fieldData['content_model'] = $content->getModel(); } Hooks::run( 'SearchDataForIndex', [ &$fieldData, $this, $page, $output, $engine ] ); diff --git a/includes/content/FileContentHandler.php b/includes/content/FileContentHandler.php index 26f119065df9..843d54056ab3 100644 --- a/includes/content/FileContentHandler.php +++ b/includes/content/FileContentHandler.php @@ -30,8 +30,11 @@ class FileContentHandler extends WikitextContentHandler { return $fields; } - public function getDataForSearchIndex( WikiPage $page, ParserOutput $parserOutput, - SearchEngine $engine ) { + public function getDataForSearchIndex( + WikiPage $page, + ParserOutput $parserOutput, + SearchEngine $engine + ) { $fields = []; $title = $page->getTitle(); diff --git a/includes/content/TextContentHandler.php b/includes/content/TextContentHandler.php index 09cdcee14381..698a37b6fe00 100644 --- a/includes/content/TextContentHandler.php +++ b/includes/content/TextContentHandler.php @@ -30,7 +30,7 @@ */ class TextContentHandler extends ContentHandler { - // @codingStandardsIgnoreStart bug 57585 + // @codingStandardsIgnoreStart T59585 public function __construct( $modelId = CONTENT_MODEL_TEXT, $formats = [ CONTENT_FORMAT_TEXT ] ) { parent::__construct( $modelId, $formats ); } @@ -150,8 +150,11 @@ class TextContentHandler extends ContentHandler { return $fields; } - public function getDataForSearchIndex( WikiPage $page, ParserOutput $output, - SearchEngine $engine ) { + public function getDataForSearchIndex( + WikiPage $page, + ParserOutput $output, + SearchEngine $engine + ) { $fields = parent::getDataForSearchIndex( $page, $output, $engine ); $fields['language'] = $this->getPageLanguage( $page->getTitle(), $page->getContent() )->getCode(); diff --git a/includes/content/WikiTextStructure.php b/includes/content/WikiTextStructure.php index f4a6dc6a9416..9f79aa8d088d 100644 --- a/includes/content/WikiTextStructure.php +++ b/includes/content/WikiTextStructure.php @@ -27,26 +27,32 @@ class WikiTextStructure { * @var string[] selectors to elements that are excluded entirely from search */ private $excludedElementSelectors = [ - 'audio', 'video', // "it looks like you don't have javascript enabled..." - // do not need to index - 'sup.reference', // The [1] for references - '.mw-cite-backlink', // The ↑ next to references in the references section - 'h1', 'h2', 'h3', // Headings are already indexed in their own field. - 'h5', 'h6', 'h4', - '.autocollapse', // Collapsed fields are hidden by default so we don't want them - // showing up. + // "it looks like you don't have javascript enabled..." – do not need to index + 'audio', 'video', + // The [1] for references + 'sup.reference', + // The ↑ next to references in the references section + '.mw-cite-backlink', + // Headings are already indexed in their own field. + 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', + // Collapsed fields are hidden by default so we don't want them showing up. + '.autocollapse', ]; /** * @var string[] selectors to elements that are considered auxiliary to article text for search */ private $auxiliaryElementSelectors = [ - '.thumbcaption', // Thumbnail captions aren't really part of the text proper - 'table', // Neither are tables - '.rellink', // Common style for "See also:". - '.dablink', // Common style for calling out helpful links at the top - // of the article. - '.searchaux', // New class users can use to mark stuff as auxiliary to searches. + // Thumbnail captions aren't really part of the text proper + '.thumbcaption', + // Neither are tables + 'table', + // Common style for "See also:". + '.rellink', + // Common style for calling out helpful links at the top of the article. + '.dablink', + // New class users can use to mark stuff as auxiliary to searches. + '.searchaux', ]; /** diff --git a/includes/content/WikitextContentHandler.php b/includes/content/WikitextContentHandler.php index 74b2f1aede0a..9c26ae158710 100644 --- a/includes/content/WikitextContentHandler.php +++ b/includes/content/WikitextContentHandler.php @@ -128,16 +128,20 @@ class WikitextContentHandler extends TextContentHandler { $fields['opening_text'] = $engine->makeSearchFieldMapping( 'opening_text', SearchIndexField::INDEX_TYPE_TEXT ); - $fields['opening_text']->setFlag( SearchIndexField::FLAG_SCORING | - SearchIndexField::FLAG_NO_HIGHLIGHT ); + $fields['opening_text']->setFlag( + SearchIndexField::FLAG_SCORING | SearchIndexField::FLAG_NO_HIGHLIGHT + ); // Until we have full first-class content handler for files, we invoke it explicitly here $fields = array_merge( $fields, $this->getFileHandler()->getFieldsForSearchIndex( $engine ) ); return $fields; } - public function getDataForSearchIndex( WikiPage $page, ParserOutput $parserOutput, - SearchEngine $engine ) { + public function getDataForSearchIndex( + WikiPage $page, + ParserOutput $parserOutput, + SearchEngine $engine + ) { $fields = parent::getDataForSearchIndex( $page, $parserOutput, $engine ); $structure = new WikiTextStructure( $parserOutput ); diff --git a/includes/context/ContextSource.php b/includes/context/ContextSource.php index 829dd73f0ba2..135c9b23eb62 100644 --- a/includes/context/ContextSource.php +++ b/includes/context/ContextSource.php @@ -19,6 +19,7 @@ * @file */ use Liuggio\StatsdClient\Factory\StatsdDataFactory; +use MediaWiki\MediaWikiServices; /** * The simplest way of implementing IContextSource is to hold a RequestContext as a @@ -39,7 +40,7 @@ abstract class ContextSource implements IContextSource { */ public function getContext() { if ( $this->context === null ) { - $class = get_class( $this ); + $class = static::class; wfDebug( __METHOD__ . " ($class): called and \$context is null. " . "Using RequestContext::getMain() for sanity\n" ); $this->context = RequestContext::getMain(); @@ -172,7 +173,7 @@ abstract class ContextSource implements IContextSource { * @return StatsdDataFactory */ public function getStats() { - return $this->getContext()->getStats(); + return MediaWikiServices::getInstance()->getStatsdDataFactory(); } /** diff --git a/includes/context/RequestContext.php b/includes/context/RequestContext.php index ecd274b0dbb1..3dfa4564006a 100644 --- a/includes/context/RequestContext.php +++ b/includes/context/RequestContext.php @@ -428,7 +428,7 @@ class RequestContext implements IContextSource, MutableContext { } // Normalize the key in case the user is passing gibberish - // or has old preferences (bug 69566). + // or has old preferences (T71566). $normalized = Skin::normalizeKey( $userSkin ); // Skin::normalizeKey will also validate it, so diff --git a/includes/dao/DBAccessBase.php b/includes/dao/DBAccessBase.php index 6a1bbd6e31ac..da660bdcaee9 100644 --- a/includes/dao/DBAccessBase.php +++ b/includes/dao/DBAccessBase.php @@ -1,5 +1,7 @@ <?php +use Wikimedia\Rdbms\LoadBalancer; + /** * Base class for objects that allow access to other wiki's databases using * the foreign database access mechanism implemented by LBFactoryMulti. diff --git a/includes/db/CloneDatabase.php b/includes/db/CloneDatabase.php index 2b394b6d7bf0..809b660a1b41 100644 --- a/includes/db/CloneDatabase.php +++ b/includes/db/CloneDatabase.php @@ -24,6 +24,7 @@ * @ingroup Database */ use MediaWiki\MediaWikiServices; +use Wikimedia\Rdbms\IMaintainableDatabase; class CloneDatabase { /** @var string Table prefix for cloning */ @@ -79,7 +80,7 @@ class CloneDatabase { foreach ( $this->tablesToClone as $tbl ) { if ( $wgSharedDB && in_array( $tbl, $wgSharedTables, true ) ) { // Shared tables don't work properly when cloning due to - // how prefixes are handled (bug 65654) + // how prefixes are handled (T67654) throw new RuntimeException( "Cannot clone shared table $tbl." ); } # Clean up from previous aborted run. So that table escaping diff --git a/includes/db/DatabaseOracle.php b/includes/db/DatabaseOracle.php index c3502f60adb2..ef5a8fe9423e 100644 --- a/includes/db/DatabaseOracle.php +++ b/includes/db/DatabaseOracle.php @@ -20,113 +20,8 @@ * @file * @ingroup Database */ - -/** - * The oci8 extension is fairly weak and doesn't support oci_num_rows, among - * other things. We use a wrapper class to handle that and other - * Oracle-specific bits, like converting column names back to lowercase. - * @ingroup Database - */ -class ORAResult { - private $rows; - private $cursor; - private $nrows; - - private $columns = []; - - private function array_unique_md( $array_in ) { - $array_out = []; - $array_hashes = []; - - foreach ( $array_in as $item ) { - $hash = md5( serialize( $item ) ); - if ( !isset( $array_hashes[$hash] ) ) { - $array_hashes[$hash] = $hash; - $array_out[] = $item; - } - } - - return $array_out; - } - - /** - * @param IDatabase $db - * @param resource $stmt A valid OCI statement identifier - * @param bool $unique - */ - function __construct( &$db, $stmt, $unique = false ) { - $this->db =& $db; - - $this->nrows = oci_fetch_all( $stmt, $this->rows, 0, -1, OCI_FETCHSTATEMENT_BY_ROW | OCI_NUM ); - if ( $this->nrows === false ) { - $e = oci_error( $stmt ); - $db->reportQueryError( $e['message'], $e['code'], '', __METHOD__ ); - $this->free(); - - return; - } - - if ( $unique ) { - $this->rows = $this->array_unique_md( $this->rows ); - $this->nrows = count( $this->rows ); - } - - if ( $this->nrows > 0 ) { - foreach ( $this->rows[0] as $k => $v ) { - $this->columns[$k] = strtolower( oci_field_name( $stmt, $k + 1 ) ); - } - } - - $this->cursor = 0; - oci_free_statement( $stmt ); - } - - public function free() { - unset( $this->db ); - } - - public function seek( $row ) { - $this->cursor = min( $row, $this->nrows ); - } - - public function numRows() { - return $this->nrows; - } - - public function numFields() { - return count( $this->columns ); - } - - public function fetchObject() { - if ( $this->cursor >= $this->nrows ) { - return false; - } - $row = $this->rows[$this->cursor++]; - $ret = new stdClass(); - foreach ( $row as $k => $v ) { - $lc = $this->columns[$k]; - $ret->$lc = $v; - } - - return $ret; - } - - public function fetchRow() { - if ( $this->cursor >= $this->nrows ) { - return false; - } - - $row = $this->rows[$this->cursor++]; - $ret = []; - foreach ( $row as $k => $v ) { - $lc = $this->columns[$k]; - $ret[$lc] = $v; - $ret[$k] = $v; - } - - return $ret; - } -} +use Wikimedia\Rdbms\Blob; +use Wikimedia\Rdbms\ResultWrapper; /** * @ingroup Database @@ -668,7 +563,7 @@ class DatabaseOracle extends Database { list( $startOpts, $useIndex, $tailOpts, $ignoreIndex ) = $this->makeSelectOptions( $selectOptions ); if ( is_array( $srcTable ) ) { - $srcTable = implode( ',', array_map( [ &$this, 'tableName' ], $srcTable ) ); + $srcTable = implode( ',', array_map( [ $this, 'tableName' ], $srcTable ) ); } else { $srcTable = $this->tableName( $srcTable ); } @@ -998,7 +893,7 @@ class DatabaseOracle extends Database { private function fieldInfoMulti( $table, $field ) { $field = strtoupper( $field ); if ( is_array( $table ) ) { - $table = array_map( [ &$this, 'tableNameInternal' ], $table ); + $table = array_map( [ $this, 'tableNameInternal' ], $table ); $tableWhere = 'IN ('; foreach ( $table as &$singleTable ) { $singleTable = $this->removeIdentifierQuotes( $singleTable ); @@ -1085,24 +980,18 @@ class DatabaseOracle extends Database { } } - /** - * defines must comply with ^define\s*([^\s=]*)\s*=\s?'\{\$([^\}]*)\}'; - * - * @param resource $fp - * @param bool|string $lineCallback - * @param bool|callable $resultCallback - * @param string $fname - * @param bool|callable $inputCallback - * @return bool|string - */ - function sourceStream( $fp, $lineCallback = false, $resultCallback = false, - $fname = __METHOD__, $inputCallback = false ) { + function sourceStream( + $fp, + callable $lineCallback = null, + callable $resultCallback = null, + $fname = __METHOD__, callable $inputCallback = null + ) { $cmd = ''; $done = false; $dollarquote = false; $replacements = []; - + // Defines must comply with ^define\s*([^\s=]*)\s*=\s?'\{\$([^\}]*)\}'; while ( !feof( $fp ) ) { if ( $lineCallback ) { call_user_func( $lineCallback ); diff --git a/includes/db/MWLBFactory.php b/includes/db/MWLBFactory.php index 40418fd47929..fe063f20b15f 100644 --- a/includes/db/MWLBFactory.php +++ b/includes/db/MWLBFactory.php @@ -23,6 +23,7 @@ use MediaWiki\Logger\LoggerFactory; use MediaWiki\MediaWikiServices; +use Wikimedia\Rdbms\DatabaseDomain; /** * MediaWiki-specific class for generating database load balancers @@ -58,6 +59,9 @@ abstract class MWLBFactory { 'readOnlyReason' => wfConfiguredReadOnlyReason(), ]; + // When making changes here, remember to also specify MediaWiki-specific options + // for Database classes in the relevant Installer subclass. + // Such as MysqlInstaller::openConnection and PostgresInstaller::openConnectionWithParams. if ( $lbConf['class'] === 'LBFactorySimple' ) { if ( isset( $lbConf['servers'] ) ) { // Server array is already explicitly configured; leave alone @@ -71,7 +75,13 @@ abstract class MWLBFactory { // Work around the reserved word usage in MediaWiki schema 'keywordTableMap' => [ 'user' => 'mwuser', 'text' => 'pagecontent' ] ]; + } elseif ( $server['type'] === 'mssql' ) { + $server += [ + 'port' => $mainConfig->get( 'DBport' ), + 'useWindowsAuth' => $mainConfig->get( 'DBWindowsAuthentication' ) + ]; } + if ( in_array( $server['type'], $typesWithSchema, true ) ) { $server += [ 'schema' => $mainConfig->get( 'DBmwschema' ) ]; } @@ -111,6 +121,9 @@ abstract class MWLBFactory { $server['port'] = $mainConfig->get( 'DBport' ); // Work around the reserved word usage in MediaWiki schema $server['keywordTableMap'] = [ 'user' => 'mwuser', 'text' => 'pagecontent' ]; + } elseif ( $server['type'] === 'mssql' ) { + $server['port'] = $mainConfig->get( 'DBport' ); + $server['useWindowsAuth'] = $mainConfig->get( 'DBWindowsAuthentication' ); } $lbConf['servers'] = [ $server ]; } @@ -171,6 +184,17 @@ abstract class MWLBFactory { ); } + // For configuration backward compatibility after moving classes to namespaces (1.29) + $compat = [ + 'LBFactorySingle' => Wikimedia\Rdbms\LBFactorySingle::class, + 'LBFactorySimple' => Wikimedia\Rdbms\LBFactorySimple::class, + 'LBFactoryMulti' => Wikimedia\Rdbms\LBFactoryMulti::class + ]; + + if ( isset( $compat[$class] ) ) { + $class = $compat[$class]; + } + return $class; } } diff --git a/includes/libs/rdbms/field/ORAField.php b/includes/db/ORAField.php index e48310ddc3e4..df31000364b8 100644 --- a/includes/libs/rdbms/field/ORAField.php +++ b/includes/db/ORAField.php @@ -1,4 +1,7 @@ <?php + +use Wikimedia\Rdbms\Field; + class ORAField implements Field { private $name, $tablename, $default, $max_length, $nullable, $is_pk, $is_unique, $is_multiple, $is_key, $type; diff --git a/includes/db/ORAResult.php b/includes/db/ORAResult.php new file mode 100644 index 000000000000..fbbc962e0120 --- /dev/null +++ b/includes/db/ORAResult.php @@ -0,0 +1,110 @@ +<?php + +use Wikimedia\Rdbms\IDatabase; + +/** + * The oci8 extension is fairly weak and doesn't support oci_num_rows, among + * other things. We use a wrapper class to handle that and other + * Oracle-specific bits, like converting column names back to lowercase. + * @ingroup Database + */ +class ORAResult { + private $rows; + private $cursor; + private $nrows; + + private $columns = []; + + private function array_unique_md( $array_in ) { + $array_out = []; + $array_hashes = []; + + foreach ( $array_in as $item ) { + $hash = md5( serialize( $item ) ); + if ( !isset( $array_hashes[$hash] ) ) { + $array_hashes[$hash] = $hash; + $array_out[] = $item; + } + } + + return $array_out; + } + + /** + * @param IDatabase $db + * @param resource $stmt A valid OCI statement identifier + * @param bool $unique + */ + function __construct( &$db, $stmt, $unique = false ) { + $this->db =& $db; + + $this->nrows = oci_fetch_all( $stmt, $this->rows, 0, -1, OCI_FETCHSTATEMENT_BY_ROW | OCI_NUM ); + if ( $this->nrows === false ) { + $e = oci_error( $stmt ); + $db->reportQueryError( $e['message'], $e['code'], '', __METHOD__ ); + $this->free(); + + return; + } + + if ( $unique ) { + $this->rows = $this->array_unique_md( $this->rows ); + $this->nrows = count( $this->rows ); + } + + if ( $this->nrows > 0 ) { + foreach ( $this->rows[0] as $k => $v ) { + $this->columns[$k] = strtolower( oci_field_name( $stmt, $k + 1 ) ); + } + } + + $this->cursor = 0; + oci_free_statement( $stmt ); + } + + public function free() { + unset( $this->db ); + } + + public function seek( $row ) { + $this->cursor = min( $row, $this->nrows ); + } + + public function numRows() { + return $this->nrows; + } + + public function numFields() { + return count( $this->columns ); + } + + public function fetchObject() { + if ( $this->cursor >= $this->nrows ) { + return false; + } + $row = $this->rows[$this->cursor++]; + $ret = new stdClass(); + foreach ( $row as $k => $v ) { + $lc = $this->columns[$k]; + $ret->$lc = $v; + } + + return $ret; + } + + public function fetchRow() { + if ( $this->cursor >= $this->nrows ) { + return false; + } + + $row = $this->rows[$this->cursor++]; + $ret = []; + foreach ( $row as $k => $v ) { + $lc = $this->columns[$k]; + $ret[$lc] = $v; + $ret[$k] = $v; + } + + return $ret; + } +} diff --git a/includes/debug/MWDebug.php b/includes/debug/MWDebug.php index 87656f28e3ff..e67a0b35c5fd 100644 --- a/includes/debug/MWDebug.php +++ b/includes/debug/MWDebug.php @@ -370,7 +370,7 @@ class MWDebug { | [\xF0-\xF4](?![\x80-\xBF]{3}) # Invalid UTF-8 Sequence Start | (?<=[\x0-\x7F\xF5-\xFF])[\x80-\xBF] # Invalid UTF-8 Sequence Middle | (?<![\xC2-\xDF]|[\xE0-\xEF]|[\xE0-\xEF][\x80-\xBF]|[\xF0-\xF4] - |[\xF0-\xF4][\x80-\xBF]|[\xF0-\xF4][\x80-\xBF]{2})[\x80-\xBF] # Overlong Sequence + | [\xF0-\xF4][\x80-\xBF]|[\xF0-\xF4][\x80-\xBF]{2})[\x80-\xBF] # Overlong Sequence | (?<=[\xE0-\xEF])[\x80-\xBF](?![\x80-\xBF]) # Short 3 byte sequence | (?<=[\xF0-\xF4])[\x80-\xBF](?![\x80-\xBF]{2}) # Short 4 byte sequence | (?<=[\xF0-\xF4][\x80-\xBF])[\x80-\xBF](?![\x80-\xBF]) # Short 4 byte sequence (2) diff --git a/includes/debug/logger/monolog/AvroFormatter.php b/includes/debug/logger/monolog/AvroFormatter.php index ce0cda12d511..2700daa66fdd 100644 --- a/includes/debug/logger/monolog/AvroFormatter.php +++ b/includes/debug/logger/monolog/AvroFormatter.php @@ -138,9 +138,7 @@ class AvroFormatter implements FormatterInterface { $this->schemas[$channel]['schema'] = AvroSchema::parse( $schema ); } else { $this->schemas[$channel]['schema'] = AvroSchema::real_parse( - $schema, - null, - new AvroNamedSchemata() + $schema ); } } diff --git a/includes/debug/logger/monolog/KafkaHandler.php b/includes/debug/logger/monolog/KafkaHandler.php index 432a9e1490bb..6670fe932d37 100644 --- a/includes/debug/logger/monolog/KafkaHandler.php +++ b/includes/debug/logger/monolog/KafkaHandler.php @@ -33,12 +33,12 @@ use Psr\Log\LoggerInterface; * * Constructor options array arguments: * * alias: map from monolog channel to kafka topic name. When no - * alias exists the topic "monolog_$channel" will be used. + * alias exists the topic "monolog_$channel" will be used. * * swallowExceptions: Swallow exceptions that occur while talking to - * kafka. Defaults to false. + * kafka. Defaults to false. * * logExceptions: Log exceptions talking to kafka here. Either null, - * the name of a channel to log to, or an object implementing - * FormatterInterface. Defaults to null. + * the name of a channel to log to, or an object implementing + * FormatterInterface. Defaults to null. * * Requires the nmred/kafka-php library, version >= 1.3.0 * diff --git a/includes/debug/logger/monolog/LogstashFormatter.php b/includes/debug/logger/monolog/LogstashFormatter.php index 553cbf61c4ad..09ed7555dbfd 100644 --- a/includes/debug/logger/monolog/LogstashFormatter.php +++ b/includes/debug/logger/monolog/LogstashFormatter.php @@ -6,6 +6,7 @@ namespace MediaWiki\Logger\Monolog; * LogstashFormatter squashes the base message array and the context and extras subarrays into one. * This can result in unfortunately named context fields overwriting other data (T145133). * This class modifies the standard LogstashFormatter to rename such fields and flag the message. + * Also changes exception JSON-ification which is done poorly by the standard class. * * Compatible with Monolog 1.x only. * @@ -80,4 +81,31 @@ class LogstashFormatter extends \Monolog\Formatter\LogstashFormatter { } return $fields; } + + /** + * Use a more user-friendly trace format than NormalizerFormatter + * @param \Exception|\Throwable $e + * @return array + */ + protected function normalizeException( $e ) { + if ( !$e instanceof \Exception && !$e instanceof \Throwable ) { + throw new \InvalidArgumentException( 'Exception/Throwable expected, got ' + . gettype( $e ) . ' / ' . get_class( $e ) ); + } + + $data = [ + 'class' => get_class( $e ), + 'message' => $e->getMessage(), + 'code' => $e->getCode(), + 'file' => $e->getFile() . ':' . $e->getLine(), + 'trace' => \MWExceptionHandler::getRedactedTraceAsString( $e ), + ]; + + $previous = $e->getPrevious(); + if ( $previous ) { + $data['previous'] = $this->normalizeException( $previous ); + } + + return $data; + } } diff --git a/includes/deferred/AtomicSectionUpdate.php b/includes/deferred/AtomicSectionUpdate.php index 6585575dc352..8b62989b53ab 100644 --- a/includes/deferred/AtomicSectionUpdate.php +++ b/includes/deferred/AtomicSectionUpdate.php @@ -1,5 +1,7 @@ <?php +use Wikimedia\Rdbms\IDatabase; + /** * Deferrable Update for closure/callback updates via IDatabase::doAtomicSection() * @since 1.27 diff --git a/includes/deferred/AutoCommitUpdate.php b/includes/deferred/AutoCommitUpdate.php index d61dec2cb282..f9297af5840d 100644 --- a/includes/deferred/AutoCommitUpdate.php +++ b/includes/deferred/AutoCommitUpdate.php @@ -1,5 +1,7 @@ <?php +use Wikimedia\Rdbms\IDatabase; + /** * Deferrable Update for closure/callback updates that should use auto-commit mode * @since 1.28 diff --git a/includes/deferred/DeferredUpdates.php b/includes/deferred/DeferredUpdates.php index 1ba6c1febdfe..bbe8687b51f6 100644 --- a/includes/deferred/DeferredUpdates.php +++ b/includes/deferred/DeferredUpdates.php @@ -19,7 +19,10 @@ * * @file */ +use Wikimedia\Rdbms\IDatabase; use MediaWiki\MediaWikiServices; +use Wikimedia\Rdbms\LBFactory; +use Wikimedia\Rdbms\LoadBalancer; /** * Class for managing the deferred updates @@ -335,6 +338,21 @@ class DeferredUpdates { } /** + * @param integer $stage DeferredUpdates constant (PRESEND, POSTSEND, or ALL) + * @since 1.29 + */ + public static function getPendingUpdates( $stage = self::ALL ) { + $updates = []; + if ( $stage === self::ALL || $stage === self::PRESEND ) { + $updates = array_merge( $updates, self::$preSendUpdates ); + } + if ( $stage === self::ALL || $stage === self::POSTSEND ) { + $updates = array_merge( $updates, self::$postSendUpdates ); + } + return $updates; + } + + /** * Clear all pending updates without performing them. Generally, you don't * want or need to call this. Unit tests need it though. */ diff --git a/includes/deferred/LinksDeletionUpdate.php b/includes/deferred/LinksDeletionUpdate.php index 7215696c142e..ca29078c6319 100644 --- a/includes/deferred/LinksDeletionUpdate.php +++ b/includes/deferred/LinksDeletionUpdate.php @@ -21,6 +21,7 @@ */ use MediaWiki\MediaWikiServices; use Wikimedia\ScopedCallback; +use Wikimedia\Rdbms\IDatabase; /** * Update object handling the cleanup of links tables after a page was deleted. diff --git a/includes/deferred/LinksUpdate.php b/includes/deferred/LinksUpdate.php index 229a9a258fd4..56979609f202 100644 --- a/includes/deferred/LinksUpdate.php +++ b/includes/deferred/LinksUpdate.php @@ -20,6 +20,7 @@ * @file */ +use Wikimedia\Rdbms\IDatabase; use MediaWiki\MediaWikiServices; use Wikimedia\ScopedCallback; @@ -145,7 +146,7 @@ class LinksUpdate extends DataUpdate implements EnqueueableDataUpdate { # If the sortkey is longer then 255 bytes, # it truncated by DB, and then doesn't get # matched when comparing existing vs current - # categories, causing bug 25254. + # categories, causing T27254. # Also. substr behaves weird when given "". if ( $sortkey !== '' ) { $sortkey = substr( $sortkey, 0, 255 ); @@ -154,7 +155,9 @@ class LinksUpdate extends DataUpdate implements EnqueueableDataUpdate { $this->mRecursive = $recursive; - Hooks::run( 'LinksUpdateConstructed', [ &$this ] ); + // Avoid PHP 7.1 warning from passing $this by reference + $linksUpdate = $this; + Hooks::run( 'LinksUpdateConstructed', [ &$linksUpdate ] ); } /** @@ -169,7 +172,9 @@ class LinksUpdate extends DataUpdate implements EnqueueableDataUpdate { $scopedLock = self::acquirePageLock( $this->getDB(), $this->mId ); } - Hooks::run( 'LinksUpdate', [ &$this ] ); + // Avoid PHP 7.1 warning from passing $this by reference + $linksUpdate = $this; + Hooks::run( 'LinksUpdate', [ &$linksUpdate ] ); $this->doIncrementalUpdate(); // Commit and release the lock (if set) @@ -177,7 +182,9 @@ class LinksUpdate extends DataUpdate implements EnqueueableDataUpdate { // Run post-commit hooks without DBO_TRX $this->getDB()->onTransactionIdle( function () { - Hooks::run( 'LinksUpdateComplete', [ &$this, $this->ticket ] ); + // Avoid PHP 7.1 warning from passing $this by reference + $linksUpdate = $this; + Hooks::run( 'LinksUpdateComplete', [ &$linksUpdate, $this->ticket ] ); }, __METHOD__ ); diff --git a/includes/deferred/MWCallableUpdate.php b/includes/deferred/MWCallableUpdate.php index 5247e97cf106..5b822af492b4 100644 --- a/includes/deferred/MWCallableUpdate.php +++ b/includes/deferred/MWCallableUpdate.php @@ -1,5 +1,7 @@ <?php +use Wikimedia\Rdbms\IDatabase; + /** * Deferrable Update for closure/callback */ diff --git a/includes/deferred/SiteStatsUpdate.php b/includes/deferred/SiteStatsUpdate.php index ab4a609d012f..aefa7f5d54de 100644 --- a/includes/deferred/SiteStatsUpdate.php +++ b/includes/deferred/SiteStatsUpdate.php @@ -17,7 +17,9 @@ * * @file */ +use MediaWiki\MediaWikiServices; use Wikimedia\Assert\Assert; +use Wikimedia\Rdbms\IDatabase; /** * Class for handling updates to the site_stats table @@ -169,7 +171,7 @@ class SiteStatsUpdate implements DeferrableUpdate, MergeableUpdate { } protected function doUpdateContextStats() { - $stats = RequestContext::getMain()->getStats(); + $stats = MediaWikiServices::getInstance()->getStatsdDataFactory(); foreach ( [ 'edits', 'articles', 'pages', 'users', 'images' ] as $type ) { $delta = $this->$type; if ( $delta !== 0 ) { diff --git a/includes/deferred/SqlDataUpdate.php b/includes/deferred/SqlDataUpdate.php index 25e884114b44..2411beff8950 100644 --- a/includes/deferred/SqlDataUpdate.php +++ b/includes/deferred/SqlDataUpdate.php @@ -21,6 +21,8 @@ * @file */ +use Wikimedia\Rdbms\IDatabase; + /** * @deprecated Since 1.28 Use DataUpdate directly, injecting the database */ diff --git a/includes/deferred/WANCacheReapUpdate.php b/includes/deferred/WANCacheReapUpdate.php new file mode 100644 index 000000000000..b12af1965559 --- /dev/null +++ b/includes/deferred/WANCacheReapUpdate.php @@ -0,0 +1,127 @@ +<?php + +use Psr\Log\LoggerInterface; +use Wikimedia\Rdbms\IDatabase; + +/** + * Class for fixing stale WANObjectCache keys using a purge event source + * + * This is useful for expiring keys that missed fire-and-forget purges. This uses the + * recentchanges table as a reliable stream to make certain keys reach consistency + * as soon as the underlying replica database catches up. These means that critical + * keys will not escape getting purged simply due to brief hiccups in the network, + * which are more prone to happen accross datacenters. + * + * ---- + * "I was trying to cheat death. I was only trying to surmount for a little while the + * darkness that all my life I surely knew was going to come rolling in on me some day + * and obliterate me. I was only to stay alive a little brief while longer, after I was + * already gone. To stay in the light, to be with the living, a little while past my time." + * -- Notes for "Blues of a Lifetime", by [[Cornell Woolrich]] + * + * @since 1.28 + */ +class WANCacheReapUpdate implements DeferrableUpdate { + /** @var IDatabase */ + private $db; + /** @var LoggerInterface */ + private $logger; + + /** + * @param IDatabase $db + * @param LoggerInterface $logger + */ + public function __construct( IDatabase $db, LoggerInterface $logger ) { + $this->db = $db; + $this->logger = $logger; + } + + function doUpdate() { + $reaper = new WANObjectCacheReaper( + ObjectCache::getMainWANInstance(), + ObjectCache::getLocalClusterInstance(), + [ $this, 'getTitleChangeEvents' ], + [ $this, 'getEventAffectedKeys' ], + [ + 'channel' => 'table:recentchanges:' . $this->db->getWikiID(), + 'logger' => $this->logger + ] + ); + + $reaper->invoke( 100 ); + } + + /** + * @see WANObjectCacheRepear + * + * @param int $start + * @param int $id + * @param int $end + * @param int $limit + * @return TitleValue[] + */ + public function getTitleChangeEvents( $start, $id, $end, $limit ) { + $db = $this->db; + $encStart = $db->addQuotes( $db->timestamp( $start ) ); + $encEnd = $db->addQuotes( $db->timestamp( $end ) ); + $id = (int)$id; // cast NULL => 0 since rc_id is an integer + + $res = $db->select( + 'recentchanges', + [ 'rc_namespace', 'rc_title', 'rc_timestamp', 'rc_id' ], + [ + $db->makeList( [ + "rc_timestamp > $encStart", + "rc_timestamp = $encStart AND rc_id > " . $db->addQuotes( $id ) + ], LIST_OR ), + "rc_timestamp < $encEnd" + ], + __METHOD__, + [ 'ORDER BY' => 'rc_timestamp ASC, rc_id ASC', 'LIMIT' => $limit ] + ); + + $events = []; + foreach ( $res as $row ) { + $events[] = [ + 'id' => (int)$row->rc_id, + 'pos' => (int)wfTimestamp( TS_UNIX, $row->rc_timestamp ), + 'item' => new TitleValue( (int)$row->rc_namespace, $row->rc_title ) + ]; + } + + return $events; + } + + /** + * Gets a list of important cache keys associated with a title + * + * @see WANObjectCacheRepear + * @param WANObjectCache $cache + * @param TitleValue $t + * @returns string[] + */ + public function getEventAffectedKeys( WANObjectCache $cache, TitleValue $t ) { + /** @var WikiPage[]|LocalFile[]|User[] $entities */ + $entities = []; + + $entities[] = WikiPage::factory( Title::newFromTitleValue( $t ) ); + if ( $t->inNamespace( NS_FILE ) ) { + $entities[] = wfLocalFile( $t->getText() ); + } + if ( $t->inNamespace( NS_USER ) ) { + $entities[] = User::newFromName( $t->getText(), false ); + } + + $keys = []; + foreach ( $entities as $entity ) { + if ( $entity ) { + $keys = array_merge( $keys, $entity->getMutableCacheKeys( $cache ) ); + } + } + if ( $keys ) { + $this->logger->debug( __CLASS__ . ': got key(s) ' . implode( ', ', $keys ) ); + } + + return $keys; + } +} diff --git a/includes/diff/DifferenceEngine.php b/includes/diff/DifferenceEngine.php index 559a5ec667cf..b0ab24488f56 100644 --- a/includes/diff/DifferenceEngine.php +++ b/includes/diff/DifferenceEngine.php @@ -20,6 +20,7 @@ * @file * @ingroup DifferenceEngine */ +use MediaWiki\MediaWikiServices; /** @deprecated use class constant instead */ define( 'MW_DIFF_VERSION', '1.11a' ); @@ -82,7 +83,7 @@ class DifferenceEngine extends ContextSource { /** * Set this to true to add debug info to the HTML output. * Warning: this may cause RSS readers to spuriously mark articles as "new" - * (bug 20601) + * (T22601) */ public $enableDebugComment = false; @@ -762,8 +763,11 @@ class DifferenceEngine extends ContextSource { $difftext = $this->generateContentDiffBody( $this->mOldContent, $this->mNewContent ); + // Avoid PHP 7.1 warning from passing $this by reference + $diffEngine = $this; + // Save to cache for 7 days - if ( !Hooks::run( 'AbortDiffCache', [ &$this ] ) ) { + if ( !Hooks::run( 'AbortDiffCache', [ &$diffEngine ] ) ) { wfIncrStats( 'diff_cache.uncacheable' ); } elseif ( $key !== false && $difftext !== false ) { wfIncrStats( 'diff_cache.miss' ); @@ -849,7 +853,7 @@ class DifferenceEngine extends ContextSource { $result = $this->textDiff( $otext, $ntext ); $time = intval( ( microtime( true ) - $time ) * 1000 ); - $this->getStats()->timing( 'diff_time', $time ); + MediaWikiServices::getInstance()->getStatsdDataFactory()->timing( 'diff_time', $time ); // Log requests slower than 99th percentile if ( $time > 100 && $this->mOldPage && $this->mNewPage ) { wfDebugLog( 'diff', @@ -982,7 +986,7 @@ class DifferenceEngine extends ContextSource { public function localiseLineNumbers( $text ) { return preg_replace_callback( '/<!--LINE (\d+)-->/', - [ &$this, 'localiseLineNumbersCb' ], + [ $this, 'localiseLineNumbersCb' ], $text ); } diff --git a/includes/diff/WordAccumulator.php b/includes/diff/WordAccumulator.php index a26775ffa80c..ad802756efc7 100644 --- a/includes/diff/WordAccumulator.php +++ b/includes/diff/WordAccumulator.php @@ -46,11 +46,9 @@ class WordAccumulator { private function flushGroup( $new_tag ) { if ( $this->group !== '' ) { if ( $this->tag == 'ins' ) { - $this->line .= "<ins{$this->insClass}>" . - htmlspecialchars( $this->group ) . '</ins>'; + $this->line .= "<ins{$this->insClass}>" . htmlspecialchars( $this->group ) . '</ins>'; } elseif ( $this->tag == 'del' ) { - $this->line .= "<del{$this->delClass}>" . - htmlspecialchars( $this->group ) . '</del>'; + $this->line .= "<del{$this->delClass}>" . htmlspecialchars( $this->group ) . '</del>'; } else { $this->line .= htmlspecialchars( $this->group ); } diff --git a/includes/exception/ErrorPageError.php b/includes/exception/ErrorPageError.php index 9b5a26821fa7..2bed87af362d 100644 --- a/includes/exception/ErrorPageError.php +++ b/includes/exception/ErrorPageError.php @@ -39,7 +39,7 @@ class ErrorPageError extends MWException implements ILocalizedException { $this->msg = $msg; $this->params = $params; - // Bug 44111: Messages in the log files should be in English and not + // T46111: Messages in the log files should be in English and not // customized by the local wiki. So get the default English version for // passing to the parent constructor. Our overridden report() below // makes sure that the page shown to the user is not forced to English. diff --git a/includes/exception/MWContentSerializationException.php b/includes/exception/MWContentSerializationException.php index ed3bd27fb51f..500cf7ce90e0 100644 --- a/includes/exception/MWContentSerializationException.php +++ b/includes/exception/MWContentSerializationException.php @@ -6,4 +6,3 @@ */ class MWContentSerializationException extends MWException { } - diff --git a/includes/exception/MWException.php b/includes/exception/MWException.php index e958c9449f54..4ff8636e2e6a 100644 --- a/includes/exception/MWException.php +++ b/includes/exception/MWException.php @@ -112,7 +112,7 @@ class MWException extends Exception { "</p>\n"; } else { $logId = WebRequest::getRequestId(); - $type = get_class( $this ); + $type = static::class; return "<div class=\"errorbox\">" . '[' . $logId . '] ' . gmdate( 'Y-m-d H:i:s' ) . ": " . @@ -164,7 +164,7 @@ class MWException extends Exception { if ( $this->useOutputPage() ) { $wgOut->prepareErrorPage( $this->getPageTitle() ); - $hookResult = $this->runHooks( get_class( $this ) ); + $hookResult = $this->runHooks( static::class ); if ( $hookResult ) { $wgOut->addHTML( $hookResult ); } else { @@ -183,7 +183,7 @@ class MWException extends Exception { '<style>body { font-family: sans-serif; margin: 0; padding: 0.5em 2em; }</style>' . "</head><body>\n"; - $hookResult = $this->runHooks( get_class( $this ) . 'Raw' ); + $hookResult = $this->runHooks( static::class . 'Raw' ); if ( $hookResult ) { echo $hookResult; } else { @@ -203,7 +203,7 @@ class MWException extends Exception { if ( defined( 'MW_API' ) ) { // Unhandled API exception, we can't be sure that format printer is alive - self::header( 'MediaWiki-API-Error: internal_api_error_' . get_class( $this ) ); + self::header( 'MediaWiki-API-Error: internal_api_error_' . static::class ); wfHttpError( 500, 'Internal Server Error', $this->getText() ); } elseif ( self::isCommandLine() ) { $message = $this->getText(); diff --git a/includes/exception/MWExceptionHandler.php b/includes/exception/MWExceptionHandler.php index bef379eedacc..749be3c6cb43 100644 --- a/includes/exception/MWExceptionHandler.php +++ b/includes/exception/MWExceptionHandler.php @@ -20,6 +20,7 @@ use MediaWiki\Logger\LoggerFactory; use MediaWiki\MediaWikiServices; +use Psr\Log\LogLevel; /** * Handler class for MWExceptions @@ -174,31 +175,37 @@ class MWExceptionHandler { switch ( $level ) { case E_RECOVERABLE_ERROR: $levelName = 'Error'; + $severity = LogLevel::ERROR; break; case E_WARNING: case E_CORE_WARNING: case E_COMPILE_WARNING: case E_USER_WARNING: $levelName = 'Warning'; + $severity = LogLevel::WARNING; break; case E_NOTICE: case E_USER_NOTICE: $levelName = 'Notice'; + $severity = LogLevel::INFO; break; case E_STRICT: $levelName = 'Strict Standards'; + $severity = LogLevel::DEBUG; break; case E_DEPRECATED: case E_USER_DEPRECATED: $levelName = 'Deprecated'; + $severity = LogLevel::INFO; break; default: $levelName = 'Unknown error'; + $severity = LogLevel::ERROR; break; } $e = new ErrorException( "PHP $levelName: $message", 0, $level, $file, $line ); - self::logError( $e, 'error' ); + self::logError( $e, 'error', $severity ); // This handler is for logging only. Return false will instruct PHP // to continue regular handling. @@ -335,7 +342,7 @@ TXT; $text .= "{$pad}#{$level} {$frame['file']}({$frame['line']}): "; } else { // 'file' and 'line' are unset for calls via call_user_func - // (bug 55634) This matches behaviour of + // (T57634) This matches behaviour of // Exception::getTraceAsString to instead display "[internal // function]". $text .= "{$pad}#{$level} [internal function]: "; @@ -621,8 +628,11 @@ TXT; * @since 1.25 * @param ErrorException $e * @param string $channel + * @param string $level */ - protected static function logError( ErrorException $e, $channel ) { + protected static function logError( + ErrorException $e, $channel, $level = LogLevel::ERROR + ) { $catcher = self::CAUGHT_BY_HANDLER; // The set_error_handler callback is independent from error_reporting. // Filter out unwanted errors manually (e.g. when @@ -630,7 +640,8 @@ TXT; $suppressed = ( error_reporting() & $e->getSeverity() ) === 0; if ( !$suppressed ) { $logger = LoggerFactory::getInstance( $channel ); - $logger->error( + $logger->log( + $level, self::getLogMessage( $e ), self::getLogContext( $e, $catcher ) ); @@ -640,7 +651,7 @@ TXT; $json = self::jsonSerializeException( $e, false, FormatJson::ALL_OK, $catcher ); if ( $json !== false ) { $logger = LoggerFactory::getInstance( "{$channel}-json" ); - $logger->error( $json, [ 'private' => true ] ); + $logger->log( $level, $json, [ 'private' => true ] ); } Hooks::run( 'LogException', [ $e, $suppressed ] ); diff --git a/includes/exception/MWUnknownContentModelException.php b/includes/exception/MWUnknownContentModelException.php index 3524d09af9db..df7111acd7cd 100644 --- a/includes/exception/MWUnknownContentModelException.php +++ b/includes/exception/MWUnknownContentModelException.php @@ -23,4 +23,3 @@ class MWUnknownContentModelException extends MWException { return $this->modelId; } } - diff --git a/includes/exception/PermissionsError.php b/includes/exception/PermissionsError.php index 5ecd23764049..cc69a762c119 100644 --- a/includes/exception/PermissionsError.php +++ b/includes/exception/PermissionsError.php @@ -45,10 +45,10 @@ class PermissionsError extends ErrorPageError { $this->permission = $permission; if ( !count( $errors ) ) { - $groups = array_map( - [ 'User', 'makeGroupLinkWiki' ], - User::getGroupsWithPermission( $this->permission ) - ); + $groups = []; + foreach ( User::getGroupsWithPermission( $this->permission ) as $group ) { + $groups[] = UserGroupMembership::getLink( $group, RequestContext::getMain(), 'wiki' ); + } if ( $groups ) { $errors[] = [ 'badaccess-groups', $wgLang->commaList( $groups ), count( $groups ) ]; diff --git a/includes/exception/UserNotLoggedIn.php b/includes/exception/UserNotLoggedIn.php index 43c5b09126e4..6086d559f59d 100644 --- a/includes/exception/UserNotLoggedIn.php +++ b/includes/exception/UserNotLoggedIn.php @@ -32,7 +32,7 @@ * @par Example: * @code * if( $user->isAnon() ) { - * throw new UserNotLoggedIn(); + * throw new UserNotLoggedIn(); * } * @endcode * @@ -42,11 +42,11 @@ * @par Example: * @code * if( $user->isAnon() ) { - * throw new UserNotLoggedIn( 'action-require-loggedin' ); + * throw new UserNotLoggedIn( 'action-require-loggedin' ); * } * @endcode * - * @see bug 37627 + * @see T39627 * @since 1.20 * @ingroup Exception */ diff --git a/includes/export/WikiExporter.php b/includes/export/WikiExporter.php index c1f2d59dcc47..a307468a7132 100644 --- a/includes/export/WikiExporter.php +++ b/includes/export/WikiExporter.php @@ -27,6 +27,9 @@ * @defgroup Dump Dump */ +use Wikimedia\Rdbms\ResultWrapper; +use Wikimedia\Rdbms\IDatabase; + /** * @ingroup SpecialPage Dump */ @@ -343,7 +346,7 @@ class WikiExporter { # query optimization for history stub dumps if ( $this->text == WikiExporter::STUB && $orderRevs ) { $tables = [ 'revision', 'page' ]; - $opts[] = 'STRAIGHT_JOIN'; + $opts[] = 'STRAIGHT_JOIN'; $opts['ORDER BY'] = [ 'rev_page ASC', 'rev_id ASC' ]; $opts['USE INDEX']['revision'] = 'rev_page_id'; $join['page'] = [ 'INNER JOIN', 'rev_page=page_id' ]; diff --git a/includes/export/XmlDumpWriter.php b/includes/export/XmlDumpWriter.php index 5be166b29d47..5a1f92c4cc43 100644 --- a/includes/export/XmlDumpWriter.php +++ b/includes/export/XmlDumpWriter.php @@ -269,7 +269,9 @@ class XmlDumpWriter { $out .= " <sha1/>\n"; } - Hooks::run( 'XmlDumpWriterWriteRevision', [ &$this, &$out, $row, $text ] ); + // Avoid PHP 7.1 warning from passing $this by reference + $writer = $this; + Hooks::run( 'XmlDumpWriterWriteRevision', [ &$writer, &$out, $row, $text ] ); $out .= " </revision>\n"; @@ -431,6 +433,9 @@ class XmlDumpWriter { global $wgContLang; $prefix = $wgContLang->getFormattedNsText( $title->getNamespace() ); + // @todo Emit some kind of warning to the user if $title->getNamespace() !== + // NS_MAIN and $prefix === '' (viz. pages in an unregistered namespace) + if ( $prefix !== '' ) { $prefix .= ':'; } diff --git a/includes/externalstore/ExternalStoreDB.php b/includes/externalstore/ExternalStoreDB.php index 52c1a4c1f13b..6bb1618ff686 100644 --- a/includes/externalstore/ExternalStoreDB.php +++ b/includes/externalstore/ExternalStoreDB.php @@ -20,6 +20,11 @@ * @file */ +use Wikimedia\Rdbms\LoadBalancer; +use Wikimedia\Rdbms\IDatabase; +use Wikimedia\Rdbms\DBConnRef; +use Wikimedia\Rdbms\MaintainableDBConnRef; + /** * DB accessable external objects. * @@ -106,9 +111,7 @@ class ExternalStoreDB extends ExternalStoreMedium { * @return LoadBalancer */ private function getLoadBalancer( $cluster ) { - $wiki = isset( $this->params['wiki'] ) ? $this->params['wiki'] : false; - - return wfGetLBFactory()->getExternalLB( $cluster, $wiki ); + return wfGetLBFactory()->getExternalLB( $cluster ); } /** diff --git a/includes/filebackend/filejournal/DBFileJournal.php b/includes/filebackend/filejournal/DBFileJournal.php index 2e06c40f728f..d09c24587750 100644 --- a/includes/filebackend/filejournal/DBFileJournal.php +++ b/includes/filebackend/filejournal/DBFileJournal.php @@ -22,6 +22,9 @@ * @author Aaron Schulz */ +use MediaWiki\MediaWikiServices; +use Wikimedia\Rdbms\IDatabase; + /** * Version of FileJournal that logs to a DB table * @since 1.20 @@ -180,7 +183,7 @@ class DBFileJournal extends FileJournal { protected function getMasterDB() { if ( !$this->dbw ) { // Get a separate connection in autocommit mode - $lb = wfGetLBFactory()->newMainLB(); + $lb = MediaWikiServices::getInstance()->getDBLoadBalancerFactory()->newMainLB(); $this->dbw = $lb->getConnection( DB_MASTER, [], $this->wiki ); $this->dbw->clearFlag( DBO_TRX ); } diff --git a/includes/filebackend/lockmanager/MySqlLockManager.php b/includes/filebackend/lockmanager/MySqlLockManager.php index 5936e7d1d2a8..8510d0c18cc5 100644 --- a/includes/filebackend/lockmanager/MySqlLockManager.php +++ b/includes/filebackend/lockmanager/MySqlLockManager.php @@ -1,4 +1,7 @@ <?php + +use Wikimedia\Rdbms\IDatabase; + /** * MySQL version of DBLockManager that supports shared locks. * diff --git a/includes/filerepo/FileBackendDBRepoWrapper.php b/includes/filerepo/FileBackendDBRepoWrapper.php index 5bc60a0e0af8..856dc36521ac 100644 --- a/includes/filerepo/FileBackendDBRepoWrapper.php +++ b/includes/filerepo/FileBackendDBRepoWrapper.php @@ -23,6 +23,9 @@ * @author Aaron Schulz */ +use Wikimedia\Rdbms\DBConnRef; +use Wikimedia\Rdbms\MaintainableDBConnRef; + /** * @brief Proxy backend that manages file layout rewriting for FileRepo. * diff --git a/includes/filerepo/FileRepo.php b/includes/filerepo/FileRepo.php index 0e4b2f0b50eb..8edf81f13139 100644 --- a/includes/filerepo/FileRepo.php +++ b/includes/filerepo/FileRepo.php @@ -1448,7 +1448,7 @@ class FileRepo { 'dst' => $archivePath, // We may have 2+ identical files being deleted, // all of which will map to the same destination file - 'overwriteSame' => true // also see bug 31792 + 'overwriteSame' => true // also see T33792 ]; } diff --git a/includes/filerepo/ForeignAPIRepo.php b/includes/filerepo/ForeignAPIRepo.php index ca417187e0ca..43f1d211b38d 100644 --- a/includes/filerepo/ForeignAPIRepo.php +++ b/includes/filerepo/ForeignAPIRepo.php @@ -571,7 +571,7 @@ class ForeignAPIRepo extends FileRepo { $cache = ObjectCache::getMainWANInstance(); return $cache->getWithSetCallback( - $this->getLocalCacheKey( get_class( $this ), $target, md5( $url ) ), + $this->getLocalCacheKey( static::class, $target, md5( $url ) ), $cacheTTL, function ( $curValue, &$ttl ) use ( $url, $cache ) { $html = self::httpGet( $url, 'default', [], $mtime ); @@ -593,13 +593,13 @@ class ForeignAPIRepo extends FileRepo { * @throws MWException */ function enumFiles( $callback ) { - throw new MWException( 'enumFiles is not supported by ' . get_class( $this ) ); + throw new MWException( 'enumFiles is not supported by ' . static::class ); } /** * @throws MWException */ protected function assertWritableRepo() { - throw new MWException( get_class( $this ) . ': write operations are not supported.' ); + throw new MWException( static::class . ': write operations are not supported.' ); } } diff --git a/includes/filerepo/ForeignDBRepo.php b/includes/filerepo/ForeignDBRepo.php index f49ef88c5cd4..29c017cba808 100644 --- a/includes/filerepo/ForeignDBRepo.php +++ b/includes/filerepo/ForeignDBRepo.php @@ -21,6 +21,8 @@ * @ingroup FileRepo */ +use Wikimedia\Rdbms\IDatabase; + /** * A foreign repository with an accessible MediaWiki database * @@ -138,7 +140,7 @@ class ForeignDBRepo extends LocalRepo { } protected function assertWritableRepo() { - throw new MWException( get_class( $this ) . ': write operations are not supported.' ); + throw new MWException( static::class . ': write operations are not supported.' ); } /** diff --git a/includes/filerepo/ForeignDBViaLBRepo.php b/includes/filerepo/ForeignDBViaLBRepo.php index a9cd03086936..f83fd1c8138d 100644 --- a/includes/filerepo/ForeignDBViaLBRepo.php +++ b/includes/filerepo/ForeignDBViaLBRepo.php @@ -100,7 +100,7 @@ class ForeignDBViaLBRepo extends LocalRepo { } protected function assertWritableRepo() { - throw new MWException( get_class( $this ) . ': write operations are not supported.' ); + throw new MWException( static::class . ': write operations are not supported.' ); } public function getInfo() { diff --git a/includes/filerepo/LocalRepo.php b/includes/filerepo/LocalRepo.php index d49ae7bf4b49..9c92bc0b8a62 100644 --- a/includes/filerepo/LocalRepo.php +++ b/includes/filerepo/LocalRepo.php @@ -22,6 +22,9 @@ * @ingroup FileRepo */ +use Wikimedia\Rdbms\ResultWrapper; +use Wikimedia\Rdbms\IDatabase; + /** * A repository that stores files in the local filesystem and registers them * in the wiki's own database. This is the most commonly used repository class. diff --git a/includes/filerepo/NullRepo.php b/includes/filerepo/NullRepo.php index f2b7395c7bd4..1c12e0274acd 100644 --- a/includes/filerepo/NullRepo.php +++ b/includes/filerepo/NullRepo.php @@ -33,6 +33,6 @@ class NullRepo extends FileRepo { } protected function assertWritableRepo() { - throw new MWException( get_class( $this ) . ': write operations are not supported.' ); + throw new MWException( static::class . ': write operations are not supported.' ); } } diff --git a/includes/filerepo/file/ArchivedFile.php b/includes/filerepo/file/ArchivedFile.php index 9a7a55be9eb2..6984d48c6d8b 100644 --- a/includes/filerepo/file/ArchivedFile.php +++ b/includes/filerepo/file/ArchivedFile.php @@ -493,22 +493,6 @@ class ArchivedFile { } /** - * Return the user name of the uploader. - * - * @deprecated since 1.23 Use getUser( 'text' ) instead. - * @return string|int - */ - public function getUserText() { - wfDeprecated( __METHOD__, '1.23' ); - $this->load(); - if ( $this->isDeleted( File::DELETED_USER ) ) { - return 0; - } else { - return $this->user_text; - } - } - - /** * Return upload description. * * @return string|int diff --git a/includes/filerepo/file/File.php b/includes/filerepo/file/File.php index be784626d922..e367812a8c7c 100644 --- a/includes/filerepo/file/File.php +++ b/includes/filerepo/file/File.php @@ -5,6 +5,7 @@ * * Represents files in a repository. */ +use MediaWiki\MediaWikiServices; /** * Base code for files. @@ -436,7 +437,7 @@ abstract class File implements IDBAccessObject { $this->fsFile = $this->repo->getLocalReference( $this->getPath() ); $statTiming = microtime( true ) - $starttime; - RequestContext::getMain()->getStats()->timing( + MediaWikiServices::getInstance()->getStatsdDataFactory()->timing( 'media.thumbnail.generate.fetchoriginal', 1000 * $statTiming ); if ( !$this->fsFile ) { @@ -919,7 +920,7 @@ abstract class File implements IDBAccessObject { return $this->iconThumb(); } $hp['width'] = $width; - // be sure to ignore any height specification as well (bug 62258) + // be sure to ignore any height specification as well (T64258) unset( $hp['height'] ); return $this->transform( $hp ); @@ -1039,7 +1040,7 @@ abstract class File implements IDBAccessObject { break; // not a bitmap or renderable image, don't try } - // Get the descriptionUrl to embed it as comment into the thumbnail. Bug 19791. + // Get the descriptionUrl to embed it as comment into the thumbnail. T21791. $descriptionUrl = $this->getDescriptionUrl(); if ( $descriptionUrl ) { $params['descriptionUrl'] = wfExpandUrl( $descriptionUrl, PROTO_CANONICAL ); @@ -1117,7 +1118,7 @@ abstract class File implements IDBAccessObject { public function generateAndSaveThumb( $tmpFile, $transformParams, $flags ) { global $wgIgnoreImageErrors; - $stats = RequestContext::getMain()->getStats(); + $stats = MediaWikiServices::getInstance()->getStatsdDataFactory(); $handler = $this->getHandler(); @@ -1227,7 +1228,7 @@ abstract class File implements IDBAccessObject { // this object exists $tmpFile->bind( $this ); - RequestContext::getMain()->getStats()->timing( + MediaWikiServices::getInstance()->getStatsdDataFactory()->timing( 'media.thumbnail.generate.bucket', 1000 * $buckettime ); return true; @@ -1766,7 +1767,7 @@ abstract class File implements IDBAccessObject { * @throws MWException */ function readOnlyError() { - throw new MWException( get_class( $this ) . ': write operations are not supported' ); + throw new MWException( static::class . ': write operations are not supported' ); } /** diff --git a/includes/filerepo/file/ForeignDBFile.php b/includes/filerepo/file/ForeignDBFile.php index c6c49b4ca49e..f6f44e610a5d 100644 --- a/includes/filerepo/file/ForeignDBFile.php +++ b/includes/filerepo/file/ForeignDBFile.php @@ -120,10 +120,10 @@ class ForeignDBFile extends LocalFile { } /** - * @param bool|Language $lang Optional language to fetch description in. + * @param Language|null $lang Optional language to fetch description in. * @return string|false */ - function getDescriptionText( $lang = false ) { + function getDescriptionText( $lang = null ) { global $wgLang; if ( !$this->repo->fetchDescription ) { diff --git a/includes/filerepo/file/LocalFile.php b/includes/filerepo/file/LocalFile.php index 16fe72d37cdf..c109fba9163d 100644 --- a/includes/filerepo/file/LocalFile.php +++ b/includes/filerepo/file/LocalFile.php @@ -22,6 +22,7 @@ */ use \MediaWiki\Logger\LoggerFactory; +use Wikimedia\Rdbms\IDatabase; /** * Class to represent a local file in the wiki's own database @@ -241,6 +242,15 @@ class LocalFile extends File { } /** + * @param WANObjectCache $cache + * @return string[] + * @since 1.28 + */ + public function getMutableCacheKeys( WANObjectCache $cache ) { + return [ $this->getCacheKey() ]; + } + + /** * Try to load file metadata from memcached, falling back to the database */ private function loadFromCache() { @@ -382,7 +392,7 @@ class LocalFile extends File { * @param int $flags */ function loadFromDB( $flags = 0 ) { - $fname = get_class( $this ) . '::' . __FUNCTION__; + $fname = static::class . '::' . __FUNCTION__; # Unconditionally set loaded=true, we don't want the accessors constantly rechecking $this->dataLoaded = true; @@ -407,7 +417,7 @@ class LocalFile extends File { * This covers fields that are sometimes not cached. */ protected function loadExtraFromDB() { - $fname = get_class( $this ) . '::' . __FUNCTION__; + $fname = static::class . '::' . __FUNCTION__; # Unconditionally set loaded=true, we don't want the accessors constantly rechecking $this->extraDataLoaded = true; @@ -882,7 +892,7 @@ class LocalFile extends File { $files[] = $file; } } catch ( FileBackendError $e ) { - } // suppress (bug 54674) + } // suppress (T56674) return $files; } @@ -1061,7 +1071,9 @@ class LocalFile extends File { $opts['ORDER BY'] = "oi_timestamp $order"; $opts['USE INDEX'] = [ 'oldimage' => 'oi_name_timestamp' ]; - Hooks::run( 'LocalFile::getHistory', [ &$this, &$tables, &$fields, + // Avoid PHP 7.1 warning from passing $this by reference + $localFile = $this; + Hooks::run( 'LocalFile::getHistory', [ &$localFile, &$tables, &$fields, &$conds, &$opts, &$join_conds ] ); $res = $dbr->select( $tables, $fields, $conds, __METHOD__, $opts, $join_conds ); @@ -1089,7 +1101,7 @@ class LocalFile extends File { */ public function nextHistoryLine() { # Polymorphic function name to distinguish foreign and local fetches - $fname = get_class( $this ) . '::' . __FUNCTION__; + $fname = static::class . '::' . __FUNCTION__; $dbr = $this->repo->getReplicaDB(); @@ -1342,7 +1354,7 @@ class LocalFile extends File { } } - # (bug 34993) Note: $oldver can be empty here, if the previous + # (T36993) Note: $oldver can be empty here, if the previous # version of the file was broken. Allow registration of the new # version to continue anyway, because that's better than having # an image that's not fixable by user operations. @@ -1996,7 +2008,7 @@ class LocalFile extends File { $dbw = $this->repo->getMasterDB(); $makesTransaction = !$dbw->trxLevel(); $dbw->startAtomic( self::ATOMIC_SECTION_LOCK ); - // Bug 54736: use simple lock to handle when the file does not exist. + // T56736: use simple lock to handle when the file does not exist. // SELECT FOR UPDATE prevents changes, not other SELECTs with FOR UPDATE. // Also, that would cause contention on INSERT of similarly named rows. $status = $this->acquireFileLock(); // represents all versions of the file @@ -3024,7 +3036,7 @@ class LocalFileMoveBatch { $status->failCount++; } $status->successCount += $oldRowCount; - // Bug 34934: oldCount is based on files that actually exist. + // T36934: oldCount is based on files that actually exist. // There may be more DB rows than such files, in which case $affected // can be greater than $total. We use max() to avoid negatives here. $status->failCount += max( 0, $this->oldCount - $oldRowCount ); diff --git a/includes/htmlform/HTMLForm.php b/includes/htmlform/HTMLForm.php index e627cfdc6903..399147b89afe 100644 --- a/includes/htmlform/HTMLForm.php +++ b/includes/htmlform/HTMLForm.php @@ -165,6 +165,7 @@ class HTMLForm extends ContextSource { 'url' => 'HTMLTextField', 'title' => 'HTMLTitleTextField', 'user' => 'HTMLUserTextField', + 'usersmultiselect' => 'HTMLUsersMultiselectField', ]; public $mFieldData; @@ -1258,7 +1259,7 @@ class HTMLForm extends ContextSource { * * @param string|array|Status $elements The set of errors/warnings to process. * @param string $elementsType Should warnings or errors be returned. This is meant - * for Status objects, all other valid types are always considered as errors. + * for Status objects, all other valid types are always considered as errors. * @return string */ public function getErrorsOrWarnings( $elements, $elementsType ) { diff --git a/includes/htmlform/HTMLFormField.php b/includes/htmlform/HTMLFormField.php index 487d6f647bbf..83a80233f9f5 100644 --- a/includes/htmlform/HTMLFormField.php +++ b/includes/htmlform/HTMLFormField.php @@ -475,7 +475,7 @@ abstract class HTMLFormField { public function getTableRow( $value ) { list( $errors, $errorClass ) = $this->getErrorsAndErrorClass( $value ); $inputHtml = $this->getInputHTML( $value ); - $fieldType = get_class( $this ); + $fieldType = static::class; $helptext = $this->getHelpTextHtmlTable( $this->getHelpText() ); $cellAttributes = []; $rowAttributes = []; @@ -533,7 +533,7 @@ abstract class HTMLFormField { public function getDiv( $value ) { list( $errors, $errorClass ) = $this->getErrorsAndErrorClass( $value ); $inputHtml = $this->getInputHTML( $value ); - $fieldType = get_class( $this ); + $fieldType = static::class; $helptext = $this->getHelpTextHtmlDiv( $this->getHelpText() ); $cellAttributes = []; $label = $this->getLabelHtml( $cellAttributes ); @@ -601,7 +601,7 @@ abstract class HTMLFormField { $infusable = false; } - $fieldType = get_class( $this ); + $fieldType = static::class; $help = $this->getHelpText(); $errors = $this->getErrorsRaw( $value ); foreach ( $errors as &$error ) { @@ -1145,6 +1145,9 @@ abstract class HTMLFormField { * @since 1.18 */ protected static function formatErrors( $errors ) { + // Note: If you change the logic in this method, change + // htmlform.Checker.js to match. + if ( is_array( $errors ) && count( $errors ) === 1 ) { $errors = array_shift( $errors ); } diff --git a/includes/htmlform/fields/HTMLCheckMatrix.php b/includes/htmlform/fields/HTMLCheckMatrix.php index 46172a581bd2..fa18a3cdadfc 100644 --- a/includes/htmlform/fields/HTMLCheckMatrix.php +++ b/includes/htmlform/fields/HTMLCheckMatrix.php @@ -189,7 +189,7 @@ class HTMLCheckMatrix extends HTMLFormField implements HTMLNestedFilterable { public function getTableRow( $value ) { list( $errors, $errorClass ) = $this->getErrorsAndErrorClass( $value ); $inputHtml = $this->getInputHTML( $value ); - $fieldType = get_class( $this ); + $fieldType = static::class; $helptext = $this->getHelpTextHtmlTable( $this->getHelpText() ); $cellAttributes = [ 'colspan' => 2 ]; diff --git a/includes/htmlform/fields/HTMLFormFieldCloner.php b/includes/htmlform/fields/HTMLFormFieldCloner.php index 8fb840a136cb..dd9184bf33bf 100644 --- a/includes/htmlform/fields/HTMLFormFieldCloner.php +++ b/includes/htmlform/fields/HTMLFormFieldCloner.php @@ -46,7 +46,7 @@ class HTMLFormFieldCloner extends HTMLFormField { protected $uniqueId; public function __construct( $params ) { - $this->uniqueId = get_class( $this ) . ++self::$counter . 'x'; + $this->uniqueId = static::class . ++self::$counter . 'x'; parent::__construct( $params ); if ( empty( $this->mParams['fields'] ) || !is_array( $this->mParams['fields'] ) ) { diff --git a/includes/htmlform/fields/HTMLMultiSelectField.php b/includes/htmlform/fields/HTMLMultiSelectField.php index 23044bd6ff2a..2b6e0665d55d 100644 --- a/includes/htmlform/fields/HTMLMultiSelectField.php +++ b/includes/htmlform/fields/HTMLMultiSelectField.php @@ -17,6 +17,11 @@ class HTMLMultiSelectField extends HTMLFormField implements HTMLNestedFilterable public function __construct( $params ) { parent::__construct( $params ); + // If the disabled-options parameter is not provided, use an empty array + if ( isset( $this->mParams['disabled-options'] ) === false ) { + $this->mParams['disabled-options'] = []; + } + // For backwards compatibility, also handle the old way with 'cssclass' => 'mw-chosen' if ( isset( $params['dropdown'] ) || strpos( $this->mClass, 'mw-chosen' ) !== false ) { $this->mClass .= ' mw-htmlform-dropdown'; @@ -75,6 +80,9 @@ class HTMLMultiSelectField extends HTMLFormField implements HTMLNestedFilterable 'id' => "{$this->mID}-$info", 'value' => $info, ]; + if ( in_array( $info, $this->mParams['disabled-options'], true ) ) { + $thisAttribs['disabled'] = 'disabled'; + } $checked = in_array( $info, $value, true ); $checkbox = $this->getOneCheckbox( $checked, $attribs + $thisAttribs, $label ); @@ -113,6 +121,18 @@ class HTMLMultiSelectField extends HTMLFormField implements HTMLNestedFilterable } /** + * Get options and make them into arrays suitable for OOUI. + * @return array Options for inclusion in a select or whatever. + */ + public function getOptionsOOUI() { + $options = parent::getOptionsOOUI(); + foreach ( $options as &$option ) { + $option['disabled'] = in_array( $option['data'], $this->mParams['disabled-options'], true ); + } + return $options; + } + + /** * Get the OOUI version of this field. * * @since 1.28 diff --git a/includes/htmlform/fields/HTMLTextField.php b/includes/htmlform/fields/HTMLTextField.php index c3da74618bd1..b0b66cab9fd9 100644 --- a/includes/htmlform/fields/HTMLTextField.php +++ b/includes/htmlform/fields/HTMLTextField.php @@ -187,6 +187,7 @@ class HTMLTextField extends HTMLFormField { 'name' => $this->mName, 'value' => $value, 'type' => $type, + 'dir' => $this->mDir, ] + $attribs ); } diff --git a/includes/htmlform/fields/HTMLUsersMultiselectField.php b/includes/htmlform/fields/HTMLUsersMultiselectField.php new file mode 100644 index 000000000000..8c1241d015fe --- /dev/null +++ b/includes/htmlform/fields/HTMLUsersMultiselectField.php @@ -0,0 +1,86 @@ +<?php + +use MediaWiki\Widget\UsersMultiselectWidget; + +/** + * Implements a capsule multiselect input field for user names. + * + * Besides the parameters recognized by HTMLUserTextField, additional recognized + * parameters are: + * default - (optional) Array of usernames to use as preset data + * placeholder - (optional) Custom placeholder message for input + * + * The result is the array of usernames + * + * @note This widget is not likely to remain functional in non-OOUI forms. + */ +class HTMLUsersMultiselectField extends HTMLUserTextField { + + public function loadDataFromRequest( $request ) { + if ( !$request->getCheck( $this->mName ) ) { + return $this->getDefault(); + } + + $usersArray = explode( "\n", $request->getText( $this->mName ) ); + // Remove empty lines + $usersArray = array_values( array_filter( $usersArray, function( $username ) { + return trim( $username ) !== ''; + } ) ); + return $usersArray; + } + + public function validate( $value, $alldata ) { + if ( !$this->mParams['exists'] ) { + return true; + } + + if ( is_null( $value ) ) { + return false; + } + + foreach ( $value as $username ) { + $result = parent::validate( $username, $alldata ); + if ( $result !== true ) { + return $result; + } + } + + return true; + } + + public function getInputHTML( $values ) { + $this->mParent->getOutput()->enableOOUI(); + return $this->getInputOOUI( $values ); + } + + public function getInputOOUI( $values ) { + $params = [ 'name' => $this->mName ]; + + if ( isset( $this->mParams['default'] ) ) { + $params['default'] = $this->mParams['default']; + } + + if ( isset( $this->mParams['placeholder'] ) ) { + $params['placeholder'] = $this->mParams['placeholder']; + } else { + $params['placeholder'] = $this->msg( 'mw-widgets-usersmultiselect-placeholder' ) + ->inContentLanguage() + ->plain(); + } + + if ( !is_null( $values ) ) { + $params['default'] = $values; + } + + return new UsersMultiselectWidget( $params ); + } + + protected function shouldInfuseOOUI() { + return true; + } + + protected function getOOUIModules() { + return [ 'mediawiki.widgets.UsersMultiselectWidget' ]; + } + +} diff --git a/includes/http/Http.php b/includes/http/Http.php index 779d60634674..889cb6031659 100644 --- a/includes/http/Http.php +++ b/includes/http/Http.php @@ -46,13 +46,15 @@ class Http { * - caInfo Provide CA information * - maxRedirects Maximum number of redirects to follow (defaults to 5) * - followRedirects Whether to follow redirects (defaults to false). - * Note: this should only be used when the target URL is trusted, - * to avoid attacks on intranet services accessible by HTTP. + * Note: this should only be used when the target URL is trusted, + * to avoid attacks on intranet services accessible by HTTP. * - userAgent A user agent, if you want to override the default * MediaWiki/$wgVersion * - logger A \Psr\Logger\LoggerInterface instance for debug logging * - username Username for HTTP Basic Authentication * - password Password for HTTP Basic Authentication + * - originalRequest Information about the original request (as a WebRequest object or + * an associative array with 'ip' and 'userAgent'). * @param string $caller The method making this request, for profiling * @return string|bool (bool)false on failure or a string on success */ diff --git a/includes/http/MWHttpRequest.php b/includes/http/MWHttpRequest.php index fac052fffc1d..88cc510219c7 100644 --- a/includes/http/MWHttpRequest.php +++ b/includes/http/MWHttpRequest.php @@ -125,6 +125,9 @@ class MWHttpRequest implements LoggerAwareInterface { 'Basic ' . base64_encode( $options['username'] . ':' . $options['password'] ) ); } + if ( isset( $options['originalRequest'] ) ) { + $this->setOriginalRequest( $options['originalRequest'] ); + } $members = [ "postData", "proxy", "noProxy", "sslVerifyHost", "caInfo", "method", "followRedirects", "maxRedirects", "sslVerifyCert", "callback" ]; @@ -132,7 +135,7 @@ class MWHttpRequest implements LoggerAwareInterface { foreach ( $members as $o ) { if ( isset( $options[$o] ) ) { // ensure that MWHttpRequest::method is always - // uppercased. Bug 36137 + // uppercased. T38137 if ( $o == 'method' ) { $options[$o] = strtoupper( $options[$o] ); } @@ -580,7 +583,7 @@ class MWHttpRequest implements LoggerAwareInterface { * * Note that the multiple Location: headers are an artifact of * CURL -- they shouldn't actually get returned this way. Rewrite - * this when bug 29232 is taken care of (high-level redirect + * this when T31232 is taken care of (high-level redirect * handling rewrite). * * @return string @@ -606,19 +609,17 @@ class MWHttpRequest implements LoggerAwareInterface { } } - if ( $foundRelativeURI ) { - if ( $domain ) { - return $domain . $locations[$countLocations - 1]; - } else { - $url = parse_url( $this->url ); - if ( isset( $url['host'] ) ) { - return $url['scheme'] . '://' . $url['host'] . - $locations[$countLocations - 1]; - } - } - } else { + if ( !$foundRelativeURI ) { return $locations[$countLocations - 1]; } + if ( $domain ) { + return $domain . $locations[$countLocations - 1]; + } + $url = parse_url( $this->url ); + if ( isset( $url['host'] ) ) { + return $url['scheme'] . '://' . $url['host'] . + $locations[$countLocations - 1]; + } } return $this->url; @@ -632,4 +633,34 @@ class MWHttpRequest implements LoggerAwareInterface { public function canFollowRedirects() { return true; } + + /** + * Set information about the original request. This can be useful for + * endpoints/API modules which act as a proxy for some service, and + * throttling etc. needs to happen in that service. + * Calling this will result in the X-Forwarded-For and X-Original-User-Agent + * headers being set. + * @param WebRequest|array $originalRequest When in array form, it's + * expected to have the keys 'ip' and 'userAgent'. + * @note IP/user agent is personally identifiable information, and should + * only be set when the privacy policy of the request target is + * compatible with that of the MediaWiki installation. + */ + public function setOriginalRequest( $originalRequest ) { + if ( $originalRequest instanceof WebRequest ) { + $originalRequest = [ + 'ip' => $originalRequest->getIP(), + 'userAgent' => $originalRequest->getHeader( 'User-Agent' ), + ]; + } elseif ( + !is_array( $originalRequest ) + || array_diff( [ 'ip', 'userAgent' ], array_keys( $originalRequest ) ) + ) { + throw new InvalidArgumentException( __METHOD__ . ': $originalRequest must be a ' + . "WebRequest or an array with 'ip' and 'userAgent' keys" ); + } + + $this->reqHeaders['X-Forwarded-For'] = $originalRequest['ip']; + $this->reqHeaders['X-Original-User-Agent'] = $originalRequest['userAgent']; + } } diff --git a/includes/import/WikiImporter.php b/includes/import/WikiImporter.php index 1769924ab06b..06b579a7d9f7 100644 --- a/includes/import/WikiImporter.php +++ b/includes/import/WikiImporter.php @@ -546,7 +546,7 @@ class WikiImporter { public function doImport() { // Calls to reader->read need to be wrapped in calls to // libxml_disable_entity_loader() to avoid local file - // inclusion attacks (bug 46932). + // inclusion attacks (T48932). $oldDisable = libxml_disable_entity_loader( true ); $this->reader->read(); diff --git a/includes/import/WikiRevision.php b/includes/import/WikiRevision.php index 23db3e2e7ba7..f6becb9c927f 100644 --- a/includes/import/WikiRevision.php +++ b/includes/import/WikiRevision.php @@ -28,86 +28,165 @@ * Represents a revision, log entry or upload during the import process. * This class sticks closely to the structure of the XML dump. * + * @since 1.2 + * * @ingroup SpecialPage */ class WikiRevision { - /** @todo Unused? */ + + /** + * @since 1.17 + * @deprecated in 1.29. Unused. + * @note Introduced in 9b3128eb2b654761f21fd4ca1d5a1a4b796dc912, unused there, unused now. + */ public $importer = null; - /** @var Title */ + /** + * @since 1.2 + * @var Title + */ public $title = null; - /** @var int */ + /** + * @since 1.6.4 + * @var int + */ public $id = 0; - /** @var string */ + /** + * @since 1.2 + * @var string + */ public $timestamp = "20010115000000"; /** + * @since 1.2 * @var int - * @todo Can't find any uses. Public, because that's suspicious. Get clarity. */ + * @deprecated in 1.29. Unused. + * @note Introduced in 436a028086fb3f01c4605c5ad2964d56f9306aca, unused there, unused now. + */ public $user = 0; - /** @var string */ + /** + * @since 1.2 + * @var string + */ public $user_text = ""; - /** @var User */ + /** + * @since 1.27 + * @var User + */ public $userObj = null; - /** @var string */ + /** + * @since 1.21 + * @var string + */ public $model = null; - /** @var string */ + /** + * @since 1.21 + * @var string + */ public $format = null; - /** @var string */ + /** + * @since 1.2 + * @var string + */ public $text = ""; - /** @var int */ + /** + * @since 1.12.2 + * @var int + */ protected $size; - /** @var Content */ + /** + * @since 1.21 + * @var Content + */ public $content = null; - /** @var ContentHandler */ + /** + * @since 1.24 + * @var ContentHandler + */ protected $contentHandler = null; - /** @var string */ + /** + * @since 1.2.6 + * @var string + */ public $comment = ""; - /** @var bool */ + /** + * @since 1.5.7 + * @var bool + */ public $minor = false; - /** @var string */ + /** + * @since 1.12.2 + * @var string + */ public $type = ""; - /** @var string */ + /** + * @since 1.12.2 + * @var string + */ public $action = ""; - /** @var string */ + /** + * @since 1.12.2 + * @var string + */ public $params = ""; - /** @var string */ + /** + * @since 1.17 + * @var string + */ public $fileSrc = ''; - /** @var bool|string */ + /** + * @since 1.17 + * @var bool|string + */ public $sha1base36 = false; /** - * @var bool - * @todo Unused? + * @since 1.17 + * @var string */ - public $isTemp = false; - - /** @var string */ public $archiveName = ''; + /** + * @since 1.12.2 + */ protected $filename; - /** @var mixed */ + /** + * @since 1.12.2 + * @var mixed + */ protected $src; - /** @todo Unused? */ + /** + * @since 1.18 + * @var bool + * @todo Unused? + */ + public $isTemp = false; + + /** + * @since 1.18 + * @deprecated 1.29 use Wikirevision::isTempSrc() + * First written to in 43d5d3b682cc1733ad01a837d11af4a402d57e6a + * Actually introduced in 52cd34acf590e5be946b7885ffdc13a157c1c6cf + */ public $fileIsTemp; /** @var bool */ @@ -121,10 +200,11 @@ class WikiRevision { } /** + * @since 1.7 taking a Title object (string before) * @param Title $title * @throws MWException */ - function setTitle( $title ) { + public function setTitle( $title ) { if ( is_object( $title ) ) { $this->title = $title; } elseif ( is_null( $title ) ) { @@ -136,142 +216,163 @@ class WikiRevision { } /** + * @since 1.6.4 * @param int $id */ - function setID( $id ) { + public function setID( $id ) { $this->id = $id; } /** + * @since 1.2 * @param string $ts */ - function setTimestamp( $ts ) { + public function setTimestamp( $ts ) { # 2003-08-05T18:30:02Z $this->timestamp = wfTimestamp( TS_MW, $ts ); } /** + * @since 1.2 * @param string $user */ - function setUsername( $user ) { + public function setUsername( $user ) { $this->user_text = $user; } /** + * @since 1.27 * @param User $user */ - function setUserObj( $user ) { + public function setUserObj( $user ) { $this->userObj = $user; } /** + * @since 1.2 * @param string $ip */ - function setUserIP( $ip ) { + public function setUserIP( $ip ) { $this->user_text = $ip; } /** + * @since 1.21 * @param string $model */ - function setModel( $model ) { + public function setModel( $model ) { $this->model = $model; } /** + * @since 1.21 * @param string $format */ - function setFormat( $format ) { + public function setFormat( $format ) { $this->format = $format; } /** + * @since 1.2 * @param string $text */ - function setText( $text ) { + public function setText( $text ) { $this->text = $text; } /** + * @since 1.2.6 * @param string $text */ - function setComment( $text ) { + public function setComment( $text ) { $this->comment = $text; } /** + * @since 1.5.7 * @param bool $minor */ - function setMinor( $minor ) { + public function setMinor( $minor ) { $this->minor = (bool)$minor; } /** + * @since 1.12.2 * @param mixed $src */ - function setSrc( $src ) { + public function setSrc( $src ) { $this->src = $src; } /** + * @since 1.17 * @param string $src * @param bool $isTemp */ - function setFileSrc( $src, $isTemp ) { + public function setFileSrc( $src, $isTemp ) { $this->fileSrc = $src; $this->fileIsTemp = $isTemp; + $this->isTemp = $isTemp; } /** + * @since 1.17 * @param string $sha1base36 */ - function setSha1Base36( $sha1base36 ) { + public function setSha1Base36( $sha1base36 ) { $this->sha1base36 = $sha1base36; } /** + * @since 1.12.2 * @param string $filename */ - function setFilename( $filename ) { + public function setFilename( $filename ) { $this->filename = $filename; } /** + * @since 1.17 * @param string $archiveName */ - function setArchiveName( $archiveName ) { + public function setArchiveName( $archiveName ) { $this->archiveName = $archiveName; } /** + * @since 1.12.2 * @param int $size */ - function setSize( $size ) { + public function setSize( $size ) { $this->size = intval( $size ); } /** + * @since 1.12.2 * @param string $type */ - function setType( $type ) { + public function setType( $type ) { $this->type = $type; } /** + * @since 1.12.2 * @param string $action */ - function setAction( $action ) { + public function setAction( $action ) { $this->action = $action; } /** + * @since 1.12.2 * @param array $params */ - function setParams( $params ) { + public function setParams( $params ) { $this->params = $params; } /** + * @since 1.18 * @param bool $noupdates */ public function setNoUpdates( $noupdates ) { @@ -279,51 +380,58 @@ class WikiRevision { } /** + * @since 1.2 * @return Title */ - function getTitle() { + public function getTitle() { return $this->title; } /** + * @since 1.6.4 * @return int */ - function getID() { + public function getID() { return $this->id; } /** + * @since 1.2 * @return string */ - function getTimestamp() { + public function getTimestamp() { return $this->timestamp; } /** + * @since 1.2 * @return string */ - function getUser() { + public function getUser() { return $this->user_text; } /** + * @since 1.27 * @return User */ - function getUserObj() { + public function getUserObj() { return $this->userObj; } /** + * @since 1.2 * @return string */ - function getText() { + public function getText() { return $this->text; } /** + * @since 1.24 * @return ContentHandler */ - function getContentHandler() { + public function getContentHandler() { if ( is_null( $this->contentHandler ) ) { $this->contentHandler = ContentHandler::getForModelID( $this->getModel() ); } @@ -332,9 +440,10 @@ class WikiRevision { } /** + * @since 1.21 * @return Content */ - function getContent() { + public function getContent() { if ( is_null( $this->content ) ) { $handler = $this->getContentHandler(); $this->content = $handler->unserializeContent( $this->text, $this->getFormat() ); @@ -344,9 +453,10 @@ class WikiRevision { } /** + * @since 1.21 * @return string */ - function getModel() { + public function getModel() { if ( is_null( $this->model ) ) { $this->model = $this->getTitle()->getContentModel(); } @@ -355,9 +465,10 @@ class WikiRevision { } /** + * @since 1.21 * @return string */ - function getFormat() { + public function getFormat() { if ( is_null( $this->format ) ) { $this->format = $this->getContentHandler()->getDefaultFormat(); } @@ -366,30 +477,34 @@ class WikiRevision { } /** + * @since 1.2.6 * @return string */ - function getComment() { + public function getComment() { return $this->comment; } /** + * @since 1.5.7 * @return bool */ - function getMinor() { + public function getMinor() { return $this->minor; } /** + * @since 1.12.2 * @return mixed */ - function getSrc() { + public function getSrc() { return $this->src; } /** + * @since 1.17 * @return bool|string */ - function getSha1() { + public function getSha1() { if ( $this->sha1base36 ) { return Wikimedia\base_convert( $this->sha1base36, 36, 16 ); } @@ -397,65 +512,74 @@ class WikiRevision { } /** + * @since 1.17 * @return string */ - function getFileSrc() { + public function getFileSrc() { return $this->fileSrc; } /** + * @since 1.17 * @return bool */ - function isTempSrc() { + public function isTempSrc() { return $this->isTemp; } /** + * @since 1.12.2 * @return mixed */ - function getFilename() { + public function getFilename() { return $this->filename; } /** + * @since 1.17 * @return string */ - function getArchiveName() { + public function getArchiveName() { return $this->archiveName; } /** + * @since 1.12.2 * @return mixed */ - function getSize() { + public function getSize() { return $this->size; } /** + * @since 1.12.2 * @return string */ - function getType() { + public function getType() { return $this->type; } /** + * @since 1.12.2 * @return string */ - function getAction() { + public function getAction() { return $this->action; } /** + * @since 1.12.2 * @return string */ - function getParams() { + public function getParams() { return $this->params; } /** + * @since 1.4.1 * @return bool */ - function importOldRevision() { + public function importOldRevision() { $dbw = wfGetDB( DB_MASTER ); # Sneak a single revision into place @@ -554,7 +678,11 @@ class WikiRevision { return true; } - function importLogItem() { + /** + * @since 1.12.2 + * @return bool + */ + public function importLogItem() { $dbw = wfGetDB( DB_MASTER ); $user = $this->getUserObj() ?: User::newFromName( $this->getUser() ); @@ -611,9 +739,10 @@ class WikiRevision { } /** + * @since 1.12.2 * @return bool */ - function importUpload() { + public function importUpload() { # Construct a file $archiveName = $this->getArchiveName(); if ( $archiveName ) { @@ -682,9 +811,10 @@ class WikiRevision { } /** + * @since 1.12.2 * @return bool|string */ - function downloadSource() { + public function downloadSource() { if ( !$this->config->get( 'EnableUploads' ) ) { return false; } diff --git a/includes/installer/DatabaseInstaller.php b/includes/installer/DatabaseInstaller.php index 50d73de4c846..22717fc28301 100644 --- a/includes/installer/DatabaseInstaller.php +++ b/includes/installer/DatabaseInstaller.php @@ -20,6 +20,8 @@ * @file * @ingroup Deployment */ +use Wikimedia\Rdbms\LBFactorySingle; +use Wikimedia\Rdbms\IDatabase; /** * Base class for DBMS-specific installation helper classes. diff --git a/includes/installer/DatabaseUpdater.php b/includes/installer/DatabaseUpdater.php index 6a8a99ff092c..8913c775bdb0 100644 --- a/includes/installer/DatabaseUpdater.php +++ b/includes/installer/DatabaseUpdater.php @@ -20,6 +20,7 @@ * @file * @ingroup Deployment */ +use Wikimedia\Rdbms\IDatabase; use MediaWiki\MediaWikiServices; require_once __DIR__ . '/../../maintenance/Maintenance.php'; @@ -59,6 +60,11 @@ abstract class DatabaseUpdater { */ protected $db; + /** + * @var Maintenance + */ + protected $maintenance; + protected $shared = false; /** @@ -387,7 +393,7 @@ abstract class DatabaseUpdater { * Writes the schema updates desired to a file for the DB Admin to run. * @param array $schemaUpdate */ - private function writeSchemaUpdateFile( $schemaUpdate = [] ) { + private function writeSchemaUpdateFile( array $schemaUpdate = [] ) { $updates = $this->updatesSkipped; $this->updatesSkipped = []; @@ -420,7 +426,7 @@ abstract class DatabaseUpdater { * * @param array $what What updates to perform */ - public function doUpdates( $what = [ 'core', 'extensions', 'stats' ] ) { + public function doUpdates( array $what = [ 'core', 'extensions', 'stats' ] ) { $this->db->setSchemaVars( $this->getSchemaVars() ); $what = array_flip( $what ); @@ -485,7 +491,7 @@ abstract class DatabaseUpdater { public function updateRowExists( $key ) { $row = $this->db->selectRow( 'updatelog', - # Bug 65813 + # T67813 '1 AS X', [ 'ul_key' => $key ], __METHOD__ diff --git a/includes/installer/Installer.php b/includes/installer/Installer.php index 9dc80326347b..0a2b8084e2f5 100644 --- a/includes/installer/Installer.php +++ b/includes/installer/Installer.php @@ -216,7 +216,7 @@ abstract class Installer { '_UpgradeKeySupplied' => false, '_ExistingDBSettings' => false, - // $wgLogo is probably wrong (bug 48084); set something that will work. + // $wgLogo is probably wrong (T50084); set something that will work. // Single quotes work fine here, as LocalSettingsGenerator outputs this unescaped. 'wgLogo' => '$wgResourceBasePath/resources/assets/wiki.png', 'wgAuthenticationTokenVersion' => 1, @@ -446,6 +446,8 @@ abstract class Installer { $this->parserTitle = Title::newFromText( 'Installer' ); $this->parserOptions = new ParserOptions( $wgUser ); // language will be wrong :( $this->parserOptions->setEditSection( false ); + // Don't try to access DB before user language is initialised + $this->setParserLanguage( Language::factory( 'en' ) ); } /** @@ -674,7 +676,7 @@ abstract class Installer { try { $out = $wgParser->parse( $text, $this->parserTitle, $this->parserOptions, $lineStart ); $html = $out->getText(); - } catch ( DBAccessError $e ) { + } catch ( MediaWiki\Services\ServiceDisabledException $e ) { $html = '<!--DB access attempted during parse--> ' . htmlspecialchars( $text ); if ( !empty( $this->debug ) ) { @@ -1421,6 +1423,7 @@ abstract class Installer { $wgAutoloadClasses += $data['autoload']; $hooksWeWant = isset( $wgHooks['LoadExtensionSchemaUpdates'] ) ? + /** @suppress PhanUndeclaredVariable $wgHooks is set by DefaultSettings */ $wgHooks['LoadExtensionSchemaUpdates'] : []; if ( isset( $data['globals']['wgHooks']['LoadExtensionSchemaUpdates'] ) ) { @@ -1430,7 +1433,7 @@ abstract class Installer { ); } // Unset everyone else's hooks. Lord knows what someone might be doing - // in ParserFirstCallInit (see bug 27171) + // in ParserFirstCallInit (see T29171) $GLOBALS['wgHooks'] = [ 'LoadExtensionSchemaUpdates' => $hooksWeWant ]; return Status::newGood(); @@ -1655,8 +1658,13 @@ abstract class Installer { */ protected function createMainpage( DatabaseInstaller $installer ) { $status = Status::newGood(); + $title = Title::newMainPage(); + if ( $title->exists() ) { + $status->warning( 'config-install-mainpage-exists' ); + return $status; + } try { - $page = WikiPage::factory( Title::newMainPage() ); + $page = WikiPage::factory( $title ); $content = new WikitextContent( wfMessage( 'mainpagetext' )->inContentLanguage()->text() . "\n\n" . wfMessage( 'mainpagedocfooter' )->inContentLanguage()->text() diff --git a/includes/installer/MssqlInstaller.php b/includes/installer/MssqlInstaller.php index 5e8ed3fb8c82..d6efeb2deb42 100644 --- a/includes/installer/MssqlInstaller.php +++ b/includes/installer/MssqlInstaller.php @@ -214,6 +214,7 @@ class MssqlInstaller extends DatabaseInstaller { try { $db = Database::factory( 'mssql', [ 'host' => $this->getVar( 'wgDBserver' ), + 'port' => $this->getVar( 'wgDBport' ), 'user' => $user, 'password' => $password, 'dbname' => false, diff --git a/includes/installer/MssqlUpdater.php b/includes/installer/MssqlUpdater.php index 968ee15dba48..988ec41aceea 100644 --- a/includes/installer/MssqlUpdater.php +++ b/includes/installer/MssqlUpdater.php @@ -97,6 +97,8 @@ class MssqlUpdater extends DatabaseUpdater { // 1.29 [ 'addField', 'externallinks', 'el_index_60', 'patch-externallinks-el_index_60.sql' ], + [ 'dropIndex', 'oldimage', 'oi_name_archive_name', + 'patch-alter-table-oldimage.sql' ], ]; } diff --git a/includes/installer/MysqlUpdater.php b/includes/installer/MysqlUpdater.php index d95222cbe1d1..70e790c1a614 100644 --- a/includes/installer/MysqlUpdater.php +++ b/includes/installer/MysqlUpdater.php @@ -20,6 +20,9 @@ * @file * @ingroup Deployment */ +use Wikimedia\Rdbms\Field; +use Wikimedia\Rdbms\MySQLField; +use MediaWiki\MediaWikiServices; /** * Mysql update list and mysql-specific update functions. @@ -291,9 +294,13 @@ class MysqlUpdater extends DatabaseUpdater { [ 'addField', 'change_tag', 'ct_id', 'patch-change_tag-ct_id.sql' ], [ 'addField', 'tag_summary', 'ts_id', 'patch-tag_summary-ts_id.sql' ], [ 'modifyField', 'recentchanges', 'rc_ip', 'patch-rc_ip_modify.sql' ], + [ 'addIndex', 'archive', 'usertext_timestamp', 'patch-rename-ar_usertext_timestamp.sql' ], // 1.29 [ 'addField', 'externallinks', 'el_index_60', 'patch-externallinks-el_index_60.sql' ], + [ 'dropIndex', 'user_groups', 'ug_user_group', 'patch-user_groups-primary-key.sql' ], + [ 'addField', 'user_groups', 'ug_expiry', 'patch-user_groups-ug_expiry.sql' ], + [ 'addIndex', 'image', 'img_user_timestamp', 'patch-image-user-index-2.sql' ], ]; } @@ -819,7 +826,7 @@ class MysqlUpdater extends DatabaseUpdater { /** * Set page_random field to a random value where it is equals to 0. * - * @see bug 3946 + * @see T5946 */ protected function doPageRandomUpdate() { $page = $this->db->tableName( 'page' ); @@ -851,7 +858,8 @@ class MysqlUpdater extends DatabaseUpdater { foreach ( $res as $row ) { $count = ( $count + 1 ) % 100; if ( $count == 0 ) { - wfGetLBFactory()->waitForReplication( [ 'wiki' => wfWikiID() ] ); + $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory(); + $lbFactory->waitForReplication( [ 'wiki' => wfWikiID() ] ); } $this->db->insert( 'templatelinks', [ diff --git a/includes/installer/OracleUpdater.php b/includes/installer/OracleUpdater.php index 1f0e411af08d..e262eda63512 100644 --- a/includes/installer/OracleUpdater.php +++ b/includes/installer/OracleUpdater.php @@ -121,6 +121,7 @@ class OracleUpdater extends DatabaseUpdater { // 1.29 [ 'addField', 'externallinks', 'el_index_60', 'patch-externallinks-el_index_60.sql' ], + [ 'addField', 'user_groups', 'ug_expiry', 'patch-user_groups-ug_expiry.sql' ], // KEEP THIS AT THE BOTTOM!! [ 'doRebuildDuplicateFunction' ], @@ -284,7 +285,7 @@ class OracleUpdater extends DatabaseUpdater { * * @param array $what */ - public function doUpdates( $what = [ 'core', 'extensions', 'purge', 'stats' ] ) { + public function doUpdates( array $what = [ 'core', 'extensions', 'purge', 'stats' ] ) { parent::doUpdates( $what ); $this->db->query( 'BEGIN fill_wiki_info; END;' ); diff --git a/includes/installer/PostgresInstaller.php b/includes/installer/PostgresInstaller.php index 6dfa28b21c6e..906768f48997 100644 --- a/includes/installer/PostgresInstaller.php +++ b/includes/installer/PostgresInstaller.php @@ -156,10 +156,13 @@ class PostgresInstaller extends DatabaseInstaller { try { $db = Database::factory( 'postgres', [ 'host' => $this->getVar( 'wgDBserver' ), + 'port' => $this->getVar( 'wgDBport' ), 'user' => $user, 'password' => $password, 'dbname' => $dbName, - 'schema' => $schema ] ); + 'schema' => $schema, + 'keywordTableMap' => [ 'user' => 'mwuser', 'text' => 'pagecontent' ], + ] ); $status->value = $db; } catch ( DBConnectionError $e ) { $status->fatal( 'config-connection-error', $e->getMessage() ); diff --git a/includes/installer/PostgresUpdater.php b/includes/installer/PostgresUpdater.php index 1eb3f41731df..1a7b208af000 100644 --- a/includes/installer/PostgresUpdater.php +++ b/includes/installer/PostgresUpdater.php @@ -448,6 +448,8 @@ class PostgresUpdater extends DatabaseUpdater { [ 'addPgField', 'externallinks', 'el_index_60', "BYTEA NOT NULL DEFAULT ''" ], [ 'addPgIndex', 'externallinks', 'el_index_60', '( el_index_60, el_id )' ], [ 'addPgIndex', 'externallinks', 'el_from_index_60', '( el_from, el_index_60, el_id )' ], + [ 'addPgField', 'user_groups', 'ug_expiry', "TIMESTAMPTZ NULL" ], + [ 'addPgIndex', 'user_groups', 'user_groups_expiry', '( ug_expiry )' ], ]; } @@ -494,8 +496,8 @@ class PostgresUpdater extends DatabaseUpdater { $q = <<<END SELECT attname, attnum FROM pg_namespace, pg_class, pg_attribute WHERE pg_class.relnamespace = pg_namespace.oid - AND attrelid=pg_class.oid AND attnum > 0 - AND relname=%s AND nspname=%s + AND attrelid=pg_class.oid AND attnum > 0 + AND relname=%s AND nspname=%s END; $res = $this->db->query( sprintf( $q, $this->db->addQuotes( $table ), @@ -521,9 +523,9 @@ END; $q = <<<END SELECT indkey, indrelid FROM pg_namespace, pg_class, pg_index WHERE nspname=%s - AND pg_class.relnamespace = pg_namespace.oid - AND relname=%s - AND indexrelid=pg_class.oid + AND pg_class.relnamespace = pg_namespace.oid + AND relname=%s + AND indexrelid=pg_class.oid END; $res = $this->db->query( sprintf( @@ -549,8 +551,8 @@ END; $query = <<<END SELECT attname FROM pg_class, pg_attribute WHERE attrelid=$relid - AND attnum=%d - AND attrelid=pg_class.oid + AND attnum=%d + AND attrelid=pg_class.oid END; $r2 = $this->db->query( sprintf( $query, $rid ) ); if ( !$r2 ) { @@ -570,8 +572,8 @@ END; $q = <<<END SELECT confdeltype FROM pg_constraint, pg_namespace WHERE connamespace=pg_namespace.oid - AND nspname=%s - AND conname=%s; + AND nspname=%s + AND conname=%s; END; $r = $this->db->query( sprintf( @@ -592,8 +594,8 @@ END; $q = <<<END SELECT definition FROM pg_rules WHERE schemaname = %s - AND tablename = %s - AND rulename = %s + AND tablename = %s + AND rulename = %s END; $r = $this->db->query( sprintf( @@ -979,10 +981,10 @@ END; protected function rebuildTextSearch() { if ( $this->updateRowExists( 'patch-textsearch_bug66650.sql' ) ) { - $this->output( "...bug 66650 already fixed or not applicable.\n" ); + $this->output( "...T68650 already fixed or not applicable.\n" ); return; }; $this->applyPatch( 'patch-textsearch_bug66650.sql', false, - 'Rebuilding text search for bug 66650' ); + 'Rebuilding text search for T68650' ); } } diff --git a/includes/installer/SqliteInstaller.php b/includes/installer/SqliteInstaller.php index c5c4a7cc1486..0fe7068ba070 100644 --- a/includes/installer/SqliteInstaller.php +++ b/includes/installer/SqliteInstaller.php @@ -244,9 +244,9 @@ class SqliteInstaller extends DatabaseInstaller { $sql = <<<EOT CREATE TABLE IF NOT EXISTS objectcache ( - keyname BLOB NOT NULL default '' PRIMARY KEY, - value BLOB, - exptime TEXT + keyname BLOB NOT NULL default '' PRIMARY KEY, + value BLOB, + exptime TEXT ) EOT; $conn->query( $sql ); diff --git a/includes/installer/SqliteUpdater.php b/includes/installer/SqliteUpdater.php index 32068e65e8de..dcd66ddeeeb5 100644 --- a/includes/installer/SqliteUpdater.php +++ b/includes/installer/SqliteUpdater.php @@ -161,6 +161,8 @@ class SqliteUpdater extends DatabaseUpdater { // 1.29 [ 'addField', 'externallinks', 'el_index_60', 'patch-externallinks-el_index_60.sql' ], + [ 'addField', 'user_groups', 'ug_expiry', 'patch-user_groups-ug_expiry.sql' ], + [ 'addIndex', 'image', 'img_user_timestamp', 'patch-image-user-index-2.sql' ], ]; } diff --git a/includes/installer/WebInstaller.php b/includes/installer/WebInstaller.php index c08212e39035..c94f0bfaa4e4 100644 --- a/includes/installer/WebInstaller.php +++ b/includes/installer/WebInstaller.php @@ -1083,7 +1083,7 @@ class WebInstaller extends Installer { foreach ( $varNames as $name ) { $value = $this->request->getVal( $prefix . $name ); - // bug 30524, do not trim passwords + // T32524, do not trim passwords if ( stripos( $name, 'password' ) === false ) { $value = trim( $value ); } diff --git a/includes/installer/WebInstallerName.php b/includes/installer/WebInstallerName.php index e6deed52b617..81a107dea30f 100644 --- a/includes/installer/WebInstallerName.php +++ b/includes/installer/WebInstallerName.php @@ -251,7 +251,7 @@ class WebInstallerName extends WebInstallerPage { $retVal = false; } // If they asked to subscribe to mediawiki-announce but didn't give - // an e-mail, show an error. Bug 29332 + // an e-mail, show an error. T31332 if ( !$email && $this->getVar( '_Subscribe' ) ) { $this->parent->showError( 'config-subscribe-noemail' ); $retVal = false; diff --git a/includes/installer/WebInstallerOutput.php b/includes/installer/WebInstallerOutput.php index 62fe7852b74a..e4eb255bbd52 100644 --- a/includes/installer/WebInstallerOutput.php +++ b/includes/installer/WebInstallerOutput.php @@ -299,9 +299,9 @@ class WebInstallerOutput { <div id="mw-panel"> <div class="portal" id="p-logo"> - <a style="background-image: url(images/installer-logo.png);" - href="https://www.mediawiki.org/" - title="Main Page"></a> + <a style="background-image: url(images/installer-logo.png);" + href="https://www.mediawiki.org/" + title="Main Page"></a> </div> <?php $message = wfMessage( 'config-sidebar' )->plain(); diff --git a/includes/installer/WebInstallerPage.php b/includes/installer/WebInstallerPage.php index 2ab055464dce..3aad6f879395 100644 --- a/includes/installer/WebInstallerPage.php +++ b/includes/installer/WebInstallerPage.php @@ -92,7 +92,7 @@ abstract class WebInstallerPage { } if ( $continue ) { - // Fake submit button for enter keypress (bug 26267) + // Fake submit button for enter keypress (T28267) // Messages: config-continue, config-restart, config-regenerate $s .= Xml::submitButton( wfMessage( "config-$continue" )->text(), @@ -133,7 +133,7 @@ abstract class WebInstallerPage { * @return string */ public function getName() { - return str_replace( 'WebInstaller', '', get_class( $this ) ); + return str_replace( 'WebInstaller', '', static::class ); } /** diff --git a/includes/installer/WebInstallerUpgrade.php b/includes/installer/WebInstallerUpgrade.php index 72973e7d2e14..bf732a4b3e15 100644 --- a/includes/installer/WebInstallerUpgrade.php +++ b/includes/installer/WebInstallerUpgrade.php @@ -67,7 +67,7 @@ class WebInstallerUpgrade extends WebInstallerPage { if ( $result ) { // If they're going to possibly regenerate LocalSettings, we - // need to create the upgrade/secret keys. Bug 26481 + // need to create the upgrade/secret keys. T28481 if ( !$this->getVar( '_ExistingDBSettings' ) ) { $this->parent->generateKeys(); } diff --git a/includes/installer/i18n/ast.json b/includes/installer/i18n/ast.json index c591f1eba984..d47334cca994 100644 --- a/includes/installer/i18n/ast.json +++ b/includes/installer/i18n/ast.json @@ -89,6 +89,7 @@ "config-db-name": "Nome de base de datos:", "config-db-name-help": "Escueye un nome qu'identifique la to wiki. Nun tien de contener espacios. \nSi tas utilizando agospiamientu web compartíu, el to provisor va date un nome específicu de base de datos por que lu utilices, o bien va dexate crear bases de datos al traviés d'un panel de control.", "config-db-name-oracle": "Esquema de la base de datos:", + "config-db-account-oracle-warn": "Hai tres escenarios compatibles pa la instalación de Oracle como motor de base de datos:\n\nSi desees crear una cuenta de base de datos como parte del procesu d'instalación, por favor apurre una cuenta con rol SYSDBA como cuenta de base de datos pa la instalación y especifica les credenciales que quies tener pal accesu a la web a la cuenta; d'otra miente, puedes crear manualmente la cuenta d'accesu a la web y suministrar namái esa cuenta (si tien los permisos necesarios pa crear los oxetos d'esquema) o dar dos cuentes distintos, una con privilexos de creación y otra con accesu acutáu a la web\n\nLa secuencia de comandos (script) pa crear una cuenta colos privilexos necesarios puede atopase nel direutoriu \"maintenance/oracle/\" d'esta instalación. Ten en cuenta qu'utilizar una cuenta acutada va desactivar toles capacidaes de caltenimientu cola cuenta predeterminada.", "config-db-install-account": "Cuenta d'usuariu pa la instalación", "config-db-username": "Nome d'usuariu de base de datos:", "config-db-password": "Contraseña de base de datos:", @@ -100,6 +101,12 @@ "config-db-wiki-help": "Escribe'l nome d'usuariu y la contraseña que se van utilizar p'aportar a la base de datos mientres la operación normal de la wiki.\nSi esta cuenta nun esiste y la cuenta d'instalación tien permisos bastante, va crease esta cuenta d'usuariu colos mínimos permisos necesarios pa operar normalmente la wiki.", "config-db-prefix": "Prefixu de tables de la base de datos:", "config-db-prefix-help": "Si precises compartir una base de datos ente múltiples wikis, o ente MediaWiki y otra aplicación web, puedes optar por amestar un prefixu a tolos nomes de tabla pa evitar conflictos.\nNun utilices espacios.\n\nDe normal déxase esti campu vacío.", + "config-mysql-old": "Precísase MySQL $1 o posterior. Tienes $2.", + "config-db-port": "Puertu de la base de datos:", + "config-db-schema": "Esquema pa MediaWiki:", + "config-db-schema-help": "Esti esquema de vezu va tar bien.\nCamúdalos solo si sabes que lo precises.", + "config-pg-test-error": "Nun puede coneutase cola base de datos <strong>$1</strong>: $2", + "config-sqlite-dir": "Direutoriu de datos SQLite:", "config-type-mysql": "MySQL (o compatible)", "config-type-mssql": "Microsoft SQL Server", "config-invalid-db-type": "Triba non válida de base de datos.", @@ -125,5 +132,5 @@ "config-help": "Ayuda", "config-nofile": "Nun pudo atopase'l ficheru \"$1\". ¿Desaniciose?", "mainpagetext": "<strong>Instalóse MediaWiki.</strong>", - "mainpagedocfooter": "Consulta [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents] pa saber cómo usar el software wiki.\n\n== Primeros pasos ==\n\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings Llista de les opciones de configuración]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ EMF de MediaWiki]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Llista de corréu de llanzamientos de MediaWiki]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation#Translation_resources Llocaliza MediaWiki na to llingua]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Combating_spam Depriende como combatir la puxarra na to wiki]" + "mainpagedocfooter": "Consulta la [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents Guía del usuariu] pa saber cómo usar el software wiki.\n\n== Primeros pasos ==\n\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings Llista de les opciones de configuración]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ EMF de MediaWiki]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Llista de corréu de llanzamientos de MediaWiki]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation#Translation_resources Llocaliza MediaWiki na to llingua]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Combating_spam Depriende como combatir la puxarra na to wiki]" } diff --git a/includes/installer/i18n/be-tarask.json b/includes/installer/i18n/be-tarask.json index c17de3f2eb6a..1335b9c528bc 100644 --- a/includes/installer/i18n/be-tarask.json +++ b/includes/installer/i18n/be-tarask.json @@ -306,6 +306,7 @@ "config-install-subscribe-fail": "Немагчыма падпісацца на «mediawiki-announce»: $1", "config-install-subscribe-notpossible": "cURL не ўсталяваны, <code>allow_url_fopen</code> недаступны.", "config-install-mainpage": "Стварэньне галоўнай старонкі са зьместам па змоўчваньні", + "config-install-mainpage-exists": "Галоўная старонка ўжо існуе, прапускаем", "config-install-extension-tables": "Стварэньне табліцаў для ўключаных пашырэньняў", "config-install-mainpage-failed": "Немагчыма ўставіць галоўную старонку: $1", "config-install-done": "<strong>Віншуем!</strong>\nВы ўсталявалі MediaWiki.\n\nПраграма ўсталяваньня стварыла файл <code>LocalSettings.php</code>.\nЁн утрымлівае ўсе Вашыя налады.\n\nВам неабходна загрузіць яго і захаваць у карэнную дырэкторыю Вашай вікі (у тую ж самую дырэкторыю, дзе знаходзіцца index.php). Загрузка павінна пачацца аўтаматычна.\n\nКалі загрузка не пачалася, ці Вы яе адмянілі, Вы можаце перазапусьціць яе націснуўшы на спасылку ніжэй:\n\n$3\n\n<strong>Заўвага</strong>: калі Вы гэтага ня зробіце зараз, то створаны файл ня будзе даступны Вам потым, калі Вы выйдзеце з праграмы ўсталяваньня безь яго загрузкі.\n\nКалі Вы гэта зробіце, Вы можаце <strong>[$2 ўвайсьці ў Вашую вікі]</strong>.", diff --git a/includes/installer/i18n/bg.json b/includes/installer/i18n/bg.json index 4a1ed6595460..7a03c87d094f 100644 --- a/includes/installer/i18n/bg.json +++ b/includes/installer/i18n/bg.json @@ -65,6 +65,7 @@ "config-apc": "[http://www.php.net/apc APC] е инсталиран", "config-apcu": "[http://www.php.net/apc APC] е инсталиран", "config-wincache": "[http://www.iis.net/download/WinCacheForPhp WinCache] е инсталиран", + "config-no-cache-apcu": "<strong>Внимание:</strong> [http://www.php.net/apcu APCu], [http://xcache.lighttpd.net/ XCache] и [http://www.iis.net/download/WinCacheForPhp WinCache] не могат да бъдат открити.\nКеширането на обекти не е активирано.", "config-mod-security": "<strong>Предупреждение:</strong> [http://modsecurity.org/ mod_security]/mod_security2 е включено на вашия уеб сървър. Много от обичайните му конфигурации пораждат проблеми с МедияУики и друг софтуер, който позволява публикуване на произволно съдържание.\nАко е възможно, моля изключете го. В противен случай се обърнете към [http://modsecurity.org/documentation/ документацията на mod_security] или се свържете с поддръжката на хостинга си, ако се сблъскате със случайни грешки.", "config-diff3-bad": "GNU diff3 не беше намерен.", "config-git": "Налична е системата за контрол на версиите Git: <code>$1</code>.", @@ -84,10 +85,12 @@ "config-db-host": "Хост на базата от данни:", "config-db-host-help": "Ако базата от данни е на друг сървър, в кутията се въвежда името на хоста или IP адреса.\n\nАко се използва споделен уеб хостинг, доставчикът на услугата би трябвало да е предоставил в документацията си коректния хост.\n\nАко инсталацията протича на Windows-сървър и се използва MySQL, използването на \"localhost\" може да е неприемливо. В такива случаи се използва \"127.0.0.1\" за локален IP адрес.\n\nПри използване на PostgreSQL, това поле се оставя празно, за свързване чрез Unix socket.", "config-db-host-oracle": "TNS на базата данни:", + "config-db-host-oracle-help": "Въведете валидно [http://download.oracle.com/docs/cd/B28359_01/network.111/b28317/tnsnames.htm Local Connect Name]; файлът tnsnames.ora трябва да бъде видим за инсталацията.<br />Ако използвате клиентска библиотека версия 10g или по-нова можете да използвате метода [http://download.oracle.com/docs/cd/E11882_01/network.112/e10836/naming.htm Easy Connect].", "config-db-wiki-settings": "Идентифициране на това уики", "config-db-name": "Име на базата от данни:", "config-db-name-help": "Избира се име, което да идентифицира уикито.\nТо не трябва да съдържа интервали.\n\nАко се използва споделен хостинг, доставчикът на услугата би трябвало да е предоставил или име на базата от данни, която да бъде използвана, или да позволява създаването на бази от данни чрез контролния панел.", "config-db-name-oracle": "Схема на базата от данни:", + "config-db-account-oracle-warn": "Има три поддържани сценария за инсталиране на Oracle като бекенд база данни:\n\nАко искате да създадете профил в базата данни като част от процеса на инсталиране, моля, посочете профил със SYSDBA като профил в базата данни за инсталиране и посочете желаните данни за влизане (име и парола) за профил с уеб достъп; в противен случай можете да създадете профил с уеб достъп ръчно и предоставите само него (ако той има необходимите права за създаване на схематични обекти), или да предоставите два различни профила - един с привилегии за създаване на обекти, и друг - с ограничения за уеб достъп.\n\nСкрипт за създаването на профил с необходимите привилегии може да се намери в папката \"maintenance/oracle/\" на тази инсталация. Имайте в предвид, че използването на ограничен профил ще деактивира всички възможности за обслужване на профила по подразбиране.", "config-db-install-account": "Потребителска сметка за инсталацията", "config-db-username": "Потребителско име за базата от данни:", "config-db-password": "Парола за базата от данни:", @@ -152,18 +155,21 @@ "config-db-web-account": "Сметка за уеб достъп до базата от данни", "config-db-web-help": "Избиране на потребителско име и парола, които уеб сървърът ще използва да се свързва с базата от данни при обичайната работа на уикито.", "config-db-web-account-same": "Използване на същата сметка като при инсталацията.", - "config-db-web-create": "Създаване на сметката ако все още не съществува", + "config-db-web-create": "Създаване на сметката, ако все още не съществува", "config-db-web-no-create-privs": "Посочената сметка за инсталацията не разполага с достатъчно права за създаване на нова сметка.\nНеобходимо е посочената сметка вече да съществува.", "config-mysql-engine": "Хранилище на данни:", "config-mysql-innodb": "InnoDB", "config-mysql-myisam": "MyISAM", "config-mysql-myisam-dep": "'''Предупреждение''': Избрана е MyISAM като система за складиране в MySQL, която не се препоръчва за използване с МедияУики, защото:\n* почти не поддържа паралелност заради заключване на таблиците\n* е по-податлива на повреди в сравнение с други системи\n* кодът на МедияУики не винаги поддържа MyISAM коректно\n\nАко инсталацията на MySQL поддържа InnoDB, силно е препоръчително да се използва тя.\nАко инсталацията на MySQL не поддържа InnoDB, вероятно е време за обновяване.", + "config-mysql-only-myisam-dep": "<strong>Внимание:</strong> MyISAM e единственият наличен на тази машина тип на таблиците за MySQL и не е препоръчителен за употреба при МедияУики защото:\n* има слаба поддръжка на конкурентност на заявките, поради закючването на таблиците\n* е много по-податлив на грешки в базите от данни от другите типове таблици\n* кодът на МедияУики не винаги работи с MyISAM както трябва\n\nВашият MySQL не поддържа InnoDB, така че може би е дошло време за актуализиране.", "config-mysql-engine-help": "'''InnoDB''' почти винаги е най-добрата възможност заради навременната си поддръжка.\n\n'''MyISAM''' може да е по-бърза при инсталации с един потребител или само за четене.\nБазите от данни MyISAM се повреждат по-често от InnoDB.", "config-mysql-charset": "Набор от символи в базата от данни:", "config-mysql-binary": "Бинарен", "config-mysql-utf8": "UTF-8", "config-mysql-charset-help": "В '''бинарен режим''' МедияУики съхранява текстовете в UTF-8 в бинарни полета в базата от данни.\nТова е по-ефективно от UTF-8 режима на MySQL и позволява използването на пълния набор от символи в Уникод.\n\nВ '''UTF-8 режим''' MySQL ще знае в кой набор от символи са данните от уикито и ще може да ги показва и променя по подходящ начин, но няма да позволява складиране на символи извън [https://en.wikipedia.org/wiki/Mapping_of_Unicode_character_planes Основния многоезичен набор].", "config-mssql-auth": "Тип на удостоверяването:", + "config-mssql-install-auth": "Изберете начин за удостоверяване, който ще бъде използван за връзка с базата от данни по време на инсталацията.\nАко изберете \"{{int:config-mssql-windowsauth}}\", ще се използват идентификационните данни на потребителя под който работи уеб сървъра.", + "config-mssql-web-auth": "Изберете начина за удостоверяване, който ще се използва от уеб сървъра за връзка със сървъра за бази от данни по време на нормалните операции на уикито.\nАко изберете \"{{int:config-mssql-windowsauth}}\", ще се използват идентификационните данни на потребителя под който работи уеб сървъра.", "config-mssql-sqlauth": "Удостоверяване чрез SQL Server", "config-mssql-windowsauth": "Удостоверяване чрез Windows", "config-site-name": "Име на уикито:", @@ -295,6 +301,7 @@ "config-install-subscribe-fail": "Невъзможно беше абонирането за mediawiki-announce: $1", "config-install-subscribe-notpossible": "не е инсталиран cURL и <code>allow_url_fopen</code> не е налична.", "config-install-mainpage": "Създаване на Началната страница със съдържание по подразбиране", + "config-install-mainpage-exists": "Главната страница вече съществува, преминаване напред", "config-install-extension-tables": "Създаване на таблици за включените разширения", "config-install-mainpage-failed": "Вмъкването на Началната страница беше невъзможно: $1", "config-install-done": "<strong>Поздравления!</strong>\nИнсталирането на МедияУики приключи успешно.\n\nИнсталаторът създаде файл <code>LocalSettings.php</code>.\nТой съдържа всичката необходима основна конфигурация на уикито.\n\nНеобходимо е той да бъде изтеглен и поставен в основната директория на уикито (директорията, в която е и index.php). Изтеглянето би трябвало да започне автоматично.\n\nАко изтеглянето не започне автоматично или е било прекратено, файлът може да бъде изтеглен чрез щракване на препратката по-долу:\n\n$3\n\n<strong>Забележка:</strong> Ако това не бъде извършено сега, генерираният конфигурационен файл няма да е достъпен на по-късен етап ако не бъде изтеглен сега или инсталацията приключи без изтеглянето му.\n\nКогато файлът вече е в основната директория, <strong>[$2 уикито ще е достъпно на този адрес]</strong>.", diff --git a/includes/installer/i18n/bn.json b/includes/installer/i18n/bn.json index f9a8da863e90..34efde4bc581 100644 --- a/includes/installer/i18n/bn.json +++ b/includes/installer/i18n/bn.json @@ -7,7 +7,8 @@ "Tauhid16", "Aftabuzzaman", "Hasive", - "আজিজ" + "আজিজ", + "Elias Ahmmad" ] }, "config-desc": "মিডিয়াউইকির জন্য ইন্সটলার", @@ -137,6 +138,7 @@ "config-install-user-alreadyexists": "ব্যবহারকারী \"$1\" ইতিমধ্যে বিদ্যমান আছে", "config-install-tables": "টেবিল তৈরি", "config-install-keys": "গোপন কি তৈরি", + "config-install-mainpage-exists": "প্রধান পাতা ইতিমধ্যেই বিদ্যমান, এডিয়ে যাওয়া হচ্ছে", "config-help": "সাহায্য", "config-help-tooltip": "প্রসারিত করতে ক্লিক করুন", "mainpagetext": "<strong>মিডিয়াউইকি ইনস্টল করা হয়েছে।</strong>", diff --git a/includes/installer/i18n/br.json b/includes/installer/i18n/br.json index 1211929fe18f..64f6197d19c6 100644 --- a/includes/installer/i18n/br.json +++ b/includes/installer/i18n/br.json @@ -15,7 +15,7 @@ "config-localsettings-upgrade": "Kavet ez eus bet ur restr <code>LocalSettings.php</code>.\nEvit hizivaat ar staliadur-se, merkit an talvoud <code>$wgUpgradeKey</code> er voest dindan.\nE gavout a rit e <code>LocalSettings.php</code>.", "config-localsettings-cli-upgrade": "Dinoet ez eus bet ur restr <code>LocalSettings.php</code>.\nEvit lakaat ar staliadur-mañ a-live, implijit <code>update.php</code> e plas", "config-localsettings-key": "Alc'hwez hizivaat :", - "config-localsettings-badkey": "Direizh eo an alc'hwez merket ganeoc'h", + "config-localsettings-badkey": "Direizh eo an alc'hwez hizivaat lakaet ganeoc'h.", "config-upgrade-key-missing": "Kavet ez eus bet ur staliadur kent eus MediaWiki.\nEvit hizivaat ar staliadur-se, ouzhpennit al linenn da-heul e traoñ ho restr <code>LocalSettings.php</code>:\n\n$1", "config-localsettings-incomplete": "Diglok e seblant bezañ ar restr <code>LocalSettings.php</code> zo anezhi dija.\nAn argemmenn $1 n'eo ket termenet.\nKemmit <code>LocalSettings.php</code> evit ma vo termenet an argemmenn-se, ha klikit war « {{int:Config-continue}} ».", "config-localsettings-connection-error": "C'hoarvezet ez eus ur fazi en ur gevreañ ouzh an diaz roadennoù oc'h implijout an arventennoù diferet e <code>LocalSettings.php</code>. Reizhit an arventennoù-se hag esaeit en-dro.\n\n$1", @@ -53,33 +53,45 @@ "config-env-php": "Staliet eo PHP $1.", "config-env-hhvm": "HHVM $1 zo staliet.", "config-unicode-using-intl": "Oc'h implijout [http://pecl.php.net/intl an astenn PECL intl] evit ar reolata Unicode.", - "config-unicode-pure-php-warning": "'''Diwallit''' : N'haller ket kaout an [http://pecl.php.net/intl intl PECL astenn] evit merañ reoladur Unicode, a zistro d'ar stumm gorrek emplementet e-PHP.\nMa lakait da dreiñ ul lec'hienn darempredet-stank e vo mat deoc'h lenn un tammig bihan diwar-benn se war [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations Unicode normalization]. (e saozneg)", + "config-unicode-pure-php-warning": "<strong>Diwallit</strong> : N'haller ket ober gant an [http://pecl.php.net/intl intl astenn PECL] evit merañ reoladur Unicode; distreiñ d'ar stumm gorrek emplementet e PHP-rik.\nMa rit war-dro ul lec'hienn darempredet-stank e vo mat deoc'h lenn un tammig bihan diwar-benn se war [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations reolennadur Unicode].", "config-unicode-update-warning": "'''Diwallit''': ober a ra stumm staliet endalc'her skoueriekaat Unicode gant ur stumm kozh eus [http://site.icu-project.org/ levraoueg meziantoù ar raktres ICU].\nDleout a rafec'h [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations hizivaat] ma seblant deoc'h bezañ pouezus ober gant Unicode.", - "config-no-db": "N'eus ket bet gallet kavout ur sturier diazoù roadennoù a zere ! Ret eo deoc'h staliañ ur sturier diazoù roadennoù evit PHP.\nSkoret eo an diazoù roadennoù da-heul : $1.\n\nMa rit gant un herberc'hiañ kenrannet, goulennit digant ho herberc'hier staliañ ur sturier diaz roadennoù azas.\nMa kempunit PHP c'hwi hoc'h-unan, adkeflugnit-eñ en ur weredekaat un arval diaz roadennoù, da skouer en ur ober gant <code>./configure --mysql</code>.\nM'hoc'h eus staliet PHP adalek ur pakad Debian pe Ubuntu, eo ret deoc'h staliañ ar vodulenn php5-mysql ivez.", + "config-no-db": "N'eus ket bet gallet kavout ur sturier diazoù roadennoù a zere ! Ret eo deoc'h staliañ ur sturier diazoù roadennoù evit PHP.\nSkoret eo {{PLURAL:$2|ar seurt|ar seurtoù}} diazoù roadennoù da-heul : $1.\n\nMard eo bet kempunet PHP ganeoc'h-c'hwi hoc'h-unan, adkeflugnit-eñ en ur weredekaat un arval diaz roadennoù, da skouer en ur ober gant <code>/configure --with-mysqli</code>.\nM'hoc'h eus staliet PHP adalek ur pakad Debian pe Ubuntu, eo ret deoc'h staliañ, da skouer, ar pakad <code>php5-mysql</code> ivez.", + "config-outdated-sqlite": "<strong>Taolit pled :</strong> ober a rit gant SQLite $1, hag a zo izeloc'h eget ar stumm $2 ret bihanañ. Ne vo ket posupl ober gant SQLite.", "config-no-fts3": "'''Diwallit ''': Kempunet eo SQLite hep ar [//sqlite.org/fts3.html vodulenn FTS3]; ne vo ket posupl ober gant an arc'hwelioù klask er staliadur-mañ", + "config-pcre-old": "<strong>Fazo groñs :</strong> rekis eo ober gant PCRE $1 pe nevesoc'h.\nLiammet eo ho PHP binarel gant PCRE $2.\n[https://www.mediawiki.org/wiki/Manual:Errors_and_symptoms/PCRE Gouzout hiroc'h].", "config-pcre-no-utf8": "'''Fazi groñs ''': evit doare eo bet kempunet modulenn PCRE PHP hep ar skor PCRE_UTF8.\nEzhomm en deus MediaWiki eus UTF-8 evit mont plaen en-dro.", "config-memory-raised": "<code>memory_limit</code> ar PHP zo $1, kemmet e $2.", "config-memory-bad": "'''Diwallit :''' Da $1 emañ arventenn <code>memory_limit</code> PHP.\nRe izel eo moarvat.\nMarteze e c'hwito ar staliadenn !", "config-xcache": "Staliet eo [http://xcache.lighttpd.net/ XCache]", "config-apc": "Staliet eo [http://www.php.net/apc APC]", + "config-apcu": "Staliet eo [http://www.php.net/apcu APCu]", "config-wincache": "Staliet eo [http://www.iis.net/download/WinCacheForPhp WinCache]", + "config-no-cache-apcu": "<strong>Taolit pled :</strong> N'eus ket bet gallet kavout [http://www.php.net/apcu APCu], [http://xcache.lighttpd.net/ XCache] pe [http://www.iis.net/download/WinCacheForPhp WinCache].\nN'eo ket gweredekaet ar c'hrubuilhañ traezoù.", + "config-mod-security": "<strong>Taolit pled :</strong> Gweredekaet eo [http://modsecurity.org/ mod_security]/mod_security2 gant ho servijer web. Ma n'eo ket kfluniet mat e c'hall tegas trubuilhoù da MediaWiki ha meziantoù all a aotre implijerien da ouzhpennañ danvez evel ma karont.\nE kement ha m'eo posupl e tlefe bezañ diweredekaet. A-hend-all, sellit ouzh [http://modsecurity.org/documentation/ mod_security an teuliadur] pe kit e darempred gant skoazell ho herberc'hier m'en em gavit gant fazioù dargouezhek.", "config-diff3-bad": "N'eo ket bet kavet GNU diff3.", + "config-git": "Kavet eo bet ar meziant kontrolliñ adstummoù Git : <code>$1</code>.", + "config-git-bad": "N'eo ket bet kavet ar meziant kontrolliñ stummoù Git.", "config-imagemagick": "ImageMagick kavet : <code>$1</code>.\nGweredekaet e vo ar bihanaat skeudennoù ma vez gweredekaet ganeoc'h ar pellgargañ restroù.", "config-gd": "Kavet eo bet al levraoueg c'hrafek GD enframmet.\nGweredekaet e vo ar bihanaat skeudennoù ma vez gweredekaet an enporzhiañ restroù.", "config-no-scaling": "N'eus ket bet gallet kavout al levraoueg GD pe ImageMagick.\nDiweredekaet e vo ar bihanaat skeudennoù.", "config-no-uri": "'''Fazi :''' N'eus ket tu da anavezout URI ar skript red.\nStaliadur nullet.", + "config-no-cli-uri": "<strong>Diwallit :</strong> N'eus bet spisaet <code>--scriptpath</code> ebet, graet e vo, dre ziouer, gant : <code>$1</code>.", "config-using-server": "Oc'h implijout an anv servijer \"<nowiki>$1</nowiki>\".", "config-using-uri": "Oc'h implijout ar servijour URL \"<nowiki>$1$2</nowiki>\".", "config-uploads-not-safe": "'''Diwallit :'''Bresk eo ho kavlec'h pellgargañ dre ziouer <code>$1</code> rak gallout a ra erounit ne vern pe skript.\nha pa vefe gwiriet gant MediaWiki an holl restroù pellgarget eo erbedet-groñs da [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Security#Upload_security serriñ ar breskter surentez-mañ] a-rao gweredekaat ar pellgargañ.", + "config-no-cli-uploads-check": "<strong>Diwallit :</strong> N'eo ket bet gwiriet ho kavlec'h enporzhiañ dre ziouer (<code>$1</code>) e-keñver breskted erounezadur skriptoù tidek e-pad staliadur CLI.", "config-brokenlibxml": "Ur meskad stummoù PHP ha libxml2 dreinek a vez implijet gant ho reizhiad. Gallout a ra breinañ ar roadennoù e MediaWiki hag en arloadoù web all.\nHizivait da libxml2 2.7.3 pe nevesoc'h ([https://bugs.php.net/bug.php?id=45996 draen renablet gant PHP]).\nStaliadur c'hwitet.", + "config-suhosin-max-value-length": "Staliet eo Suhosin ha bevennin a ra <code>hirder</code> an arventenn GET da $1 okted.\nParzh ResourceLoader Mediawiki a zoujo d'ar vevenn-se met se a zisteray ar varregezh. \nMa c'hallit e tlefec'h spisaat <code>suhosin.get.max_value_length</code> da 1024 pe uheloc'h e <code>php.ini</code>, ha merkañ <code>$wgResourceLoaderMaxQueryLength</code> gant an hevelep talvoud e <code>LocalSettings.php</code>.", "config-db-type": "Doare an diaz roadennoù :", "config-db-host": "Anv implijer an diaz roadennoù :", "config-db-host-help": "M'emañ ho servijer roadennoù war ur servijer disheñvel, merkit amañ anv an ostiz pe ar chomlec'h IP.\n\nMa rit gant un herberc'hiañ kenrannet, e tlefe ho herberc'hier bezañ pourchaset deoc'h an anv ostiz reizh en teulioù titouriñ.\n\nM'emaoc'h o staliañ ur servijer Windows ha ma rit gant MySQL, marteze ne'z aio ket en-dro \"localhost\" evel anv servijer. Ma ne dro ket, klaskit ober gant \"127.0.0.1\" da chomlec'h IP lechel.", "config-db-host-oracle": "TNS an diaz roadennoù :", + "config-db-host-oracle-help": "Merkit un [http://download.oracle.com/docs/cd/B28359_01/network.111/b28317/tnsnames.htm anv kevreañ lec'hel] reizh; dleout a ra ur restr tnsnames.ora bezañ hewel e-pad ar staliadur.<br /> Ma rit gant al levraouegoù arval 10g pe nevesoc'h e c'hallit ivez ober gant an hentenn envel [http://download.oracle.com/docs/cd/E11882_01/network.112/e10836/naming.htm Easy Connect].", "config-db-wiki-settings": "Anavezout ar wiki-mañ", "config-db-name": "Anv an diaz roadennoù :", "config-db-name-help": "Dibabit un anv evit ho wiki.\nNa lakait ket a esaouennoù ennañ.\n\nMa ri gant un herberc'hiañ kenrannet e vo pourchaset deoc'h un anv diaz roadennoù dibar da vezañ graet gantañ gant ho herberc'hier pe e lezo ac'hanoc'h da grouiñ diazoù roadennoù dre ur banell gontrolliñ.", "config-db-name-oracle": "Brastres diaz roadennoù :", + "config-db-account-oracle-warn": "Skoret ez eus tri doare evit staliañ Oracle da v/backend diaz roadennoù :\n\nMar fell deoc'h krouiñ ur gont diaz roadennoù e-ser an argerzh staliañ eo rekis pourchas ur gont gant ur roll SYSDBA evel kont diaz roadennoù evit ar staliañ, ha spisaat an titouroù anaout a fell deoc'h evit ar gont moned ouzh ar web. A-hend-all, e c'hallit krouiñ ar gont moned ouzh ar web gant an dorn ha pourchas hepken ar gont-se (ma'z eus bet ranket diskouez aotreoù ret evit krouiñ traezoù ar brastres) pe pourveziñ div gont disheñvel, unan gant dreistwirioù krouiñ hag eben, gant gwirioù strishaet, evit moned ouzh ar web.\n\nGallout a reer kaout ar skript evit kouiñ ur gont a zo rekis dreistwirioù eviti e kavlec'h \"trezalc'h/oracle/\" ar staliadur-mañ. Na zisoñjit ket e vo diweredekaet holl varregezhioù trezalc'h ar gont dre ziouer ma rit gant ur gont strishaet he gwirioù.", "config-db-install-account": "Kont implijer evit ar staliadur", "config-db-username": "Anv implijer an diaz roadennoù :", "config-db-password": "Ger-tremen an diaz roadennoù :", @@ -88,13 +100,16 @@ "config-db-install-help": "Merkañ anv an implijer hag ar ger-tremen a vo implijet evit kevreañ ouzh an diaz roadennoù e-pad an argerzh staliañ.", "config-db-account-lock": "Implijout ar memes anv implijer ha ger-tremen e-kerzh oberiadurioù boutin", "config-db-wiki-account": "Kont implijer evit oberiadurioù boutin", + "config-db-wiki-help": "Merkañ an anv-implijer hag ar ger-tremen a vo implijet evit kevreañ ouzh an diaz roadennoù e-pad oberiadurioù normal ar wiki.\nMa n'eus ket eus ar gont ha ma'z eus gwirioù a-walc'h gant ar gont staliañ, e vo krouet ar gont implijer-mañ gant al live gwirioù rekis izelañ evit gallout lakaat ar wiki da vont en-dro.", "config-db-prefix": "Rakrann taolennoù an diaz roadennoù :", + "config-db-prefix-help": "Mard eo ret deoc'h rannañ un diaz roadennoù gant meur a wiki, pe etre MediaWiki hag un arload benak all e c'hallit dibab ouzhpennañ ur rakger da holl anvioù an taolennoù kuit na vije tabutoù.\nArabat ober gant esaouennoù.\n\nPeurliesañ e vez laosket goullo ar vaezienn-mañ.", "config-mysql-old": "Rekis eo MySQL $1 pe ur stumm nevesoc'h; ober a rit gant $2.", "config-db-port": "Porzh an diaz roadennoù :", "config-db-schema": "Brastres evit MediaWiki", "config-db-schema-help": "Peurliesañ e vo digudenn ar chema-mañ.\nArabat cheñch anezho ma n'hoc'h eus ket ezhomm d'en ober.", "config-pg-test-error": "N'haller ket kevreañ ouzh an diaz-titouroù '''$1''' : $2", "config-sqlite-dir": "Kavlec'h roadennoù SQLite :", + "config-sqlite-dir-help": "Stokañ a ra SQLite an holl roadennoù en ur restr nemetken.\n\nE-pad ar staliañ, rankout a ra ar servijer web gallout skrivañ er c'havlec'h pourchaset ganeoc'h.\n\nNe zlefe <strong>ket</strong> bezañ tizhadus dre ar web; setu perak ne lakaomp ket anezhañ el lec'h m'emañ ho restroù PHP.\n\nSkivañ a raio ar stalier ur restr <code>.htaccess</code> war un dro gantañ met ma c'hoarvez ur fazi e c'hallfe unan bennak tapout krog en ho roadennoù.\nKement-se a sell ouzh ar roadennoù implijer (chomlec'hioù postel, gerioù-tremen hachet) hag ouzh an adweladennoù diverket ha takadoù gwarzeet all eus ar wiki.\n\nEn em soñjit ha ne vefe ket gwelloc'h lakaat an diaz roadennoù en un tu bennak all, da skouer e <code>/var/lib/mediawiki/yourwiki</code>.", "config-oracle-def-ts": "Esaouenn stokañ (\"tablespace\") dre ziouer :", "config-oracle-temp-ts": "Esaouenn stokañ (''tablespace'') da c'hortoz :", "config-type-mysql": "MySQL (pe kenglotus)", @@ -103,10 +118,11 @@ "config-type-oracle": "Oracle", "config-type-mssql": "Microsoft SQL Server", "config-support-info": "Skoret eo ar reizhiadoù diaz titouroù da-heul gant MediaWiki :\n\n$1\n\nMa ne welit ket amañ dindan ar reizhiad diaz titouroù a fell deoc'h ober ganti, heuilhit an titouroù a-us (s.o. al liammoù) evit gweredekaat ar skorañ.", - "config-dbsupport-mysql": "* $1 eo an dibab kentañ evit MediaWiki hag an hini skoret ar gwellañ ([http://www.php.net/manual/en/mysql.installation.php penaos kempunañ PHP gant skor MySQL])", - "config-dbsupport-postgres": "* Ur reizhiad diaz titouroù brudet ha digor eo $1. Gallout a ra ober evit MySQL ([http://www.php.net/manual/en/pgsql.installation.php Penaos kempunañ PHP gant skor PostgreSQL]). Gallout a ra bezañ un nebeud drein bihan enni ha n'eo ket erbedet he implijout en un endro produiñ.", - "config-dbsupport-sqlite": "* $1 zo ur reizhiad diaz titouroù skañv skoret eus ar c'hentañ. ([http://www.php.net/manual/en/pdo.installation.php Penaos kempunañ PHP gant skor SQLite], implijout a ra PDO)", - "config-dbsupport-oracle": "* $1 zo un diaz titouroù kenwerzhel. ([http://www.php.net/manual/en/oci8.installation.php Penaos kempunañ PHP gant skor OCI8])", + "config-dbsupport-mysql": "* [{{int:version-db-mysql-url}} MySQL] eo an dibab kentañ evit MediaWiki hag an hini skoret ar gwellañ. Mont a ra MediaWiki en-dro gant [{{int:version-db-mariadb-url}} MariaDB] ha [{{int:version-db-percona-url}} Percona Server] ivez, kenglotus o-daou gant MySQL. ([http://www.php.net/manual/en/mysqli.installation.php Penaos kempunañ PHP gant skor MySQL])", + "config-dbsupport-postgres": "* [{{int:version-db-postgres-url}} PostgreSQL] zo anezhi ur reizhiad diaz roadennoù frank a wirioù brudet-mat a c'haller ober gantañ e plas MySQL. ([http://www.php.net/manual/en/pgsql.installation.php Penaos kempunañ PHP gant skor PostgreSQL])", + "config-dbsupport-sqlite": "* [{{int:version-db-sqlite-url}} SQLite] zo anezhi ur reizhiad diaz roadennoù skañv skoret eus ar c'hentañ. ([http://www.php.net/manual/en/pdo.installation.php Penaos kempunañ PHP gant skor SQLite], implijout a ra PDO)", + "config-dbsupport-oracle": "* Un embregerezh kenwerzhel diaz roadennoù eo [{{int:version-db-oracle-url}} Oracle]. ([http://www.php.net/manual/en/oci8.installation.php Penaos kempunañ PHP gant skor OCI8])", + "config-dbsupport-mssql": "* Un embregerezh kenwerzhel diaz roadennoù evit Windows eo [{{int:version-db-mssql-url}} Microsoft SQL Server]. ([http://www.php.net/manual/en/sqlsrv.installation.php Penaos kempunañ PHP gant skor SQLSRV])", "config-header-mysql": "Arventennoù MySQL", "config-header-postgres": "Arventennoù PostgreSQL", "config-header-sqlite": "Arventennoù SQLite", @@ -116,7 +132,7 @@ "config-missing-db-name": "Ret eo deoc'h merkañ un dalvoudenn evit \"{{int:config-db-name}}\".", "config-missing-db-host": "Ret eo deoc'h merkañ un dalvoudenn evit \"{{int:config-db-host}}\"", "config-missing-db-server-oracle": "Ret eo deoc'h merkañ un dalvoudenn evit \"{{int:config-db-host-oracle}}\".", - "config-invalid-db-server-oracle": "Direizh eo anv TNS an diaz titouroù \"$1\".\nOber hepken gant lizherennoù ASCII (a-z, A-Z), sifroù (0-9), arouezennoù islinennañ (_) ha pikoù (.).", + "config-invalid-db-server-oracle": "Direizh eo anv TNS an diaz roadennoù \"$1\".\nOber gant an neudennad \"TNS Name\" pe c'hoazh gant \"Easy Connect ([http://docs.oracle.com/cd/E11882_01/network.112/e10836/naming.htm Hentennoù envel Oracle]).", "config-invalid-db-name": "Direizh eo anv an diaz titouroù \"$1\".\nOber hepken gant lizherennoù ASCII (a-z, A-Z), sifroù (0-9), arouezennoù islinennañ (_) ha tiredoù (-).", "config-invalid-db-prefix": "Direizh eo rakger an diaz titouroù \"$1\".\nOber hepken gant lizherennoù ASCII (a-z, A-Z), sifroù (0-9), arouezennoù islinennañ (_) ha tiredoù (-).", "config-connection-error": "$1.\n\nGwiriit anv an ostiz, an anv implijer, ar ger-tremen ha klaskit en-dro.", @@ -126,6 +142,7 @@ "config-postgres-old": "Rekis eo PostgreSQL $1 pe ur stumm nevesoc'h; ober a rit gant $2.", "config-mssql-old": "Stumm $1 Microsoft SQL Server, pe unan nevesoc'h, zo rekis. Ganeoc'h emañ ar stumm $2.", "config-sqlite-name-help": "Dibabit un anv dibar d'ho wiki.\nArabat ober gant esaouennoù pe barrennigoù-stagañ.\nImplijet e vo evit ar restr roadennoù SQLite.", + "config-sqlite-parent-unwritable-group": "N'haller ket krouiñ ar c'havlec'h roadennoù <code><nowiki>$1</nowiki></code> peogwir n'hall ket ar servijer Web skrivañ war ar c'havlec'h kar <code><nowiki>$2</nowiki></code>.\n\nKavet eo bet gant ar stalier an anv implijer m'eo oberiant ar servijer drezañ. Evit gallout kenderc'hel, lakait ar c'havlec'h <code><nowiki>$3</nowiki></code> da vezañ tizhus evit ar skrivañ.\nWar ur reizhiad Unix/Linux system ober :\n\n<pre>cd $2\nmkdir $3\nchgrp $4 $3\nchmod g+w $3</pre>", "config-sqlite-mkdir-error": "Ur fazi zo bet e-ser krouiñ ar c'havlec'h roadennoù \"$1\".\nGwiriañ al lec'hiadur ha klask en-dro.", "config-sqlite-dir-unwritable": "Dibosupl skrivañ er c'havlec'h \"$1\".\nCheñchit ar aotreoù evit ma c'hallfe ar servijer web skrivañ ennañ ha klaskit en-dro.", "config-sqlite-connection-error": "$1.\n\nGwiriañ ar c'havlec'h roadennoù hag anv an diaz roadennoù a-is ha klaskit en-dro.", @@ -175,7 +192,8 @@ "config-admin-error-user": "Fazi diabarzh en ur grouiñ ur merer gant an anv \"<nowiki>$1</nowiki>\".", "config-admin-error-password": "Fazi diabarzh o lakaat ur ger-tremen evit ar merour « <nowiki>$1</nowiki> » : <pre>$2</pre>", "config-admin-error-bademail": "Ebarzhet hoc'h eus ur chomlec'h postel direizh.", - "config-subscribe": "Koumanantit da [https://lists.wikimedia.org/mailman/listinfo/mediawiki-listenn kemennadoù evit ar stummoù nevez].", + "config-subscribe": "Koumanantit d'ar [https://lists.wikimedia.org/mailman/listinfo/mediawiki-roll kemennoù evit ar stummoù nevez].", + "config-pingback": "Rannañ roadennoù diwar-benn ar staliadur-mañ gant diorroerien Mediawiki.", "config-almost-done": "Kazi echu eo !\nGellout a rit tremen ar c'hefluniadur nevez ha staliañ ar wiki war-eeun.", "config-optional-continue": "Sevel muioc'h a goulennoù ouzhin.", "config-optional-skip": "Aet on skuizh, staliañ ar wiki hepken.", @@ -190,6 +208,7 @@ "config-license-cc-by": "Creative Commons Deroadenn", "config-license-cc-by-nc-sa": "Creative Commons Deroadenn Angenwerzhel Kenrannañ heñvel", "config-license-cc-0": "Creative Commons Zero (Domani foran)", + "config-license-gfdl": "Aotre implijout teuliadur frank GNU 1.3 pe nevesoc'h", "config-license-pd": "Domani foran", "config-license-cc-choose": "Dibabit un aotre-implijout Creative Commons personelaet", "config-email-settings": "Arventennoù ar postel", @@ -198,9 +217,9 @@ "config-email-user": "Gweredekaat ar posteloù a implijer da implijer", "config-email-user-help": "Aotren a ra an holl implijerien da gas posteloù an eil d'egile mard eo bet gweredekaet an arc'hwel ganto en ho penndibaboù.", "config-email-usertalk": "Gweredekaat kemennadur pajennoù kaozeal an implijerien", - "config-email-usertalk-help": "Talvezout a ra d'an implijerien da resev kemennadennoù ma vez kemmet o fajennoù kaozeal, ma vez gweredekaet en o fenndibaboù.", + "config-email-usertalk-help": "Talvezout a ra d'an implijerien da resev kemennoù ma vez kemmet o fajennoù kaozeal, gant ma vo gweredekaet en o fenndibaboù.", "config-email-watchlist": "Gweredekaat ar c'hemenn listenn evezhiañ", - "config-email-watchlist-help": "Talvezout a ra d'an implijerien da resev kemennadennoù diwar-benn ar pajennoù evezhiet ganto, ma vez gweredekaet en o fenndibaboù.", + "config-email-watchlist-help": "Talvezout a ra d'an implijerien da resev kemennoù diwar-benn ar pajennoù evezhiet ganto, gant ma vo gweredekaet en o fenndibaboù.", "config-email-auth": "Gweredekaat an dilesadur dre bostel", "config-email-sender": "Chomlec'h postel respont :", "config-email-sender-help": "Merkit ar chomlec'h postel da vezañ implijet da chomlec'h distreiñ ar posteloù a ya er-maez.\nDi e vo kaset ar posteloù distaolet.\nNiverus eo ar servijerioù postel a c'houlenn da nebeutañ un [http://fr.wikipedia.org/wiki/Nom_de_domaine anv domani] reizh.", @@ -215,17 +234,20 @@ "config-cc-not-chosen": "Dibabit an aotre-implijout Creative Commons a fell deoc'h ober gantañ ha klikit war \"proceed\".", "config-advanced-settings": "Kefluniadur araokaet", "config-cache-options": "Arventennoù evit krubuilhañ traezoù :", - "config-cache-accel": "Krubuilhañ traezoù PHP (APC, XCache pe WinCache)", + "config-cache-accel": "Krubuilhañ traezoù PHP (APC, APCu, XCache pe WinCache)", "config-cache-memcached": "Implijout Memcached (en deus ezhomm bezañ staliet ha kefluniet)", "config-memcached-servers": "Servijerioù Memcached :", "config-memcached-help": "Roll ar chomlec'hioù IP da implijout evit Memcached.\nRet eo spisaat unan dre linenn ha spisaat ar porzh da vezañ implijet. Da skouer :\n127.0.0.1:11211\n192.168.1.25:1234", "config-memcache-needservers": "Diuzet hoc'h eus Memcached evel seurt krubuilh met n'hoc'h eus spisaet servijer ebet.", "config-memcache-badip": "Ur chomlec'h IP direizh hoc'h eus lakaet evit Memcached : $1.", + "config-memcache-noport": "N'eus ket bet spisaet porzh ebet ganeoc'h evit servijer Memcached : $1.\nMa n'anavezit ket ar porzh, setu an hini dre ziouer : 11211.", "config-memcache-badport": "Niverennoù porzh Memcached a zlefe bezañ etre $1 ha $2.", "config-extensions": "Astennoù", "config-extensions-help": "N'eo ket bet detektet an astennoù rollet a-us en ho kavlec'h <code>./astennoù</code>.\n\nMarteze e vo ezhomm kefluniañ pelloc'h met gallout a rit o gweredekaat bremañ.", "config-skins": "Gwiskadurioù", + "config-skins-help": "Kavet eo bet ar gwiskadurioù renablet a-us en ho kavlec'h <code>./skins</code>. Ret eo deoc'h gweredekaat unan da nebeutañ, ha dibab an hini dre ziouer.", "config-skins-use-as-default": "Implijout ar gwiskadur-mañ dre ziouer", + "config-skins-missing": "N'eus bet kavet gwiskadur ebet : ober a raio MediaWiki gant unan dre ziouer betek ma vo staliet reoù a zegouezh.", "config-skins-must-enable-some": "Ret eo deoc'h dibab da nebautañ ur gwiskadur da weredekaat.", "config-skins-must-enable-default": "Ar gwiskadur dre ziouer dibabet a rank bezañ gweredekaet.", "config-install-alreadydone": "'''Diwallit''': Staliet hoc'h eus MediaWiki dija war a seblant hag emaoc'h o klask e staliañ c'hoazh.\nKit d'ar bajenn war-lerc'h, mar plij.", @@ -248,7 +270,7 @@ "config-install-user-missing": "N'eus ket eus an implijer \"$1\"", "config-install-user-missing-create": "N'eus ket eus an implijer \"$1\".\nMa fell deoc'h krouiñ anezhañ, klikit war ar voest \"krouiñ ur gont\" amañ dindan.", "config-install-tables": "Krouiñ taolennoù", - "config-install-tables-exist": "<strong>Kemenn :</strong> An taolennoù MediaWiki zo anezho dija war a seblant.\nN'eo ket bet graet ar grouidigezh.", + "config-install-tables-exist": "<strong>Diwallit :</strong> An taolennoù MediaWiki zo anezho dija war a seblant.\nN'int ket bet adkrouet.", "config-install-tables-failed": "'''Fazi :''' c'hwitet eo krouidigezh an daolenn gant ar fazi-mañ : $1", "config-install-interwiki": "O leuniañ dre ziouer an daolenn etrewiki", "config-install-interwiki-list": "Ne c'haller ket kavout ar restr <code>interwiki.list</code>.", @@ -259,12 +281,13 @@ "config-install-subscribe-fail": "N'haller ket koumanantiñ da mediawiki-announce : $1", "config-install-subscribe-notpossible": "cURL n'eo ket staliet ha ne c'haller ket ober gant <code>allow_url_fopen</code>.", "config-install-mainpage": "O krouiñ ar bajenn bennañ gant un endalc'had dre ziouer", + "config-install-mainpage-exists": "Bez' ez eus eus ar bajenn bennañ c'hoazh, lezel a-gostez", "config-install-extension-tables": "O krouiñ taolennoù evit an astennoù gweredekaet", "config-install-mainpage-failed": "Ne c'haller ket ensoc'hañ ar bajenn bennañ: $1", "config-download-localsettings": "Pellgargañ <code>LocalSettings.php</code>", "config-help": "skoazell", "config-help-tooltip": "klikañ evit astenn", "config-nofile": "N'eus ket bet gallet kavout ar restr \"$1\". Daoust ha dilamet eo bet ?", - "mainpagetext": "'''Meziant MediaWiki staliet.'''", + "mainpagetext": "<strong>Staliet eo bet MediaWiki.</strong>", "mainpagedocfooter": "Sellit ouzh [https://meta.wikimedia.org/wiki/Help:Contents Sturlevr an implijerien] evit gouzout hiroc'h war an doare da implijout ar meziant wiki.\n\n== Kregiñ ganti ==\n\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings Roll an arventennoù kefluniañ]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ FAG MediaWiki]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Roll ar c'haozeadennoù diwar-benn dasparzhoù MediaWiki]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation#Translation_resources Lec'hiañ MediaWiki en ho yezh" } diff --git a/includes/installer/i18n/ca.json b/includes/installer/i18n/ca.json index 003583dce82c..559e7dd9d98b 100644 --- a/includes/installer/i18n/ca.json +++ b/includes/installer/i18n/ca.json @@ -20,7 +20,7 @@ "config-localsettings-upgrade": "S'ha detectat un fitxer <code>LocalSettings.php</code>. \nPer tal d'actualitzar la instal·lació, introduïu el valor de <code>$wgUpgradeKey</code> en el quadre a continuació. El trobareu a <code>LocalSettings.php</code>.", "config-localsettings-cli-upgrade": "S'ha detectat un fitxer <code>LocalSettings.php</code>.\nPer a actualitzar la instal·lació, executeu <code>update.php</code>.", "config-localsettings-key": "Clau d'actualització:", - "config-localsettings-badkey": "La clau que heu proporcionat no és correcta.", + "config-localsettings-badkey": "La clau d'actualització que heu proporcionat no és correcta.", "config-upgrade-key-missing": "S'ha detectat una instal·lació ja existent del MediaWiki.\nPer actualitzar-la, poseu la línia següent al final de <code>LocalSettings.php</code>:\n\n$1", "config-localsettings-incomplete": "El <code>LocalSettings.php</code> que hi ha sembla incomplet.\nLa variable $1 no està definida.\nCanvieu <code>LocalSettings.php</code> perquè la variable estigui definida i feu clic a «{{int:Config-continue}}».", "config-localsettings-connection-error": "S'ha trobat un error en connectar-se amb la base de dades fent servir els paràmetres especificats a <code>LocalSettings.php</code>. Corregiu aquests paràmetres i torneu-ho a provar.\n\n$1", @@ -52,7 +52,7 @@ "config-restart": "Sí, torna a començar", "config-welcome": "=== Comprovacions de l'entorn ===\nS'efectuaran comprovacions bàsiques per veure si l'entorn és adequat per a la instal·lació del MediaWiki.\nRecordeu d'incloure aquesta informació si heu de demanar ajuda sobre com completar la instal·lació.", "config-copyright": "=== Drets d'autor i condicions ===\n\n$1\n\nAquest programa és de programari lliure; podeu redistribuir-lo i/o modificar-lo sota les condicions de la Llicència Pública General GNU com es publicada per la Free Software Foundation; qualsevol versió 2 de la llicència, o (opcionalment) qualsevol versió posterior.\n\nAquest programa és distribueix amb l'esperança que serà útil, però <strong>sense cap garantia</strong>; sense ni tan sols la garantia implícita de <strong>\ncomerciabilitat</strong> o <strong>idoneïtat per a un propòsit particular</strong>.\nConsulteu la Llicència Pública General GNU, per a més detalls.\n\nHauríeu d'haver rebut <doclink href=\"Copying\">una còpia de la Llicència Pública General GNU</doclink> amb aquest programa; si no, escriviu a la Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA o [http://www.gnu.org/copyleft/gpl.html per llegir-lo en línia].", - "config-sidebar": "* [https://www.mediawiki.org la Pàgina d'inici]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents Guia de l'Usuari]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Contents Guia de l'Administrador]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ FAQ]\n----\n* <doclink href=Readme>Llegeix-me</doclink>\n* <doclink href=ReleaseNotes>Notes de la versió</doclink>\n* <doclink href=Còpia>Còpia</doclink>\n* <doclink href=UpgradeDoc>Actualització</doclink>", + "config-sidebar": "* [https://www.mediawiki.org la Pàgina d'inici]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents Guia de l'usuari]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Contents Guia de l'administrador]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ PMF]\n----\n* <doclink href=Readme>Llegeix-me</doclink>\n* <doclink href=ReleaseNotes>Notes de la versió</doclink>\n* <doclink href=Còpia>Còpia</doclink>\n* <doclink href=UpgradeDoc>Actualització</doclink>", "config-env-good": "S'ha comprovat l'entorn.\nPodeu instal·lar el MediaWiki.", "config-env-bad": "S'ha comprovat l'entorn.\nNo podeu instal·lar el MediaWiki.", "config-env-php": "El PHP $1 està instal·lat.", @@ -163,6 +163,8 @@ "config-admin-error-user": "S'ha produït un error intern en crear un administrador amb el nom «<nowiki>$1</nowiki>».", "config-admin-error-password": "S'ha produït un error intern en definir una contrasenya per a l'administrador «<nowiki>$1</nowiki>»: <pre>$2</pre>", "config-admin-error-bademail": "Heu introduït una adreça electrònica no vàlida.", + "config-subscribe": "Subscriu a la [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce llista de correu d'anunci de noves versions].", + "config-pingback": "Comparteix dades d'aquesta instal·lació amb els desenvolupadors de MediaWiki.", "config-almost-done": "Gairebé ja heu acabat!\nPodeu ometre el que queda de la configuració i procedir amb la instal·lació del wiki.", "config-optional-continue": "Fes-me més preguntes.", "config-optional-skip": "Ja estic avorrit. Simplement instal·leu el wiki.", @@ -242,12 +244,14 @@ "config-install-subscribe-fail": "No s'ha pogut subscriure a mediawiki-announce: $1", "config-install-subscribe-notpossible": "El cURL no està instal·lat i <code>allow_url_fopen</code> no està disponible.", "config-install-mainpage": "S'està creant la pàgina principal amb el contingut per defecte", + "config-install-mainpage-exists": "La pàgina principal ja existeix, per tant s'omet", "config-install-extension-tables": "S'estan creant taules de les extensions habilitades", "config-install-mainpage-failed": "No s'ha pogut inserir la pàgina principal: $1", + "config-install-done": "<strong>Enhorabona!</strong>\nHeu instal·lat MediaWiki.\n\nL'instal·lador a generat un fitxer <code>LocalSettings.php</code>.\nConté tota la configuració.\n\nCaldrà que el baixeu i el poseu al directori base on heu instal·lat al wiki (el mateix directori on es troba index.php). La baixada hauria d'haver començat automàticament.\n\nSi la baixada no comença, o si l'heu cancel·lat, podeu reiniciar-la fent clic a l'enllaç de sota:\n\n$3\n\n<strong>Nota:</strong> Si no ho feu ara, no podreu accedir a aquest fitxer de configuració més endavant si no l'heu baixat abans.\n\nUna vegada tot això fet, podeu <strong>[$2 entrar al vostre wiki]</strong>.", "config-download-localsettings": "Baixa <code>LocalSettings.php</code>", "config-help": "ajuda", "config-help-tooltip": "feu clic per ampliar", "config-nofile": "No s'ha pogut trobar el fitxer «$1». S'ha suprimit?", - "mainpagetext": "'''El MediaWiki s'ha instal·lat correctament.'''", + "mainpagetext": "<strong>MediaWiki s'ha instal·lat.</strong>", "mainpagedocfooter": "Consulteu la [https://meta.wikimedia.org/wiki/Help:Contents Guia d'Usuari] per a més informació sobre com utilitzar-lo.\n\n== Per a començar ==\n\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings Llista de característiques configurables]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ PMF del MediaWiki]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Llista de correu (''listserv'') per a anuncis del MediaWiki]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation#Translation_resources Traduïu MediaWiki en la vostra llengua]" } diff --git a/includes/installer/i18n/ckb.json b/includes/installer/i18n/ckb.json index 583b80c0d87e..3bfa8a6f54c2 100644 --- a/includes/installer/i18n/ckb.json +++ b/includes/installer/i18n/ckb.json @@ -3,7 +3,8 @@ "authors": [ "Asoxor", "Calak", - "Muhammed taha" + "Muhammed taha", + "Lost Whispers" ] }, "config-desc": "دامەزرێنەرەکە بۆ میدیاویکی", @@ -44,6 +45,8 @@ "config-admin-password": "تێپەڕوشە:", "config-admin-password-confirm": "دووبارە تێپەڕوشە:", "config-admin-email": "ناونیشانی ئیمەیل:", + "config-admin-email-help": "ناونیشانی ئیمەیڵەکەت لێرەدا دابنێ بۆئەوەی بتوانیت ئیمەیڵت لە بەکارھێنەرانی ترەوە پێ بگات، تێپەڕ وشە ڕێک بخەیتەوە و ئاگادار بکرێیتەوە لەو گۆڕانکاریانەی کە لەو پەڕانەدا دەکرێن کە چاودێرییان دەکەیت. دەتوانیت ئەم بۆشاییە بە بەتاڵی جێبھێڵیت.", + "config-admin-error-bademail": "تۆ ناونیشانی ئیمەیڵێکی ھەڵەت داخڵ کردووە.", "config-profile-wiki": "ویکیی کراوە", "config-profile-no-anon": "دروستکردنی ھەژمارە پێویستە", "config-profile-fishbowl": "تەنھا دەستکاریکەری ڕێگەپێدراوە", diff --git a/includes/installer/i18n/cs.json b/includes/installer/i18n/cs.json index 9f24ec6b6ca4..2d2f2072b76f 100644 --- a/includes/installer/i18n/cs.json +++ b/includes/installer/i18n/cs.json @@ -308,6 +308,7 @@ "config-install-subscribe-fail": "Nelze se přihlásit k odběru mediawiki-announce: $1", "config-install-subscribe-notpossible": "Není nainstalován cURL a není dostupné <code>allow_url_fopen</code>.", "config-install-mainpage": "Vytváří se počáteční obsah hlavní strany", + "config-install-mainpage-exists": "Hlavní strana již existuje, přeskakuji.", "config-install-extension-tables": "Vytvářejí se tabulky pro zapnutá rozšíření", "config-install-mainpage-failed": "Nepodařilo se vložit hlavní stranu: $1", "config-install-done": "<strong>Gratulujeme!</strong>\nNainstalovali jste MediaWiki.\n\nInstalátor vytvořil soubor <code>LocalSettings.php</code>.\nTen obsahuje veškerou vaši konfiguraci.\n\nBudete si ho muset stáhnout a uložit do základního adresáře vaší instalace wiki (do stejného adresáře jako soubor index.php). Stažení souboru se mělo spustit automaticky.\n\nPokud se vám stažení nenabídlo nebo jste ho zrušili, můžete ho spustit znovu kliknutím na následující odkaz:\n\n$3\n\n<strong>Poznámka</strong>: Pokud to neuděláte hned, tento vygenerovaný konfigurační soubor nebude později dostupný, pokud instalaci opustíte, aniž byste si ho stáhli.\n\nAž to dokončíte, můžete <strong>[$2 vstoupit do své wiki]</strong>.", diff --git a/includes/installer/i18n/de.json b/includes/installer/i18n/de.json index c7690e2a788b..3babc3f85147 100644 --- a/includes/installer/i18n/de.json +++ b/includes/installer/i18n/de.json @@ -315,6 +315,7 @@ "config-install-subscribe-fail": "Abonnieren von „mediawiki-announce“ ist gescheitert: $1", "config-install-subscribe-notpossible": "cURL ist nicht installiert und <code>allow_url_fopen</code> ist nicht verfügbar.", "config-install-mainpage": "Erstellung der Hauptseite mit Standardinhalten", + "config-install-mainpage-exists": "Die Hauptseite ist bereits vorhanden. Überspringe.", "config-install-extension-tables": "Erstellung der Tabellen für die aktivierten Erweiterungen", "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 <code>LocalSettings.php</code> 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 <code>index.php</code> 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.", diff --git a/includes/installer/i18n/el.json b/includes/installer/i18n/el.json index a11c301f5e64..679c0a8cba7d 100644 --- a/includes/installer/i18n/el.json +++ b/includes/installer/i18n/el.json @@ -223,6 +223,7 @@ "config-install-subscribe-fail": "Ανίκανος να εγγραφείτε στο mediawiki-ανακοινώση: $1", "config-install-subscribe-notpossible": "Το cURL δεν είναι εγκατεστημένο και το <code>allow_url_fopen</code> δεν είναι διαθέσιμο.", "config-install-mainpage": "Γίνεται δημιουργία της αρχικής σελίδας με προεπιλεγμένο περιεχόμενο", + "config-install-mainpage-exists": "Κύρια σελίδα ήδη υπάρχει, παρακάμπτεται", "config-install-extension-tables": "Γίνεται δημιουργία πινάκων για τις εγκατεστημένες επεκτάσεις", "config-install-mainpage-failed": "Δεν ήταν δυνατή η εισαγωγή της αρχικής σελίδας: $1", "config-install-done": "<strong>Συγχαρητήρια!</strong>\nΈχετε εγκαταστήσει με επιτυχία το MediaWiki.\n\nΤο πρόγραμμα εγκατάστασης έχει δημιουργήσει το αρχείο <code>LocalSettings.php</code>.\nΠεριέχει όλες τις ρυθμίσεις παραμέτρων σας.\n\nΘα πρέπει να το κατεβάσετε και να το βάλετε στη βάση της εγκατάστασης του wiki σας (στον ίδιο κατάλογο όπως το index.php). Η λήψη θα αρχίσει αυτόματα.\n\nΑν η λήψη δεν προσφέφθηκε, ή αν την ακυρώσατε, μπορείτε να επανεκκινήσετε τη λήψη κάνοντας κλικ στο παρακάτω link:\n\n$3\n\n<strong>Σημείωση:</strong> Εάν δεν το κάνετε αυτό τώρα, αυτό το αρχείο ρύθμισης παραμέτρων δεν θα είναι διαθέσιμο για σας αργότερα, αν βγείτε από την εγκατάσταση, χωρίς να το κατεβάσετε!\n\nΌταν γίνει αυτό, μπορείτε να <strong>[$2 μπείτε στο wiki σας]</strong>.", diff --git a/includes/installer/i18n/en.json b/includes/installer/i18n/en.json index 95d2ba31c470..db92652d93ba 100644 --- a/includes/installer/i18n/en.json +++ b/includes/installer/i18n/en.json @@ -298,6 +298,7 @@ "config-install-subscribe-fail": "Unable to subscribe to mediawiki-announce: $1", "config-install-subscribe-notpossible": "cURL is not installed and <code>allow_url_fopen</code> is not available.", "config-install-mainpage": "Creating main page with default content", + "config-install-mainpage-exists": "Main page already exists, skipping", "config-install-extension-tables": "Creating tables for enabled extensions", "config-install-mainpage-failed": "Could not insert main page: $1", "config-install-done": "<strong>Congratulations!</strong>\nYou have installed MediaWiki.\n\nThe installer has generated a <code>LocalSettings.php</code> 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\n<strong>Note:</strong> 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 <strong>[$2 enter your wiki]</strong>.", diff --git a/includes/installer/i18n/es.json b/includes/installer/i18n/es.json index c40183273f8b..8c4914c0565d 100644 --- a/includes/installer/i18n/es.json +++ b/includes/installer/i18n/es.json @@ -332,6 +332,7 @@ "config-install-subscribe-fail": "No se ha podido suscribir a mediawiki-announce: $1", "config-install-subscribe-notpossible": "cURL no está instalado y <code>allow_url_fopen</code> no está disponible.", "config-install-mainpage": "Creando página principal con contenido predeterminado", + "config-install-mainpage-exists": "La página principal ya existe, se omite", "config-install-extension-tables": "Creando las tablas para las extensiones habilitadas", "config-install-mainpage-failed": "No se pudo insertar la página principal: $1", "config-install-done": "<strong>¡Felicidades!</strong>\nHas instalado MediaWiki.\n\nEl instalador ha generado un archivo <code>LocalSettings.php</code>.\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\n<strong>Nota</strong>: 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 <strong>[$2 entrar en tu wiki]</strong>.", diff --git a/includes/installer/i18n/fa.json b/includes/installer/i18n/fa.json index 527554808ab8..411b2d0f7256 100644 --- a/includes/installer/i18n/fa.json +++ b/includes/installer/i18n/fa.json @@ -311,6 +311,7 @@ "config-install-subscribe-fail": "قادر تصدیق اعلام مدیاویکی نیست:$1", "config-install-subscribe-notpossible": "سییوآرال نصب نشدهاست و <code>allow_url_fopen</code> در دسترس نیست.", "config-install-mainpage": "ایجاد صفحهٔ اصلی با محتوای پیشفرض", + "config-install-mainpage-exists": "صفحهٔ اصلی موجود است، رها شد", "config-install-extension-tables": "ایجاد جداول برای افزونههای فعال", "config-install-mainpage-failed": "قادر به درج صفحهٔ اصلی نمیباشد:$1", "config-install-done": "'''تبریک!'''\nبا موفقیت مدیاویکی را نصب کردید.\nبرنامه نصبکننده پرونده <code>LocalSettings.php</code> را درست کرد.\nکه شامل تمام تنظیمات میباشد.\n\nشما نیاز به دریافت آن دارید و آن را در پایهٔ نصب ویکی قرار دهید (همان پوشهٔ index.php). دریافت باید به صورت خودکار شروع شدهباشد.\n\nاگر دریافت شروع نشد یا اگر آن را لغو کردید با کلیک روی پیوند زیر میتوانید آن را دریافت کنید:\n\n$3\n\n'''توجه داشته باشید:''' اگر این را الآن انجام ندهید، این پرونده تولیدشده در صورتی که نصب را بدون دریافت آن تمام کردید بعداً در اختیار شما قرار نخواهد گرفت.\n\nوقتی انجام شد شما میتوانید '''[$2 وارد ویکی شوید]'''.", @@ -321,5 +322,5 @@ "config-nofile": "پروندهٔ «$1» یافت نشد. آیا حذف شدهاست؟", "config-extension-link": "آیا میدانستید که ویکی شما [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Extensions extensions] را پشتیبانی میکند؟\nشما میتوانید [https://www.mediawiki.org/wiki/Special:MyLanguage/Category:Extensions_by_category extensions by category]", "mainpagetext": "'''مدیاویکی با موفقیت نصب شد.'''", - "mainpagedocfooter": "از [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents]\nبرای اطلاعات بیشتر در مورد بهکارگیری نرمافزار ویکی استفاده کنید.\n\n== آغاز به کار ==\n\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings فهرست تنظیمات پیکربندی]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ پرسشهای متداول مدیاویکی]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce فهرست ایمیلی نسخههای مدیاویکی]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation#Translation_resources محلیسازی مدیاویکی به زبان شما]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Combating_spam یادگیری روشهای مقابله با هرزنگاری در ویکی]" + "mainpagedocfooter": "از [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents راهنمای کاربری]\nبرای اطلاعات بیشتر در مورد بهکارگیری نرمافزار ویکی استفاده کنید.\n\n== آغاز به کار ==\n\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings فهرست تنظیمات پیکربندی]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ پرسشهای متداول مدیاویکی]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce فهرست ایمیلی نسخههای مدیاویکی]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation#Translation_resources محلیسازی مدیاویکی به زبان شما]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Combating_spam یادگیری روشهای مقابله با هرزنگاری در ویکی]" } diff --git a/includes/installer/i18n/fi.json b/includes/installer/i18n/fi.json index 59392917975c..6c99a4234e9a 100644 --- a/includes/installer/i18n/fi.json +++ b/includes/installer/i18n/fi.json @@ -20,7 +20,8 @@ "Jaakkoh", "Mikahama", "Olimar", - "01miki10" + "01miki10", + "Pyscowicz" ] }, "config-desc": "MediaWiki-asennin", @@ -126,7 +127,7 @@ "config-type-mssql": "Microsoft SQL Server", "config-support-info": "MediaWiki tukee seuraavia tietokantajärjestelmiä:\n\n$1\n\nJos et näe tietokantajärjestelmää, jota yrität käyttää, listattuna alhaalla, seuraa yläpuolella olevia ohjeita tuen aktivoimiseksi.", "config-dbsupport-mysql": "* [{{int:version-db-mysql-url}} MySQL] on MediaWikin ensisijainen kohde ja se on myös parhaiten tuettu. MediaWiki voi myös käyttää [{{int:version-db-mariadb-url}} MariaDB]- sekä [{{int:version-db-percona-url}} Percona Server]-järjestelmiä, jotka ovat MySQL-yhteensopivia. ([http://www.php.net/manual/en/mysqli.installation.php Miten käännetään PHP MySQL-tuella])", - "config-dbsupport-postgres": "* [{{int:version-db-postgres-url}} PostgreSQL] on suosittu avoimen lähdekoodin tietokantajärjestelmä vaihtoehtona MySQL:lle. Tuessa saattaa olla pieniä puutteita, eikä sitä suositella käytettäväksi tuotantoympäristössä. ([http://www.php.net/manual/en/pgsql.installation.php Kuinka käännetään PHP PostgreSQL-tuella])", + "config-dbsupport-postgres": "* [{{int:version-db-postgres-url}} PostgreSQL] on suosittu avoimen lähdekoodin tietokantajärjestelmä vaihtoehtona MySQL:lle. ([http://www.php.net/manual/en/pgsql.installation.php Kuinka käännetään PHP PostgreSQL-tuella])", "config-dbsupport-sqlite": "* [{{int:version-db-sqlite-url}} SQLite] on kevyt tietokantajärjestelmä, jota tuetaan hyvin. ([http://www.php.net/manual/en/pdo.installation.php Miten käännetään PHP SQLite-tuella], käyttää PDO:ta)", "config-dbsupport-oracle": "* [{{int:version-db-oracle-url}} Oracle] on kaupallinen yritystietokanta. ([http://www.php.net/manual/en/oci8.installation.php Kuinka käännetään PHP OCI8-tuella])", "config-dbsupport-mssql": "* [{{int:version-db-mssql-url}} Microsoft SQL Server] on kaupallinen yritystietokanta Windowsille. ([http://www.php.net/manual/en/sqlsrv.installation.php Miten käännetään PHP SQLSRV-tuella])", @@ -150,6 +151,7 @@ "config-mssql-old": "Vaaditaan Microsoft SQL Server $1 tai uudempi. Sinulla on käytössä $2.", "config-sqlite-name-help": "Valitse nimi, joka yksilöi tämän wikin.\nÄlä käytä välilyöntejä tai viivoja.\nNimeä käytetään SQLite-tietokannan tiedostonimessä.", "config-sqlite-dir-unwritable": "Hakemistoon ”$1” kirjoittaminen epäonnistui.\nMuuta hakemiston käyttöoikeuksia siten, että palvelinohjelmisto voi kirjoittaa siihen ja yritä uudelleen.", + "config-sqlite-connection-error": "$1.\n\nTarkista tietohakemiston ja tietokannan nimi alla ja yritä uudelleen.", "config-sqlite-readonly": "Tiedostoon <code>$1</code> ei voi kirjoittaa.", "config-sqlite-cant-create-db": "Tietokantatiedostoa <code>$1</code> ei voitu luoda.", "config-sqlite-fts3-downgrade": "PHP:stä puuttuu FTS3-tuki. Poistetaan ominaisuus käytöstä tietokantatauluista.", @@ -172,6 +174,7 @@ "config-mssql-auth": "Varmennuksen tyyppi:", "config-mssql-install-auth": "Valitse varmennuksen tyyppi, jota käytetään yhdistäessä tietokantaan asennuksen aikana.\nJos valitset \"{{int:config-mssql-windowsauth}}\", käytetään verkkopalvelimen käyttäjän kirjautumistietoja.", "config-mssql-web-auth": "Valitse varmennuksen tyyppi, jota verkkopalvelin käyttää yhdistäessään tietokantapalvelimeen wikin tavallisen toiminnan aikana.\nJos valitset \"{{int:config-mssql-windowsauth}}\", käytetään verkkopalvelimen käyttäjän kirjautumistietoja.", + "config-mssql-sqlauth": "SQL Server varmennus", "config-mssql-windowsauth": "Windows-varmennus", "config-site-name": "Wikin nimi", "config-site-name-help": "Tämä näkyy selaimen otsikkona ja muissa kohdissa.", @@ -292,6 +295,7 @@ "config-install-subscribe-fail": "Liittyminen mediawiki-announce listalle epäonnistui: $1", "config-install-subscribe-notpossible": "cURL-ohjelmaa ei ole asennettu eikä <code>allow_url_fopen</code> ole saatavilla.", "config-install-mainpage": "Luodaan etusivu oletussisällöllä", + "config-install-mainpage-exists": "Etusivu on jo olemassa, ohitetaan", "config-install-extension-tables": "Luodaan tauluja käyttöönotetuille laajuennuksille", "config-install-mainpage-failed": "Etusivun lisääminen ei onnistunut: $1", "config-install-done": "<strong>Onnittelut!</strong>\nOlet asentanut MediaWikin.\n\nAsennusohjelma on luonut <code>LocalSettings.php</code> -tiedoston.\nSiinä on kaikki MediaWikin asetukset.\n\nLataa tiedosto ja laita se MediaWikin asennushakemistoon (sama kuin missä on index.php). Lataamisen olisi pitänyt alkaa automaattisesti.\n\nMikäli latausta ei tarjottu tai keskeytit latauksen, käynnistä se uudestaan tästä linkistä:\n\n$3\n\n<strong>Huom:</strong> Mikäli et nyt lataa tiedostoa, luotu tiedosto ei ole saatavissa myöhemmin, jos poistut asennuksesta lataamatta sitä.\n\nKun olet laittanut tiedoston oikeaan paikkaan, voit <strong>[$2 mennä wikiisi]</strong>.", @@ -302,5 +306,5 @@ "config-nofile": "Tiedostoa \"$1\" ei löytynyt. Onko se poistettu?", "config-extension-link": "Tiesitkö että wiki tukee [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Extensions laajennuksia]?\n\nLaajennuksia voi hakea myös [https://www.mediawiki.org/wiki/Special:MyLanguage/Category:Extensions_by_category luokittain].", "mainpagetext": "<strong>MediaWiki on onnistuneesti asennettu.</strong>", - "mainpagedocfooter": "Lisätietoja wiki-ohjelmiston käytöstä on [https://meta.wikimedia.org/wiki/Help:Contents käyttöoppaassa].\n\n=== Aloittaminen ===\n\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings Asetusten teko-ohjeita]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ MediaWikin FAQ]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Sähköpostilista, jolla tiedotetaan MediaWikin uusista versioista]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation#Translation_resources Käännä MediaWikiä kielellesi]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Combating_spam Katso, kuinka torjua spämmiä wikissäsi]\n\n=== Asetukset ===\n\nTarkista, että alla olevat taivutusmuodot ovat oikein. Jos eivät, tee tarvittavat muutokset tiedostoon LocalSettings.php seuraavasti:\n $wgGrammarForms['fi']['genitive']['{{SITENAME}}'] = '...';\n $wgGrammarForms['fi']['partitive']['{{SITENAME}}'] = '...';\n $wgGrammarForms['fi']['elative']['{{SITENAME}}'] = '...';\n $wgGrammarForms['fi']['inessive']['{{SITENAME}}'] = '...';\n $wgGrammarForms['fi']['illative']['{{SITENAME}}'] = '...';\nTaivutusmuodot: {{GRAMMAR:genitive|{{SITENAME}}}} (yön) – {{GRAMMAR:partitive|{{SITENAME}}}} (yötä) – {{GRAMMAR:elative|{{SITENAME}}}} (yöstä) – {{GRAMMAR:inessive|{{SITENAME}}}} (yössä) – {{GRAMMAR:illative|{{SITENAME}}}} (yöhön)." + "mainpagedocfooter": "Lisätietoja wiki-ohjelmiston käytöstä on [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents käyttöoppaassa].\n\n=== Aloittaminen ===\n\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings Asetusten teko-ohjeita]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ MediaWikin FAQ]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Sähköpostilista, jolla tiedotetaan MediaWikin uusista versioista]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation#Translation_resources Käännä MediaWikiä kielellesi]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Combating_spam Katso, kuinka torjua spämmiä wikissäsi]\n\n=== Asetukset ===\n\nTarkista, että alla olevat taivutusmuodot ovat oikein. Jos eivät, tee tarvittavat muutokset tiedostoon LocalSettings.php seuraavasti:\n $wgGrammarForms['fi']['genitive']['{{SITENAME}}'] = '...';\n $wgGrammarForms['fi']['partitive']['{{SITENAME}}'] = '...';\n $wgGrammarForms['fi']['elative']['{{SITENAME}}'] = '...';\n $wgGrammarForms['fi']['inessive']['{{SITENAME}}'] = '...';\n $wgGrammarForms['fi']['illative']['{{SITENAME}}'] = '...';\nTaivutusmuodot: {{GRAMMAR:genitive|{{SITENAME}}}} (yön) – {{GRAMMAR:partitive|{{SITENAME}}}} (yötä) – {{GRAMMAR:elative|{{SITENAME}}}} (yöstä) – {{GRAMMAR:inessive|{{SITENAME}}}} (yössä) – {{GRAMMAR:illative|{{SITENAME}}}} (yöhön)." } diff --git a/includes/installer/i18n/fr.json b/includes/installer/i18n/fr.json index 882ab92091c1..953d427febae 100644 --- a/includes/installer/i18n/fr.json +++ b/includes/installer/i18n/fr.json @@ -327,6 +327,7 @@ "config-install-subscribe-fail": "Impossible de s'abonner à mediawiki-announce : $1", "config-install-subscribe-notpossible": "cURL n’est pas installé et <code>allow_url_fopen</code> n’est pas disponible.", "config-install-mainpage": "Création de la page principale avec un contenu par défaut", + "config-install-mainpage-exists": "La page principale existe déjà, ignoré", "config-install-extension-tables": "Création de tables pour les extensions activées", "config-install-mainpage-failed": "Impossible d’insérer la page principale : $1", "config-install-done": "<strong>Félicitations!</strong>\nVous avez installé MediaWiki.\n\nLe programme d'installation a généré un fichier <code>LocalSettings.php</code>. 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\n<strong>Note :</strong> 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 <strong>[$2 accéder à votre wiki]</strong> .", diff --git a/includes/installer/i18n/gl.json b/includes/installer/i18n/gl.json index ab1fd7d05955..d4ac7e3a0bf0 100644 --- a/includes/installer/i18n/gl.json +++ b/includes/installer/i18n/gl.json @@ -306,6 +306,7 @@ "config-install-subscribe-fail": "Non se puido subscribir á lista mediawiki-announce: $1", "config-install-subscribe-notpossible": "cURL non está instalado e <code>allow_url_fopen</code> non está dispoñible.", "config-install-mainpage": "Creando a páxina principal co contido por defecto", + "config-install-mainpage-exists": "A páxina principal xa existe, saltando", "config-install-extension-tables": "Creando as táboas para as extensións activadas", "config-install-mainpage-failed": "Non se puido inserir a páxina principal: $1", "config-install-done": "<strong>Parabéns!</strong>\nInstalou MediaWiki.\n\nO programa de instalación xerou un ficheiro <code>LocalSettings.php</code>.\nEste ficheiro contén toda a súa configuración.\n\nTerá que descargalo e poñelo na base da instalación do seu wiki (no mesmo directorio ca index.php). A descarga debería comezar automaticamente.\n\nSe non comezou a descarga ou se a cancelou, pode facer que comece de novo premendo na ligazón que aparece a continuación:\n\n$3\n\n<strong>Nota:</strong> Se non fai iso agora, este ficheiro de configuración xerado non estará dispoñible máis adiante se sae da instalación sen descargalo.\n\nCando faga todo isto, xa poderá <strong>[$2 entrar no seu wiki]</strong>.", diff --git a/includes/installer/i18n/he.json b/includes/installer/i18n/he.json index e98988c0610e..161c5db59ef0 100644 --- a/includes/installer/i18n/he.json +++ b/includes/installer/i18n/he.json @@ -9,7 +9,8 @@ "Yona b", "Rotemliss", "Macofe", - "Guycn2" + "Guycn2", + "שמזן" ] }, "config-desc": "תכנית ההתקנה של מדיה־ויקי", @@ -119,7 +120,7 @@ "config-type-mssql": "Microsoft SQL Server", "config-support-info": "מדיה־ויקי תומכת במערכות מסדי הנתונים הבאות:\n\n$1\n\nאם אינך רואה את מסד הנתונים שלך ברשימה, יש לעקוב אחר ההוראות המקושרות לעיל כדי להפעיל את התמיכה.", "config-dbsupport-mysql": "* [{{int:version-db-mysql-url}} MySQL] הוא היעד העיקרי עבור מדיה־ויקי ולו התמיכה הטובה ביותר. מדיה־ויקי עובדת גם עם [{{int:version-db-mariadb-url}} MariaDB] ועם [{{int:version-db-percona-url}} Percona Server], שתואמים ל־MySQL. (ר׳ [http://www.php.net/manual/en/mysql.installation.php how to compile PHP with MySQL support])", - "config-dbsupport-postgres": "* [{{int:version-db-postgres-url}} PostgreSQL] הוא מסד נתונים נפוץ בקוד פתוח והוא נפוץ בתור חלופה ל־MySQL. ייתכן שיש בתצורה הזאת באגים מסוימים והיא לא מומלצת לסביבות מבצעיות. (ר׳ [http://www.php.net/manual/en/pgsql.installation.php how to compile PHP with PostgreSQL support]).", + "config-dbsupport-postgres": "* [{{int:version-db-postgres-url}} PostgreSQL] הוא מסד נתונים נפוץ בקוד פתוח והוא נפוץ בתור חלופה ל־MySQL. (ר׳ [http://www.php.net/manual/en/pgsql.installation.php how to compile PHP with PostgreSQL support]).", "config-dbsupport-sqlite": "* [{{int:version-db-sqlite-url}} SQLite] הוא מסד נתונים קליל עם תמיכה טובה מאוד. (ר׳ [http://www.php.net/manual/en/pdo.installation.php How to compile PHP with SQLite support], משתמש ב־PDO)", "config-dbsupport-oracle": "* [{{int:version-db-oracle-url}} Oracle] הוא מסד נתונים עסקי מסחרי. (ר׳ [http://www.php.net/manual/en/oci8.installation.php How to compile PHP with OCI8 support])", "config-dbsupport-mssql": "* [{{int:version-db-mssql-url}} Microsoft SQL Server] הוא מסד נתונים עסקי מסחרי לחלונות. ([http://www.php.net/manual/en/sqlsrv.installation.php How to compile PHP with SQLSRV support])", @@ -254,7 +255,7 @@ "config-cache-options": "הגדרות למטמון עצמים (object caching):", "config-cache-help": "מטמון עצמים משמש לשיפור המהירות של מדיה־ויקי על־ידי שמירה של נתונים שהשימוש בהם נפוץ במטמון.\nלאתרים בינוניים וגדולים כדאי מאוד להפעיל את זה, וגם אתרים קטנים ייהנו מזה.", "config-cache-none": "ללא מטמון (שום יכולת אינה מוּסרת, אבל הביצועים באתרים גדולים ייפגעו)", - "config-cache-accel": "מטמון עצמים (object caching) של PHP‏ (APC‏, XCache או WinCache)", + "config-cache-accel": "מטמון עצמים (object caching) של PHP‏ (APC,‏ APCu,‏ XCache או WinCache)", "config-cache-memcached": "להשתמש ב־Memcached (דורש התקנות והגדרות נוספות)", "config-memcached-servers": "שרתי Memcached:", "config-memcached-help": "רשימת כתובות IP ש־Memcached ישתמש בהן.\nיש לרשום כתובת אחת בכל שורה ולציין את הפִּתְחָה (port), למשל:\n 127.0.0.1:11211\n 192.168.1.25:1234", @@ -305,6 +306,7 @@ "config-install-subscribe-fail": "הרישום ל־mediawiki-announce לא הצליח: $1", "config-install-subscribe-notpossible": "cURL אינה מותקנת ו־<code>allow_url_fopen</code> אינה זמינה.", "config-install-mainpage": "יצירת דף ראשי עם תוכן התחלתי", + "config-install-mainpage-exists": "העמוד הראשי כבר קיים, לדלג", "config-install-extension-tables": "יצירת טבלאות להרחבות מופעלות", "config-install-mainpage-failed": "לא הצליחה הכנסת דף ראשי: $1.", "config-install-done": "<strong>מזל טוב!</strong>\nהתקנת את תוכנת מדיה־ויקי.\n\nתוכנת ההתקנה יצרה את הקובץ <code>LocalSettings.php</code>.\nהוא מכיל את כל ההגדרות שלך.\n\nיש להוריד אותו ולהכניס אותו לתיקיית הבסיס שבה הותקן הוויקי שלך (אותה התיקייה שבה נמצא הקובץ index.php). ההורדה אמורה להתחיל באופן אוטומטי.\n\nאם ההורדה לא התחילה, או אם ביטלת אותה, אפשר להתחיל אותה מחדש באמצעות לחיצה על הקישור הבא:\n\n$3\n\n<strong>לתשומת לבך:</strong> אם ההורדה לא תבוצע כעת, קובץ ההגדרות <strong>לא</strong> יהיה זמין מאוחר יותר אם תוכנת ההתקנה תיסגר לפני שהקובץ יורד.\n\nלאחר שביצעת את הפעולות שלהלן, באפשרותך <strong>[$2 להיכנס לאתר הוויקי שלך]</strong>.", @@ -315,5 +317,5 @@ "config-nofile": "הקובץ \"$1\" לא נמצא. האם הוא נמחק?", "config-extension-link": "הידעת שמדיה־ויקי תומכת ב־[https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Extensions הרחבות]?\n\nבאפשרותך לעיין ב־[https://www.mediawiki.org/wiki/Special:MyLanguage/Category:Extensions_by_category הרחבות לפי קטגוריה].", "mainpagetext": "<strong>תוכנת מדיה־ויקי הותקנה בהצלחה.</strong>", - "mainpagedocfooter": "היעזרו ב[https://meta.wikimedia.org/wiki/Help:Contents מדריך למשתמש] למידע על שימוש בתוכנת הוויקי.\n\n== קישורים שימושיים ==\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings רשימת ההגדרות]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ שאלות ותשובות על מדיה־ויקי]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce רשימת התפוצה על השקת גרסאות]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation#Translation_resources תרגום מדיה־ויקי לשפה שלך]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Combating_spam איך להיאבק נגד ספאם באתר הוויקי שלך]" + "mainpagedocfooter": "היעזרו ב[https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents מדריך למשתמש] למידע על שימוש בתוכנת הוויקי.\n\n== קישורים שימושיים ==\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings רשימת ההגדרות]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ שאלות ותשובות על מדיה־ויקי]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce רשימת התפוצה על השקת גרסאות]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation#Translation_resources תרגום מדיה־ויקי לשפה שלך]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Combating_spam איך להיאבק נגד ספאם באתר הוויקי שלך]" } diff --git a/includes/installer/i18n/hi.json b/includes/installer/i18n/hi.json index e05b700f17ff..98af26744329 100644 --- a/includes/installer/i18n/hi.json +++ b/includes/installer/i18n/hi.json @@ -5,7 +5,8 @@ "Vivek Rai", "Phoenix303", "संजीव कुमार", - "Sahilrathod" + "Sahilrathod", + "Shyamal" ] }, "config-desc": "साँचा लिए इंस्टॉलर", @@ -16,7 +17,9 @@ "config-localsettings-key": "नवीनीकरण कुंजी", "config-localsettings-badkey": "आपकी दी गई कुंजी ग़लत है।", "config-your-language": "आपकी भाषा:", + "config-your-language-help": "स्थापन के लिए भाषा चुनें", "config-wiki-language": "विकी भाषा:", + "config-wiki-language-help": "भाषा चुनें जिस में अधिकतर लेख लिखा जाएगा", "config-back": "← वापस", "config-continue": "आगे बढ़ें →", "config-page-language": "भाषा", diff --git a/includes/installer/i18n/hu.json b/includes/installer/i18n/hu.json index 1c5c2ee5a5aa..5aece05f9a8d 100644 --- a/includes/installer/i18n/hu.json +++ b/includes/installer/i18n/hu.json @@ -129,7 +129,7 @@ "config-missing-db-name": "Meg kell adnod a(z) „{{int:config-db-name}}” értékét.", "config-missing-db-host": "Meg kell adnod az „{{int:config-db-host}}” értékét.", "config-missing-db-server-oracle": "Meg kell adnod az „{{int:config-db-host-oracle}}” értékét.", - "config-invalid-db-server-oracle": "Érvénytelen adatbázis TNS: „$1”\nCsak ASCII betűk (a-z, A-Z), számok (0-9), alulvonás (_) és pont (.) használható.", + "config-invalid-db-server-oracle": "Érvénytelen adatbázis TNS: „$1”\nHasználd a „TNS Name” vagy az Easy Connect” sztringet!\n([http://docs.oracle.com/cd/E11882_01/network.112/e10836/naming.htm Oracle Naming Methods]).", "config-invalid-db-name": "Érvénytelen adatbázisnév: „$1”.\nCsak ASCII-karakterek (a-z, A-Z), számok (0-9), alulvonás (_) és kötőjel (-) használható.", "config-invalid-db-prefix": "Érvénytelen adatbázisnév-előtag: „$1”.\nCsak ASCII-karakterek (a-z, A-Z), számok (0-9), alulvonás (_) és kötőjel (-) használható.", "config-connection-error": "$1.\n\nEllenőrizd a hosztot, felhasználónevet és jelszót, majd próbáld újra.", @@ -303,5 +303,5 @@ "config-nofile": "\"$1\" fájl nem található. Törölve lett?", "config-extension-link": "Tudtad, hogy a wikid támogat [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Extensions kiterjesztéseket]?\n\nBöngészhetsz [https://www.mediawiki.org/wiki/Special:MyLanguage/Category:Extensions_by_category kiterjesztéseket kategóriánként] vagy válogathatsz a [https://www.mediawiki.org/wiki/Extension_Matrix kiterjesztésmátrixból] az összes kiterjesztés áttekintéséhez.", "mainpagetext": "<strong>A MediaWiki telepítése sikeresen befejeződött.</strong>", - "mainpagedocfooter": "Ha segítségre van szükséged a wikiszoftver használatához, akkor keresd fel a [https://meta.wikimedia.org/wiki/Help:Contents User's Guide] oldalt.\n\n== Alapok (angol nyelven) ==\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings Beállítások listája]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ MediaWiki GyIK]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki-kiadások levelezőlistája]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation#Translation_resources A MediaWiki fordítása a saját nyelvedre]" + "mainpagedocfooter": "Ha segítségre van szükséged a wikiszoftver használatához, akkor keresd fel a [https://meta.wikimedia.org/wiki/Help:Contents User's Guide] oldalt.\n\n== Alapok (angol nyelven) ==\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings Beállítások listája]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ MediaWiki GyIK]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki-kiadások levelezőlistája]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation#Translation_resources A MediaWiki fordítása a saját nyelvedre]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Combating_spam Tudd meg többet, hogyan küzdhetsz a kéretlen levelek ellen a wikiden]" } diff --git a/includes/installer/i18n/ia.json b/includes/installer/i18n/ia.json index 1d522739f1ab..13e7a6b6916c 100644 --- a/includes/installer/i18n/ia.json +++ b/includes/installer/i18n/ia.json @@ -302,6 +302,7 @@ "config-install-subscribe-fail": "Impossibile subscriber a mediawiki-announce: $1", "config-install-subscribe-notpossible": "cURL non es installate e <code>allow_url_fopen</code> non es disponibile.", "config-install-mainpage": "Crea pagina principal con contento predefinite", + "config-install-mainpage-exists": "Le pagina principal jam existe, es omittite", "config-install-extension-tables": "Creation de tabellas pro le extensiones activate", "config-install-mainpage-failed": "Non poteva inserer le pagina principal: $1", "config-install-done": "<strong>Felicitationes!</strong>\nTu ha installate MediaWiki.\n\nLe installator ha generate un file <code>LocalSettings.php</code>.\nIste contine tote le configuration.\n\nEs necessari discargar lo e poner lo in le base del installation wiki (le mesme directorio que index.php).\nLe discargamento debe haber comenciate automaticamente.\n\nSi le discargamento non ha comenciate, o si illo esseva cancellate, recomencia le discargamento con un clic sur le ligamine sequente:\n\n$3\n\n<strong>Nota:</strong> Si tu non discarga iste file de configuration ora, illo non essera disponibile plus tarde.\n\nPost facer isto, tu pote <strong>[$2 entrar in tu wiki]</strong>.", diff --git a/includes/installer/i18n/it.json b/includes/installer/i18n/it.json index b3b78508053a..5453e4533b5c 100644 --- a/includes/installer/i18n/it.json +++ b/includes/installer/i18n/it.json @@ -316,6 +316,7 @@ "config-install-subscribe-fail": "Impossibile sottoscrivere mediawiki-announce: $1", "config-install-subscribe-notpossible": "cURL non è installato e <code>allow_url_fopen</code> non è disponibile.", "config-install-mainpage": "Creazione della pagina principale con contenuto predefinito", + "config-install-mainpage-exists": "La pagina principale già esiste, saltata", "config-install-extension-tables": "Creazione delle tabelle per le estensioni attivate", "config-install-mainpage-failed": "Impossibile inserire la pagina principale: $1", "config-install-done": "<strong>Complimenti!</strong>\nHai installato MediaWiki.\n\nIl programma di installazione ha generato un file <code>LocalSettings.php</code> che contiene tutte le impostazioni.\n\nDevi scaricarlo ed inserirlo nella directory base del tuo wiki (la stessa dove è presente index.php). Il download dovrebbe partire automaticamente.\n\nSe il download non si avvia, o se è stato annullato, puoi riavviarlo cliccando sul collegamento di seguito:\n\n$3\n\n<strong>Nota:</strong> se esci ora dall'installazione senza scaricare il file di configurazione che è stato generato, questo poi non sarà più disponibile in seguito.\n\nQuando hai fatto, puoi <strong>[$2 entrare nel tuo wiki]</strong>.", diff --git a/includes/installer/i18n/ko.json b/includes/installer/i18n/ko.json index 18acff0e8d6f..f292ab353c24 100644 --- a/includes/installer/i18n/ko.json +++ b/includes/installer/i18n/ko.json @@ -11,7 +11,8 @@ "Hwangjy9", "Macofe", "Mooozi", - "Ykhwong" + "Ykhwong", + "Jerrykim306" ] }, "config-desc": "미디어위키를 위한 설치 관리자", @@ -308,6 +309,7 @@ "config-install-subscribe-fail": "미디어위키 알림을 구독할 수 없습니다: $1", "config-install-subscribe-notpossible": "cURL이 설치되어 있지 않고 <code>allow_url_fopen</code>을 사용할 수 없습니다.", "config-install-mainpage": "기본 내용으로 대문을 만드는 중", + "config-install-mainpage-exists": "대문은 이미 존재함, 건너뜀", "config-install-extension-tables": "활성화된 확장 기능을 위한 테이블을 만드는 중", "config-install-mainpage-failed": "대문을 삽입할 수 없습니다: $1", "config-install-done": "<strong>축하합니다!</strong>\n미디어위키를 설치했습니다.\n\n설치 관리자가 <code>LocalSettings.php</code> 파일을 만들었습니다.\n여기에 모든 설정이 포함되어 있습니다.\n\n파일을 다운로드하여 위키 설치의 거점에 넣어야 합니다. (index.php와 같은 디렉터리) 다운로드가 자동으로 시작됩니다.\n\n다운로드가 제공되지 않을 경우나 그것을 취소한 경우에는 아래의 링크를 클릭하여 다운로드를 다시 시작할 수 있습니다:\n\n$3\n\n<strong>참고:</strong> 이 생성한 설정 파일을 다운로드하지 않고 설치를 끝내면 이 파일은 나중에 사용할 수 없습니다.\n\n완료되었으면 <strong>[$2 위키에 들어갈 수 있습니다]</strong>.", @@ -318,5 +320,5 @@ "config-nofile": "\"$1\" 파일을 찾을 수 없습니다. 이미 삭제되었나요?", "config-extension-link": "당신의 위키가 [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Extensions 확장 기능]을 지원한다는 것을 알고 계십니까?\n\n[https://www.mediawiki.org/wiki/Special:MyLanguage/Category:Extensions_by_category 분류별 확장 기능]을 찾아보실 수 있습니다.", "mainpagetext": "<strong>미디어위키가 설치되었습니다.</strong>", - "mainpagedocfooter": "[https://meta.wikimedia.org/wiki/Help:Contents 이곳]에서 위키 소프트웨어에 대한 정보를 얻을 수 있습니다.\n\n== 시작하기 ==\n\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings 설정하기 목록]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ 미디어위키 FAQ]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce 미디어위키 릴리스 메일링 리스트]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation#Translation_resources 내 언어로 미디어위키 지역화]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Combating_spam 당신의 위키에서 스팸에 대처하는 법을 배우세요]" + "mainpagedocfooter": "[https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents 이곳]에서 위키 소프트웨어에 대한 정보를 얻을 수 있습니다.\n\n== 시작하기 ==\n\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings 설정하기 목록]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ 미디어위키 FAQ]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce 미디어위키 릴리스 메일링 리스트]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation#Translation_resources 내 언어로 미디어위키 지역화]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Combating_spam 당신의 위키에서 스팸에 대처하는 법을 배우세요]" } diff --git a/includes/installer/i18n/lb.json b/includes/installer/i18n/lb.json index 10de248ce938..7859d7ae8223 100644 --- a/includes/installer/i18n/lb.json +++ b/includes/installer/i18n/lb.json @@ -199,6 +199,7 @@ "config-install-updates": "Net néideg Aktualiséierungen net maachen", "config-install-sysop": "Administrateur Benotzerkont gëtt ugeluecht", "config-install-mainpage": "Haaptsäit mat Standard-Inhalt gëtt ugeluecht", + "config-install-mainpage-exists": "Haaptsäit gëtt et schonn, iwwersprangen", "config-install-extension-tables": "D'Tabelle fir déi aktivéiert Erweiderunge ginn ugeluecht", "config-install-mainpage-failed": "D'Haaptsäit konnt net dragesat ginn: $1", "config-download-localsettings": "<code>LocalSettings.php</code> eroflueden", diff --git a/includes/installer/i18n/mg.json b/includes/installer/i18n/mg.json index 15ccd0172e39..2a270fc3ffe3 100644 --- a/includes/installer/i18n/mg.json +++ b/includes/installer/i18n/mg.json @@ -11,6 +11,7 @@ "config-localsettings-upgrade": "Hita ny <code>LocalSettings.php</code>.\nMba hanavao ity fametrahana ity, atsofohy ny sandan'i <code>$wgUpgradeKey</code> amin'ny saha eo ambany.\nHo hitanao eo amin'i <code>LocalSettings.php</code> ilay izy.", "config-localsettings-key": "Lakile fanavaozana:", "config-localsettings-badkey": "Diso ilay lakile fanavaozana natsofokao.", + "config-localsettings-connection-error": "Nisy hadisoana nitranga teo am-pametrahana ny fifandraisana amin'ny banky angona miaraka amin'ny parametatra voalaza ao amin'i <code>LocalSettings.php</code>. Vahao ny olan'ireo parametatra ireo dia avereno fanindroany.\n\n$1", "config-session-error": "Hadisoana teo am-panombohana ny fidirana : $1", "config-your-language": "Ny fiteninao :", "config-your-language-help": "Hifidy ny teny ilaina amin'ny piraosesy fametrahana.", @@ -44,6 +45,10 @@ "config-env-hhvm": "Misy ato HHVM $1.", "config-unicode-using-intl": "Mampiasa ny [http://pecl.php.net/intl itatra PECL intl] ho an'ny fampifenerana Unicode.", "config-unicode-pure-php-warning": "<strong>Fampitandremana: </strong> Ny [http://pecl.php.net/intl itatra PECL intl] dia tsy misy mba hahazakana ny fampifenerana Unicode, ka mitontona amin'ny implementasiona PHP ranoray noho ny tsifisiany.\nRaha hametraka tranonkala be mpamangy ianao dia tokony mamaky ny [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations ''Unicode normalization''] (amin'ny teny anglisy)", + "config-db-type": "Karazana banky angona:", + "config-db-host": "Anaran'ny lohamilin'ny banky angona:", + "config-db-host-help": "Raha lohamila hafa ny lohamilin'ny banky angona, dia atsofohy eto ny anaran'ilay lohamilina na ny adiresy IP-ny.\n\nRaha mampiasa fampiantranoana iombonana ianao dia tokony hanome anao ny anaran-dohamilina izy ao amin'ny toromariny.\n\nRaha mametraka amin'ny lohamilina Windows ianao sady mampiasa MySQL, dia mety tsy mandeha ny anaran-dohamilina \"localhost\". Raha tsy mandeha ilay izy dia \"127.0.0.1\" no atao adiresy IP an-toerana.\n\nRaha mampiasa PostgreSQL ianao, dia avelaho ho fotsy ity saha ity ahafahana mifandray amin'ny alalan'ny socket Unix.", + "config-db-host-oracle": "TNS an'ny banky angona:", "config-db-username": "Anaram-pikamban'ny banky angona :", "config-db-password": "Tenimiafin'ny banky angona :", "config-db-prefix": "Tovom-banky angona:", diff --git a/includes/installer/i18n/mk.json b/includes/installer/i18n/mk.json index b0b78db3ad40..b8dab2192439 100644 --- a/includes/installer/i18n/mk.json +++ b/includes/installer/i18n/mk.json @@ -86,7 +86,7 @@ "config-db-host-oracle-help": "Внесете важечко [http://download.oracle.com/docs/cd/B28359_01/network.111/b28317/tnsnames.htm месно име за поврзување]. На оваа воспоставка мора да ѝ биде видлива податотеката tnsnames.ora.<br />Ако користите клиентски библиотеки 10g или понови, тогаш можете да го користите и методот на иметнување на [http://download.oracle.com/docs/cd/E11882_01/network.112/e10836/naming.htm Easy Connect].", "config-db-wiki-settings": "Идентификувај го викиво", "config-db-name": "Име на базата:", - "config-db-name-help": "Одберете име што ќе го претставува вашето вики.\nИмето не смее да содржи празни места.\n\nАко користите заедничко (споделено) вдомување, тогаш вашиот вдомител ќе ви даде конкретно име на база за користење, или пак ќе ви даде да создавате бази преку контролната табла.", + "config-db-name-help": "Одберете име што ќе го претставува вашето вики.\nИмето не смее да содржи празни места.\n\nАко користите заедничко (споделено) вдомување, тогаш вашиот вдомител ќе ви даде конкретно име на база за користење, или пак ќе ви даде да создавате бази преку управувачницата.", "config-db-name-oracle": "Шема на базата:", "config-db-account-oracle-warn": "Постојат три поддржани сценарија за воспоставка на Oracle како базен услужник:\n\nАко сакате да создадете сметка на базата како дел од постапката за воспоставка, наведете сметка со SYSDBA-улога како сметка за базата што ќе се воспостави и наведете ги саканите податоци за сметката за мрежен пристап. Во друг случај, можете да создадете сметка за мрежен пристап рачно и да ја наведете само таа сметка (ако има дозволи за создавање на шематски објекти) или пак да наведете две различни сметки, една со привилегии за создавање, а друга (ограничена) за мрежен пристап.\n\nСкриптата за создавање сметка со задолжителни привилегии ќе ја најдете во папката „maintenance/oracle/“ од оваа воспоставка. Имајте на ум дека ако користите ограничена сметка ќе ги оневозможите сите функции за одржување со основната сметка.", "config-db-install-account": "Корисничка смета за воспоставка", @@ -214,9 +214,9 @@ "config-profile-help": "Викијата функционираат најдобро кога имаат што повеќе уредници.\nВо МедијаВики лесно се проверуваат скорешните промени, и лесно се исправа (технички: „враќа“) штетата направена од неупатени или злонамерни корисници.\n\nМногумина имаат најдено најразлични полезни примени за МедијаВики, но понекогаш не е лесно да убедите некого во предностите на вики-концептот.\nЗначи имате избор.\n\n'''{{int:config-profile-wiki}}''' — модел според кој секој може да уредува, дури и без најавување.\nАко имате вики со '''задолжително отворање на сметка''', тогаш добивате повеќе контрола, но ова може даги одврати спонтаните учесници.\n\n'''{{int:config-profile-fishbowl}}''' — може да уредуваат само уредници што имаат добиено дозвола за тоа, но јавноста може да ги гледа страниците, вклучувајќи ја нивната историја.\n'''{{int:config-profile-private}}''' — страниците се видливи и уредливи само за овластени корисници.\n\nПо воспоставката имате на избор и посложени кориснички права и поставки. Погледајте во [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:User_rights прирачникот].", "config-license": "Авторски права и лиценца:", "config-license-none": "Без подножје за лиценца", - "config-license-cc-by-sa": "Криејтив комонс НаведиИзвор СподелиПодИстиУслови", - "config-license-cc-by": "Криејтив комонс НаведиИзвор", - "config-license-cc-by-nc-sa": "Криејтив комонс НаведиИзвор-Некомерцијално-СподелиПодИстиУслови", + "config-license-cc-by-sa": "Криејтив комонс Наведи извор-Сподели под исти услови", + "config-license-cc-by": "Криејтив комонс Наведи извор", + "config-license-cc-by-nc-sa": "Криејтив комонс Наведи извор-Сподели под исти услови", "config-license-cc-0": "Криејтив комонс Нула (јавна сопственост)", "config-license-gfdl": "ГНУ-ова лиценца за слободна документација 1.3 или понова", "config-license-pd": "Јавна сопственост", @@ -244,7 +244,7 @@ "config-logo-help": "Матичното руво на МедијаВики има простор за лого од 135x160 пиксели над страничникот.\n\nМожете да употребите <code>$wgStylePath</code> или <code>$wgScriptPath</code> ако вашето лого е релативно на тие патеки.\n\nАко не сакате да имате лого, тогаш оставете го ова поле празно.", "config-instantcommons": "Овозможи Instant Commons", "config-instantcommons-help": "[https://www.mediawiki.org/wiki/InstantCommons Instant Commons] е функција која им овозможува на викијата да користат слики, звучни записи и други мултимедијални содржини од [https://commons.wikimedia.org/ Ризницата].\nЗа да може ова да работи, МедијаВики бара пристап до семрежјето.\n\nЗа повеќе информации за оваа функција и напатствија за нејзино поставување на вики (сите други освен Ризницата), коносултирајте го [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:$wgForeignFileRepos прирачникот].", - "config-cc-error": "Изборникот на лиценци од Криејтив комонс не даде резултати.\nВнесете го името на лиценцата рачно.", + "config-cc-error": "Изборникот на лиценци од Криејтив комонс не даде лиценца.\nВнесете го името на лиценцата рачно.", "config-cc-again": "Одберете повторно...", "config-cc-not-chosen": "Одберете ја саканата лиценца од Криејтив комонс и стиснете на „proceed“.", "config-advanced-settings": "Напредни нагодувања", @@ -302,6 +302,7 @@ "config-install-subscribe-fail": "Не можам да ве претплатам на известувањето mediawiki-announce: $1", "config-install-subscribe-notpossible": "cURL не е воспоставен, а <code>allow_url_fopen</code> не е достапно.", "config-install-mainpage": "Создавам главна страница со стандардна содржина", + "config-install-mainpage-exists": "Главната страница веќе постои. Преоѓам на следно.", "config-install-extension-tables": "Изработка на табели за овозможени додатоци", "config-install-mainpage-failed": "Не можев да вметнам главна страница: $1", "config-install-done": "<strong>Честитаме!</strong>\nУспешно го воспоставивте МедијаВики.\n\nВоспоставувачот создаде податотека <code>LocalSettings.php</code>.\nТаму се содржат сите ваши нагодувања.\n\nЌе треба да ја преземете и да ја ставите во основата на воспоставката (истата папка во која се наоѓа index.php). Преземањето треба да е започнато автоматски.\n\nАко не ви е понудено преземање, или пак ако сте го откажале, можете да го почнете одново стискајќи на следнава врска:\n\n$3\n\n<strong>Напомена</strong>: Ако ова не го направите сега, податотеката со поставки повеќе нема да биде на достапна.\n\nОткога ќе завршите со тоа, можете да <strong>[$2 влезете на вашето вики]</strong>.", diff --git a/includes/installer/i18n/nb.json b/includes/installer/i18n/nb.json index 46156b90d65a..b7a7289a8a3c 100644 --- a/includes/installer/i18n/nb.json +++ b/includes/installer/i18n/nb.json @@ -308,6 +308,7 @@ "config-install-subscribe-fail": "Ikke mulig å abonnere på mediawiki-announce: $1", "config-install-subscribe-notpossible": "cURL er ikke installert og <code>allow_url_fopen</code> er ikke tilgjengelig.", "config-install-mainpage": "Oppretter hovedside med standard innhold", + "config-install-mainpage-exists": "Hovedsiden eksisterer allerede, hopper over", "config-install-extension-tables": "Oppretter tabeller for aktiviserte utvidelser", "config-install-mainpage-failed": "Kunne ikke sette inn hovedside: $1", "config-install-done": "<strong>Gratulrerer!</strong>\nDu har lykkes i å installere MediaWiki.\n\nInstallasjonsprogrammet har generert en <code>LocalSettings.php</code>-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\n<strong>OBS:</strong> 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 <strong>[$2 gå inn i wikien]</strong>.", diff --git a/includes/installer/i18n/nl.json b/includes/installer/i18n/nl.json index 24c3e3b519d1..a4995538af4c 100644 --- a/includes/installer/i18n/nl.json +++ b/includes/installer/i18n/nl.json @@ -18,7 +18,8 @@ "JaapDeKleine", "Macofe", "Hex", - "Mainframe98" + "Mainframe98", + "Rcdeboer" ] }, "config-desc": "Het installatieprogramma voor MediaWiki", @@ -71,7 +72,7 @@ "config-outdated-sqlite": "''' Waarschuwing:''' u gebruikt SQLite $1. SQLite is niet beschikbaar omdat de minimaal vereiste versie $2 is.", "config-no-fts3": "<strong>Waarschuwing:</strong> SQLite is gecompileerd zonder de module [//sqlite.org/fts3.html FTS3]; zoekfuncties zijn niet beschikbaar.", "config-pcre-old": "'''Onherstelbare fout:''' PCRE $1 of een latere versie is vereist.\nUw uitvoerbare versie van PHP is gekoppeld met PCRE $2.\n[https://www.mediawiki.org/wiki/Manual:Errors_and_symptoms/PCRE Meer informatie].", - "config-pcre-no-utf8": "'''Fataal:''' de module PRCE van PHP lijkt te zijn gecompileerd zonder ondersteuning voor PCRE_UTF8.\nMediaWiki heeft ondersteuning voor UTF-8 nodig om correct te kunnen werken.", + "config-pcre-no-utf8": "<strong>Onherstelbare fout:</strong> de module PRCE van PHP lijkt te zijn gecompileerd zonder ondersteuning voor PCRE_UTF8.\nMediaWiki heeft ondersteuning voor UTF-8 nodig om correct te kunnen werken.", "config-memory-raised": "PHP's <code>memory_limit</code> is $1 en is verhoogd tot $2.", "config-memory-bad": "'''Waarschuwing:''' PHP's <code>memory_limit</code> is $1.\nDit is waarschijnlijk te laag.\nDe installatie kan mislukken!", "config-xcache": "[http://xcache.lighttpd.net/ XCache] is op dit moment geïnstalleerd", @@ -79,7 +80,7 @@ "config-apcu": "[http://www.php.net/apcu APCu] is geïnstalleerd", "config-wincache": "[http://www.iis.net/download/WinCacheForPhp WinCache] is op dit moment geïnstalleerd", "config-no-cache-apcu": "<strong>Waarschuwing:</strong> [http://www.php.net/apcu APCu], [http://xcache.lighttpd.net/ XCache] of [http://www.iis.net/download/WinCacheForPhp WinCache] is niet aangetroffen.\nHet cachen van objecten is niet ingeschakeld.", - "config-mod-security": "'''Waarschuwing:''' uw webserver heeft de module [http://modsecurity.org/ mod_security] ingeschakeld. Als deze onjuist is ingesteld, kan dit problemen geven in combinatie met MediaWiki of andere software die gebruikers in staat stelt willekeurige inhoud te posten.\nLees de [http://modsecurity.org/documentation/ documentatie over mod_security] of neem contact op met de helpdesk van uw provider als u tegen problemen aanloopt.", + "config-mod-security": "<strong>Waarschuwing:</strong> Uw webserver heeft de module [http://modsecurity.org/ mod_security]/mod_security2 ingeschakeld. Veel standaard instellingen hiervan zorgen voor problemen in combinatie met MediaWiki en andere software die gebruikers in staat stelt willekeurige inhoud te posten.\nIndien mogelijk, zou deze moeten worden uitgeschakeld. Lees anders de [http://modsecurity.org/documentation/ documentatie over mod_security] of neem contact op met de helpdesk van uw provider als u tegen problemen aanloopt.", "config-diff3-bad": "GNU diff3 niet aangetroffen.", "config-git": "Versiecontrolesoftware git is aangetroffen: <code>$1</code>", "config-git-bad": "Geen git versiecontrolesoftware aangetroffen.", @@ -226,7 +227,7 @@ "config-profile-no-anon": "Account aanmaken verplicht", "config-profile-fishbowl": "Alleen voor geautoriseerde bewerkers", "config-profile-private": "Privéwiki", - "config-profile-help": "Wiki's werken het beste als ze door zoveel mogelijk gebruikers worden bewerkt.\nIn MediaWiki is het eenvoudig om de recente wijzigingen te controleren en eventuele foutieve of kwaadwillende bewerkingen terug te draaien.\n\nDaarnaast vinden velen MediaWiki goed inzetbaar in vele andere rollen, en soms is het niet handig om helemaal \"op de wikimanier\" te werken.\nDaarom biedt dit installatieprogramma u de volgende keuzes voor de basisinstelling van gebruikersvrijheden:\n\nHet profiel '''{{int:config-profile-wiki}}''' staat iedereen toe te bewerken, zonder zelfs aan te melden.\nEen wiki met '''{{int:config-profile-no-anon}}\" biedt extra verantwoordelijkheid, maar kan afschrikken toevallige gebruikers afschrikken.\n\nHet scenario '''{{int:config-profile-fishbowl}}''' laat gebruikers waarvoor dat is ingesteld bewerkt, maar andere gebruikers kunnen alleen pagina's bekijken, inclusief de bewerkingsgeschiedenis.\nIn een '''{{int:config-profile-private}}''' kunnen alleen goedgekeurde gebruikers pagina's bekijken en bewerken.\n\nMeer complexe instellingen voor gebruikersrechten zijn te maken na de installatie; hierover is meer te lezen in de [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:User_rights handleiding].", + "config-profile-help": "Wiki's werken het beste als ze door zoveel mogelijk gebruikers worden bewerkt.\nIn MediaWiki is het eenvoudig om de recente wijzigingen te controleren en eventuele foutieve of kwaadwillende bewerkingen terug te draaien.\n\nDaarnaast vinden velen MediaWiki goed inzetbaar in vele andere rollen, en soms is het niet handig om helemaal \"op de wikimanier\" te werken.\nDaarom biedt dit installatieprogramma u de volgende keuzes voor de basisinstelling van gebruikersvrijheden:\n\nHet profiel '''{{int:config-profile-wiki}}''' staat iedereen toe te bewerken, zonder zelfs aan te melden.\nEen wiki met '''{{int:config-profile-no-anon}}''' biedt extra verantwoordelijkheid, maar kan toevallige gebruikers afschrikken.\n\nHet scenario '''{{int:config-profile-fishbowl}}''' laat gebruikers waarvoor dat is ingesteld bewerken, maar andere gebruikers kunnen alleen pagina's bekijken, inclusief de bewerkingsgeschiedenis.\nIn een '''{{int:config-profile-private}}''' kunnen alleen goedgekeurde gebruikers pagina's bekijken en bewerken.\n\nMeer complexe instellingen voor gebruikersrechten zijn te maken na de installatie; hierover is meer te lezen in de [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:User_rights handleiding].", "config-license": "Auteursrechten en licentie:", "config-license-none": "Geen licentie in de voettekst", "config-license-cc-by-sa": "Creative Commons Naamsvermelding-Gelijk delen", @@ -317,6 +318,7 @@ "config-install-subscribe-fail": "Het is niet mogelijk te abonneren op mediawiki-announce: $1", "config-install-subscribe-notpossible": "cURL is niet geïnstalleerd en <code>allow_url_fopen</code> is niet beschikbaar.", "config-install-mainpage": "Hoofdpagina aanmaken met standaard inhoud", + "config-install-mainpage-exists": "Hoofdpagina bestaat al, overslaan", "config-install-extension-tables": "Tabellen voor ingeschakelde uitbreidingen worden aangemaakt", "config-install-mainpage-failed": "Het was niet mogelijk de hoofdpagina in te voegen: $1", "config-install-done": "<strong>Gefeliciteerd!</strong>\nU hebt MediaWiki geïnstalleerd.\n\nHet installatieprogramma heeft het bestand <code>LocalSettings.php</code> aangemaakt.\nDit bevat al uw instellingen.\n\nU moet het bestand downloaden en in de hoofdmap van uw wiki-installatie plaatsen, in dezelfde map als index.php.\nDe download zou automatisch moeten zijn gestart.\n\nAls de download niet is gestart of als u de download hebt geannuleerd, dan kunt u de download opnieuw starten door op de onderstaande koppeling te klikken:\n\n$3\n\n<strong>Let op:</strong> als u dit niet nu doet, dan is het bestand als u later de installatieprocedure afsluit zonder het bestand te downloaden niet meer beschikbaar.\n\nNa het plaatsen van het bestand met instellingen kunt u <strong>[$2 uw wiki gebruiken]</strong>.", diff --git a/includes/installer/i18n/pl.json b/includes/installer/i18n/pl.json index f224b5de23f7..d712afe102f6 100644 --- a/includes/installer/i18n/pl.json +++ b/includes/installer/i18n/pl.json @@ -318,6 +318,7 @@ "config-install-subscribe-fail": "Nie można zapisać na listę „mediawiki-announce” – $1", "config-install-subscribe-notpossible": "cURL nie jest zainstalowany, więc <code>allow_url_fopen</code> nie jest dostępne.", "config-install-mainpage": "Tworzenie strony głównej z domyślną zawartością", + "config-install-mainpage-exists": "Strona główna już istnieje, pomijanie", "config-install-extension-tables": "Tworzenie tabel dla aktywnych rozszerzeń", "config-install-mainpage-failed": "Nie udało się wstawić strony głównej: $1", "config-install-done": "<strong>'''Gratulacje!</strong>\nUdało Ci się zainstalować MediaWiki.\n\nInstalator wygenerował plik konfiguracyjny <code>LocalSettings.php</code>.\n\nMusisz go pobrać i umieścić w katalogu głównym Twojej instalacji wiki (tym samym katalogu co index.php). Pobieranie powinno zacząć się automatycznie.\n\nJeżeli pobieranie nie zostało zaproponowane lub jeśli użytkownik je anulował, można ponownie uruchomić pobranie klikając poniższe łącze:\n\n$3\n\n<strong>Uwaga</strong>: Jeśli nie zrobisz tego teraz, wygenerowany plik konfiguracyjny nie będzie już dostępny po zakończeniu instalacji.\n\nPo załadowaniu pliku konfiguracyjnego możesz <strong>[$2 wejść na wiki]</strong>.", @@ -328,5 +329,5 @@ "config-nofile": "Nie udało się odnaleźć pliku \"$1\". Czy nie został usunięty?", "config-extension-link": "Czy wiesz, że twoja wiki obsługuje [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Extensions rozszerzenia]?\n\nMożesz przejrzeć [https://www.mediawiki.org/wiki/Category:Extensions_by_category rozszerzenia według kategorii] lub [https://www.mediawiki.org/wiki/Extension_Matrix Extension Matrix], aby zobaczyć pełną listę rozszerzeń.", "mainpagetext": "<strong>Instalacja MediaWiki powiodła się.</strong>", - "mainpagedocfooter": "Zapoznaj się z [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents informacjami o działaniu oprogramowania wiki].\n\n== Na początek ==\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings Lista ustawień konfiguracyjnych]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ MediaWiki FAQ]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Komunikaty o nowych wersjach MediaWiki (lista dyskusyjna)]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation#Translation_resources Przetłumacz MediaWiki na swój język]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Combating_spam Dowiedz się, jak walczyć ze spamem na swojej wiki]" + "mainpagedocfooter": "Zapoznaj się z [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents Podręcznikiem użytkownika] zawierającym informacje o tym jak korzystać z oprogramowania wiki.\n\n== Na początek ==\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings Lista ustawień konfiguracyjnych]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ MediaWiki FAQ]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Komunikaty o nowych wersjach MediaWiki (lista dyskusyjna)]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation#Translation_resources Przetłumacz MediaWiki na swój język]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Combating_spam Dowiedz się, jak walczyć ze spamem na swojej wiki]" } diff --git a/includes/installer/i18n/pt-br.json b/includes/installer/i18n/pt-br.json index fd8a2633b7e9..35fa8e8d7877 100644 --- a/includes/installer/i18n/pt-br.json +++ b/includes/installer/i18n/pt-br.json @@ -20,7 +20,8 @@ "Walesson", "Almondega", "Luk3", - "Eduardo Addad de Oliveira" + "Eduardo Addad de Oliveira", + "Warley Felipe C." ] }, "config-desc": "O instalador do MediaWiki", @@ -68,7 +69,7 @@ "config-env-hhvm": "HHVM $1 está instalado.", "config-unicode-using-intl": "Usando a [http://pecl.php.net/intl extensão intl PECL] para a normalização Unicode.", "config-unicode-pure-php-warning": "<strong>Aviso</strong>: A [http://pecl.php.net/intl extensão intl PECL] não está disponível para efetuar a normalização Unicode, abortando e passando para a lenta implementação de PHP puro.\nSe o seu site tem um alto volume de tráfego, informe-se sobre a [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations normalização Unicode].", - "config-unicode-update-warning": "<strong>Aviso:</strong> A versão instalada do wrapper de normalização Unicode usa uma versão mais antiga da biblioteca do [//www.site.icu-project.org/projeto ICU].\nVocê deve [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations atualizar] se você tem quaisquer preocupações com o uso do Unicode.", + "config-unicode-update-warning": "<strong>Aviso:</strong> A versão instalada do wrapper de normalização Unicode usa uma versão mais antiga da biblioteca do [http://www.site.icu-project.org/projeto ICU].\nVocê deve [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations atualizar] se você tem quaisquer preocupações com o uso do Unicode.", "config-no-db": "Não foi possível encontrar um driver apropriado para a banco de dados! Você precisa instalar um driver de banco de dados para PHP. {{PLURAL:$2|É aceite o seguinte tipo|São aceites os seguintes tipos}} de banco de dados: $1.\n\nSe compilou o PHP você mesmo, reconfigure-o com um cliente de banco de dados ativado, por exemplo, usando <code>./configure --with-mysqli</code>.\nSe instalou o PHP a partir de um pacote Debian ou Ubuntu, então também precisa instalar, por exemplo, o pacote <code>php5-mysql</code>.", "config-outdated-sqlite": "<strong>Aviso:</strong> você tem o SQLite versão $1, que é menor do que a versão mínima necessária $2. O SQLite não estará disponível.", "config-no-fts3": "<strong>Aviso</strong> O SQLite foi compilado sem o [//sqlite.org/fts3.html módulo FTS3], as funcionalidades de pesquisa não estarão disponíveis nesta instalação.", @@ -92,8 +93,10 @@ "config-no-cli-uri": "<strong>Aviso:</strong> Nenhum <code>--scriptpath</code> foi especificado, usando o padrão: <code>$1</code>.", "config-using-server": "Utilizando o nome do servidor \"<nowiki>$1</nowiki>\".", "config-using-uri": "Usando URL do servidor \"<nowiki>$1$2</nowiki>\".", + "config-uploads-not-safe": "<strong>Aviso:</strong> O seu diretório atual de envios <code>$1</code> está vulnerável a execuções de script arbitrárias.\nEmbora o MediaWiki verifique todos os arquivos enviados por ameaças de segurança, é altamente recomendável [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Security#Upload_security evitar essa vulnerabilidade de segurança] antes de permitir envios.", "config-no-cli-uploads-check": "<strong>Atenção:</strong> O seu diretório padrão para envios (<code>$1</code>) não está marcado para vulnerabilidade\npara execução de script arbitrário durante a instalação do CLI.", "config-brokenlibxml": "O sistema tem uma combinação de PHP e libxml2 que é conflitante e pode causar corrupção de dados ocultos no MediaWiki e outros aplicativos da web.\nAtualize para o libxml2 2.7.3 ou mais recente ([https://bugs.php.net/bug.php?id=45996 bugs com o PHP]).\nInstalação abortada.", + "config-suhosin-max-value-length": "O Suhosin está instalado e limita o parâmetro GET <code>length</code> para $1 bytes. O componente ResourceLoader trabalhará em torno deste limite, mas rebaixará a performance.\nSe possível, defina <code>suhosin.get.max_value_length</code> em <code>php.ini</code> para 1024 ou mais, e defina <code>$wgResourceLoaderMaxQueryLength</code> em <code>LocalSettings.php</code> para o mesmo valor.", "config-db-type": "Tipo de base de dados:", "config-db-host": "Servidor da base de dados:", "config-db-host-help": "Se a base de dados do seu servidor está em um servidor diferente, digite o nome do hospedeiro ou o endereço IP aqui.\n\nSe você está utilizando um hospedeiro web compartilhado, o seu provedor de hospedagem deverá fornecer o nome do hospedeiro correto na sua documentação.\n\nSe você está instalando em um servidor Windows e usando o MySQL, usar \"localhost\" pode não funcionar para o nome de servidor. Se não funcionar, tente \"127.0.01\" para o endereço de IP local.\n\nSe você está usando PostgreSQl, deixe este campo em branco para se conectar através de um socket Unix.", @@ -253,6 +256,7 @@ "config-install-pg-commit": "Enviando alterações", "config-install-user": "Criando usuário de banco de dados", "config-install-user-alreadyexists": "O usuário \"$1\" já existe!", + "config-install-user-create-failed": "Criando usuário \"$1\" falhou: $2", "config-install-user-missing": "O usuário especificado, \"$1\", não existe.", "config-install-user-missing-create": "O usuário especificado \" $1 \" não existe.\nPor favor, clique na opção de \"criar conta\" abaixo se você deseja criá-lo.", "config-install-tables": "Criando tabelas", @@ -267,6 +271,7 @@ "config-install-subscribe-fail": "Não foi possível subscrever o mediawiki-announce: $1", "config-install-subscribe-notpossible": "cURL não está instalada e <code>allow_url_fopen</code> não está disponível.", "config-install-mainpage": "Criando página principal com o conteúdo padrão", + "config-install-mainpage-exists": "A página principal já existe, pulando", "config-install-extension-tables": "Criando tabelas para extensões habilitadas", "config-install-mainpage-failed": "Não foi possível inserir a página principal: $1", "config-install-done": "<strong>Parabéns!</strong>\nVocê instalou do MediaWiki.\n\nO instalador gerou um arquivo <code>LocalSettings.php</code>.\nEste arquivo contém todas as suas configurações.\n\nVocê precisa fazer o download desse arquivo e colocá-lo na raiz da sua instalação (o mesmo diretório onde está o arquivo <code>index.php</code>). Este download deve ter sido iniciado automaticamente.\n\nSe o download não foi iniciado, ou se ele foi cancelado, pode recomeçá-lo clicando no link abaixo:\n\n$3\n\n<strong>Nota</strong>: Se não fizer isto agora, o arquivo que foi gerado não estará disponível depois que você sair do processo de instalação sem baixá-lo.\n\nQuando isso tiver sido feito, pode <strong>[$2 entrar na sua wiki]</strong>.", diff --git a/includes/installer/i18n/pt.json b/includes/installer/i18n/pt.json index cc8f993cfcf7..779a32fe8fb9 100644 --- a/includes/installer/i18n/pt.json +++ b/includes/installer/i18n/pt.json @@ -65,21 +65,21 @@ "config-env-php": "O PHP $1 está instalado.", "config-env-hhvm": "HHVM $1 está instalado.", "config-unicode-using-intl": "A usar a [http://pecl.php.net/intl extensão intl PECL] para a normalização Unicode.", - "config-unicode-pure-php-warning": "'''Aviso''': A [http://pecl.php.net/intl extensão intl PECL] não está disponível para efetuar a normalização Unicode. Irá recorrer-se à implementação em PHP puro, que é mais lenta.\nSe o seu site tem alto volume de tráfego, devia informar-se um pouco sobre a [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations/pt normalização Unicode].", - "config-unicode-update-warning": "'''Aviso''': A versão instalada do wrapper de normalização Unicode usa uma versão mais antiga da biblioteca do [http://site.icu-project.org/ projeto ICU].\nDevia [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations atualizá-la] se tem quaisquer preocupações sobre o uso do Unicode.", + "config-unicode-pure-php-warning": "<strong>Aviso:</strong> A [http://pecl.php.net/intl extensão intl PECL] não está disponível para efetuar a normalização Unicode. Irá recorrer-se à implementação em PHP puro, que é mais lenta.\nSe o seu site tem alto volume de tráfego, devia informar-se um pouco sobre a [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations/pt normalização Unicode].", + "config-unicode-update-warning": "<strong>Aviso:</strong> A versão instalada do wrapper de normalização Unicode usa uma versão mais antiga da biblioteca do [http://site.icu-project.org/ projeto ICU].\nDevia [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations atualizá-la] se tem quaisquer preocupações sobre o uso do Unicode.", "config-no-db": "Não foi possível encontrar um controlador apropriado da base de dados! Precisa de instalar um controlador da base de dados para o PHP. {{PLURAL:$2|É aceite o seguinte tipo|São aceites os seguintes tipos}} de base de dados: $1.\n\nSe fez a compilação do PHP, reconfigure-o com um cliente de base de dados ativado; por exemplo, usando <code>./configure --with-mysqli</code>.\nSe instalou o PHP a partir de um pacote Debian ou Ubuntu, então precisa de instalar também, por exemplo, o pacote <code>php5-mysql</code>.", - "config-outdated-sqlite": "'''Aviso''': Tem a versão $1 do SQLite, que é anterior à versão mínima necessária, a $2. O SQLite não estará disponível.", - "config-no-fts3": "'''Aviso''': O SQLite foi compilado sem o módulo [//sqlite.org/fts3.html FTS3]; as funcionalidades de pesquisa não estarão disponíveis nesta instalação.", + "config-outdated-sqlite": "<strong>Aviso:</strong> Tem a versão $1 do SQLite, que é anterior à versão mínima necessária, a $2. O SQLite não estará disponível.", + "config-no-fts3": "<strong>Aviso:</strong> O SQLite foi compilado sem o módulo [//sqlite.org/fts3.html FTS3]; as funcionalidades de pesquisa não estarão disponíveis nesta instalação.", "config-pcre-old": "<strong>Erro fatal:</strong> É necessário o PCRE $1 ou versão posterior.\nO <i>link</i> do seu binário PHP foi feito com o PCRE $2.\n[https://www.mediawiki.org/wiki/Manual:Errors_and_symptoms/PCRE Mais informações].", "config-pcre-no-utf8": "'''Erro fatal''': O módulo PCRE do PHP parece ter sido compilado sem suporte PCRE_UTF8.\nO MediaWiki necessita do suporte UTF-8 para funcionar corretamente.", "config-memory-raised": "A configuração <code>memory_limit</code> do PHP era $1; foi aumentada para $2.", - "config-memory-bad": "'''Aviso:''' A configuração <code>memory_limit</code> do PHP é $1.\nIsto é provavelmente demasiado baixo.\nA instalação poderá falhar!", + "config-memory-bad": "<strong>Aviso:</strong> A configuração <code>memory_limit</code> do PHP é $1.\nIsto é provavelmente demasiado baixo.\nA instalação poderá falhar!", "config-xcache": "[http://xcache.lighttpd.net/ XCache] instalada", "config-apc": "[http://www.php.net/apc APC] instalada", - "config-apcu": "[http://www.php.net/apcu APCu] está instalado", + "config-apcu": "[http://www.php.net/apcu APCu] instalado", "config-wincache": "[http://www.iis.net/download/WinCacheForPhp WinCache] instalada", "config-no-cache-apcu": "<strong>Aviso:</strong> Não foram encontrados o [http://www.php.net/apcu APCu], o [http://xcache.lighttpd.net/ XCache] ou o [http://www.iis.net/download/WinCacheForPhp WinCache].\nA cache de objetos não está ativa.", - "config-mod-security": "'''Aviso''': O seu servidor de internet tem o [http://modsecurity.org/ mod_security] ativado. Se este estiver mal configurado, pode causar problemas ao MediaWiki ou a outros programas, permitindo que os utilizadores publiquem conteúdos arbitrários.\nConsulte a [http://modsecurity.org/documentation/ mod_security documentação] ou peça apoio ao fornecedor do alojamento do seu servidor se encontrar erros aleatórios.", + "config-mod-security": "<strong>Aviso:</strong> O seu servidor de Internet tem o [http://modsecurity.org/ mod_security]/mod_security2 ativado. Muitas das suas configurações normais podem causar problemas ao MediaWiki e a outros programas, permitindo que os utilizadores publiquem conteúdos arbitrários.\nSe possível, isto deve ser desativado. Se não, consulte a [http://modsecurity.org/documentation/ mod_security documentação] ou peça apoio ao fornecedor do alojamento do seu servidor se encontrar erros aleatórios.", "config-diff3-bad": "O GNU diff3 não foi encontrado.", "config-git": "Foi encontrado o software de controlo de versões Git: <code>$1</code>.", "config-git-bad": "Não foi encontrado o software de controlo de versões Git.", @@ -87,12 +87,12 @@ "config-gd": "Foi encontrada a biblioteca gráfica GD.\nSe possibilitar uploads, a miniaturização de imagens será ativada.", "config-no-scaling": "Não foi encontrada a biblioteca gráfica GD nem o ImageMagick.\nA miniaturização de imagens será desativada.", "config-no-uri": "<strong>Erro:</strong> Não foi possível determinar o URI atual.\nA instalação foi abortada.", - "config-no-cli-uri": "'''Aviso''': Não foi especificado um <code>--scriptpath</code>; por omissão, será usado: <code>$1</code>.", + "config-no-cli-uri": "<strong>Aviso:</strong> Não foi especificado um <code>--scriptpath</code>; por omissão, será usado: <code>$1</code>.", "config-using-server": "Será usado o nome do servidor \"<nowiki>$1</nowiki>\".", "config-using-uri": "Será usado o URL do servidor \"<nowiki>$1$2</nowiki>\".", - "config-uploads-not-safe": "'''Aviso:''' O diretório por omissão para carregamentos <code>$1</code>, está vulnerável à execução arbitrária de scripts.\nEmbora o MediaWiki verifique a existência de ameaças de segurança em todos os ficheiros enviados, é altamente recomendado que [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Security#Upload_security vede esta vulnerabilidade de segurança] antes de possibilitar uploads.", - "config-no-cli-uploads-check": "'''Aviso:''' O diretório por omissão para carregamentos, <code>$1</code>, não é verificado para determinar se é vulnerável à execução de código arbitrário durante a instalação por CLI (\"Command-line Interface\").", - "config-brokenlibxml": "O seu sistema tem uma combinação de versões do PHP e do libxml2 conhecida por ser problemática, podendo causar corrupção de dados no MediaWiki e noutras aplicações da internet.\nAtualize para a versão 2.7.3 ou posterior do libxml2 ([https://bugs.php.net/bug.php?id=45996 incidência reportada no PHP]).\nInstalação cancelada.", + "config-uploads-not-safe": "<strong>Aviso:</strong> O diretório por omissão para carregamentos, <code>$1</code>, está vulnerável à execução arbitrária de listas de comandos (scripts).\nEmbora o MediaWiki verifique a existência de ameaças de segurança em todos os ficheiros enviados, é altamente recomendado que [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Security#Upload_security vede esta vulnerabilidade de segurança] antes de possibilitar uploads.", + "config-no-cli-uploads-check": "<strong>Aviso:</strong> O diretório por omissão para carregamentos, <code>$1</code>, não é verificado para determinar se é vulnerável à execução de listas arbitrárias de comandos durante a instalação por CLI (\"Command-line Interface\").", + "config-brokenlibxml": "O seu sistema tem uma combinação de versões do PHP e do libxml2 conhecida por ser problemática, podendo causar corrupção de dados no MediaWiki e noutras aplicações da Internet.\nAtualize para a versão 2.7.3 ou posterior do libxml2 ([https://bugs.php.net/bug.php?id=45996 incidência reportada no PHP]).\nInstalação cancelada.", "config-suhosin-max-value-length": "O Suhosin está instalado e limita o parâmetro GET <code>length</code> a $1 bytes.\nO componente ResourceLoader do MediaWiki consegue exceder este limite, mas prejudicando o desempenho.\nSe lhe for possível, deve atribuir ao parâmetro <code>suhosin.get.max_value_length</code> o valor 1024 ou maior no ficheiro <code>php.ini</code>, e definir o mesmo valor para <code>$wgResourceLoaderMaxQueryLength</code> no ficheiro LocalSettings.php.", "config-db-type": "Tipo da base de dados:", "config-db-host": "Servidor da base de dados:", @@ -119,9 +119,9 @@ "config-db-port": "Porta da base de dados:", "config-db-schema": "Esquema ''(schema)'' do MediaWiki", "config-db-schema-help": "Normalmente, este esquema estará correto.\nAltere-o só se souber que precisa de o fazer.", - "config-pg-test-error": "Não foi possível criar uma ligação à base de dados '''$1''': $2", + "config-pg-test-error": "Não foi possível criar uma ligação à base de dados <strong>$1</strong>: $2", "config-sqlite-dir": "Diretório de dados do SQLite:", - "config-sqlite-dir-help": "O SQLite armazena todos os dados num único ficheiro.\n\nDurante a instalação, o servidor de Internet precisa de ter permissão de escrita no diretório que especificar.\n\nEste diretório '''não''' deve poder ser acedido diretamente da Internet, por isso está a ser colocado onde estão os seus ficheiros PHP.\n\nJuntamente com o diretório, o instalador irá criar um ficheiro <code>.htaccess</code>, mas se esta operação falhar é possível que alguém venha a ter acesso direto à base de dados.\nIsto inclui acesso aos dados dos utilizadores (endereços de correio eletrónico, palavras-passe encriptadas), às revisões eliminadas e a outros dados de acesso restrito na wiki.\n\nConsidere colocar a base de dados num local completamente diferente, como, por exemplo, em <code>/var/lib/mediawiki/asuawiki</code>.", + "config-sqlite-dir-help": "O SQLite armazena todos os dados num único ficheiro.\n\nDurante a instalação, o servidor de Internet precisa de ter permissão de escrita no diretório que especificar.\n\nEste diretório <strong>não</strong> deve poder ser acedido diretamente da Internet, por isso está a ser colocado onde estão os seus ficheiros PHP.\n\nJuntamente com o diretório, o instalador irá criar um ficheiro <code>.htaccess</code>, mas se esta operação falhar é possível que alguém venha a ter acesso direto à base de dados.\nIsto inclui acesso aos dados dos utilizadores (endereços de correio eletrónico, palavras-passe encriptadas), às revisões eliminadas e a outros dados de acesso restrito na wiki.\n\nConsidere colocar a base de dados num local completamente diferente, como, por exemplo, em <code>/var/lib/mediawiki/asuawiki</code>.", "config-oracle-def-ts": "Tablespace padrão:", "config-oracle-temp-ts": "Tablespace temporário:", "config-type-mysql": "MySQL (ou compatível)", @@ -282,7 +282,7 @@ "config-skins-missing": "Não foi encontrado nenhum tema; o MediaWiki usará um tema de recurso até instalar temas adequados.", "config-skins-must-enable-some": "Deve escolher pelo menos um tema para ativar.", "config-skins-must-enable-default": "O tema escolhido como padrão deve ser ativado.", - "config-install-alreadydone": "'''Aviso:''' Parece que já instalou o MediaWiki e está a tentar instalá-lo novamente.\nPasse para a próxima página, por favor.", + "config-install-alreadydone": "<strong>Aviso:</strong> Parece que já instalou o MediaWiki e está a tentar instalá-lo novamente.\nPasse para a próxima página, por favor.", "config-install-begin": "Ao clicar \"{{int:config-continue}}\", vai iniciar a instalação do MediaWiki.\nSe quiser fazer mais alterações, clique \"{{int:config-back}}\".", "config-install-step-done": "terminado", "config-install-step-failed": "falhou", @@ -303,20 +303,21 @@ "config-install-user-missing": "O utilizador especificado, \"$1\", não existe.", "config-install-user-missing-create": "O utilizador especificado, \"$1\", não existe.\nMarque a caixa de seleção \"criar conta\" abaixo se pretende criá-la, por favor.", "config-install-tables": "A criar as tabelas", - "config-install-tables-exist": "'''Aviso''': As tabelas do MediaWiki parecem já existir.\nA criação das tabelas será saltada.", - "config-install-tables-failed": "'''Erro''': A criação das tabelas falhou com o seguinte erro: $1", - "config-install-interwiki": "A preencher a tabela padrão de interwikis", - "config-install-interwiki-list": "Não foi possível encontrar o ficheiro <code>interwiki.list</code>.", - "config-install-interwiki-exists": "'''Aviso''': A tabela de interwikis parece já conter entradas.\nO preenchimento padrão desta tabela será saltado.", + "config-install-tables-exist": "<strong>Aviso:</strong> As tabelas do MediaWiki parecem já existir.\nA criação das tabelas será saltada.", + "config-install-tables-failed": "<strong>Erro:</strong> A criação das tabelas falhou com o seguinte erro: $1", + "config-install-interwiki": "A preencher a tabela padrão de links interwikis", + "config-install-interwiki-list": "Não foi possível ler o ficheiro <code>interwiki.list</code>.", + "config-install-interwiki-exists": "<strong>Aviso:</strong> A tabela de interwikis parece já conter entradas.\nO preenchimento padrão desta tabela será saltado.", "config-install-stats": "A inicializar as estatísticas", "config-install-keys": "A gerar as chaves secretas", - "config-insecure-keys": "'''Aviso:''' {{PLURAL:$2|A chave segura|As chaves seguras}} ($1) {{PLURAL:$2|gerada durante a instalação não é completamente segura|geradas durante a instalação não são completamente seguras}}. Considere a possibilidade de {{PLURAL:$2|alterá-la|alterá-las}} manualmente.", + "config-insecure-keys": "<strong>Aviso:</strong> {{PLURAL:$2|Uma chave segura|Chaves seguras}} ($1) {{PLURAL:$2|gerada durante a instalação não é completamente segura|geradas durante a instalação não são completamente seguras}}. Considere a possibilidade de {{PLURAL:$2|alterá-la|alterá-las}} manualmente.", "config-install-updates": "Evitar executar atualizações desnecessárias", "config-install-updates-failed": "<strong>Erro:</strong> A inserção de chaves de atualização nas tabelas falhou com o seguinte erro: $1", "config-install-sysop": "A criar a conta de administrador", "config-install-subscribe-fail": "Não foi possível subscrever a lista mediawiki-announce: $1", "config-install-subscribe-notpossible": "cURL não está instalado e <code>allow_url_fopen</code> não está disponível.", - "config-install-mainpage": "A criar a página principal com o conteúdo padrão.", + "config-install-mainpage": "A criar a página principal com o conteúdo padrão", + "config-install-mainpage-exists": "A página principal já existe; a saltar este passo", "config-install-extension-tables": "A criar as tabelas das extensões ativadas", "config-install-mainpage-failed": "Não foi possível inserir a página principal: $1", "config-install-done": "<strong>Parabéns!</strong>\nTerminou a instalação do MediaWiki.\n\nO instalador gerou um ficheiro <code>LocalSettings.php</code>.\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\n<strong>Nota</strong>: 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 <strong>[$2 entrar na wiki]</strong>.", diff --git a/includes/installer/i18n/qqq.json b/includes/installer/i18n/qqq.json index 7b60ed0d52ec..8d10b51f468b 100644 --- a/includes/installer/i18n/qqq.json +++ b/includes/installer/i18n/qqq.json @@ -317,6 +317,7 @@ "config-install-subscribe-fail": "{{doc-important|\"[[m:mail:mediawiki-announce|mediawiki-announce]]\" is the name of a mailing list and should not be translated.}}\nA message displayed if the MediaWiki installer encounters an error making a request to lists.wikimedia.org which hosts the mailing list.\n* $1 - the HTTP error encountered, reproduced as is (English string)", "config-install-subscribe-notpossible": "Error shown when automatically subscribing to the MediaWiki announcements mailing list fails.", "config-install-mainpage": "*{{msg-mw|Config-install-database}}\n*{{msg-mw|Config-install-tables}}\n*{{msg-mw|Config-install-schema}}\n*{{msg-mw|Config-install-user}}\n*{{msg-mw|Config-install-interwiki}}\n*{{msg-mw|Config-install-stats}}\n*{{msg-mw|Config-install-keys}}\n*{{msg-mw|Config-install-sysop}}\n*{{msg-mw|Config-install-mainpage}}", + "config-install-mainpage-exists": "Warning shown when installer attempts to create main page but it already exists.", "config-install-extension-tables": "Notice shown to the user during the install about progress.", "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.", diff --git a/includes/installer/i18n/roa-tara.json b/includes/installer/i18n/roa-tara.json index 198422adf166..1d4fc6172717 100644 --- a/includes/installer/i18n/roa-tara.json +++ b/includes/installer/i18n/roa-tara.json @@ -8,7 +8,7 @@ "config-title": "Installazzione de MediaUicchi $1", "config-information": "'Mbormaziune", "config-localsettings-key": "Chiave de aggiornamende:", - "config-localsettings-badkey": "'A chiave ca è date non g'è corrette.", + "config-localsettings-badkey": "'A chiave de aggiornamende ca è date non g'è corrette.", "config-session-error": "Errore facenne accumenzà 'a sessione: $1", "config-your-language": "'A lènga toje:", "config-your-language-help": "Scacchie 'na lènghe da ausà duranne 'u processe de installazzione:", @@ -36,6 +36,8 @@ "config-db-type": "Tipe de database:", "config-db-host-oracle": "Database TNS:", "config-db-name-oracle": "Scheme d'u database:", + "config-db-username": "Nome utende d'u database:", + "config-db-password": "Password d'u database:", "config-db-port": "Porte d'u database:", "config-db-schema": "Scheme pe MediaUicchi:", "config-type-mysql": "MySQL (o combatibbile)", diff --git a/includes/installer/i18n/ru.json b/includes/installer/i18n/ru.json index 15d54b71a1b7..fc9984efa960 100644 --- a/includes/installer/i18n/ru.json +++ b/includes/installer/i18n/ru.json @@ -322,6 +322,7 @@ "config-install-subscribe-fail": "Не удаётся подписаться на mediawiki-announce: $1", "config-install-subscribe-notpossible": "cURL не установлен и не доступна опция <code>allow_url_fopen</code>.", "config-install-mainpage": "Создание главной страницы с содержимым по умолчанию", + "config-install-mainpage-exists": "Главная страница уже существует, пропускаем", "config-install-extension-tables": "Создание таблиц для включённых расширений", "config-install-mainpage-failed": "Не удаётся вставить главную страницу: $1", "config-install-done": "<strong>Поздравляем!</strong>\nВы установили MediaWiki.\n\nВо время установки был создан файл <code>LocalSettings.php</code>.\nОн содержит все ваши настройки.\n\nВам необходимо скачать его и положить в корневую директорию вашей вики (ту же директорию, где находится файл index.php). Его загрузка должна начаться автоматически.\n\nЕсли автоматическая загрузка не началась или вы её отменили, вы можете скачать по ссылке ниже:\n\n$3\n\n<strong>Примечание</strong>: Если вы не сделаете этого сейчас, то сгенерированный файл конфигурации не будет доступен вам в дальнейшем, если вы выйдете из установки, не скачивая его.\n\nПо окончании действий, описанных выше, вы сможете <strong>[$2 войти в вашу вики]</strong>.", diff --git a/includes/installer/i18n/sr-ec.json b/includes/installer/i18n/sr-ec.json index f4343560ba3c..02f633593ef2 100644 --- a/includes/installer/i18n/sr-ec.json +++ b/includes/installer/i18n/sr-ec.json @@ -5,7 +5,8 @@ "Михајло Анђелковић", "Milicevic01", "Aktron", - "Сербијана" + "Сербијана", + "Zoranzoki21" ] }, "config-desc": "Инсталација за Медијавики", @@ -82,8 +83,9 @@ "config-skins": "Теме", "config-install-step-done": "готово", "config-install-step-failed": "није успело", + "config-install-mainpage-exists": "Главна страна већ постоји, прескакање", "config-help": "помоћ", "config-help-tooltip": "кликните да проширите", "mainpagetext": "<strong>Медијавики је успешно инсталиран.</strong>", - "mainpagedocfooter": "Погледајте [https://meta.wikimedia.org/wiki/Help:Contents кориснички водич] за коришћење програма.\n\n== Увод ==\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings Помоћ у вези са подешавањима]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ Често постављена питања]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Дописна листа о издањима Медијавикија]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Combating_spam Научите како да се борете против спама на Вашој вики]" + "mainpagedocfooter": "Погледајте [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents кориснички водич] за коришћење програма.\n\n== Увод ==\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings Помоћ у вези са подешавањима]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ Често постављена питања]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Дописна листа о издањима Медијавикија]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Combating_spam Научите како да се борете против спама на Вашој вики]" } diff --git a/includes/installer/i18n/sv.json b/includes/installer/i18n/sv.json index 45c5a7d1e2ec..4f52403f37e9 100644 --- a/includes/installer/i18n/sv.json +++ b/includes/installer/i18n/sv.json @@ -119,7 +119,7 @@ "config-type-mssql": "Microsoft SQL Server", "config-support-info": "MediaWiki stöder följande databassystem:\n\n$1\n\nOm du inte ser det databassystem som du försöker använda nedanstående, följ då instruktionerna länkade ovan för aktivera stöd för det.", "config-dbsupport-mysql": "* [{{int:version-db-mysql-url}} MySQL] är det primära målet för MediaWiki och det stöds bäst. MediaWiki fungerar även med [{{int:version-db-mariadb-url}} MariaDB] och [{{int:version-db-percona-url}} Percona Server], som är kompatibla med MySQL. ([http://www.php.net/manual/en/mysqli.installation.php Hur man kompilerar PHP med stöd för MySQL])", - "config-dbsupport-postgres": "* [{{int:version-db-postgres-url}} PostgreSQL] är ett populärt databassystem med öppen källkod som ett alternativ till MySQL. Det kan finnas några mindre kvarvarande buggar, och den rekommenderas inte för användning i en produktionsmiljö. ([http://www.php.net/manual/en/pgsql.installation.php Hur man kompilerar PHP med PostgreSQL stöd])", + "config-dbsupport-postgres": "* [{{int:version-db-postgres-url}} PostgreSQL] är ett populärt databassystem med öppen källkod som ett alternativ till MySQL. ([http://www.php.net/manual/en/pgsql.installation.php Hur man kompilerar PHP med PostgreSQL stöd])", "config-dbsupport-sqlite": "* [{{int:version-db-sqlite-url}} SQLite] är en lättviktsdatabassystem med väldigt bra stöd. ([http://www.php.net/manual/en/pdo.installation.php Hur man kompilerar PHP med SQLite stöd], använder PDO)", "config-dbsupport-oracle": "* [{{int:version-db-oracle-url}} Oracle] är en kommersiellt databas för företag. ([http://www.php.net/manual/en/oci8.installation.php Hur man kompilerar PHP med OCI8 stöd])", "config-dbsupport-mssql": "* [{{int:version-db-mssql-url}} Microsoft SQL Server] är en kommersiellt databas för företag för Windows. ([http://www.php.net/manual/en/sqlsrv.installation.php Hur man kompilerar PHP med SQLSRV stöd])", @@ -305,6 +305,7 @@ "config-install-subscribe-fail": "Det gick inte att prenumerera på mediawiki-announce: $1", "config-install-subscribe-notpossible": "cURL är inte installerad och <code>allow_url_fopen</code> är inte tillgänglig.", "config-install-mainpage": "Skapa huvudsida med standardinnehåll", + "config-install-mainpage-exists": "Huvudsidan finns redan, hoppar över", "config-install-extension-tables": "Skapar tabeller för aktiverade tillägg", "config-install-mainpage-failed": "Kunde inte infoga huvudsidan: $1", "config-install-done": "<strong>Grattis!</strong>\nDu har installerat MediaWiki.\n\nInstallationsprogrammet har genererat filen <code>LocalSettings.php</code>.\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\n<strong>OBS</strong>: 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 <strong>[$2 gå in på din wiki]</strong>", @@ -315,5 +316,5 @@ "config-nofile": "Filen \"$1\" kunde inte hittas. Har den raderats?", "config-extension-link": "Visste du att din wiki stödjer [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Extensions tillägg]?\n\nDu kan bläddra [https://www.mediawiki.org/wiki/Special:MyLanguage/Category:Extensions_by_category tillägg efter kategori].", "mainpagetext": "<strong>MediaWiki har installerats utan problem.</strong>", - "mainpagedocfooter": "Information om hur wiki-programvaran används finns i [https://meta.wikimedia.org/wiki/Help:Contents användarguiden].\n\n== Att komma igång ==\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings Lista över konfigurationsinställningar]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ MediaWiki FAQ]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce E-postlista för nya versioner av MediaWiki]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation#Translation_resources Lokalisera MediaWiki för ditt språk]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Combating_spam Läs om hur du bekämpar spam på din wiki]" + "mainpagedocfooter": "Information om hur wiki-programvaran används finns i [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents användarguiden].\n\n== Att komma igång ==\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings Lista över konfigurationsinställningar]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ MediaWiki FAQ]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce E-postlista för nya versioner av MediaWiki]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation#Translation_resources Lokalisera MediaWiki för ditt språk]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Combating_spam Läs om hur du bekämpar spam på din wiki]" } diff --git a/includes/installer/i18n/th.json b/includes/installer/i18n/th.json index ecb8e87e9664..ccc745537633 100644 --- a/includes/installer/i18n/th.json +++ b/includes/installer/i18n/th.json @@ -9,11 +9,11 @@ }, "config-desc": "ตัวติดตั้งสำหรับมีเดียวิกิ", "config-title": "การติดตั้งมีเดียวิกิ $1", - "config-information": "สารสนเทศ", + "config-information": "ข้อมูล", "config-localsettings-upgrade": "ตรวจพบไฟล์ <code>LocalSettings.php</code>\nเพื่ออัปเกรดการติดตั้งนี้ กรุณากรอกค่าของ <code>$wgUpgradeKey</code> ในกล่องด้านล่าง\nคุณจะพบมันได้ใน <code>LocalSettings.php</code>", "config-localsettings-cli-upgrade": "ตรวจพบไฟล์ <code>LocalSettings.php</code>\nเพื่ออัปเกรดการติดตั้งนี้ กรุณาดำเนินงาน <code>update.php</code> แทน", - "config-localsettings-key": "กุญแจอัปเกรด:", - "config-localsettings-badkey": "กุญแจที่คุณกรอกไม่ถูกต้อง", + "config-localsettings-key": "คีย์อัปเกรด:", + "config-localsettings-badkey": "คีย์อัปเกรดที่คุณกรอกไม่ถูกต้อง", "config-upgrade-key-missing": "ตรวจพบการติดตั้งมีเดียวิกิที่มีอยู่แล้ว\nเพื่ออัปเกรดการติดตั้งนี้ กรุณาใส่บรรทัดต่อไปนี้ที่ท้ายไฟล์ <code>LocalSettings.php</code> ของคุณ:\n\n$1", "config-localsettings-incomplete": "<code>LocalSettings.php</code> ที่มีอยู่ดูเหมือนว่าไม่สมบูรณ์\nตัวแปร $1 ไม่ถูกกำหนด\nกรุณาเปลี่ยนแปลง <code>LocalSettings.php</code> เพื่อกำหนดตัวแปรนี้ และคลิก \"{{int:Config-continue}}\"", "config-localsettings-connection-error": "ความผิดพลาดเกิดขึ้นเมื่อเชื่อมต่อฐานข้อมูลโดยใช้การตั้งค่าที่ระบุใน <code>LocalSettings.php</code> กรุณาแก้ไขการตั้งค่าเหล่านี้และลองอีกครั้ง\n\n$1", @@ -61,8 +61,10 @@ "config-imagemagick": "พบ ImageMagick: <code>$1</code>\nการย่อรูปภาพจะถูกเปิดใช้งาน ถ้าคุณเปิดใช้งานการอัปโหลด", "config-gd": "พบไลบรารีกราฟิก GD ภายใน\nการย่อรูปภาพจะถูกเปิดใช้งาน ถ้าคุณเปิดใช้งานการอัปโหลด", "config-no-scaling": "ไม่พบไลบรารี GD หรือ ImageMagick\nการย่อรูปภาพจะถูกปิดใช้งาน", + "config-no-uri": "<strong>ข้อผิดพลาด:</strong> ไม่สามารถทำการตรวจสอบ URI ปัจจุบันได้\nการติดตั้งถูกยกเลิกแล้ว", "config-using-server": "ใช้ชื่อเซิร์ฟเวอร์ \"<nowiki>$1</nowiki>\"", "config-using-uri": "ใช้ยูอาร์แอลของเซิร์ฟเวอร์ \"<nowiki>$1$2</nowiki>\"", + "config-db-name": "ชื่อฐานข้อมูล:", "config-mysql-innodb": "อินโนดีบี", "config-mysql-myisam": "มายไอแซม", "config-mysql-binary": "ไบนารี", diff --git a/includes/installer/i18n/tl.json b/includes/installer/i18n/tl.json index 2ba2c27137bf..e078752f49a7 100644 --- a/includes/installer/i18n/tl.json +++ b/includes/installer/i18n/tl.json @@ -6,7 +6,8 @@ "아라", "Amire80", "Jojit fb", - "Macofe" + "Macofe", + "Emem.calist" ] }, "config-desc": "Ang tagapagluklok para sa MediaWiki", @@ -279,6 +280,7 @@ "config-install-subscribe-fail": "Hindi nagawang magpasipi mula sa mediawiki-announce: $1", "config-install-subscribe-notpossible": "Hindi nakalagak ang cURL at hindi makukuha ang <code>allow_url_fopen</code>", "config-install-mainpage": "Nililikha ang pangunahing pahina na may likas na nakatakdang nilalaman", + "config-install-mainpage-exists": "Ang pangunahing pahina ay nakasaad na, ipagpatuloy ang paglalathala", "config-install-extension-tables": "Nililikha ang mga talahanayan para sa pinagaganang mga dugtong", "config-install-mainpage-failed": "Hindi maisingit ang pangunahing pahina: $1", "config-install-done": "'''Maligayang bati!'''\nMatagumpay mong nailuklok ang MediaWiki.\n\nAng tagapagluklok ay nakagawa ng isang talaksan ng <code>LocalSettings.php</code>.\nNaglalaman ito ng lahat ng iyong mga pagsasaayos.\n\nKailangan mo itong ikargang paibaba at ilagay ito sa lipon ng iyong pagluluklok ng wiki (katulad ng direktoryo ng index.php). Ang pagkakargang paibaba ay dapat na kusang magsimula.\n\nKung ang pagkakargang paibaba ay hindi inialok, o kung hindi mo ito itinuloy, maaari mong muling simulan ang pagkakargang paibaba sa pamamagitan ng pagpindot sa kawing na nasa ibaba:\n\n$3\n\n'''Paunawa''': Kapag hindi mo ito ginawa ngayon, ang nagawang talaksang ito ng pagkakaayos ay hindi mo na makukuha mamaya kapag lumabas ka mula sa pagluluklok na hindi ikinakarga itong paibaba.\n\nKapag nagawa na iyan, maaari ka nang '''[$2 pumasok sa wiki mo]'''.", diff --git a/includes/installer/i18n/tr.json b/includes/installer/i18n/tr.json index 823b1ba64f77..f7394c8dcb1d 100644 --- a/includes/installer/i18n/tr.json +++ b/includes/installer/i18n/tr.json @@ -16,7 +16,8 @@ "Meelo", "HakanIST", "McAang", - "Elftrkn" + "Elftrkn", + "Vito Genovese" ] }, "config-desc": "MediaWiki yükleyicisi", @@ -164,13 +165,13 @@ "config-ns-conflict": "Belirtilen ad \"<nowiki> $1 </nowiki>\" varsayılan MediaWiki ad alanı ile çakışıyor.\nFarklı proje isim alanı belirtin.", "config-admin-box": "Yönetici hesabı", "config-admin-name": "Kullanıcı adınız:", - "config-admin-password": "Şifre:", - "config-admin-password-confirm": "Şifre tekrar:", + "config-admin-password": "Parola:", + "config-admin-password-confirm": "Yeniden parola:", "config-admin-help": "Buraya tercih ettiğiniz kullanıcı adını girin; örneğin \"Joe Bloggs\". Bu vikide oturum açmak için kullanacağınız addır.", "config-admin-name-blank": "Bir yönetici kullanıcı adını giriniz.", "config-admin-name-invalid": "Belirtilen ad \"<nowiki> $1 </nowiki>\" geçersiz.\nFarklı bir kullanıcı adı belirtin.", "config-admin-password-blank": "Yönetici hesabı için bir parola girin.", - "config-admin-password-mismatch": "Girdiğiniz şifreler birbirleriyle uyuşmuyor.", + "config-admin-password-mismatch": "Girdiğiniz iki parola eşleşmiyor.", "config-admin-email": "E-posta adresi:", "config-admin-email-help": "Wiki'de diğer kullanıcılardan e-posta almak, parolanızı sıfırlamak ve sizin izlediğiniz sayfalarda yapılan değişikliklerin bildirilmesini sağlamak için e-posta adresinizi girin. Bu alanı boş bırakabilirsiniz.", "config-admin-error-user": "Bir yönetici adı ile oluşturma sırasında iç hata \"<nowiki> $1 </nowiki>\".", diff --git a/includes/installer/i18n/uk.json b/includes/installer/i18n/uk.json index 7644f417ab9b..78730351b680 100644 --- a/includes/installer/i18n/uk.json +++ b/includes/installer/i18n/uk.json @@ -308,6 +308,7 @@ "config-install-subscribe-fail": "Не можливо підписатись на mediawiki-announce: $1", "config-install-subscribe-notpossible": "cURL не встановлено і опція <code>allow_url_fopen</code> не доступна.", "config-install-mainpage": "Створення головної сторінки із вмістом за замовчуванням", + "config-install-mainpage-exists": "Головна сторінка вже існує, пропускаємо", "config-install-extension-tables": "Створення таблиць для увімкнених розширень", "config-install-mainpage-failed": "Не вдається вставити головну сторінку: $1", "config-install-done": "<strong>Вітаємо!</strong>\nВи успішно встановили MediaWiki.\n\nІнсталятор згенерував файл <code>LocalSettings.php</code>, який містить усі Ваші налаштування.\n\nВам необхідно завантажити його і помістити у кореневу папку Вашої вікі (туди ж, де index.php). Завантаження мало початись автоматично.\n\nЯкщо завантаження не почалось або Ви його скасували, можете заново його почати, натиснувши на посилання внизу:\n\n$3\n\n<strong>Примітка</strong>: Якщо Ви не зробите цього зараз, цей файл не буде доступним пізніше, коли Ви вийдете з встановлення, не скачавши його.\n\nПісля виконання дій, описаних вище, Ви зможете <strong>[$2 увійти у свою вікі]</strong>.", diff --git a/includes/installer/i18n/vi.json b/includes/installer/i18n/vi.json index 0820ff561505..0a7f8cd248b4 100644 --- a/includes/installer/i18n/vi.json +++ b/includes/installer/i18n/vi.json @@ -116,7 +116,7 @@ "config-type-mssql": "Microsoft SQL Server", "config-support-info": "MediaWiki hỗ trợ các hệ thống cơ sở dữ liệu sau đây:\n\n$1\n\nNếu bạn không thấy hệ thống cơ sở dữ liệu mà bạn đang muốn sử dụng được liệt kê dưới đây, thì hãy theo chỉ dẫn được liên kết ở trên để kích hoạt tính năng hỗ trợ.", "config-dbsupport-mysql": "* [{{int:version-db-mysql-url}} MySQL] là mục tiêu chính cho MediaWiki và được hỗ trợ tốt nhất. MediaWiki cũng làm việc với [{{int:version-db-mariadb-url}} MariaDB] và [{{int:version-db-percona-url}} Percona Server], là những cơ sở dữ liệu tương thích với MySQL. ([http://www.php.net/manual/en/mysqli.installation.php Làm thế nào để biên dịch PHP với sự hỗ trợ của MySQL])", - "config-dbsupport-postgres": "* [{{int:version-db-postgres-url}} PostgreSQL] là một hệ thống cơ sở dữ liệu mã nguồn mở phổ biến như là một thay thế cho MySQL. Có thể có một số lỗi nhỏ lâu đời, và nó không được khuyến cáo sử dụng trong môi trường sản xuất. ([http://www.php.net/manual/en/pgsql.installation.php Làm thế nào để biên dịch PHP với sự hỗ trợ của PostgreSQL])", + "config-dbsupport-postgres": "* [{{int:version-db-postgres-url}} PostgreSQL] là một hệ thống cơ sở dữ liệu mã nguồn mở phổ biến như là một thay thế cho MySQL. ([http://www.php.net/manual/en/pgsql.installation.php Làm thế nào để biên dịch PHP với sự hỗ trợ của PostgreSQL])", "config-dbsupport-sqlite": "* [{{int:version-db-sqlite-url}} SQLite] là một hệ thống cơ sở dữ liệu dung lượng nhẹ được hỗ trợ rất tốt. ([http://www.php.net/manual/en/pdo.installation.php Làm thế nào để biên dịch PHP với sự hỗ trợ của SQLite], sử dụng PDO)", "config-dbsupport-oracle": "* [{{int:version-db-oracle-url}} Oracle] là một cơ sở dữ liệu doanh nghiệp thương mại. ([http://www.php.net/manual/en/oci8.installation.php Làm thế nào để biên dịch PHP với sự hỗ trợ của OCI8])", "config-dbsupport-mssql": "* [{{int:version-db-mssql-url}} Microsoft SQL Server] là một cơ sở dữ liệu doanh nghiệp thương mại cho Windows. ([http://www.php.net/manual/en/sqlsrv.installation.php Làm thế nào để biên dịch PHP với sự hỗ trợ của SQLSRV])", @@ -302,6 +302,7 @@ "config-install-subscribe-fail": "Không thể theo dõi mediawiki-announce: $1", "config-install-subscribe-notpossible": "cURL không được cài đặt và <code>allow_url_fopen</code> không có sẵn.", "config-install-mainpage": "Đang tạo trang đầu với nội dung mặc định", + "config-install-mainpage-exists": "Bỏ qua trang chính đã tồn tại", "config-install-extension-tables": "Đang tạo bảng cho các phần mở rộng được kích hoạt", "config-install-mainpage-failed": "Không thể chèn trang đầu: $1", "config-install-done": "<strong>Xin chúc mừng!</strong>\nBạn đã cài đặt MediaWiki.\n\nBộ cài đặt đã tạo ra một tập tin <code>LocalSettings.php</code>.\nTập tin này chứa tất cả các cấu hình của bạn.\n\nBạn sẽ cần phải tải nó về và đặt nó trong thư mục cài đặt wiki của bạn (cùng thư mục với index.php). Việc tải về có lẽ sẽ được khởi động tự động.\n\nNếu bản tải về không được cung cấp, hoặc nếu bạn hủy bỏ nó, bạn có thể khởi động lại tải về bằng cách nhấn vào liên kết dưới đây:\n\n$3\n\n<strong>Lưu ý:</strong> Nếu bạn không làm điều này ngay bây giờ, điều này sẽ tạo ra tập tin cấu hình sẽ không có giá trị cho bạn sau này nếu bạn thoát khỏi trình cài đặt mà không tải nó về.\n\nKhi đã việc tải về đã hoàn thành, bạn có thể <strong>[$2 truy cập trang wiki của bạn]</strong>.", @@ -312,5 +313,5 @@ "config-nofile": "Không tìm thấy tập tin “$1”. Nó có phải bị xóa không?", "config-extension-link": "Bạn có biết rằng wiki của bạn có hỗ trợ [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Extensions mở rộng]?\n\nBạn có thể truy cập [https://www.mediawiki.org/wiki/Special:MyLanguage/Category:Extensions_by_category phần mở rộng theo thể loại] hoặc [https://www.mediawiki.org/wiki/Extension_Matrix Ma trận Mở rộng] để xem danh sách đầy đủ các phần mở rộng.", "mainpagetext": "'''MediaWiki đã được cài đặt.'''", - "mainpagedocfooter": "Xin đọc [https://meta.wikimedia.org/wiki/Help:Contents Hướng dẫn sử dụng] để biết thêm thông tin về cách sử dụng phần mềm wiki.\n\n== Để bắt đầu ==\n\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings Danh sách các thiết lập cấu hình]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ Các câu hỏi thường gặp MediaWiki]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Danh sách gửi thư về việc phát hành MediaWiki]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Combating_spam Tìm hiểu cách chống spam tại wiki của bạn]" + "mainpagedocfooter": "Xin đọc [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents Hướng dẫn sử dụng] để biết thêm thông tin về cách sử dụng phần mềm wiki.\n\n== Để bắt đầu ==\n\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings Danh sách các thiết lập cấu hình]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ Các câu hỏi thường gặp MediaWiki]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Danh sách gửi thư về việc phát hành MediaWiki]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Combating_spam Tìm hiểu cách chống spam tại wiki của bạn]" } diff --git a/includes/installer/i18n/war.json b/includes/installer/i18n/war.json index a3c3d246a7dd..ef7694a74381 100644 --- a/includes/installer/i18n/war.json +++ b/includes/installer/i18n/war.json @@ -10,12 +10,16 @@ "config-information": "Impormasyon", "config-localsettings-upgrade": "Mayda <code>LocalSettings.php</code> nga paypay nga nabilngan. Basi ma-upgrade ini nga pag-installar, alayon pagbutáng han value han <code>$wgUpgradeKey</code> ha kahon ha ubós. Mabibilngan mo ini ha <code>LocalSettings.php</code>.", "config-localsettings-cli-upgrade": "Mayda <code>LocalSettings.php</code> nga paypay nga nabilngan. Basi ma-upgrade ini nga pag-installar, alayon pagpadalagan lugod han <code>update.php</code>", - "config-localsettings-badkey": "An key nga imo ginhatag in diri asya.", + "config-localsettings-key": "Upgrade nga yabi:", + "config-localsettings-badkey": "Sayop an upgrade nga yabi nga imo ginhátag", "config-upgrade-key-missing": "Mayda daan na ng gin-installar nga MediaWiki nga nabilngan.\nBasi ma-upgrade ini nga pag-instalar, alayon pagbutang han nahasunod nga linya ha ubós han imo <code>LocalSettings.php</code>:\n\n$1", "config-localsettings-incomplete": "An yana nga <code>LocalSettings.php</code> in baga diri kompleto.\nAn $1 variable in diri naka-set.\nAlayon igsaliwan an <code>LocalSettings.php</code> para ini nga variable in mai-set, ngan pidlita an \"{{int:Config-continue}}\".", "config-localsettings-connection-error": "May-ada pagsayop an nahitabo han pagpapakabit ngada ha database nga gingagamitan hin mga kamumutangan nga dapat unta ginpapatuman han <code>LocalSettings.php</code>. Alayon ayda ini nga mga kamumutangan ngan utrohon nala.\n\n$1", "config-session-error": "Pakyas an pagtikang han session: $1", + "config-session-expired": "An imo sesyon nga data baga na hin naglahós na hin panahón\nIt mga sesyon gin-configure hin pagkaiha nga $1\nPuyde mo ini paiha-on ha pagset hit <code>session.gc_maxlifetime</code> ha php.ini.\nIgtikang hin utro an pag-instalar nga proseso.", + "config-no-session": "¡Nawarâ an imo sesyon nga data!\nKitaa an imo php.ini ngan siguroa nga an <code>session.save_path</code> ginkadâ hin naangay nga direktory.", "config-your-language": "Imo pinulongán", + "config-your-language-help": "Pili-a in yinaknan nga gagamiton dida han proseso han pag-instalar.", "config-wiki-language": "Pinulongán han wiki", "config-wiki-language-help": "Pilía an pinulongán nga kauróg igsúsurat hit wiki", "config-back": "Bálik", @@ -23,10 +27,19 @@ "config-page-language": "Pinulongán", "config-page-welcome": "Maupay nga pag-abot ha MediaWiki!", "config-page-dbconnect": "Igsumpay ha database", + "config-page-upgrade": "Ig-upgrade it aada nga na-instalar", + "config-page-dbsettings": "Mga setting ha database", "config-page-name": "Ngaran", + "config-page-options": "Mga pagpipilian", + "config-page-install": "Ig-instalar", "config-page-complete": "Nakompleto!", + "config-page-restart": "Igbalik hin utro in pag-instalar", "config-page-readme": "Basaha ako", + "config-page-releasenotes": "Mga nota han ginpagawás", "config-page-copying": "Nagkokopya", + "config-page-upgradedoc": "Pag-upgrade", + "config-page-existingwiki": "Aada nga wiki", + "config-help-restart": "¿Karúyag mo ba ighawan an tanan nga gin-save nga data nga imo gin-enter ngan igbalik hin utro an proseso hin pag-instalar?", "config-restart": "Oo, utroha patikanga", "config-welcome": "=== Mga pagpanginano panlibong ===\nMagkakamay-ada yano nga panginano para masabtan kun ini nga libong in naaangay para hiton pagtataod hiton MediaWiki. Hinumdomi iton paglakip hinin nga impormasyon kun karuyag mo mangaro hin suporta kun paunan-on humanon an pagtataod.", "config-env-php": "Gin-install an PHP $1.", diff --git a/includes/installer/i18n/zh-hans.json b/includes/installer/i18n/zh-hans.json index d2d5d3c28da7..d0c0026a7c7e 100644 --- a/includes/installer/i18n/zh-hans.json +++ b/includes/installer/i18n/zh-hans.json @@ -80,7 +80,7 @@ "config-memory-bad": "<strong>警告:</strong>PHP的内存使用上限<code>memory_limit</code>为$1。\n该设定可能过低,并导致安装失败!", "config-xcache": "[http://xcache.lighttpd.net/ XCache]已安装", "config-apc": "[http://www.php.net/apc APC]已安装", - "config-apcu": "[http://www.php.net/apcu APCu]已安装", + "config-apcu": "已安装[http://www.php.net/apcu APCu]", "config-wincache": "[http://www.iis.net/download/WinCacheForPhp WinCache]已安装", "config-no-cache-apcu": "<strong>警告:</strong>找不到[http://www.php.net/apcu APCu]、[http://xcache.lighttpd.net/ XCache]或[http://www.iis.net/download/WinCacheForPhp WinCache]。\n对象缓存未启用。", "config-mod-security": "<strong>警告:</strong>您的web服务器已启用[http://modsecurity.org/ mod_security]/mod_security2。它的很多常见配置可能导致MediaWiki及其他软件允许用户发布任意内容的问题。如果可能,这应当被禁用。否则,当您遭遇随机错误时,请参考[http://modsecurity.org/documentation/ mod_security 文档]或联络您的主机支持。", @@ -318,6 +318,7 @@ "config-install-subscribe-fail": "无法订阅mediawiki-announce:$1", "config-install-subscribe-notpossible": "没有安装cURL,<code>allow_url_fopen</code>也不可用。", "config-install-mainpage": "正在创建显示默认内容的首页", + "config-install-mainpage-exists": "首页已存在,正在跳过", "config-install-extension-tables": "正在创建已启用扩展程序表", "config-install-mainpage-failed": "无法插入首页:$1", "config-install-done": "<strong>恭喜!</strong>\n您已经安装了MediaWiki。\n\n安装程序已经生成了<code>LocalSettings.php</code>文件,其中包含了您所有的配置。\n\n您需要下载该文件,并将其放在您wiki的根目录(index.php的同级目录)中。稍后下载将自动开始。\n\n如果浏览器没有提示您下载,或者您取消了下载,您可以点击下面的链接重新开始下载:\n\n$3\n\n<strong>注意:</strong>如果您现在不完成本步骤,而是没有下载便退出了安装过程,此后您将无法获得自动生成的配置文件。\n\n当本步骤完成后,您可以<strong>[$2 进入您的wiki]</strong>。", diff --git a/includes/installer/i18n/zh-hant.json b/includes/installer/i18n/zh-hant.json index e7f69d3a8f5e..faf519487cb9 100644 --- a/includes/installer/i18n/zh-hant.json +++ b/includes/installer/i18n/zh-hant.json @@ -16,7 +16,9 @@ "NigelSoft", "Macofe", "Reke", - "Suchichi02" + "Suchichi02", + "Winstonyin", + "Wehwei" ] }, "config-desc": "MediaWiki 安裝程式", @@ -74,7 +76,9 @@ "config-memory-bad": "<strong>警告:</strong>PHP 的記憶體使用上限 <code>memory_limit</code> 為 $1。\n該設定值可能過低。\n這可能導致後續的安裝失敗!", "config-xcache": "[http://xcache.lighttpd.net/ XCache] 已安裝", "config-apc": "[http://www.php.net/apc APC] 已安裝", + "config-apcu": "已安裝[http://www.php.net/apcu APCu]", "config-wincache": "[http://www.iis.net/download/WinCacheForPhp WinCache] 已安裝", + "config-no-cache-apcu": "<strong>警告:</strong>找不到[http://www.php.net/apcu APCu], [http://xcache.lighttpd.net/ XCache]或[http://www.iis.net/download/WinCacheForPhp WinCache]。未開啟物件緩存。", "config-mod-security": "<strong>警告:</strong>您的網頁伺服器已開啟 [http://modsecurity.org/ mod_security] 模組,如果設定不恰當會導致使用者可在 MediaWiki 或其他應用程式發佈任意的內容。\n若您遇到任何問題,請參考 [http://modsecurity.org/documentation/ mod_security 文件] 或聯繫您的伺服器技術支援人員。", "config-diff3-bad": "找不到 GNU diff3。", "config-git": "找到 Git 版本控制軟體:<code>$1</code>。", @@ -124,7 +128,7 @@ "config-type-mssql": "Microsoft SQL Server", "config-support-info": "MediaWiki 支援以下資料庫系統:\n\n$1\n\n如果您下方沒有看到您要使用的資料庫系統,請根據上方連結指示開啟資料庫的支援。", "config-dbsupport-mysql": "* [{{int:version-db-mysql-url}} MySQL] 是 MediaWiki 主要支援的資料庫系統。MediaWiki 也同時可運作與於 [{{int:version-db-mariadb-url}} MariaDB] 和[{{int:version-db-percona-url}} Percona 伺服器],上述這些與 MySQL 相容的資料庫系統。([http://www.php.net/manual/en/mysqli.installation.php 如何編譯支援 MySQL 的 PHP])", - "config-dbsupport-postgres": "* [{{int:version-db-postgres-url}} PostgreSQL] 是一套受歡迎的開源資料庫系統,在開源方案當中,可用來替代 MySQL。目前仍有一些次要的問題需要解決,較不建議使用在上線環境當中。 ([http://www.php.net/manual/en/pgsql.installation.php 如何編譯支援 PostgreSQL 的 PHP])。", + "config-dbsupport-postgres": "* [{{int:version-db-postgres-url}} PostgreSQL]是一套受歡迎的開源資料庫系統,可用來替代 MySQL。([http://www.php.net/manual/en/pgsql.installation.php 如何編譯支援PostgreSQL的PHP])。", "config-dbsupport-sqlite": "* [{{int:version-db-sqlite-url}} SQLite] 是一套輕量級的資料庫系統,MediaWiki 可在此資料庫系統上良好的運作。([http://www.php.net/manual/en/pdo.installation.php 如何編譯支援 SQLite 的 PHP],須透過 PDO)", "config-dbsupport-oracle": "* [{{int:version-db-oracle-url}} Oracle] 是一套商用企業級的資料庫。([http://www.php.net/manual/en/oci8.installation.php 如何編譯支援 OCI8 的 PHP])", "config-dbsupport-mssql": "* [{{int:version-db-mssql-url}} Microsoft SQL Server] 是一套 Windows 專用的商用企業級的資料庫。 ([http://www.php.net/manual/en/sqlsrv.installation.php 如何編譯支援 SQLSRV 的 PHP])", @@ -209,6 +213,8 @@ "config-subscribe": "訂閱 [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce 發佈公告郵寄清單]。", "config-subscribe-help": "這是一個用於發佈公告的低郵件量郵寄清單,內容包括重要的安全公告。\n您應該訂閱它並在 MediaWiki 發佈新版的時候更新系統。", "config-subscribe-noemail": "您正嘗試不填寫電子郵件地址訂閱發佈公告郵寄清單。 \n請如果您希望訂閱郵寄清單,請提供一個有效的電子郵件地址。", + "config-pingback": "與MediaWiki開發人員分享此安裝過程的數據。", + "config-pingback-help": "如果您選擇此項設定,MediaWiki將會定期把有關本MediaWiki實例的基本數據傳送給https://www.mediawiki.org。數據包括系統類型、PHP版本、所選的資料庫後端等等。維基媒體基金會會向MediaWiki的開發人員分享這組數據,以幫助將來的開發計劃。將會傳送以下有關您系統的數據:\n<pre>$1</pre>", "config-almost-done": "您快要完成了!\n您現在可以跳過其餘的設定項目並且立即安裝 Wiki。", "config-optional-continue": "多問我一些問題吧。", "config-optional-skip": "我已經不耐煩了,請趕緊安裝 Wiki。", @@ -308,14 +314,16 @@ "config-install-subscribe-fail": "無法訂閱 mediawiki-announce:$1", "config-install-subscribe-notpossible": "未安裝 cURL,因此無法使用 <code>allow_url_fopen</code> 設定項目。", "config-install-mainpage": "正在使用預設的內容建立首頁", + "config-install-mainpage-exists": "首頁已存在,略過中", "config-install-extension-tables": "正在建立已啟動的擴充套件的資料表", "config-install-mainpage-failed": "無法插入首頁: $1", - "config-install-done": "<strong>恭喜!</strong>\n您已經成功地安裝了 MediaWiki。\n\n安裝程式已自動產生 <code>LocalSettings.php</code> 檔案,\n該檔案中包含了您所有的設定項目。\n\n您需要下載該檔案,並將其放置在您的 Wiki 的根目錄 (index.php 所在的目錄) 中,下載稍後會自動開始。\n\n若瀏覽器沒有提示您下載,或者您取消了下載,您可以點選下方連結重新下載:\n\n$3\n\n<strong>注意:</strong>若您現在未下載檔案,稍後結束安裝程式之後將無法下載設定檔。\n\n當您完成本步驟後,您可以 <strong>[$2 進入您的 Wiki]</strong>。", + "config-install-done": "<strong>恭喜!</strong>\n您已經成功安裝MediaWiki。\n\n安裝程式已自動產生<code>LocalSettings.php</code>檔案,\n該檔案中包含了您所有的設定項目。\n\n您需要下載該檔案,並將其放置在您的Wiki的根目錄(index.php所在的目錄)中,下載應已自動開始。\n\n若瀏覽器沒有提示您下載,或者您取消了下載,您可以點選下方連結重新下載:\n\n$3\n\n<strong>注意:</strong>如果您現在不下載此檔案,稍後結束安裝程式之後將無法再下載設定檔。\n\n當您完成本步驟後,您可以<strong>[$2 進入您的Wiki]</strong>。", + "config-install-done-path": "<strong>恭喜!</strong>\n您已經成功安裝MediaWiki。\n\n安裝程式已自動產生<code>LocalSettings.php</code>檔案,\n該檔案中包含了您所有的設定項目。\n\n您需要下載該檔案,並將其放置在<code>$4</code>中,下載應已自動開始。\n\n若瀏覽器沒有提示您下載,或者您取消了下載,您可以點選下方連結重新下載:\n\n$3\n\n<strong>注意:</strong>如果您現在不下載此檔案,稍後結束安裝程式之後將無法再下載設定檔。\n\n當您完成本步驟後,您可以<strong>[$2 進入您的Wiki]</strong>。", "config-download-localsettings": "下載 <code>LocalSettings.php</code>", "config-help": "說明", "config-help-tooltip": "點選以展開", "config-nofile": "查無檔案 \"$1\",是否已被刪除?", "config-extension-link": "您是否了解您的 Wiki 支援 [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Extensions 擴充套件]?\n\n\n您可以瀏覽 [https://www.mediawiki.org/wiki/Special:MyLanguage/Category:Extensions_by_category 擴充套件分類] 或 [https://www.mediawiki.org/wiki/Extension_Matrix 擴充套件資料表] 以取得相關的資訊。", "mainpagetext": "<strong>已安裝 MediaWiki。</strong>", - "mainpagedocfooter": "請參閱 [https://meta.wikimedia.org/wiki/Help:Contents 使用者手冊] 以取得使用 Wiki 的相關訊息!\n\n== 新手入門 ==\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings MediaWiki 系統設定]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ MediaWiki 常見問答集]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki 發佈郵寄清單]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation#Translation_resources MediaWiki 介面在地化]" + "mainpagedocfooter": "有關使用wiki的訊息,請參閱[https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents 使用者指南]。\n\n== 新手入門 ==\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings 系統設定]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ MediaWiki常見問題]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki郵寄清單]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation#Translation_resources 將MediaWiki翻譯至您的語言]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Combating_spam 了解如何在您的wiki上防禦破壞]" } diff --git a/includes/interwiki/ClassicInterwikiLookup.php b/includes/interwiki/ClassicInterwikiLookup.php index 4ccca9785e85..f19e3dbcfb1f 100644 --- a/includes/interwiki/ClassicInterwikiLookup.php +++ b/includes/interwiki/ClassicInterwikiLookup.php @@ -221,7 +221,7 @@ class ClassicInterwikiLookup implements InterwikiLookup { } } - $value = $this->getCacheValue( wfMemcKey( $prefix ) ); + $value = $this->getCacheValue( wfWikiID() . ':' . $prefix ); // Site level if ( $value == '' && $this->interwikiScopes >= 3 ) { $value = $this->getCacheValue( "_{$this->thisSite}:{$prefix}" ); @@ -288,7 +288,7 @@ class ClassicInterwikiLookup implements InterwikiLookup { $row = $dbr->selectRow( 'interwiki', - ClassicInterwikiLookup::selectFields(), + self::selectFields(), [ 'iw_prefix' => $prefix ], __METHOD__ ); @@ -408,7 +408,7 @@ class ClassicInterwikiLookup implements InterwikiLookup { } $res = $db->select( 'interwiki', - $this->selectFields(), + self::selectFields(), $where, __METHOD__, [ 'ORDER BY' => 'iw_prefix' ] ); diff --git a/includes/jobqueue/JobQueue.php b/includes/jobqueue/JobQueue.php index 020a68472852..9701dd992986 100644 --- a/includes/jobqueue/JobQueue.php +++ b/includes/jobqueue/JobQueue.php @@ -21,6 +21,7 @@ * @defgroup JobQueue JobQueue * @author Aaron Schulz */ +use MediaWiki\MediaWikiServices; /** * Class to handle enqueueing and running of background jobs @@ -709,7 +710,7 @@ abstract class JobQueue { public static function incrStats( $key, $type, $delta = 1 ) { static $stats; if ( !$stats ) { - $stats = RequestContext::getMain()->getStats(); + $stats = MediaWikiServices::getInstance()->getStatsdDataFactory(); } $stats->updateCount( "jobqueue.{$key}.all", $delta ); $stats->updateCount( "jobqueue.{$key}.{$type}", $delta ); diff --git a/includes/jobqueue/JobQueueDB.php b/includes/jobqueue/JobQueueDB.php index 0a8ae7fd51f5..540b8c546138 100644 --- a/includes/jobqueue/JobQueueDB.php +++ b/includes/jobqueue/JobQueueDB.php @@ -20,8 +20,10 @@ * @file * @author Aaron Schulz */ +use Wikimedia\Rdbms\IDatabase; use MediaWiki\MediaWikiServices; use Wikimedia\ScopedCallback; +use Wikimedia\Rdbms\DBConnRef; /** * Class to handle job queues stored in the DB @@ -346,7 +348,7 @@ class JobQueueDB extends JobQueue { continue; // try the other direction } } else { // table *may* have >= MAX_OFFSET rows - // Bug 42614: "ORDER BY job_random" with a job_random inequality causes high CPU + // T44614: "ORDER BY job_random" with a job_random inequality causes high CPU // in MySQL if there are many rows for some reason. This uses a small OFFSET // instead of job_random for reducing excess claim retries. $row = $dbw->selectRow( 'job', self::selectFields(), // find a random job diff --git a/includes/jobqueue/JobQueueGroup.php b/includes/jobqueue/JobQueueGroup.php index 71d68d9f9336..9f78404efea2 100644 --- a/includes/jobqueue/JobQueueGroup.php +++ b/includes/jobqueue/JobQueueGroup.php @@ -170,7 +170,7 @@ class JobQueueGroup { * @since 1.26 */ public function lazyPush( $jobs ) { - if ( PHP_SAPI === 'cli' ) { + if ( PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg' ) { $this->push( $jobs ); return; } diff --git a/includes/jobqueue/JobQueueRedis.php b/includes/jobqueue/JobQueueRedis.php index 25a271ca14ec..c2c9d6611991 100644 --- a/includes/jobqueue/JobQueueRedis.php +++ b/includes/jobqueue/JobQueueRedis.php @@ -793,9 +793,9 @@ LUA; private function getGlobalKey( $name ) { $parts = [ 'global', 'jobqueue', $name ]; foreach ( $parts as $part ) { - if ( !preg_match( '/[a-zA-Z0-9_-]+/', $part ) ) { - throw new InvalidArgumentException( "Key part characters are out of range." ); - } + if ( !preg_match( '/[a-zA-Z0-9_-]+/', $part ) ) { + throw new InvalidArgumentException( "Key part characters are out of range." ); + } } return implode( ':', $parts ); diff --git a/includes/jobqueue/JobRunner.php b/includes/jobqueue/JobRunner.php index cacccbec08bd..baff288e853c 100644 --- a/includes/jobqueue/JobRunner.php +++ b/includes/jobqueue/JobRunner.php @@ -27,6 +27,7 @@ use Liuggio\StatsdClient\Factory\StatsdDataFactory; use Psr\Log\LoggerAwareInterface; use Psr\Log\LoggerInterface; use Wikimedia\ScopedCallback; +use Wikimedia\Rdbms\LBFactory; /** * Job queue runner utility methods diff --git a/includes/jobqueue/JobSpecification.php b/includes/jobqueue/JobSpecification.php index d636dc651623..d8447951436b 100644 --- a/includes/jobqueue/JobSpecification.php +++ b/includes/jobqueue/JobSpecification.php @@ -128,7 +128,7 @@ class JobSpecification implements IJobSpecification { $this->type = $type; $this->params = $params; - $this->title = $title ?: Title::makeTitle( NS_SPECIAL, 'Badtitle/' . get_class( $this ) ); + $this->title = $title ?: Title::makeTitle( NS_SPECIAL, 'Badtitle/' . static::class ); $this->opts = $opts; } diff --git a/includes/jobqueue/jobs/CategoryMembershipChangeJob.php b/includes/jobqueue/jobs/CategoryMembershipChangeJob.php index a52ff065b0c3..3a0063c3edb7 100644 --- a/includes/jobqueue/jobs/CategoryMembershipChangeJob.php +++ b/includes/jobqueue/jobs/CategoryMembershipChangeJob.php @@ -20,6 +20,7 @@ * @file */ use MediaWiki\MediaWikiServices; +use Wikimedia\Rdbms\LBFactory; /** * Job to add recent change entries mentioning category membership changes diff --git a/includes/jobqueue/jobs/DoubleRedirectJob.php b/includes/jobqueue/jobs/DoubleRedirectJob.php index 3cd3448f5408..74c446fc3cbc 100644 --- a/includes/jobqueue/jobs/DoubleRedirectJob.php +++ b/includes/jobqueue/jobs/DoubleRedirectJob.php @@ -137,7 +137,7 @@ class DoubleRedirectJob extends Job { wfDebug( __METHOD__ . " : skipping, already good\n" ); } - // Preserve fragment (bug 14904) + // Preserve fragment (T16904) $newTitle = Title::makeTitle( $newTitle->getNamespace(), $newTitle->getDBkey(), $currentDest->getFragment(), $newTitle->getInterwiki() ); @@ -199,7 +199,7 @@ class DoubleRedirectJob extends Job { $seenTitles[$titleText] = true; if ( $title->isExternal() ) { - // If the target is interwiki, we have to break early (bug 40352). + // If the target is interwiki, we have to break early (T42352). // Otherwise it will look up a row in the local page table // with the namespace/page of the interwiki target which can cause // unexpected results (e.g. X -> foo:Bar -> Bar -> .. ) diff --git a/includes/jobqueue/jobs/HTMLCacheUpdateJob.php b/includes/jobqueue/jobs/HTMLCacheUpdateJob.php index f09ba57b5383..2d816f9f0676 100644 --- a/includes/jobqueue/jobs/HTMLCacheUpdateJob.php +++ b/includes/jobqueue/jobs/HTMLCacheUpdateJob.php @@ -22,6 +22,8 @@ * @ingroup Cache */ +use MediaWiki\MediaWikiServices; + /** * Job to purge the cache for all pages that link to or use another page or file * @@ -113,7 +115,7 @@ class HTMLCacheUpdateJob extends Job { $touchTimestamp = wfTimestampNow(); $dbw = wfGetDB( DB_MASTER ); - $factory = wfGetLBFactory(); + $factory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory(); $ticket = $factory->getEmptyTransactionTicket( __METHOD__ ); // Update page_touched (skipping pages already touched since the root job). // Check $wgUpdateRowsPerQuery for sanity; batch jobs are sized by that already. diff --git a/includes/jobqueue/jobs/PublishStashedFileJob.php b/includes/jobqueue/jobs/PublishStashedFileJob.php index 37e80c24ce14..e89812beff10 100644 --- a/includes/jobqueue/jobs/PublishStashedFileJob.php +++ b/includes/jobqueue/jobs/PublishStashedFileJob.php @@ -128,7 +128,7 @@ class PublishStashedFileJob extends Job { ); $this->setLastError( get_class( $e ) . ": " . $e->getMessage() ); // To prevent potential database referential integrity issues. - // See bug 32551. + // See T34551. MWExceptionHandler::rollbackMasterChangesAndLog( $e ); return false; diff --git a/includes/jobqueue/jobs/RecentChangesUpdateJob.php b/includes/jobqueue/jobs/RecentChangesUpdateJob.php index 0e90674a2fe0..5c733088eecd 100644 --- a/includes/jobqueue/jobs/RecentChangesUpdateJob.php +++ b/includes/jobqueue/jobs/RecentChangesUpdateJob.php @@ -128,8 +128,10 @@ class RecentChangesUpdateJob extends Job { $dbw->setSessionOptions( [ 'connTimeout' => 900 ] ); $lockKey = wfWikiID() . '-activeusers'; - if ( !$dbw->lock( $lockKey, __METHOD__, 1 ) ) { - return; // exclusive update (avoids duplicate entries) + if ( !$dbw->lockIsFree( $lockKey, __METHOD__ ) || !$dbw->lock( $lockKey, __METHOD__, 1 ) ) { + // Exclusive update (avoids duplicate entries)… it's usually fine to just drop out here, + // if the Job is already running. + return; } $nowUnix = time(); @@ -168,15 +170,6 @@ class RecentChangesUpdateJob extends Job { $names[$row->rc_user_text] = $row->lastedittime; } - // Rotate out users that have not edited in too long (according to old data set) - $dbw->delete( 'querycachetwo', - [ - 'qcc_type' => 'activeusers', - 'qcc_value < ' . $dbw->addQuotes( $nowUnix - $days * 86400 ) // TS_UNIX - ], - __METHOD__ - ); - // Find which of the recently active users are already accounted for if ( count( $names ) ) { $res = $dbw->select( 'querycachetwo', @@ -184,9 +177,13 @@ class RecentChangesUpdateJob extends Job { [ 'qcc_type' => 'activeusers', 'qcc_namespace' => NS_USER, - 'qcc_title' => array_keys( $names ) ], + 'qcc_title' => array_keys( $names ), + 'qcc_value >= ' . $dbw->addQuotes( $nowUnix - $days * 86400 ), // TS_UNIX + ], __METHOD__ ); + // Note: In order for this to be actually consistent, we would need + // to update these rows with the new lastedittime. foreach ( $res as $row ) { unset( $names[$row->user_name] ); } @@ -224,6 +221,16 @@ class RecentChangesUpdateJob extends Job { ); $dbw->unlock( $lockKey, __METHOD__ ); + + // Rotate out users that have not edited in too long (according to old data set) + $dbw->delete( 'querycachetwo', + [ + 'qcc_type' => 'activeusers', + 'qcc_value < ' . $dbw->addQuotes( $nowUnix - $days * 86400 ) // TS_UNIX + ], + __METHOD__ + ); + }, __METHOD__ ); diff --git a/includes/jobqueue/jobs/RefreshLinksJob.php b/includes/jobqueue/jobs/RefreshLinksJob.php index 651a332d5e82..f9284a57ceb8 100644 --- a/includes/jobqueue/jobs/RefreshLinksJob.php +++ b/includes/jobqueue/jobs/RefreshLinksJob.php @@ -29,7 +29,7 @@ use MediaWiki\MediaWikiServices; * - a) Recursive jobs to update links for backlink pages for a given title. * These jobs have (recursive:true,table:<table>) set. * - b) Jobs to update links for a set of pages (the job title is ignored). - * These jobs have (pages:(<page ID>:(<namespace>,<title>),...) set. + * These jobs have (pages:(<page ID>:(<namespace>,<title>),...) set. * - c) Jobs to update links for a single page (the job title) * These jobs need no extra fields set. * diff --git a/includes/jobqueue/utils/BacklinkJobUtils.php b/includes/jobqueue/utils/BacklinkJobUtils.php index 7f500554bdd6..1c12a1c9b412 100644 --- a/includes/jobqueue/utils/BacklinkJobUtils.php +++ b/includes/jobqueue/utils/BacklinkJobUtils.php @@ -33,7 +33,7 @@ * For example, if templates A and B are edited (at the same time) the queue will have: * (A base, B base) * When these jobs run, the queue will have per-title and remnant partition jobs: - * (titleX,titleY,titleZ,...,A remnant,titleM,titleN,titleO,...,B remnant) + * (titleX,titleY,titleZ,...,A remnant,titleM,titleN,titleO,...,B remnant) * * This works best when the queue is FIFO, for several reasons: * - a) Since the remnant jobs are enqueued after the leaf jobs, the slower leaf jobs have to @@ -133,7 +133,7 @@ class BacklinkJobUtils { 'table' => $params['table'], 'range' => [ 'start' => $ranges[1][0], - 'end' => $ranges[count( $ranges ) - 1][1], + 'end' => $ranges[count( $ranges ) - 1][1], 'batchSize' => $realBSize, 'subranges' => array_slice( $ranges, 1 ) ], diff --git a/includes/jobqueue/utils/PurgeJobUtils.php b/includes/jobqueue/utils/PurgeJobUtils.php index d76d8661b49f..ba80c8e450a9 100644 --- a/includes/jobqueue/utils/PurgeJobUtils.php +++ b/includes/jobqueue/utils/PurgeJobUtils.php @@ -20,6 +20,7 @@ * * @file */ +use Wikimedia\Rdbms\IDatabase; use MediaWiki\MediaWikiServices; class PurgeJobUtils { diff --git a/includes/libs/CSSMin.php b/includes/libs/CSSMin.php index bc99672f3630..bba07e263b5c 100644 --- a/includes/libs/CSSMin.php +++ b/includes/libs/CSSMin.php @@ -8,7 +8,7 @@ * not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS @@ -176,6 +176,12 @@ class CSSMin { * @return bool|string */ public static function getMimeType( $file ) { + // Infer the MIME-type from the file extension + $ext = strtolower( pathinfo( $file, PATHINFO_EXTENSION ) ); + if ( isset( self::$mimeTypes[$ext] ) ) { + return self::$mimeTypes[$ext]; + } + $realpath = realpath( $file ); if ( $realpath @@ -186,12 +192,6 @@ class CSSMin { return finfo_file( finfo_open( FILEINFO_MIME_TYPE ), $realpath ); } - // Infer the MIME-type from the file extension - $ext = strtolower( pathinfo( $file, PATHINFO_EXTENSION ) ); - if ( isset( self::$mimeTypes[$ext] ) ) { - return self::$mimeTypes[$ext]; - } - return false; } @@ -237,7 +237,7 @@ class CSSMin { // * Otherwise remap the URL to work in generated stylesheets // Guard against trailing slashes, because "some/remote/../foo.png" - // resolves to "some/remote/foo.png" on (some?) clients (bug 27052). + // resolves to "some/remote/foo.png" on (some?) clients (T29052). if ( substr( $remote, -1 ) == '/' ) { $remote = substr( $remote, 0, -1 ); } diff --git a/includes/libs/CryptHKDF.php b/includes/libs/CryptHKDF.php index 4c867574182d..6b3e4a7acacd 100644 --- a/includes/libs/CryptHKDF.php +++ b/includes/libs/CryptHKDF.php @@ -197,11 +197,11 @@ class CryptHKDF { * From http://eprint.iacr.org/2010/264.pdf: * * The scheme HKDF is specifed as: - * HKDF(XTS, SKM, CTXinfo, L) = K(1) || K(2) || ... || K(t) + * HKDF(XTS, SKM, CTXinfo, L) = K(1) || K(2) || ... || K(t) * where the values K(i) are defined as follows: - * PRK = HMAC(XTS, SKM) - * K(1) = HMAC(PRK, CTXinfo || 0); - * K(i+1) = HMAC(PRK, K(i) || CTXinfo || i), 1 <= i < t; + * PRK = HMAC(XTS, SKM) + * K(1) = HMAC(PRK, CTXinfo || 0); + * K(i+1) = HMAC(PRK, K(i) || CTXinfo || i), 1 <= i < t; * where t = [L/k] and the value K(t) is truncated to its first d = L mod k bits; * the counter i is non-wrapping and of a given fixed size, e.g., a single byte. * Note that the length of the HMAC output is the same as its key length and therefore diff --git a/includes/libs/CryptRand.php b/includes/libs/CryptRand.php index 10088f2363a5..0d3613ae230c 100644 --- a/includes/libs/CryptRand.php +++ b/includes/libs/CryptRand.php @@ -243,6 +243,21 @@ class CryptRand { } if ( strlen( $buffer ) < $bytes ) { + // If available make use of PHP 7's random_bytes + // On Linux, getrandom syscall will be used if available. + // On Windows CryptGenRandom will always be used + // On other platforms, /dev/urandom will be used. + // All error situations will throw Exceptions and or Errors + if ( function_exists( 'random_bytes' ) ) { + $rem = $bytes - strlen( $buffer ); + $buffer .= random_bytes( $rem ); + } + if ( strlen( $buffer ) >= $bytes ) { + $this->strong = true; + } + } + + if ( strlen( $buffer ) < $bytes ) { // If available make use of mcrypt_create_iv URANDOM source to generate randomness // On unix-like systems this reads from /dev/urandom but does it without any buffering // and bypasses openbasedir restrictions, so it's preferable to reading directly diff --git a/includes/libs/DnsSrvDiscoverer.php b/includes/libs/DnsSrvDiscoverer.php new file mode 100644 index 000000000000..ce8a2044f596 --- /dev/null +++ b/includes/libs/DnsSrvDiscoverer.php @@ -0,0 +1,108 @@ +<?php +/** + * Service discovery using DNS SRV records + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + */ + +/** + * @since 1.29 + */ +class DnsSrvDiscoverer { + /** + * @var string + */ + private $domain; + + /** + * @param string $domain + */ + public function __construct( $domain ) { + $this->domain = $domain; + } + + /** + * Fetch the servers with a DNS SRV request + * + * @return array + */ + public function getServers() { + $result = []; + foreach ( $this->getDnsRecords() as $record ) { + $result[] = [ + 'target' => $record['target'], + 'port' => $record['port'], + 'pri' => $record['pri'], + 'weight' => $record['weight'], + ]; + } + + return $result; + } + + /** + * Pick a server according to the priority fields. + * Note that weight is currently ignored. + * + * @param array $servers from getServers + * @return array|bool + */ + public function pickServer( array $servers ) { + if ( !$servers ) { + return false; + } + + $srvsByPrio = []; + foreach ( $servers as $server ) { + $srvsByPrio[$server['pri']][] = $server; + } + + $min = min( array_keys( $srvsByPrio ) ); + if ( count( $srvsByPrio[$min] ) == 1 ) { + return $srvsByPrio[$min][0]; + } else { + // Choose randomly + $rand = mt_rand( 0, count( $srvsByPrio[$min] ) - 1 ); + + return $srvsByPrio[$min][$rand]; + } + } + + /** + * @param array $server + * @param array $servers + * @return array[] + */ + public function removeServer( $server, array $servers ) { + foreach ( $servers as $i => $srv ) { + if ( $srv['target'] === $server['target'] && $srv['port'] === $server['port'] ) { + unset( $servers[$i] ); + break; + } + } + + return array_values( $servers ); + } + + /** + * @return array[] + */ + protected function getDnsRecords() { + return dns_get_record( $this->domain, DNS_SRV ); + } +} diff --git a/includes/libs/HttpStatus.php b/includes/libs/HttpStatus.php index 72fc33386935..27f872857c0d 100644 --- a/includes/libs/HttpStatus.php +++ b/includes/libs/HttpStatus.php @@ -101,6 +101,7 @@ class HttpStatus { return false; } + MediaWiki\HeaderCallback::warnIfHeadersSent(); if ( $version === null ) { $version = isset( $_SERVER['SERVER_PROTOCOL'] ) && $_SERVER['SERVER_PROTOCOL'] === 'HTTP/1.0' ? diff --git a/includes/libs/IP.php b/includes/libs/IP.php index 21203a47ce7a..a6aa0a3f8853 100644 --- a/includes/libs/IP.php +++ b/includes/libs/IP.php @@ -675,7 +675,7 @@ class IP { * @return string|null Valid dotted quad IPv4 address or null */ public static function canonicalize( $addr ) { - // remove zone info (bug 35738) + // remove zone info (T37738) $addr = preg_replace( '/\%.*/', '', $addr ); if ( self::isValid( $addr ) ) { diff --git a/includes/libs/StatusValue.php b/includes/libs/StatusValue.php index db085da54860..e860ec491fff 100644 --- a/includes/libs/StatusValue.php +++ b/includes/libs/StatusValue.php @@ -85,7 +85,7 @@ class StatusValue { * defined as: * [ * 0 => object(StatusValue) # the StatusValue with error messages, only - * 1 => object(StatusValue) # The StatusValue with warning messages, only + * 1 => object(StatusValue) # The StatusValue with warning messages, only * ] * * @return StatusValue[] @@ -154,7 +154,7 @@ class StatusValue { } /** - * Change operation resuklt + * Change operation result * * @param bool $ok Whether the operation completed * @param mixed $value diff --git a/includes/libs/StringUtils.php b/includes/libs/StringUtils.php index 26f3c4ac6132..cffb5a39457a 100644 --- a/includes/libs/StringUtils.php +++ b/includes/libs/StringUtils.php @@ -168,6 +168,7 @@ class StringUtils { ) { $inputPos = 0; $outputPos = 0; + $contentPos = 0; $output = ''; $foundStart = false; $encStart = preg_quote( $startDelim, '!' ); diff --git a/includes/libs/eventrelayer/EventRelayer.php b/includes/libs/eventrelayer/EventRelayer.php index b0cd413699f1..304f6c12b883 100644 --- a/includes/libs/eventrelayer/EventRelayer.php +++ b/includes/libs/eventrelayer/EventRelayer.php @@ -65,4 +65,3 @@ abstract class EventRelayer implements LoggerAwareInterface { */ abstract protected function doNotify( $channel, array $events ); } - diff --git a/includes/libs/filebackend/FSFileBackend.php b/includes/libs/filebackend/FSFileBackend.php index 8afdce4035ba..4f0805bd2ab8 100644 --- a/includes/libs/filebackend/FSFileBackend.php +++ b/includes/libs/filebackend/FSFileBackend.php @@ -21,6 +21,7 @@ * @ingroup FileBackend * @author Aaron Schulz */ +use Wikimedia\Timestamp\ConvertibleTimestamp; /** * @brief Class for a file system (FS) based file backend. diff --git a/includes/libs/filebackend/FileBackendMultiWrite.php b/includes/libs/filebackend/FileBackendMultiWrite.php index 212e84f02628..53bce33daddd 100644 --- a/includes/libs/filebackend/FileBackendMultiWrite.php +++ b/includes/libs/filebackend/FileBackendMultiWrite.php @@ -167,7 +167,7 @@ class FileBackendMultiWrite extends FileBackend { // Do a consistency check to see if the backends are consistent... $syncStatus = $this->consistencyCheck( $relevantPaths ); if ( !$syncStatus->isOK() ) { - wfDebugLog( 'FileOperation', get_class( $this ) . + wfDebugLog( 'FileOperation', static::class . " failed sync check: " . FormatJson::encode( $relevantPaths ) ); // Try to resync the clone backends to the master on the spot... if ( $this->autoResync === false @@ -378,7 +378,7 @@ class FileBackendMultiWrite extends FileBackend { } if ( !$status->isOK() ) { - wfDebugLog( 'FileOperation', get_class( $this ) . + wfDebugLog( 'FileOperation', static::class . " failed to resync: " . FormatJson::encode( $paths ) ); } diff --git a/includes/libs/filebackend/FileBackendStore.php b/includes/libs/filebackend/FileBackendStore.php index a7ceab2ed514..039bd4250874 100644 --- a/includes/libs/filebackend/FileBackendStore.php +++ b/includes/libs/filebackend/FileBackendStore.php @@ -21,6 +21,7 @@ * @ingroup FileBackend * @author Aaron Schulz */ +use Wikimedia\Timestamp\ConvertibleTimestamp; /** * @brief Base class for all backends using particular storage medium. @@ -359,7 +360,7 @@ abstract class FileBackendStore extends FileBackend { $status->merge( $this->doConcatenate( $params ) ); $sec = microtime( true ) - $start_time; if ( !$status->isOK() ) { - $this->logger->error( get_class( $this ) . "-{$this->name}" . + $this->logger->error( static::class . "-{$this->name}" . " failed to concatenate " . count( $params['srcs'] ) . " file(s) [$sec sec]" ); } } @@ -1122,7 +1123,7 @@ abstract class FileBackendStore extends FileBackend { $subStatus->success[$i] = false; ++$subStatus->failCount; } - $this->logger->error( get_class( $this ) . "-{$this->name} " . + $this->logger->error( static::class . "-{$this->name} " . " stat failure; aborted operations: " . FormatJson::encode( $ops ) ); } @@ -1199,21 +1200,20 @@ abstract class FileBackendStore extends FileBackend { * to the order in which the handles where given. * * @param FileBackendStoreOpHandle[] $fileOpHandles - * - * @throws FileBackendError * @return StatusValue[] Map of StatusValue objects + * @throws FileBackendError */ final public function executeOpHandlesInternal( array $fileOpHandles ) { $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" ); foreach ( $fileOpHandles as $fileOpHandle ) { if ( !( $fileOpHandle instanceof FileBackendStoreOpHandle ) ) { - throw new InvalidArgumentException( "Got a non-FileBackendStoreOpHandle object." ); + throw new InvalidArgumentException( "Expected FileBackendStoreOpHandle object." ); } elseif ( $fileOpHandle->backend->getName() !== $this->getName() ) { - throw new InvalidArgumentException( - "Got a FileBackendStoreOpHandle for the wrong backend." ); + throw new InvalidArgumentException( "Expected handle for this file backend." ); } } + $res = $this->doExecuteOpHandlesInternal( $fileOpHandles ); foreach ( $fileOpHandles as $fileOpHandle ) { $fileOpHandle->closeResources(); diff --git a/includes/libs/filebackend/HTTPFileStreamer.php b/includes/libs/filebackend/HTTPFileStreamer.php index 800fdfad3ee1..a7d064b6e304 100644 --- a/includes/libs/filebackend/HTTPFileStreamer.php +++ b/includes/libs/filebackend/HTTPFileStreamer.php @@ -19,6 +19,7 @@ * * @file */ +use Wikimedia\Timestamp\ConvertibleTimestamp; /** * Functions related to the output of file content diff --git a/includes/libs/filebackend/SwiftFileBackend.php b/includes/libs/filebackend/SwiftFileBackend.php index d40e896488ca..ae0ad6fb849e 100644 --- a/includes/libs/filebackend/SwiftFileBackend.php +++ b/includes/libs/filebackend/SwiftFileBackend.php @@ -287,7 +287,7 @@ class SwiftFileBackend extends FileBackendStore { if ( !empty( $params['async'] ) ) { // deferred $status->value = $opHandle; } else { // actually write the object in Swift - $status->merge( current( $this->doExecuteOpHandlesInternal( [ $opHandle ] ) ) ); + $status->merge( current( $this->executeOpHandlesInternal( [ $opHandle ] ) ) ); } return $status; @@ -348,10 +348,12 @@ class SwiftFileBackend extends FileBackendStore { }; $opHandle = new SwiftFileOpHandle( $this, $handler, $reqs ); + $opHandle->resourcesToClose[] = $handle; + if ( !empty( $params['async'] ) ) { // deferred $status->value = $opHandle; } else { // actually write the object in Swift - $status->merge( current( $this->doExecuteOpHandlesInternal( [ $opHandle ] ) ) ); + $status->merge( current( $this->executeOpHandlesInternal( [ $opHandle ] ) ) ); } return $status; @@ -399,7 +401,7 @@ class SwiftFileBackend extends FileBackendStore { if ( !empty( $params['async'] ) ) { // deferred $status->value = $opHandle; } else { // actually write the object in Swift - $status->merge( current( $this->doExecuteOpHandlesInternal( [ $opHandle ] ) ) ); + $status->merge( current( $this->executeOpHandlesInternal( [ $opHandle ] ) ) ); } return $status; @@ -458,7 +460,7 @@ class SwiftFileBackend extends FileBackendStore { if ( !empty( $params['async'] ) ) { // deferred $status->value = $opHandle; } else { // actually move the object in Swift - $status->merge( current( $this->doExecuteOpHandlesInternal( [ $opHandle ] ) ) ); + $status->merge( current( $this->executeOpHandlesInternal( [ $opHandle ] ) ) ); } return $status; @@ -498,7 +500,7 @@ class SwiftFileBackend extends FileBackendStore { if ( !empty( $params['async'] ) ) { // deferred $status->value = $opHandle; } else { // actually delete the object in Swift - $status->merge( current( $this->doExecuteOpHandlesInternal( [ $opHandle ] ) ) ); + $status->merge( current( $this->executeOpHandlesInternal( [ $opHandle ] ) ) ); } return $status; @@ -554,7 +556,7 @@ class SwiftFileBackend extends FileBackendStore { if ( !empty( $params['async'] ) ) { // deferred $status->value = $opHandle; } else { // actually change the object in Swift - $status->merge( current( $this->doExecuteOpHandlesInternal( [ $opHandle ] ) ) ); + $status->merge( current( $this->executeOpHandlesInternal( [ $opHandle ] ) ) ); } return $status; @@ -1089,7 +1091,7 @@ class SwiftFileBackend extends FileBackendStore { // good } elseif ( $rcode === 404 ) { $status->fatal( 'backend-fail-stream', $params['src'] ); - // Per bug 41113, nasty things can happen if bad cache entries get + // Per T43113, nasty things can happen if bad cache entries get // stuck in cache. It's also possible that this error can come up // with simple race conditions. Clear out the stat cache to be safe. $this->clearCache( [ $params['src'] ] ); diff --git a/includes/libs/filebackend/fileop/FileOp.php b/includes/libs/filebackend/fileop/FileOp.php index fab5a3743c1e..79af194483a0 100644 --- a/includes/libs/filebackend/fileop/FileOp.php +++ b/includes/libs/filebackend/fileop/FileOp.php @@ -461,7 +461,7 @@ abstract class FileOp { $params = $this->params; $params['failedAction'] = $action; try { - $this->logger->error( get_class( $this ) . + $this->logger->error( static::class . " failed (batch #{$this->batchId}): " . FormatJson::encode( $params ) ); } catch ( Exception $e ) { // bad config? debug log error? diff --git a/includes/libs/lockmanager/DBLockManager.php b/includes/libs/lockmanager/DBLockManager.php index b17b1a0b0d8c..8ef819600850 100644 --- a/includes/libs/lockmanager/DBLockManager.php +++ b/includes/libs/lockmanager/DBLockManager.php @@ -21,6 +21,8 @@ * @ingroup LockManager */ +use Wikimedia\Rdbms\IDatabase; + /** * Version of LockManager based on using named/row DB locks. * diff --git a/includes/libs/lockmanager/QuorumLockManager.php b/includes/libs/lockmanager/QuorumLockManager.php index a89d864ac208..1d2e21aa0d70 100644 --- a/includes/libs/lockmanager/QuorumLockManager.php +++ b/includes/libs/lockmanager/QuorumLockManager.php @@ -127,6 +127,39 @@ abstract class QuorumLockManager extends LockManager { * @return StatusValue */ final protected function doLockingRequestBucket( $bucket, array $pathsByType ) { + return $this->collectPledgeQuorum( + $bucket, + function ( $lockSrv ) use ( $pathsByType ) { + return $this->getLocksOnServer( $lockSrv, $pathsByType ); + } + ); + } + + /** + * Attempt to release locks with the peers for a bucket + * + * @param int $bucket + * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths + * @return StatusValue + */ + final protected function doUnlockingRequestBucket( $bucket, array $pathsByType ) { + return $this->releasePledges( + $bucket, + function ( $lockSrv ) use ( $pathsByType ) { + return $this->freeLocksOnServer( $lockSrv, $pathsByType ); + } + ); + } + + /** + * Attempt to acquire pledges with the peers for a bucket. + * This is all or nothing; if any key is already pledged then this totally fails. + * + * @param int $bucket + * @param callable $callback Pledge method taking a server name and yeilding a StatusValue + * @return StatusValue + */ + final protected function collectPledgeQuorum( $bucket, callable $callback ) { $status = StatusValue::newGood(); $yesVotes = 0; // locks made on trustable servers @@ -141,7 +174,7 @@ abstract class QuorumLockManager extends LockManager { continue; // server down? } // Attempt to acquire the lock on this peer - $status->merge( $this->getLocksOnServer( $lockSrv, $pathsByType ) ); + $status->merge( $callback( $lockSrv ) ); if ( !$status->isOK() ) { return $status; // vetoed; resource locked } @@ -162,13 +195,13 @@ abstract class QuorumLockManager extends LockManager { } /** - * Attempt to release locks with the peers for a bucket + * Attempt to release pledges with the peers for a bucket * * @param int $bucket - * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths + * @param callable $callback Pledge method taking a server name and yeilding a StatusValue * @return StatusValue */ - final protected function doUnlockingRequestBucket( $bucket, array $pathsByType ) { + final protected function releasePledges( $bucket, callable $callback ) { $status = StatusValue::newGood(); $yesVotes = 0; // locks freed on trustable servers @@ -180,7 +213,7 @@ abstract class QuorumLockManager extends LockManager { $status->warning( 'lockmanager-fail-svr-release', $lockSrv ); } else { // Attempt to release the lock on this peer - $status->merge( $this->freeLocksOnServer( $lockSrv, $pathsByType ) ); + $status->merge( $callback( $lockSrv ) ); ++$yesVotes; // success for this peer // Normally the first peers form the quorum, and the others are ignored. // Ignore them in this case, but not when an alternative quorum was used. diff --git a/includes/libs/mime/MimeAnalyzer.php b/includes/libs/mime/MimeAnalyzer.php index e42d1a95c174..6ea3c215bc70 100644 --- a/includes/libs/mime/MimeAnalyzer.php +++ b/includes/libs/mime/MimeAnalyzer.php @@ -83,7 +83,7 @@ class MimeAnalyzer implements LoggerAwareInterface { * what will break? In practice this probably isn't a problem anyway -- Bryan) */ protected static $wellKnownTypes = <<<EOT -application/ogg ogx ogg ogm ogv oga spx +application/ogg ogx ogg ogm ogv oga spx opus application/pdf pdf application/vnd.oasis.opendocument.chart odc application/vnd.oasis.opendocument.chart-template otc @@ -108,7 +108,8 @@ audio/midi mid midi kar audio/mpeg mpga mpa mp2 mp3 audio/x-aiff aif aiff aifc audio/x-wav wav -audio/ogg oga spx ogg +audio/ogg oga spx ogg opus +audio/opus opus ogg oga ogg spx image/x-bmp bmp image/gif gif image/jpeg jpeg jpg jpe @@ -526,7 +527,7 @@ EOT; 'xbm', // Formats we recognize magic numbers for - 'djvu', 'ogx', 'ogg', 'ogv', 'oga', 'spx', + 'djvu', 'ogx', 'ogg', 'ogv', 'oga', 'spx', 'opus', 'mid', 'pdf', 'wmf', 'xcf', 'webm', 'mkv', 'mka', 'webp', @@ -1054,6 +1055,8 @@ EOT; return MEDIATYPE_AUDIO; } elseif ( strpos( $head, 'speex' ) !== false ) { return MEDIATYPE_AUDIO; + } elseif ( strpos( $head, 'opus' ) !== false ) { + return MEDIATYPE_AUDIO; } else { return MEDIATYPE_MULTIMEDIA; } diff --git a/includes/libs/mime/XmlTypeCheck.php b/includes/libs/mime/XmlTypeCheck.php index 7f2bf5e81b02..e48cf6234657 100644 --- a/includes/libs/mime/XmlTypeCheck.php +++ b/includes/libs/mime/XmlTypeCheck.php @@ -73,19 +73,36 @@ class XmlTypeCheck { */ private $parserOptions = [ 'processing_instruction_handler' => '', + 'external_dtd_handler' => '', + 'dtd_handler' => '', + 'require_safe_dtd' => true ]; /** + * Allow filtering an XML file. + * + * Filters should return either true or a string to indicate something + * is wrong with the file. $this->filterMatch will store if the + * file failed validation (true = failed validation). + * $this->filterMatchType will contain the validation error. + * $this->wellFormed will contain whether the xml file is well-formed. + * + * @note If multiple filters are hit, only one of them will have the + * result stored in $this->filterMatchType. + * * @param string $input a filename or string containing the XML element * @param callable $filterCallback (optional) * Function to call to do additional custom validity checks from the * SAX element handler event. This gives you access to the element * namespace, name, attributes, and text contents. - * Filter should return 'true' to toggle on $this->filterMatch + * Filter should return a truthy value describing the error. * @param bool $isFile (optional) indicates if the first parameter is a * filename (default, true) or if it is a string (false) * @param array $options list of additional parsing options: * processing_instruction_handler: Callback for xml_set_processing_instruction_handler + * external_dtd_handler: Callback for the url of external dtd subset + * dtd_handler: Callback given the full text of the <!DOCTYPE declaration. + * require_safe_dtd: Only allow non-recursive entities in internal dtd (default true) */ function __construct( $input, $filterCallback = null, $isFile = true, $options = [] ) { $this->filterCallback = $filterCallback; @@ -186,6 +203,9 @@ class XmlTypeCheck { if ( $reader->nodeType === XMLReader::PI ) { $this->processingInstructionHandler( $reader->name, $reader->value ); } + if ( $reader->nodeType === XMLReader::DOC_TYPE ) { + $this->DTDHandler( $reader ); + } } while ( $reader->nodeType != XMLReader::ELEMENT ); // Process the rest of the document @@ -234,8 +254,13 @@ class XmlTypeCheck { $reader->value ); break; + case XMLReader::DOC_TYPE: + // We should never see a doctype after first + // element. + $this->wellFormed = false; + break; default: - // One of DOC, DOC_TYPE, ENTITY, END_ENTITY, + // One of DOC, ENTITY, END_ENTITY, // NOTATION, or XML_DECLARATION // xml_parse didn't send these to the filter, so we won't. } @@ -339,4 +364,140 @@ class XmlTypeCheck { $this->filterMatchType = $callbackReturn; } } + /** + * Handle coming across a <!DOCTYPE declaration. + * + * @param XMLReader $reader Reader currently pointing at DOCTYPE node. + */ + private function DTDHandler( XMLReader $reader ) { + $externalCallback = $this->parserOptions['external_dtd_handler']; + $generalCallback = $this->parserOptions['dtd_handler']; + $checkIfSafe = $this->parserOptions['require_safe_dtd']; + if ( !$externalCallback && !$generalCallback && !$checkIfSafe ) { + return; + } + $dtd = $reader->readOuterXML(); + $callbackReturn = false; + + if ( $generalCallback ) { + $callbackReturn = call_user_func( $generalCallback, $dtd ); + } + if ( $callbackReturn ) { + // Filter hit! + $this->filterMatch = true; + $this->filterMatchType = $callbackReturn; + $callbackReturn = false; + } + + $parsedDTD = $this->parseDTD( $dtd ); + if ( $externalCallback && isset( $parsedDTD['type'] ) ) { + $callbackReturn = call_user_func( + $externalCallback, + $parsedDTD['type'], + isset( $parsedDTD['publicid'] ) ? $parsedDTD['publicid'] : null, + isset( $parsedDTD['systemid'] ) ? $parsedDTD['systemid'] : null + ); + } + if ( $callbackReturn ) { + // Filter hit! + $this->filterMatch = true; + $this->filterMatchType = $callbackReturn; + $callbackReturn = false; + } + + if ( $checkIfSafe && isset( $parsedDTD['internal'] ) ) { + if ( !$this->checkDTDIsSafe( $parsedDTD['internal'] ) ) { + $this->wellFormed = false; + } + } + } + + /** + * Check if the internal subset of the DTD is safe. + * + * We whitelist an extremely restricted subset of DTD features. + * + * Safe is defined as: + * * Only contains entity defintions (e.g. No <!ATLIST ) + * * Entity definitions are not "system" entities + * * Entity definitions are not "parameter" (i.e. %) entities + * * Entity definitions do not reference other entites except & + * and quotes. Entity aliases (where the entity contains only + * another entity are allowed) + * * Entity references aren't overly long (>255 bytes). + * * <!ATTLIST svg xmlns:xlink CDATA #FIXED "http://www.w3.org/1999/xlink"> + * allowed if matched exactly for compatibility with graphviz + * * Comments. + * + * @param string $internalSubset The internal subset of the DTD + * @return bool true if safe. + */ + private function checkDTDIsSafe( $internalSubset ) { + $offset = 0; + $res = preg_match( + '/^(?:\s*<!ENTITY\s+\S+\s+' . + '(?:"(?:&[^"%&;]{1,64};|(?:[^"%&]|&|"){0,255})"' . + '|\'(?:&[^"%&;]{1,64};|(?:[^\'%&]|&|'){0,255})\')\s*>' . + '|\s*<!--(?:[^-]|-[^-])*-->' . + '|\s*<!ATTLIST svg xmlns:xlink CDATA #FIXED ' . + '"http:\/\/www.w3.org\/1999\/xlink">)*\s*$/', + $internalSubset + ); + + return (bool)$res; + } + + /** + * Parse DTD into parts. + * + * If there is an error parsing the dtd, sets wellFormed to false. + * + * @param $dtd string + * @return array Possibly containing keys publicid, systemid, type and internal. + */ + private function parseDTD( $dtd ) { + $m = []; + $res = preg_match( + '/^<!DOCTYPE\s*\S+\s*' . + '(?:(?P<typepublic>PUBLIC)\s*' . + '(?:"(?P<pubquote>[^"]*)"|\'(?P<pubapos>[^\']*)\')' . // public identifer + '\s*"(?P<pubsysquote>[^"]*)"|\'(?P<pubsysapos>[^\']*)\'' . // system identifier + '|(?P<typesystem>SYSTEM)\s*' . + '(?:"(?P<sysquote>[^"]*)"|\'(?P<sysapos>[^\']*)\')' . + ')?\s*' . + '(?:\[\s*(?P<internal>.*)\])?\s*>$/s', + $dtd, + $m + ); + if ( !$res ) { + $this->wellFormed = false; + return []; + } + $parsed = []; + foreach ( $m as $field => $value ) { + if ( $value === '' || is_numeric( $field ) ) { + continue; + } + switch ( $field ) { + case 'typepublic': + case 'typesystem': + $parsed['type'] = $value; + break; + case 'pubquote': + case 'pubapos': + $parsed['publicid'] = $value; + break; + case 'pubsysquote': + case 'pubsysapos': + case 'sysquote': + case 'sysapos': + $parsed['systemid'] = $value; + break; + case 'internal': + $parsed['internal'] = $value; + break; + } + } + return $parsed; + } } diff --git a/includes/libs/mime/mime.info b/includes/libs/mime/mime.info index b04d3c68a2d6..2468f3841db6 100644 --- a/includes/libs/mime/mime.info +++ b/includes/libs/mime/mime.info @@ -35,6 +35,7 @@ audio/wav audio/x-wav audio/wave [AUDIO] audio/midi audio/mid [AUDIO] audio/basic [AUDIO] audio/ogg [AUDIO] +audio/opus [AUDIO] audio/x-aiff [AUDIO] audio/x-pn-realaudio [AUDIO] audio/x-realaudio [AUDIO] @@ -94,9 +95,9 @@ application/vnd.ms-powerpoint [OFFICE] application/x-director [OFFICE] text/rtf [OFFICE] -application/vnd.openxmlformats-officedocument.wordprocessingml.document [OFFICE] +application/vnd.openxmlformats-officedocument.wordprocessingml.document [OFFICE] application/vnd.openxmlformats-officedocument.wordprocessingml.template [OFFICE] -application/vnd.ms-word.document.macroEnabled.12 [OFFICE] +application/vnd.ms-word.document.macroEnabled.12 [OFFICE] application/vnd.ms-word.template.macroEnabled.12 [OFFICE] application/vnd.openxmlformats-officedocument.presentationml.template [OFFICE] application/vnd.openxmlformats-officedocument.presentationml.slideshow [OFFICE] diff --git a/includes/libs/mime/mime.types b/includes/libs/mime/mime.types index b4f515af5853..f1cd59d18fc6 100644 --- a/includes/libs/mime/mime.types +++ b/includes/libs/mime/mime.types @@ -72,6 +72,7 @@ audio/basic au snd audio/midi mid midi kar audio/mpeg mpga mp2 mp3 audio/ogg oga ogg spx opus +audio/opus opus oga ogg video/webm webm audio/webm webm audio/x-aiff aif aiff aifc diff --git a/includes/libs/objectcache/BagOStuff.php b/includes/libs/objectcache/BagOStuff.php index d0b68bcbd7b2..77c4259a0d23 100644 --- a/includes/libs/objectcache/BagOStuff.php +++ b/includes/libs/objectcache/BagOStuff.php @@ -679,7 +679,7 @@ abstract class BagOStuff implements IExpiringStore, LoggerAwareInterface { protected function debug( $text ) { if ( $this->debugMode ) { $this->logger->debug( "{class} debug: $text", [ - 'class' => get_class( $this ), + 'class' => static::class, ] ); } } diff --git a/includes/libs/objectcache/CachedBagOStuff.php b/includes/libs/objectcache/CachedBagOStuff.php index 74bf4b515f7a..c85a82ea010c 100644 --- a/includes/libs/objectcache/CachedBagOStuff.php +++ b/includes/libs/objectcache/CachedBagOStuff.php @@ -81,6 +81,22 @@ class CachedBagOStuff extends HashBagOStuff { $this->backend->setDebug( $bool ); } + public function deleteObjectsExpiringBefore( $date, $progressCallback = false ) { + parent::deleteObjectsExpiringBefore( $date, $progressCallback ); + return $this->backend->deleteObjectsExpiringBefore( $date, $progressCallback ); + } + + public function makeKey() { + return call_user_func_array( [ $this->backend, __FUNCTION__ ], func_get_args() ); + } + + public function makeGlobalKey() { + return call_user_func_array( [ $this->backend, __FUNCTION__ ], func_get_args() ); + } + + // These just call the backend (tested elsewhere) + // @codeCoverageIgnoreStart + public function lock( $key, $timeout = 6, $expiry = 6, $rclass = '' ) { return $this->backend->lock( $key, $timeout, $expiry, $rclass ); } @@ -89,21 +105,17 @@ class CachedBagOStuff extends HashBagOStuff { return $this->backend->unlock( $key ); } - public function deleteObjectsExpiringBefore( $date, $progressCallback = false ) { - parent::deleteObjectsExpiringBefore( $date, $progressCallback ); - return $this->backend->deleteObjectsExpiringBefore( $date, $progressCallback ); - } - public function getLastError() { return $this->backend->getLastError(); } public function clearLastError() { - $this->backend->clearLastError(); + return $this->backend->clearLastError(); } public function modifySimpleRelayEvent( array $event ) { return $this->backend->modifySimpleRelayEvent( $event ); } + // @codeCoverageIgnoreEnd } diff --git a/includes/libs/objectcache/RESTBagOStuff.php b/includes/libs/objectcache/RESTBagOStuff.php index ae91be51de6d..730eed1e3004 100644 --- a/includes/libs/objectcache/RESTBagOStuff.php +++ b/includes/libs/objectcache/RESTBagOStuff.php @@ -63,9 +63,9 @@ class RESTBagOStuff extends BagOStuff { protected function doGet( $key, $flags = 0 ) { $req = [ 'method' => 'GET', - 'url' => $this->url . rawurlencode( $key ), - + 'url' => $this->url . rawurlencode( $key ), ]; + list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $this->client->run( $req ); if ( $rcode === 200 ) { if ( is_string( $rbody ) ) { diff --git a/includes/libs/objectcache/RedisBagOStuff.php b/includes/libs/objectcache/RedisBagOStuff.php index d852f82ea5a1..583ec37755cd 100644 --- a/includes/libs/objectcache/RedisBagOStuff.php +++ b/includes/libs/objectcache/RedisBagOStuff.php @@ -321,7 +321,7 @@ class RedisBagOStuff extends BagOStuff { */ protected function serialize( $data ) { // Serialize anything but integers so INCR/DECR work - // Do not store integer-like strings as integers to avoid type confusion (bug 60563) + // Do not store integer-like strings as integers to avoid type confusion (T62563) return is_int( $data ) ? $data : serialize( $data ); } diff --git a/includes/libs/objectcache/WANObjectCache.php b/includes/libs/objectcache/WANObjectCache.php index 171c291cf146..f0a439a21aa5 100644 --- a/includes/libs/objectcache/WANObjectCache.php +++ b/includes/libs/objectcache/WANObjectCache.php @@ -44,15 +44,20 @@ use Psr\Log\NullLogger; * * The simplest purge method is delete(). * - * There are two supported ways to handle broadcasted operations: + * There are three supported ways to handle broadcasted operations: * - a) Configure the 'purge' EventRelayer to point to a valid PubSub endpoint - * that has subscribed listeners on the cache servers applying the cache updates. + * that has subscribed listeners on the cache servers applying the cache updates. * - b) Ignore the 'purge' EventRelayer configuration (default is NullEventRelayer) - * and set up mcrouter as the underlying cache backend, using one of the memcached - * BagOStuff classes as 'cache'. Use OperationSelectorRoute in the mcrouter settings - * to configure 'set' and 'delete' operations to go to all DCs via AllAsyncRoute and - * configure other operations to go to the local DC via PoolRoute (for reference, - * see https://github.com/facebook/mcrouter/wiki/List-of-Route-Handles). + * and set up mcrouter as the underlying cache backend, using one of the memcached + * BagOStuff classes as 'cache'. Use OperationSelectorRoute in the mcrouter settings + * to configure 'set' and 'delete' operations to go to all DCs via AllAsyncRoute and + * configure other operations to go to the local DC via PoolRoute (for reference, + * see https://github.com/facebook/mcrouter/wiki/List-of-Route-Handles). + * - c) Ignore the 'purge' EventRelayer configuration (default is NullEventRelayer) + * and set up dynomite as cache middleware between the web servers and either + * memcached or redis. This will also broadcast all key setting operations, not just purges, + * which can be useful for cache warming. Writes are eventually consistent via the + * Dynamo replication model (see https://github.com/Netflix/dynomite). * * Broadcasted operations like delete() and touchCheckKey() are done asynchronously * in all datacenters this way, though the local one should likely be near immediate. @@ -1128,6 +1133,65 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface { } /** + * Locally set a key to expire soon if it is stale based on $purgeTimestamp + * + * This sets stale keys' time-to-live at HOLDOFF_TTL seconds, which both avoids + * broadcasting in mcrouter setups and also avoids races with new tombstones. + * + * @param string $key Cache key + * @param int $purgeTimestamp UNIX timestamp of purge + * @param bool &$isStale Whether the key is stale + * @return bool Success + * @since 1.28 + */ + 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 ) { + $isStale = true; + $this->logger->warning( "Reaping stale value key '$key'." ); + $ttlReap = self::HOLDOFF_TTL; // avoids races with tombstone creation + $ok = $this->cache->changeTTL( self::VALUE_KEY_PREFIX . $key, $ttlReap ); + if ( !$ok ) { + $this->logger->error( "Could not complete reap of key '$key'." ); + } + + return $ok; + } + + $isStale = false; + + return true; + } + + /** + * Locally set a "check" key to expire soon if it is stale based on $purgeTimestamp + * + * @param string $key Cache key + * @param int $purgeTimestamp UNIX timestamp of purge + * @param bool &$isStale Whether the key is stale + * @return bool Success + * @since 1.28 + */ + 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; + $this->logger->warning( "Reaping stale check key '$key'." ); + $ok = $this->cache->changeTTL( self::TIME_KEY_PREFIX . $key, 1 ); + if ( !$ok ) { + $this->logger->error( "Could not complete reap of check key '$key'." ); + } + + return $ok; + } + + $isStale = false; + + return false; + } + + /** * @see BagOStuff::makeKey() * @param string ... Key component * @return string diff --git a/includes/libs/objectcache/WANObjectCacheReaper.php b/includes/libs/objectcache/WANObjectCacheReaper.php new file mode 100644 index 000000000000..956a3a9c3a9d --- /dev/null +++ b/includes/libs/objectcache/WANObjectCacheReaper.php @@ -0,0 +1,205 @@ +<?php +/** + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @ingroup Cache + * @author Aaron Schulz + */ + +use Psr\Log\LoggerAwareInterface; +use Psr\Log\LoggerInterface; +use Psr\Log\NullLogger; +use Wikimedia\ScopedCallback; + +/** + * Class for scanning through chronological, log-structured data or change logs + * and locally purging cache keys related to entities that appear in this data. + * + * This is useful for repairing cache when purges are missed by using a reliable + * stream, such as Kafka or a replicated MySQL table. Purge loss between datacenters + * is expected to be more common than within them. + * + * @since 1.28 + */ +class WANObjectCacheReaper implements LoggerAwareInterface { + /** @var WANObjectCache */ + protected $cache; + /** @var BagOStuff */ + protected $store; + /** @var callable */ + protected $logChunkCallback; + /** @var callable */ + protected $keyListCallback; + /** @var LoggerInterface */ + protected $logger; + + /** @var string */ + protected $channel; + /** @var integer */ + protected $initialStartWindow; + + /** + * @param WANObjectCache $cache Cache to reap bad keys from + * @param BagOStuff $store Cache to store positions use for locking + * @param callable $logCallback Callback taking arguments: + * - The starting position as a UNIX timestamp + * - The starting unique ID used for breaking timestamp collisions or null + * - The ending position as a UNIX timestamp + * - The maximum number of results to return + * It returns a list of maps of (key: cache key, pos: UNIX timestamp, id: unique ID) + * for each key affected, with the corrosponding event timestamp/ID information. + * The events should be in ascending order, by (timestamp,id). + * @param callable $keyCallback Callback taking arguments: + * - The WANObjectCache instance + * - An object from the event log + * It should return a list of WAN cache keys. + * The callback must fully duck-type test the object, since can be any model class. + * @param array $params Additional options: + * - channel: the name of the update event stream. + * Default: WANObjectCache::DEFAULT_PURGE_CHANNEL. + * - initialStartWindow: seconds back in time to start if the position is lost. + * Default: 1 hour. + * - logger: an SPL monolog instance [optional] + */ + public function __construct( + WANObjectCache $cache, + BagOStuff $store, + callable $logCallback, + callable $keyCallback, + array $params + ) { + $this->cache = $cache; + $this->store = $store; + + $this->logChunkCallback = $logCallback; + $this->keyListCallback = $keyCallback; + if ( isset( $params['channel'] ) ) { + $this->channel = $params['channel']; + } else { + throw new UnexpectedValueException( "No channel specified." ); + } + + $this->initialStartWindow = isset( $params['initialStartWindow'] ) + ? $params['initialStartWindow'] + : 3600; + $this->logger = isset( $params['logger'] ) + ? $params['logger'] + : new NullLogger(); + } + + public function setLogger( LoggerInterface $logger ) { + $this->logger = $logger; + } + + /** + * Check and reap stale keys based on a chunk of events + * + * @param int $n Number of events + * @return int Number of keys checked + */ + final public function invoke( $n = 100 ) { + $posKey = $this->store->makeGlobalKey( 'WANCache', 'reaper', $this->channel ); + $scopeLock = $this->store->getScopedLock( "$posKey:busy", 0 ); + if ( !$scopeLock ) { + return 0; + } + + $now = time(); + $status = $this->store->get( $posKey ); + if ( !$status ) { + $status = [ 'pos' => $now - $this->initialStartWindow, 'id' => null ]; + } + + // Get events for entities who's keys tombstones/hold-off should have expired by now + $events = call_user_func_array( + $this->logChunkCallback, + [ $status['pos'], $status['id'], $now - WANObjectCache::HOLDOFF_TTL - 1, $n ] + ); + + $event = null; + $keyEvents = []; + foreach ( $events as $event ) { + $keys = call_user_func_array( + $this->keyListCallback, + [ $this->cache, $event['item'] ] + ); + foreach ( $keys as $key ) { + unset( $keyEvents[$key] ); // use only the latest per key + $keyEvents[$key] = [ + 'pos' => $event['pos'], + 'id' => $event['id'] + ]; + } + } + + $purgeCount = 0; + $lastOkEvent = null; + foreach ( $keyEvents as $key => $keyEvent ) { + if ( !$this->cache->reap( $key, $keyEvent['pos'] ) ) { + break; + } + ++$purgeCount; + $lastOkEvent = $event; + } + + if ( $lastOkEvent ) { + $ok = $this->store->merge( + $posKey, + function ( $bag, $key, $curValue ) use ( $lastOkEvent ) { + if ( !$curValue ) { + // Use new position + } else { + $curCoord = [ $curValue['pos'], $curValue['id'] ]; + $newCoord = [ $lastOkEvent['pos'], $lastOkEvent['id'] ]; + if ( $newCoord < $curCoord ) { + // Keep prior position instead of rolling it back + return $curValue; + } + } + + return [ + 'pos' => $lastOkEvent['pos'], + 'id' => $lastOkEvent['id'], + 'ctime' => $curValue ? $curValue['ctime'] : date( 'c' ) + ]; + }, + IExpiringStore::TTL_INDEFINITE + ); + + $pos = $lastOkEvent['pos']; + $id = $lastOkEvent['id']; + if ( $ok ) { + $this->logger->info( "Updated cache reap position ($pos, $id)." ); + } else { + $this->logger->error( "Could not update cache reap position ($pos, $id)." ); + } + } + + ScopedCallback::consume( $scopeLock ); + + return $purgeCount; + } + + /** + * @return array|bool Returns (pos, id) map or false if not set + */ + public function getState() { + $posKey = $this->store->makeGlobalKey( 'WANCache', 'reaper', $this->channel ); + + return $this->store->get( $posKey ); + } +} diff --git a/includes/libs/objectcache/WinCacheBagOStuff.php b/includes/libs/objectcache/WinCacheBagOStuff.php index d84c9591e3f5..98f44d11b5ce 100644 --- a/includes/libs/objectcache/WinCacheBagOStuff.php +++ b/includes/libs/objectcache/WinCacheBagOStuff.php @@ -41,7 +41,7 @@ class WinCacheBagOStuff extends BagOStuff { $result = wincache_ucache_set( $key, serialize( $value ), $expire ); /* wincache_ucache_set returns an empty array on success if $value - was an array, bool otherwise */ + * was an array, bool otherwise */ return ( is_array( $result ) && $result === [] ) || $result; } diff --git a/includes/libs/rdbms/ChronologyProtector.php b/includes/libs/rdbms/ChronologyProtector.php index 88af1dbdf8f4..8b1aabe3cd48 100644 --- a/includes/libs/rdbms/ChronologyProtector.php +++ b/includes/libs/rdbms/ChronologyProtector.php @@ -20,9 +20,14 @@ * @file * @ingroup Database */ + +namespace Wikimedia\Rdbms; + use Psr\Log\LoggerAwareInterface; use Psr\Log\LoggerInterface; +use Psr\Log\NullLogger; use Wikimedia\WaitConditionLoop; +use BagOStuff; /** * Class for ensuring a consistent ordering of events as seen by the user, despite replication. @@ -70,9 +75,9 @@ class ChronologyProtector implements LoggerAwareInterface { public function __construct( BagOStuff $store, array $client, $posTime = null ) { $this->store = $store; $this->clientId = md5( $client['ip'] . "\n" . $client['agent'] ); - $this->key = $store->makeGlobalKey( __CLASS__, $this->clientId ); + $this->key = $store->makeGlobalKey( __CLASS__, $this->clientId, 'v1' ); $this->waitForPosTime = $posTime; - $this->logger = new \Psr\Log\NullLogger(); + $this->logger = new NullLogger(); } public function setLogger( LoggerInterface $logger ) { @@ -114,7 +119,10 @@ class ChronologyProtector implements LoggerAwareInterface { $this->initPositions(); $masterName = $lb->getServerName( $lb->getWriterIndex() ); - if ( !empty( $this->startupPositions[$masterName] ) ) { + if ( + isset( $this->startupPositions[$masterName] ) && + $this->startupPositions[$masterName] instanceof DBMasterPos + ) { $pos = $this->startupPositions[$masterName]; $this->logger->info( __METHOD__ . ": LB for '$masterName' set to pos $pos\n" ); $lb->waitFor( $pos ); @@ -293,8 +301,9 @@ class ChronologyProtector implements LoggerAwareInterface { $min = null; foreach ( $data['positions'] as $pos ) { - /** @var DBMasterPos $pos */ - $min = $min ? min( $pos->asOfTime(), $min ) : $pos->asOfTime(); + if ( $pos instanceof DBMasterPos ) { + $min = $min ? min( $pos->asOfTime(), $min ) : $pos->asOfTime(); + } } return $min; @@ -313,8 +322,10 @@ class ChronologyProtector implements LoggerAwareInterface { $curPositions = $curValue['positions']; // Use the newest positions for each DB master foreach ( $shutdownPositions as $db => $pos ) { - if ( !isset( $curPositions[$db] ) - || $pos->asOfTime() > $curPositions[$db]->asOfTime() + if ( + !isset( $curPositions[$db] ) || + !( $curPositions[$db] instanceof DBMasterPos ) || + $pos->asOfTime() > $curPositions[$db]->asOfTime() ) { $curPositions[$db] = $pos; } diff --git a/includes/libs/rdbms/TransactionProfiler.php b/includes/libs/rdbms/TransactionProfiler.php index bf5e299865af..5d3534ffaa41 100644 --- a/includes/libs/rdbms/TransactionProfiler.php +++ b/includes/libs/rdbms/TransactionProfiler.php @@ -22,9 +22,12 @@ * @author Aaron Schulz */ +namespace Wikimedia\Rdbms; + use Psr\Log\LoggerInterface; use Psr\Log\LoggerAwareInterface; use Psr\Log\NullLogger; +use RuntimeException; /** * Helper class that detects high-contention DB queries via profiling calls diff --git a/includes/libs/rdbms/connectionmanager/ConnectionManager.php b/includes/libs/rdbms/connectionmanager/ConnectionManager.php index 4f72f77afd1d..49b2fe685ccc 100644 --- a/includes/libs/rdbms/connectionmanager/ConnectionManager.php +++ b/includes/libs/rdbms/connectionmanager/ConnectionManager.php @@ -3,10 +3,7 @@ namespace Wikimedia\Rdbms; use Database; -use DBConnRef; -use IDatabase; use InvalidArgumentException; -use LoadBalancer; /** * Database connection manager. diff --git a/includes/libs/rdbms/connectionmanager/SessionConsistentConnectionManager.php b/includes/libs/rdbms/connectionmanager/SessionConsistentConnectionManager.php index fb031822e71d..08ec74ae7b86 100644 --- a/includes/libs/rdbms/connectionmanager/SessionConsistentConnectionManager.php +++ b/includes/libs/rdbms/connectionmanager/SessionConsistentConnectionManager.php @@ -3,7 +3,6 @@ namespace Wikimedia\Rdbms; use Database; -use DBConnRef; /** * Database connection manager. diff --git a/includes/libs/rdbms/database/DBConnRef.php b/includes/libs/rdbms/database/DBConnRef.php index b268b9f28cf1..0d5fbf5c2c75 100644 --- a/includes/libs/rdbms/database/DBConnRef.php +++ b/includes/libs/rdbms/database/DBConnRef.php @@ -1,4 +1,10 @@ <?php + +namespace Wikimedia\Rdbms; + +use Database; +use InvalidArgumentException; + /** * Helper class to handle automatically marking connections as reusable (via RAII pattern) * as well handling deferring the actual network connection until the handle is used @@ -598,3 +604,5 @@ class DBConnRef implements IDatabase { } } } + +class_alias( 'Wikimedia\Rdbms\DBConnRef', 'DBConnRef' ); diff --git a/includes/libs/rdbms/database/Database.php b/includes/libs/rdbms/database/Database.php index 69cf1ac356bf..40bcc1b02c15 100644 --- a/includes/libs/rdbms/database/Database.php +++ b/includes/libs/rdbms/database/Database.php @@ -26,6 +26,15 @@ use Psr\Log\LoggerAwareInterface; use Psr\Log\LoggerInterface; use Wikimedia\ScopedCallback; +use Wikimedia\Rdbms\TransactionProfiler; +use Wikimedia\Rdbms\LikeMatch; +use Wikimedia\Rdbms\DatabaseDomain; +use Wikimedia\Rdbms\ResultWrapper; +use Wikimedia\Rdbms\DBMasterPos; +use Wikimedia\Rdbms\Blob; +use Wikimedia\Timestamp\ConvertibleTimestamp; +use Wikimedia\Rdbms\IDatabase; +use Wikimedia\Rdbms\IMaintainableDatabase; /** * Relational database abstraction object @@ -359,7 +368,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware } $class = 'Database' . ucfirst( $driver ); - if ( class_exists( $class ) && is_subclass_of( $class, 'IDatabase' ) ) { + if ( class_exists( $class ) && is_subclass_of( $class, IDatabase::class ) ) { // Resolve some defaults for b/c $p['host'] = isset( $p['host'] ) ? $p['host'] : false; $p['user'] = isset( $p['user'] ) ? $p['user'] : false; @@ -841,7 +850,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware } // Add trace comment to the begin of the sql string, right after the operator. - // Or, for one-word queries (like "BEGIN" or COMMIT") add it to the end (bug 42598) + // Or, for one-word queries (like "BEGIN" or COMMIT") add it to the end (T44598) $commentedSql = preg_replace( '/\s|$/', " /* $fname {$this->agent} */ ", $sql, 1 ); # Start implicit transactions that wrap the request if DBO_TRX is enabled @@ -1020,8 +1029,8 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware private function handleSessionLoss() { $this->mTrxLevel = 0; - $this->mTrxIdleCallbacks = []; // bug 65263 - $this->mTrxPreCommitCallbacks = []; // bug 65263 + $this->mTrxIdleCallbacks = []; // T67263 + $this->mTrxPreCommitCallbacks = []; // T67263 $this->mSessionTempTables = []; $this->mNamedLocksHeld = []; try { @@ -1136,12 +1145,6 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware $preLimitTail .= $this->makeOrderBy( $options ); - // if (isset($options['LIMIT'])) { - // $tailOpts .= $this->limitResult('', $options['LIMIT'], - // isset($options['OFFSET']) ? $options['OFFSET'] - // : false); - // } - if ( isset( $noKeyOptions['FOR UPDATE'] ) ) { $postLimitTail .= ' FOR UPDATE'; } @@ -1353,7 +1356,10 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware ) { $rows = 0; $sql = $this->selectSQLText( $tables, '1', $conds, $fname, $options, $join_conds ); - $res = $this->query( "SELECT COUNT(*) AS rowcount FROM ($sql) tmp_count", $fname ); + // The identifier quotes is primarily for MSSQL. + $rowCountCol = $this->addIdentifierQuotes( "rowcount" ); + $tableName = $this->addIdentifierQuotes( "tmp_count" ); + $res = $this->query( "SELECT COUNT(*) AS $rowCountCol FROM ($sql) $tableName", $fname ); if ( $res ) { $row = $this->fetchRow( $res ); @@ -1538,7 +1544,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware $sql .= " WHERE " . $this->makeList( $conds, self::LIST_AND ); } - return $this->query( $sql, $fname ); + return (bool)$this->query( $sql, $fname ); } public function makeList( $a, $mode = self::LIST_COMMA ) { @@ -1945,7 +1951,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware } /** - * Get the name of an index in a given table. + * Allows for index remapping in queries where this is not consistent across DBMS * * @param string $index * @return string @@ -2313,7 +2319,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware $selectOptions ); if ( is_array( $srcTable ) ) { - $srcTable = implode( ',', array_map( [ &$this, 'tableName' ], $srcTable ) ); + $srcTable = implode( ',', array_map( [ $this, 'tableName' ], $srcTable ) ); } else { $srcTable = $this->tableName( $srcTable ); } @@ -3289,26 +3295,37 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware return false; } - /** - * Lock specific tables - * - * @param array $read Array of tables to lock for read access - * @param array $write Array of tables to lock for write access - * @param string $method Name of caller - * @param bool $lowPriority Whether to indicate writes to be LOW PRIORITY - * @return bool - */ - public function lockTables( $read, $write, $method, $lowPriority = true ) { + public function tableLocksHaveTransactionScope() { return true; } - /** - * Unlock specific tables - * - * @param string $method The caller - * @return bool - */ - public function unlockTables( $method ) { + final public function lockTables( array $read, array $write, $method ) { + if ( $this->writesOrCallbacksPending() ) { + throw new DBUnexpectedError( $this, "Transaction writes or callbacks still pending." ); + } + + if ( $this->tableLocksHaveTransactionScope() ) { + $this->startAtomic( $method ); + } + + return $this->doLockTables( $read, $write, $method ); + } + + protected function doLockTables( array $read, array $write, $method ) { + return true; + } + + final public function unlockTables( $method ) { + if ( $this->tableLocksHaveTransactionScope() ) { + $this->endAtomic( $method ); + + return true; // locks released on COMMIT/ROLLBACK + } + + return $this->doUnlockTables( $method ); + } + + protected function doUnlockTables( $method ) { return true; } @@ -3411,7 +3428,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware */ public function __clone() { $this->connLogger->warning( - "Cloning " . get_class( $this ) . " is not recomended; forking connection:\n" . + "Cloning " . static::class . " is not recomended; forking connection:\n" . ( new RuntimeException() )->getTraceAsString() ); @@ -3462,4 +3479,4 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware } } -class_alias( 'Database', 'DatabaseBase' ); +class_alias( Database::class, 'DatabaseBase' ); diff --git a/includes/libs/rdbms/database/DatabaseDomain.php b/includes/libs/rdbms/database/DatabaseDomain.php index a3ae6f10a143..ef6600b4fc82 100644 --- a/includes/libs/rdbms/database/DatabaseDomain.php +++ b/includes/libs/rdbms/database/DatabaseDomain.php @@ -18,6 +18,9 @@ * @file * @ingroup Database */ +namespace Wikimedia\Rdbms; + +use InvalidArgumentException; /** * Class to handle database/prefix specification for IDatabase domains diff --git a/includes/db/DatabaseMssql.php b/includes/libs/rdbms/database/DatabaseMssql.php index 7c4bb3b72150..75ddc9d51413 100644 --- a/includes/db/DatabaseMssql.php +++ b/includes/libs/rdbms/database/DatabaseMssql.php @@ -24,11 +24,19 @@ * @author Ryan Biesemeyer <v-ryanbi at microsoft dot com> * @author Ryan Schmidt <skizzerz at gmail dot com> */ +use Wikimedia\Rdbms\Blob; +use Wikimedia\Rdbms\MssqlBlob; +use Wikimedia\Rdbms\MssqlField; +use Wikimedia\Rdbms\ResultWrapper; +use Wikimedia\Rdbms\MssqlResultWrapper; /** * @ingroup Database */ class DatabaseMssql extends Database { + protected $mPort; + protected $mUseWindowsAuth = false; + protected $mInsertId = null; protected $mLastResult = null; protected $mAffectedRows = null; @@ -40,8 +48,6 @@ class DatabaseMssql extends Database { protected $mIgnoreDupKeyErrors = false; protected $mIgnoreErrors = []; - protected $mPort; - public function implicitGroupby() { return false; } @@ -54,6 +60,13 @@ class DatabaseMssql extends Database { return false; } + public function __construct( array $params ) { + $this->mPort = $params['port']; + $this->mUseWindowsAuth = $params['UseWindowsAuth']; + + parent::__construct( $params ); + } + /** * Usually aborts on failure * @param string $server @@ -73,8 +86,6 @@ class DatabaseMssql extends Database { ); } - global $wgDBport, $wgDBWindowsAuthentication; - # e.g. the class is being loaded if ( !strlen( $user ) ) { return null; @@ -82,7 +93,6 @@ class DatabaseMssql extends Database { $this->close(); $this->mServer = $server; - $this->mPort = $wgDBport; $this->mUser = $user; $this->mPassword = $password; $this->mDBname = $dbName; @@ -94,8 +104,8 @@ class DatabaseMssql extends Database { } // Decide which auth scenerio to use - // if we are using Windows auth, don't add credentials to $connectionInfo - if ( !$wgDBWindowsAuthentication ) { + // if we are using Windows auth, then don't add credentials to $connectionInfo + if ( !$this->mUseWindowsAuth ) { $connectionInfo['UID'] = $user; $connectionInfo['PWD'] = $password; } @@ -141,20 +151,15 @@ class DatabaseMssql extends Database { /** * @param string $sql - * @return bool|MssqlResult + * @return bool|MssqlResultWrapper|resource * @throws DBUnexpectedError */ protected function doQuery( $sql ) { - if ( $this->getFlag( DBO_DEBUG ) ) { - wfDebug( "SQL: [$sql]\n" ); - } - $this->offset = 0; - // several extensions seem to think that all databases support limits - // via LIMIT N after the WHERE clause well, MSSQL uses SELECT TOP N, + // via LIMIT N after the WHERE clause, but MSSQL uses SELECT TOP N, // so to catch any of those extensions we'll do a quick check for a // LIMIT clause and pass $sql through $this->LimitToTopN() which parses - // the limit clause and passes the result to $this->limitResult(); + // the LIMIT clause and passes the result to $this->limitResult(); if ( preg_match( '/\bLIMIT\s*/i', $sql ) ) { // massage LIMIT -> TopN $sql = $this->LimitToTopN( $sql ); @@ -187,7 +192,7 @@ class DatabaseMssql extends Database { $success = (bool)$stmt; } - // make a copy so that anything we add below does not get reflected in future queries + // Make a copy to ensure what we add below does not get reflected in future queries $ignoreErrors = $this->mIgnoreErrors; if ( $this->mIgnoreDupKeyErrors ) { @@ -328,7 +333,8 @@ class DatabaseMssql extends Database { * @return string */ private function formatError( $err ) { - return '[SQLSTATE ' . $err['SQLSTATE'] . '][Error Code ' . $err['code'] . ']' . $err['message']; + return '[SQLSTATE ' . + $err['SQLSTATE'] . '][Error Code ' . $err['code'] . ']' . $err['message']; } /** @@ -520,7 +526,7 @@ class DatabaseMssql extends Database { public function indexInfo( $table, $index, $fname = __METHOD__ ) { # This does not return the same info as MYSQL would, but that's OK # because MediaWiki never uses the returned value except to check for - # the existance of indexes. + # the existence of indexes. $sql = "sp_helpindex '" . $this->tableName( $table ) . "'"; $res = $this->query( $sql, $fname ); @@ -607,9 +613,10 @@ class DatabaseMssql extends Database { $this->mIgnoreDupKeyErrors = true; } + $ret = null; foreach ( $arrToInsert as $a ) { // start out with empty identity column, this is so we can return - // it as a result of the insert logic + // it as a result of the INSERT logic $sqlPre = ''; $sqlPost = ''; $identityClause = ''; @@ -677,21 +684,23 @@ class DatabaseMssql extends Database { } $this->mScrollableCursor = true; - if ( !is_null( $identity ) ) { - // then we want to get the identity column value we were assigned and save it off + if ( $ret instanceof ResultWrapper && !is_null( $identity ) ) { + // Then we want to get the identity column value we were assigned and save it off $row = $ret->fetchObject(); if ( is_object( $row ) ) { $this->mInsertId = $row->$identity; - - // it seems that mAffectedRows is -1 sometimes when OUTPUT INSERTED.identity is used - // if we got an identity back, we know for sure a row was affected, so adjust that here + // It seems that mAffectedRows is -1 sometimes when OUTPUT INSERTED.identity is + // used if we got an identity back, we know for sure a row was affected, so + // adjust that here if ( $this->mAffectedRows == -1 ) { $this->mAffectedRows = 1; } } } } + $this->mIgnoreDupKeyErrors = false; + return $ret; } @@ -866,7 +875,7 @@ class DatabaseMssql extends Database { $select = $orderby = []; $s1 = preg_match( '#SELECT\s+(.+?)\s+FROM#Dis', $sql, $select ); $s2 = preg_match( '#(ORDER BY\s+.+?)(\s*FOR XML .*)?$#Dis', $sql, $orderby ); - $overOrder = $postOrder = ''; + $postOrder = ''; $first = $offset + 1; $last = $offset + $limit; $sub1 = 'sub_' . $this->mSubqueryId; @@ -954,13 +963,12 @@ class DatabaseMssql extends Database { if ( $db !== false ) { // remote database - wfDebug( "Attempting to call tableExists on a remote table" ); + $this->queryLogger->error( "Attempting to call tableExists on a remote table" ); return false; } if ( $schema === false ) { - global $wgDBmwschema; - $schema = $wgDBmwschema; + $schema = $this->mSchema; } $res = $this->query( "SELECT 1 FROM INFORMATION_SCHEMA.TABLES @@ -986,7 +994,7 @@ class DatabaseMssql extends Database { if ( $db !== false ) { // remote database - wfDebug( "Attempting to call fieldExists on a remote table" ); + $this->queryLogger->error( "Attempting to call fieldExists on a remote table" ); return false; } @@ -1005,7 +1013,7 @@ class DatabaseMssql extends Database { if ( $db !== false ) { // remote database - wfDebug( "Attempting to call fieldInfo on a remote table" ); + $this->queryLogger->error( "Attempting to call fieldInfo on a remote table" ); return false; } @@ -1049,32 +1057,6 @@ class DatabaseMssql extends Database { } /** - * Escapes a identifier for use inm SQL. - * Throws an exception if it is invalid. - * Reference: http://msdn.microsoft.com/en-us/library/aa224033%28v=SQL.80%29.aspx - * @param string $identifier - * @throws InvalidArgumentException - * @return string - */ - private function escapeIdentifier( $identifier ) { - if ( strlen( $identifier ) == 0 ) { - throw new InvalidArgumentException( "An identifier must not be empty" ); - } - if ( strlen( $identifier ) > 128 ) { - throw new InvalidArgumentException( "The identifier '$identifier' is too long (max. 128)" ); - } - if ( ( strpos( $identifier, '[' ) !== false ) - || ( strpos( $identifier, ']' ) !== false ) - ) { - // It may be allowed if you quoted with double quotation marks, but - // that would break if QUOTED_IDENTIFIER is OFF - throw new InvalidArgumentException( "Square brackets are not allowed in '$identifier'" ); - } - - return "[$identifier]"; - } - - /** * @param string $s * @return string */ @@ -1197,10 +1179,6 @@ class DatabaseMssql extends Database { return [ $startOpts, '', $tailOpts, '', '' ]; } - /** - * Get the type of the DBMS, as it appears in $wgDBtype. - * @return string - */ public function getType() { return 'mssql'; } @@ -1359,7 +1337,12 @@ class DatabaseMssql extends Database { * @return bool|null */ public function prepareStatements( $value = null ) { - return wfSetVar( $this->mPrepareStatements, $value ); + $old = $this->mPrepareStatements; + if ( $value !== null ) { + $this->mPrepareStatements = $value; + } + + return $old; } /** @@ -1369,16 +1352,11 @@ class DatabaseMssql extends Database { * @return bool|null */ public function scrollableCursor( $value = null ) { - return wfSetVar( $this->mScrollableCursor, $value ); - } + $old = $this->mScrollableCursor; + if ( $value !== null ) { + $this->mScrollableCursor = $value; + } - /** - * Called in the installer and updater. - * Probably doesn't need to be called anywhere else in the codebase. - * @param array|null $value - * @return array|null - */ - public function ignoreErrors( array $value = null ) { - return wfSetVar( $this->mIgnoreErrors, $value ); + return $old; } -} // end DatabaseMssql class +} diff --git a/includes/libs/rdbms/database/DatabaseMysqlBase.php b/includes/libs/rdbms/database/DatabaseMysqlBase.php index 5d680e21f079..e2b522610cb8 100644 --- a/includes/libs/rdbms/database/DatabaseMysqlBase.php +++ b/includes/libs/rdbms/database/DatabaseMysqlBase.php @@ -20,6 +20,10 @@ * @file * @ingroup Database */ +use Wikimedia\Rdbms\DBMasterPos; +use Wikimedia\Rdbms\MySQLMasterPos; +use Wikimedia\Rdbms\MySQLField; +use Wikimedia\Rdbms\ResultWrapper; /** * Database abstraction object for MySQL. @@ -817,7 +821,8 @@ abstract class DatabaseMysqlBase extends Database { $row = $res ? $this->fetchRow( $res ) : false; if ( !$row ) { - throw new DBExpectedError( $this, "Failed to query MASTER_POS_WAIT()" ); + throw new DBExpectedError( $this, + "MASTER_POS_WAIT() or MASTER_GTID_WAIT() failed: {$this->lastError()}" ); } // Result can be NULL (error), -1 (timeout), or 0+ per the MySQL manual @@ -1049,36 +1054,26 @@ abstract class DatabaseMysqlBase extends Database { return true; } - /** - * @param array $read - * @param array $write - * @param string $method - * @param bool $lowPriority - * @return bool - */ - public function lockTables( $read, $write, $method, $lowPriority = true ) { - $items = []; + public function tableLocksHaveTransactionScope() { + return false; // tied to TCP connection + } + protected function doLockTables( array $read, array $write, $method ) { + $items = []; foreach ( $write as $table ) { - $tbl = $this->tableName( $table ) . - ( $lowPriority ? ' LOW_PRIORITY' : '' ) . - ' WRITE'; - $items[] = $tbl; + $items[] = $this->tableName( $table ) . ' WRITE'; } foreach ( $read as $table ) { $items[] = $this->tableName( $table ) . ' READ'; } + $sql = "LOCK TABLES " . implode( ',', $items ); $this->query( $sql, $method ); return true; } - /** - * @param string $method - * @return bool - */ - public function unlockTables( $method ) { + protected function doUnlockTables( $method ) { $this->query( "UNLOCK TABLES", $method ); return true; @@ -1330,5 +1325,38 @@ abstract class DatabaseMysqlBase extends Database { public function isView( $name, $prefix = null ) { return in_array( $name, $this->listViews( $prefix ) ); } -} + /** + * Allows for index remapping in queries where this is not consistent across DBMS + * + * @param string $index + * @return string + */ + protected function indexName( $index ) { + /** + * When SQLite indexes were introduced in r45764, it was noted that + * SQLite requires index names to be unique within the whole database, + * not just within a schema. As discussed in CR r45819, to avoid the + * need for a schema change on existing installations, the indexes + * were implicitly mapped from the new names to the old names. + * + * This mapping can be removed if DB patches are introduced to alter + * the relevant tables in existing installations. Note that because + * this index mapping applies to table creation, even new installations + * of MySQL have the old names (except for installations created during + * a period where this mapping was inappropriately removed, see + * T154872). + */ + $renamed = [ + 'ar_usertext_timestamp' => 'usertext_timestamp', + 'un_user_id' => 'user_id', + 'un_user_ip' => 'user_ip', + ]; + + if ( isset( $renamed[$index] ) ) { + return $renamed[$index]; + } else { + return $index; + } + } +} diff --git a/includes/libs/rdbms/database/DatabaseMysqli.php b/includes/libs/rdbms/database/DatabaseMysqli.php index 2f27ff9736d2..7a2200a4da1d 100644 --- a/includes/libs/rdbms/database/DatabaseMysqli.php +++ b/includes/libs/rdbms/database/DatabaseMysqli.php @@ -21,6 +21,8 @@ * @ingroup Database */ +use Wikimedia\Rdbms\ResultWrapper; + /** * Database abstraction object for PHP extension mysqli. * diff --git a/includes/libs/rdbms/database/DatabasePostgres.php b/includes/libs/rdbms/database/DatabasePostgres.php index 42113b0851c9..5bcd4a8e5644 100644 --- a/includes/libs/rdbms/database/DatabasePostgres.php +++ b/includes/libs/rdbms/database/DatabasePostgres.php @@ -20,7 +20,12 @@ * @file * @ingroup Database */ +use Wikimedia\Timestamp\ConvertibleTimestamp; use Wikimedia\WaitConditionLoop; +use Wikimedia\Rdbms\Blob; +use Wikimedia\Rdbms\PostgresBlob; +use Wikimedia\Rdbms\PostgresField; +use Wikimedia\Rdbms\ResultWrapper; /** * @ingroup Database @@ -698,7 +703,7 @@ __INDEXATTR__; list( $startOpts, $useIndex, $tailOpts, $ignoreIndex ) = $this->makeSelectOptions( $selectOptions ); if ( is_array( $srcTable ) ) { - $srcTable = implode( ',', array_map( [ &$this, 'tableName' ], $srcTable ) ); + $srcTable = implode( ',', array_map( [ $this, 'tableName' ], $srcTable ) ); } else { $srcTable = $this->tableName( $srcTable ); } @@ -982,7 +987,7 @@ __INDEXATTR__; /** * Prepend our schema (e.g. 'mediawiki') in front * of the search path - * Fixes bug 15816 + * Fixes T17816 */ $search_path = $this->getSearchPath(); array_unshift( $search_path, @@ -1023,7 +1028,7 @@ __INDEXATTR__; // Normal client $this->numericVersion = $versionInfo['server']; } else { - // Bug 16937: broken pgsql extension from PHP<5.3 + // T18937: broken pgsql extension from PHP<5.3 $this->numericVersion = pg_parameter_status( $conn, 'server_version' ); } } @@ -1076,8 +1081,8 @@ __INDEXATTR__; $q = <<<SQL SELECT 1 FROM pg_class, pg_namespace, pg_trigger WHERE relnamespace=pg_namespace.oid AND relkind='r' - AND tgrelid=pg_class.oid - AND nspname=%s AND relname=%s AND tgname=%s + AND tgrelid=pg_class.oid + AND nspname=%s AND relname=%s AND tgname=%s SQL; $res = $this->query( sprintf( @@ -1249,15 +1254,9 @@ SQL; $preLimitTail .= $this->makeOrderBy( $options ); - // if ( isset( $options['LIMIT'] ) ) { - // $tailOpts .= $this->limitResult( '', $options['LIMIT'], - // isset( $options['OFFSET'] ) ? $options['OFFSET'] - // : false ); - // } - if ( isset( $options['FOR UPDATE'] ) ) { $postLimitTail .= ' FOR UPDATE OF ' . - implode( ', ', array_map( [ &$this, 'tableName' ], $options['FOR UPDATE'] ) ); + implode( ', ', array_map( [ $this, 'tableName' ], $options['FOR UPDATE'] ) ); } elseif ( isset( $noKeyOptions['FOR UPDATE'] ) ) { $postLimitTail .= ' FOR UPDATE'; } @@ -1306,6 +1305,33 @@ SQL; return parent::streamStatementEnd( $sql, $newLine ); } + public function doLockTables( array $read, array $write, $method ) { + $tablesWrite = []; + foreach ( $write as $table ) { + $tablesWrite[] = $this->tableName( $table ); + } + $tablesRead = []; + foreach ( $read as $table ) { + $tablesRead[] = $this->tableName( $table ); + } + + // Acquire locks for the duration of the current transaction... + if ( $tablesWrite ) { + $this->query( + 'LOCK TABLE ONLY ' . implode( ',', $tablesWrite ) . ' IN EXCLUSIVE MODE', + $method + ); + } + if ( $tablesRead ) { + $this->query( + 'LOCK TABLE ONLY ' . implode( ',', $tablesRead ) . ' IN SHARE MODE', + $method + ); + } + + return true; + } + public function lockIsFree( $lockName, $method ) { // http://www.postgresql.org/docs/8.2/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS $key = $this->addQuotes( $this->bigintFromLockName( $lockName ) ); diff --git a/includes/libs/rdbms/database/DatabaseSqlite.php b/includes/libs/rdbms/database/DatabaseSqlite.php index a4b2df01ad61..090ce8eeae39 100644 --- a/includes/libs/rdbms/database/DatabaseSqlite.php +++ b/includes/libs/rdbms/database/DatabaseSqlite.php @@ -21,6 +21,9 @@ * @file * @ingroup Database */ +use Wikimedia\Rdbms\Blob; +use Wikimedia\Rdbms\SQLiteField; +use Wikimedia\Rdbms\ResultWrapper; /** * @ingroup Database diff --git a/includes/libs/rdbms/database/IDatabase.php b/includes/libs/rdbms/database/IDatabase.php index c6055dbdbd6f..0b146cd80ba9 100644 --- a/includes/libs/rdbms/database/IDatabase.php +++ b/includes/libs/rdbms/database/IDatabase.php @@ -23,7 +23,17 @@ * @file * @ingroup Database */ +namespace Wikimedia\Rdbms; + use Wikimedia\ScopedCallback; +use DBError; +use DBConnectionError; +use DBUnexpectedError; +use DBQueryError; +use Exception; +use RuntimeException; +use UnexpectedValueException; +use stdClass; /** * Basic database interface for live and lazy-loaded relation database handles @@ -359,7 +369,7 @@ interface IDatabase { * member variables. * If no more rows are available, false is returned. * - * @param ResultWrapper|stdClass $res Object as returned from IDatabase::query(), etc. + * @param IResultWrapper|stdClass $res Object as returned from IDatabase::query(), etc. * @return stdClass|bool * @throws DBUnexpectedError Thrown if the database returns an error */ @@ -370,7 +380,7 @@ interface IDatabase { * form. Fields are retrieved with $row['fieldname']. * If no more rows are available, false is returned. * - * @param ResultWrapper $res Result object as returned from IDatabase::query(), etc. + * @param IResultWrapper $res Result object as returned from IDatabase::query(), etc. * @return array|bool * @throws DBUnexpectedError Thrown if the database returns an error */ @@ -513,7 +523,7 @@ interface IDatabase { * @param bool $tempIgnore Whether to avoid throwing an exception on errors... * maybe best to catch the exception instead? * @throws DBError - * @return bool|ResultWrapper True for a successful write query, ResultWrapper object + * @return bool|IResultWrapper True for a successful write query, IResultWrapper object * for a successful read query, or false on failure if $tempIgnore set */ public function query( $sql, $fname = __METHOD__, $tempIgnore = false ); @@ -727,7 +737,7 @@ interface IDatabase { * * [ 'page' => [ 'LEFT JOIN', 'page_latest=rev_id' ] ] * - * @return ResultWrapper|bool If the query returned no rows, a ResultWrapper + * @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. @@ -905,6 +915,8 @@ interface IDatabase { * @param array $values An array of values to SET. For each array element, * the key gives the field name, and the value gives the data to set * that field to. The data will be quoted by IDatabase::addQuotes(). + * Values with integer keys form unquoted SET statements, which can be used for + * things like "field = field + 1" or similar computed values. * @param array $conds An array of conditions (WHERE). See * IDatabase::select() for the details of the format of condition * arrays. Use '*' to update all rows. @@ -1148,6 +1160,8 @@ interface IDatabase { * @param array $set An array of values to SET. For each array element, the * key gives the field name, and the value gives the data to set that * field to. The data will be quoted by IDatabase::addQuotes(). + * Values with integer keys form unquoted SET statements, which can be used for + * things like "field = field + 1" or similar computed values. * @param string $fname Calling function name (use __METHOD__) for logs/profiling * @throws Exception * @return bool @@ -1188,7 +1202,7 @@ interface IDatabase { * for the format. Use $conds == "*" to delete all rows * @param string $fname Name of the calling function * @throws DBUnexpectedError - * @return bool|ResultWrapper + * @return bool|IResultWrapper */ public function delete( $table, $conds, $fname = __METHOD__ ); @@ -1216,7 +1230,7 @@ interface IDatabase { * @param array $selectOptions Options for the SELECT part of the query, see * IDatabase::select() for details. * - * @return ResultWrapper + * @return IResultWrapper */ public function insertSelect( $destTable, $srcTable, $varMap, $conds, $fname = __METHOD__, @@ -1793,3 +1807,5 @@ interface IDatabase { */ public function setTableAliases( array $aliases ); } + +class_alias( 'Wikimedia\Rdbms\IDatabase', 'IDatabase' ); diff --git a/includes/libs/rdbms/database/IMaintainableDatabase.php b/includes/libs/rdbms/database/IMaintainableDatabase.php index 43cec28a629e..b984c42544b9 100644 --- a/includes/libs/rdbms/database/IMaintainableDatabase.php +++ b/includes/libs/rdbms/database/IMaintainableDatabase.php @@ -22,6 +22,11 @@ * @file * @ingroup Database */ +namespace Wikimedia\Rdbms; + +use Exception; +use RuntimeException; +use DBUnexpectedError; /** * Advanced database interface for IDatabase handles that include maintenance methods @@ -205,4 +210,73 @@ interface IMaintainableDatabase extends IDatabase { public function duplicateTableStructure( $oldName, $newName, $temporary = false, $fname = __METHOD__ ); + + /** + * Checks if table locks acquired by lockTables() are transaction-bound in their scope + * + * Transaction-bound table locks will be released when the current transaction terminates. + * Table locks that are not bound to a transaction are not effected by BEGIN/COMMIT/ROLLBACK + * and will last until either lockTables()/unlockTables() is called or the TCP connection to + * the database is closed. + * + * @return bool + * @since 1.29 + */ + public function tableLocksHaveTransactionScope(); + + /** + * Lock specific tables + * + * Any pending transaction should be resolved before calling this method, since: + * a) Doing so resets any REPEATABLE-READ snapshot of the data to a fresh one. + * b) Previous row and table locks from the transaction or session may be released + * by LOCK TABLES, which may be unsafe for the changes in such a transaction. + * c) The main use case of lockTables() is to avoid deadlocks and timeouts by locking + * entire tables in order to do long-running, batched, and lag-aware, updates. Batching + * and replication lag checks do not work when all the updates happen in a transaction. + * + * Always get all relevant table locks up-front in one call, since LOCK TABLES might release + * any prior table locks on some RDBMes (e.g MySQL). + * + * For compatibility, callers should check tableLocksHaveTransactionScope() before using + * this method. If locks are scoped specifically to transactions then caller must either: + * - a) Start a new transaction and acquire table locks for the scope of that transaction, + * doing all row updates within that transaction. It will not be possible to update + * rows in batches; this might result in high replication lag. + * - b) Forgo table locks entirely and avoid calling this method. Careful use of hints like + * LOCK IN SHARE MODE and FOR UPDATE and the use of query batching may be preferrable + * to using table locks with a potentially large transaction. Use of MySQL and Postges + * style REPEATABLE-READ (Snapshot Isolation with or without First-Committer-Rule) can + * also be considered for certain tasks that require a consistent view of entire tables. + * + * If session scoped locks are not supported, then calling lockTables() will trigger + * startAtomic(), with unlockTables() triggering endAtomic(). This will automatically + * start a transaction if one is not already present and cause the locks to be released + * when the transaction finishes (normally during the unlockTables() call). + * + * In any case, avoid using begin()/commit() in code that runs while such table locks are + * acquired, as that breaks in case when a transaction is needed. The startAtomic() and + * endAtomic() methods are safe, however, since they will join any existing transaction. + * + * @param array $read Array of tables to lock for read access + * @param array $write Array of tables to lock for write access + * @param string $method Name of caller + * @return bool + * @since 1.29 + */ + public function lockTables( array $read, array $write, $method ); + + /** + * Unlock all tables locked via lockTables() + * + * If table locks are scoped to transactions, then locks might not be released until the + * transaction ends, which could happen after this method is called. + * + * @param string $method The caller + * @return bool + * @since 1.29 + */ + public function unlockTables( $method ); } + +class_alias( 'Wikimedia\Rdbms\IMaintainableDatabase', 'IMaintainableDatabase' ); diff --git a/includes/libs/rdbms/database/MaintainableDBConnRef.php b/includes/libs/rdbms/database/MaintainableDBConnRef.php index fa3ddf9eb9c4..8238f3edd312 100644 --- a/includes/libs/rdbms/database/MaintainableDBConnRef.php +++ b/includes/libs/rdbms/database/MaintainableDBConnRef.php @@ -1,4 +1,7 @@ <?php + +namespace Wikimedia\Rdbms; + /** * Helper class to handle automatically marking connections as reusable (via RAII pattern) * as well handling deferring the actual network connection until the handle is used @@ -65,4 +68,18 @@ class MaintainableDBConnRef extends DBConnRef implements IMaintainableDatabase { ) { return $this->__call( __FUNCTION__, func_get_args() ); } + + public function tableLocksHaveTransactionScope() { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function lockTables( array $read, array $write, $method ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function unlockTables( $method ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } } + +class_alias( 'Wikimedia\Rdbms\MaintainableDBConnRef', 'MaintainableDBConnRef' ); diff --git a/includes/libs/rdbms/database/position/DBMasterPos.php b/includes/libs/rdbms/database/position/DBMasterPos.php index eda0ff32569c..2f79ea9a2006 100644 --- a/includes/libs/rdbms/database/position/DBMasterPos.php +++ b/includes/libs/rdbms/database/position/DBMasterPos.php @@ -1,4 +1,7 @@ <?php + +namespace Wikimedia\Rdbms; + /** * An object representing a master or replica DB position in a replicated setup. * diff --git a/includes/libs/rdbms/database/position/MySQLMasterPos.php b/includes/libs/rdbms/database/position/MySQLMasterPos.php index 7b49ce9609a8..06776fe8fdc0 100644 --- a/includes/libs/rdbms/database/position/MySQLMasterPos.php +++ b/includes/libs/rdbms/database/position/MySQLMasterPos.php @@ -1,4 +1,9 @@ <?php + +namespace Wikimedia\Rdbms; + +use InvalidArgumentException; + /** * DBMasterPos class for MySQL/MariaDB * diff --git a/includes/libs/rdbms/database/resultwrapper/FakeResultWrapper.php b/includes/libs/rdbms/database/resultwrapper/FakeResultWrapper.php index 1a046cf69662..fd7af110e564 100644 --- a/includes/libs/rdbms/database/resultwrapper/FakeResultWrapper.php +++ b/includes/libs/rdbms/database/resultwrapper/FakeResultWrapper.php @@ -1,4 +1,9 @@ <?php + +namespace Wikimedia\Rdbms; + +use stdClass; + /** * Overloads the relevant methods of the real ResultsWrapper so it * doesn't go anywhere near an actual database. @@ -56,3 +61,6 @@ class FakeResultWrapper extends ResultWrapper { return $this->fetchObject(); } } + +class_alias( FakeResultWrapper::class, 'FakeResultWrapper' ); + diff --git a/includes/libs/rdbms/database/resultwrapper/IResultWrapper.php b/includes/libs/rdbms/database/resultwrapper/IResultWrapper.php new file mode 100644 index 000000000000..dc89a2dc04ee --- /dev/null +++ b/includes/libs/rdbms/database/resultwrapper/IResultWrapper.php @@ -0,0 +1,82 @@ +<?php + +namespace Wikimedia\Rdbms; + +use Iterator; +use DBUnexpectedError; +use stdClass; + +/** + * Result wrapper for grabbing data queried from an IDatabase object + * + * Note that using the Iterator methods in combination with the non-Iterator + * DB result iteration functions may cause rows to be skipped or repeated. + * + * By default, this will use the iteration methods of the IDatabase handle if provided. + * Subclasses can override methods to make it solely work on the result resource instead. + * If no database is provided, and the subclass does not override the DB iteration methods, + * then a RuntimeException will be thrown when iteration is attempted. + * + * The result resource field should not be accessed from non-Database related classes. + * It is database class specific and is stored here to associate iterators with queries. + * + * @ingroup Database + */ +interface IResultWrapper extends Iterator { + /** + * Get the number of rows in a result object + * + * @return int + */ + public function numRows(); + + /** + * Fetch the next row from the given result object, in object form. Fields can be retrieved with + * $row->fieldname, with fields acting like member variables. If no more rows are available, + * false is returned. + * + * @return stdClass|bool + * @throws DBUnexpectedError Thrown if the database returns an error + */ + public function fetchObject(); + + /** + * Fetch the next row from the given result object, in associative array form. Fields are + * retrieved with $row['fieldname']. If no more rows are available, false is returned. + * + * @return array|bool + * @throws DBUnexpectedError Thrown if the database returns an error + */ + public function fetchRow(); + + /** + * Change the position of the cursor in a result object. + * See mysql_data_seek() + * + * @param int $row + */ + public function seek( $row ); + + /** + * Free a result object + * + * This either saves memory in PHP (buffered queries) or on the server (unbuffered queries). + * In general, queries are not large enough in result sets for this to be worth calling. + */ + public function free(); + + /** + * @return stdClass|array|bool + */ + public function current(); + + /** + * @return int + */ + public function key(); + + /** + * @return stdClass + */ + function next(); +} diff --git a/includes/libs/rdbms/database/resultwrapper/MssqlResultWrapper.php b/includes/libs/rdbms/database/resultwrapper/MssqlResultWrapper.php index b591f4f389cb..4e28397455c8 100644 --- a/includes/libs/rdbms/database/resultwrapper/MssqlResultWrapper.php +++ b/includes/libs/rdbms/database/resultwrapper/MssqlResultWrapper.php @@ -1,4 +1,9 @@ <?php + +namespace Wikimedia\Rdbms; + +use stdClass; + class MssqlResultWrapper extends ResultWrapper { /** @var integer|null */ private $mSeekTo = null; diff --git a/includes/libs/rdbms/database/resultwrapper/ResultWrapper.php b/includes/libs/rdbms/database/resultwrapper/ResultWrapper.php index 53109c82da87..df354af8ec72 100644 --- a/includes/libs/rdbms/database/resultwrapper/ResultWrapper.php +++ b/includes/libs/rdbms/database/resultwrapper/ResultWrapper.php @@ -1,4 +1,10 @@ <?php + +namespace Wikimedia\Rdbms; + +use stdClass; +use RuntimeException; + /** * Result wrapper for grabbing data queried from an IDatabase object * @@ -15,7 +21,7 @@ * * @ingroup Database */ -class ResultWrapper implements Iterator { +class ResultWrapper implements IResultWrapper { /** @var resource|array|null Optional underlying result handle for subclass usage */ public $result; @@ -45,54 +51,22 @@ class ResultWrapper implements Iterator { } } - /** - * Get the number of rows in a result object - * - * @return int - */ public function numRows() { return $this->getDB()->numRows( $this ); } - /** - * Fetch the next row from the given result object, in object form. Fields can be retrieved with - * $row->fieldname, with fields acting like member variables. If no more rows are available, - * false is returned. - * - * @return stdClass|bool - * @throws DBUnexpectedError Thrown if the database returns an error - */ public function fetchObject() { return $this->getDB()->fetchObject( $this ); } - /** - * Fetch the next row from the given result object, in associative array form. Fields are - * retrieved with $row['fieldname']. If no more rows are available, false is returned. - * - * @return array|bool - * @throws DBUnexpectedError Thrown if the database returns an error - */ public function fetchRow() { return $this->getDB()->fetchRow( $this ); } - /** - * Change the position of the cursor in a result object. - * See mysql_data_seek() - * - * @param int $row - */ public function seek( $row ) { $this->getDB()->dataSeek( $this, $row ); } - /** - * Free a result object - * - * This either saves memory in PHP (buffered queries) or on the server (unbuffered queries). - * In general, queries are not large enough in result sets for this to be worth calling. - */ public function free() { if ( $this->db ) { $this->db->freeResult( $this ); @@ -107,7 +81,7 @@ class ResultWrapper implements Iterator { */ private function getDB() { if ( !$this->db ) { - throw new RuntimeException( get_class( $this ) . ' needs a DB handle for iteration.' ); + throw new RuntimeException( static::class . ' needs a DB handle for iteration.' ); } return $this->db; @@ -121,9 +95,6 @@ class ResultWrapper implements Iterator { $this->currentRow = null; } - /** - * @return stdClass|array|bool - */ function current() { if ( is_null( $this->currentRow ) ) { $this->next(); @@ -132,16 +103,10 @@ class ResultWrapper implements Iterator { return $this->currentRow; } - /** - * @return int - */ function key() { return $this->pos; } - /** - * @return stdClass - */ function next() { $this->pos++; $this->currentRow = $this->fetchObject(); @@ -153,3 +118,5 @@ class ResultWrapper implements Iterator { return $this->current() !== false; } } + +class_alias( ResultWrapper::class, 'ResultWrapper' ); diff --git a/includes/libs/rdbms/defines.php b/includes/libs/rdbms/defines.php index 692a704b89bd..cbc8ca31692c 100644 --- a/includes/libs/rdbms/defines.php +++ b/includes/libs/rdbms/defines.php @@ -1,5 +1,8 @@ <?php +use Wikimedia\Rdbms\ILoadBalancer; +use Wikimedia\Rdbms\IDatabase; + /**@{ * Database related constants */ diff --git a/includes/libs/rdbms/encasing/Blob.php b/includes/libs/rdbms/encasing/Blob.php index bd9033057789..db5b7e511321 100644 --- a/includes/libs/rdbms/encasing/Blob.php +++ b/includes/libs/rdbms/encasing/Blob.php @@ -1,19 +1,21 @@ <?php -/** - * Utility class - * @ingroup Database - * - * This allows us to distinguish a blob from a normal string and an array of strings - */ -class Blob { + +namespace Wikimedia\Rdbms; + +class Blob implements IBlob { /** @var string */ protected $mData; - function __construct( $data ) { + /** + * @param $data string + */ + public function __construct( $data ) { $this->mData = $data; } - function fetch() { + public function fetch() { return $this->mData; } } + +class_alias( Blob::class, 'Blob' ); diff --git a/includes/libs/rdbms/encasing/IBlob.php b/includes/libs/rdbms/encasing/IBlob.php new file mode 100644 index 000000000000..b1d7aae43884 --- /dev/null +++ b/includes/libs/rdbms/encasing/IBlob.php @@ -0,0 +1,14 @@ +<?php + +namespace Wikimedia\Rdbms; + +/** + * Wrapper allowing us to distinguish a blob from a normal string and an array of strings + * @ingroup Database + */ +interface IBlob { + /** + * @return string + */ + public function fetch(); +} diff --git a/includes/libs/rdbms/encasing/LikeMatch.php b/includes/libs/rdbms/encasing/LikeMatch.php index b0b3c8798452..98812a5a283a 100644 --- a/includes/libs/rdbms/encasing/LikeMatch.php +++ b/includes/libs/rdbms/encasing/LikeMatch.php @@ -1,4 +1,7 @@ <?php + +namespace Wikimedia\Rdbms; + /** * Used by Database::buildLike() to represent characters that have special * meaning in SQL LIKE clauses and thus need no escaping. Don't instantiate it diff --git a/includes/libs/rdbms/encasing/MssqlBlob.php b/includes/libs/rdbms/encasing/MssqlBlob.php index 35be65c26e7c..aacdf402f49b 100644 --- a/includes/libs/rdbms/encasing/MssqlBlob.php +++ b/includes/libs/rdbms/encasing/MssqlBlob.php @@ -1,5 +1,13 @@ <?php + +namespace Wikimedia\Rdbms; + class MssqlBlob extends Blob { + /** @noinspection PhpMissingParentConstructorInspection */ + + /** + * @param string $data + */ public function __construct( $data ) { if ( $data instanceof MssqlBlob ) { return $data; diff --git a/includes/libs/rdbms/encasing/PostgresBlob.php b/includes/libs/rdbms/encasing/PostgresBlob.php index cc52336c299a..7994b730197e 100644 --- a/includes/libs/rdbms/encasing/PostgresBlob.php +++ b/includes/libs/rdbms/encasing/PostgresBlob.php @@ -1,4 +1,7 @@ <?php + +namespace Wikimedia\Rdbms; + class PostgresBlob extends Blob { } diff --git a/includes/libs/rdbms/exception/DBConnectionError.php b/includes/libs/rdbms/exception/DBConnectionError.php index 47f8c9620b79..dca1302bbce3 100644 --- a/includes/libs/rdbms/exception/DBConnectionError.php +++ b/includes/libs/rdbms/exception/DBConnectionError.php @@ -18,6 +18,7 @@ * @file * @ingroup Database */ +use Wikimedia\Rdbms\IDatabase; /** * @ingroup Database diff --git a/includes/libs/rdbms/exception/DBError.php b/includes/libs/rdbms/exception/DBError.php index 526596d0b12c..226c675d6546 100644 --- a/includes/libs/rdbms/exception/DBError.php +++ b/includes/libs/rdbms/exception/DBError.php @@ -18,6 +18,7 @@ * @file * @ingroup Database */ +use Wikimedia\Rdbms\IDatabase; /** * Database error base class diff --git a/includes/libs/rdbms/exception/DBExpectedError.php b/includes/libs/rdbms/exception/DBExpectedError.php index 7d303b1d8f6f..57538a8ac7e5 100644 --- a/includes/libs/rdbms/exception/DBExpectedError.php +++ b/includes/libs/rdbms/exception/DBExpectedError.php @@ -18,6 +18,7 @@ * @file * @ingroup Database */ +use Wikimedia\Rdbms\IDatabase; /** * Base class for the more common types of database errors. These are known to occur diff --git a/includes/libs/rdbms/exception/DBQueryError.php b/includes/libs/rdbms/exception/DBQueryError.php index 002d25392410..b4c3d529f58b 100644 --- a/includes/libs/rdbms/exception/DBQueryError.php +++ b/includes/libs/rdbms/exception/DBQueryError.php @@ -18,6 +18,7 @@ * @file * @ingroup Database */ +use Wikimedia\Rdbms\IDatabase; /** * @ingroup Database diff --git a/includes/libs/rdbms/exception/DBReplicationWaitError.php b/includes/libs/rdbms/exception/DBReplicationWaitError.php index f1dabd5d50c6..c5e1ed7033cf 100644 --- a/includes/libs/rdbms/exception/DBReplicationWaitError.php +++ b/includes/libs/rdbms/exception/DBReplicationWaitError.php @@ -25,4 +25,3 @@ */ class DBReplicationWaitError extends DBExpectedError { } - diff --git a/includes/libs/rdbms/field/Field.php b/includes/libs/rdbms/field/Field.php index ed102f40c17c..7918f360678f 100644 --- a/includes/libs/rdbms/field/Field.php +++ b/includes/libs/rdbms/field/Field.php @@ -1,4 +1,7 @@ <?php + +namespace Wikimedia\Rdbms; + /** * Base for all database-specific classes representing information about database fields * @ingroup Database @@ -28,3 +31,5 @@ interface Field { */ function isNullable(); } + +class_alias( Field::class, 'Field' ); diff --git a/includes/libs/rdbms/field/MssqlField.php b/includes/libs/rdbms/field/MssqlField.php index 80e19245bf40..98cc2b189338 100644 --- a/includes/libs/rdbms/field/MssqlField.php +++ b/includes/libs/rdbms/field/MssqlField.php @@ -1,4 +1,7 @@ <?php + +namespace Wikimedia\Rdbms; + class MssqlField implements Field { private $name, $tableName, $default, $max_length, $nullable, $type; @@ -35,4 +38,3 @@ class MssqlField implements Field { return $this->type; } } - diff --git a/includes/libs/rdbms/field/MySQLField.php b/includes/libs/rdbms/field/MySQLField.php index 8cf964cc3d9a..709c61eb2df3 100644 --- a/includes/libs/rdbms/field/MySQLField.php +++ b/includes/libs/rdbms/field/MySQLField.php @@ -1,4 +1,7 @@ <?php + +namespace Wikimedia\Rdbms; + class MySQLField implements Field { private $name, $tablename, $default, $max_length, $nullable, $is_pk, $is_unique, $is_multiple, $is_key, $type, $binary, @@ -103,4 +106,3 @@ class MySQLField implements Field { return $this->is_zerofill; } } - diff --git a/includes/libs/rdbms/field/PostgresField.php b/includes/libs/rdbms/field/PostgresField.php index d34c125bb953..c5819a32ca10 100644 --- a/includes/libs/rdbms/field/PostgresField.php +++ b/includes/libs/rdbms/field/PostgresField.php @@ -1,4 +1,9 @@ <?php + +namespace Wikimedia\Rdbms; + +use DatabasePostgres; + class PostgresField implements Field { private $name, $tablename, $type, $nullable, $max_length, $deferred, $deferrable, $conname, $has_default, $default; diff --git a/includes/libs/rdbms/field/SQLiteField.php b/includes/libs/rdbms/field/SQLiteField.php index 0a2389bfb0a7..39f8f01182ac 100644 --- a/includes/libs/rdbms/field/SQLiteField.php +++ b/includes/libs/rdbms/field/SQLiteField.php @@ -1,4 +1,7 @@ <?php + +namespace Wikimedia\Rdbms; + class SQLiteField implements Field { private $info, $tableName; diff --git a/includes/libs/rdbms/lbfactory/ILBFactory.php b/includes/libs/rdbms/lbfactory/ILBFactory.php index 5288c24908da..faf7fb179c29 100644 --- a/includes/libs/rdbms/lbfactory/ILBFactory.php +++ b/includes/libs/rdbms/lbfactory/ILBFactory.php @@ -21,6 +21,12 @@ * @ingroup Database */ +namespace Wikimedia\Rdbms; + +use InvalidArgumentException; +use DBTransactionError; +use DBReplicationWaitError; + /** * An interface for generating database load balancers * @ingroup Database @@ -178,7 +184,7 @@ interface ILBFactory { * @param string $fname Caller name * @param array $options Options map: * - maxWriteDuration: abort if more than this much time was spent in write queries - * @throws Exception + * @throws DBTransactionError */ public function commitMasterChanges( $fname = __METHOD__, array $options = [] ); diff --git a/includes/libs/rdbms/lbfactory/LBFactory.php b/includes/libs/rdbms/lbfactory/LBFactory.php index 15a5c0d78fdf..86547b9a77a3 100644 --- a/includes/libs/rdbms/lbfactory/LBFactory.php +++ b/includes/libs/rdbms/lbfactory/LBFactory.php @@ -21,8 +21,17 @@ * @ingroup Database */ +namespace Wikimedia\Rdbms; + use Psr\Log\LoggerInterface; use Wikimedia\ScopedCallback; +use BagOStuff; +use EmptyBagOStuff; +use WANObjectCache; +use Exception; +use RuntimeException; +use DBTransactionError; +use DBReplicationWaitError; /** * An interface for generating database load balancers @@ -100,7 +109,7 @@ abstract class LBFactory implements ILBFactory { trigger_error( E_USER_WARNING, get_class( $e ) . ': ' . $e->getMessage() ); }; - $this->profiler = isset( $params['profiler'] ) ? $params['profiler'] : null; + $this->profiler = isset( $conf['profiler'] ) ? $conf['profiler'] : null; $this->trxProfiler = isset( $conf['trxProfiler'] ) ? $conf['trxProfiler'] : new TransactionProfiler(); @@ -111,9 +120,9 @@ abstract class LBFactory implements ILBFactory { 'ChronologyProtection' => 'true' ]; - $this->cliMode = isset( $params['cliMode'] ) ? $params['cliMode'] : PHP_SAPI === 'cli'; + $this->cliMode = isset( $conf['cliMode'] ) ? $conf['cliMode'] : PHP_SAPI === 'cli'; $this->hostname = isset( $conf['hostname'] ) ? $conf['hostname'] : gethostname(); - $this->agent = isset( $params['agent'] ) ? $params['agent'] : ''; + $this->agent = isset( $conf['agent'] ) ? $conf['agent'] : ''; $this->ticket = mt_rand(); } @@ -326,7 +335,7 @@ abstract class LBFactory implements ILBFactory { $masterPositions = array_fill( 0, count( $lbs ), false ); foreach ( $lbs as $i => $lb ) { if ( $lb->getServerCount() <= 1 ) { - // Bug 27975 - Don't try to wait for replica DBs if there are none + // T29975 - Don't try to wait for replica DBs if there are none // Prevents permission error when getting master position continue; } elseif ( $opts['ifWritesSince'] @@ -498,7 +507,8 @@ abstract class LBFactory implements ILBFactory { 'errorLogger' => $this->errorLogger, 'hostname' => $this->hostname, 'cliMode' => $this->cliMode, - 'agent' => $this->agent + 'agent' => $this->agent, + 'chronologyProtector' => $this->getChronologyProtector() ]; } diff --git a/includes/libs/rdbms/lbfactory/LBFactoryMulti.php b/includes/libs/rdbms/lbfactory/LBFactoryMulti.php index 4158e616bd90..0384588ddca9 100644 --- a/includes/libs/rdbms/lbfactory/LBFactoryMulti.php +++ b/includes/libs/rdbms/lbfactory/LBFactoryMulti.php @@ -21,6 +21,10 @@ * @ingroup Database */ +namespace Wikimedia\Rdbms; + +use InvalidArgumentException; + /** * A multi-database, multi-master factory for Wikimedia and similar installations. * Ignores the old configuration globals. @@ -247,9 +251,7 @@ class LBFactoryMulti extends LBFactory { public function getMainLB( $domain = false ) { $section = $this->getSectionForDomain( $domain ); if ( !isset( $this->mainLBs[$section] ) ) { - $lb = $this->newMainLB( $domain ); - $this->getChronologyProtector()->initLB( $lb ); - $this->mainLBs[$section] = $lb; + $this->mainLBs[$section] = $this->newMainLB( $domain ); } return $this->mainLBs[$section]; @@ -278,7 +280,6 @@ class LBFactoryMulti extends LBFactory { public function getExternalLB( $cluster ) { if ( !isset( $this->extLBs[$cluster] ) ) { $this->extLBs[$cluster] = $this->newExternalLB( $cluster ); - $this->getChronologyProtector()->initLB( $this->extLBs[$cluster] ); } return $this->extLBs[$cluster]; diff --git a/includes/libs/rdbms/lbfactory/LBFactorySimple.php b/includes/libs/rdbms/lbfactory/LBFactorySimple.php index 5bf50323dfb6..df0a806b4145 100644 --- a/includes/libs/rdbms/lbfactory/LBFactorySimple.php +++ b/includes/libs/rdbms/lbfactory/LBFactorySimple.php @@ -21,6 +21,10 @@ * @ingroup Database */ +namespace Wikimedia\Rdbms; + +use InvalidArgumentException; + /** * A simple single-master LBFactory that gets its configuration from the b/c globals */ @@ -85,7 +89,6 @@ class LBFactorySimple extends LBFactory { public function getMainLB( $domain = false ) { if ( !isset( $this->mainLB ) ) { $this->mainLB = $this->newMainLB( $domain ); - $this->getChronologyProtector()->initLB( $this->mainLB ); } return $this->mainLB; @@ -102,7 +105,6 @@ class LBFactorySimple extends LBFactory { public function getExternalLB( $cluster ) { if ( !isset( $this->extLBs[$cluster] ) ) { $this->extLBs[$cluster] = $this->newExternalLB( $cluster ); - $this->getChronologyProtector()->initLB( $this->extLBs[$cluster] ); } return $this->extLBs[$cluster]; diff --git a/includes/libs/rdbms/lbfactory/LBFactorySingle.php b/includes/libs/rdbms/lbfactory/LBFactorySingle.php index 819375d797d6..cd998c3e7b0a 100644 --- a/includes/libs/rdbms/lbfactory/LBFactorySingle.php +++ b/includes/libs/rdbms/lbfactory/LBFactorySingle.php @@ -21,6 +21,11 @@ * @ingroup Database */ +namespace Wikimedia\Rdbms; + +use InvalidArgumentException; +use BadMethodCallException; + /** * An LBFactory class that always returns a single database object. */ diff --git a/includes/libs/rdbms/loadbalancer/ILoadBalancer.php b/includes/libs/rdbms/loadbalancer/ILoadBalancer.php index fc306b49b4c2..2ea0e4ef83a1 100644 --- a/includes/libs/rdbms/loadbalancer/ILoadBalancer.php +++ b/includes/libs/rdbms/loadbalancer/ILoadBalancer.php @@ -21,6 +21,15 @@ * @ingroup Database * @author Aaron Schulz */ +namespace Wikimedia\Rdbms; + +use Database; +use DBError; +use DBAccessError; +use DBTransactionError; +use DBExpectedError; +use Exception; +use InvalidArgumentException; /** * Database cluster connection, tracking, load balancing, and transaction manager interface @@ -93,6 +102,7 @@ interface ILoadBalancer { * - srvCache : BagOStuff object for server cache [optional] * - memCache : BagOStuff object for cluster memory cache [optional] * - wanCache : WANObjectCache object [optional] + * - chronologyProtector: ChronologyProtector object [optional] * - hostname : The name of the current server [optional] * - cliMode: Whether the execution context is a CLI script. [optional] * - profiler : Class name or instance with profileIn()/profileOut() methods. [optional] @@ -126,7 +136,7 @@ interface ILoadBalancer { * If a DB_REPLICA connection has been opened already, then wait immediately. * Otherwise sets a variable telling it to wait if such a connection is opened. * - * @param DBMasterPos $pos + * @param DBMasterPos|bool $pos Master position or false */ public function waitFor( $pos ); @@ -135,7 +145,7 @@ interface ILoadBalancer { * * This can be used a faster proxy for waitForAll() * - * @param DBMasterPos $pos + * @param DBMasterPos|bool $pos Master position or false * @param int $timeout Max seconds to wait; default is mWaitTimeout * @return bool Success (able to connect and no timeouts reached) */ @@ -144,7 +154,7 @@ interface ILoadBalancer { /** * Set the master wait position and wait for ALL replica DBs to catch up to it * - * @param DBMasterPos $pos + * @param DBMasterPos|bool $pos Master position or false * @param int $timeout Max seconds to wait; default is mWaitTimeout * @return bool Success (able to connect and no timeouts reached) */ @@ -228,9 +238,6 @@ interface ILoadBalancer { * Index must be an actual index into the array. * If the server is already open, returns it. * - * On error, returns false, and the connection which caused the - * error will be available via $this->mErrorConnection. - * * @note If disable() was called on this LoadBalancer, this method will throw a DBAccessError. * * @param int $i Server index or DB_MASTER/DB_REPLICA @@ -370,7 +377,7 @@ interface ILoadBalancer { * * Use this only for mutli-database commits * - * @param integer $type IDatabase::TRIGGER_* constant + * @param int $type IDatabase::TRIGGER_* constant * @return Exception|null The first exception or null if there were none */ public function runMasterPostTrxCallbacks( $type ); @@ -447,7 +454,7 @@ interface ILoadBalancer { /** * @note This method may trigger a DB connection if not yet done * @param string|bool $domain Domain ID, or false for the current domain - * @param IDatabase|null DB master connection; used to avoid loops [optional] + * @param IDatabase|null $conn DB master connection; used to avoid loops [optional] * @return string|bool Reason the master is read-only or false if it is not */ public function getReadOnlyReason( $domain = false, IDatabase $conn = null ); @@ -532,10 +539,10 @@ interface ILoadBalancer { * * @param IDatabase $conn Replica DB * @param DBMasterPos|bool $pos Master position; default: current position - * @param integer|null $timeout Timeout in seconds [optional] + * @param int $timeout Timeout in seconds [optional] * @return bool Success */ - public function safeWaitForMasterPos( IDatabase $conn, $pos = false, $timeout = null ); + public function safeWaitForMasterPos( IDatabase $conn, $pos = false, $timeout = 10 ); /** * Set a callback via IDatabase::setTransactionListener() on diff --git a/includes/libs/rdbms/loadbalancer/LoadBalancer.php b/includes/libs/rdbms/loadbalancer/LoadBalancer.php index 95f55b6a86d7..d178657d44e1 100644 --- a/includes/libs/rdbms/loadbalancer/LoadBalancer.php +++ b/includes/libs/rdbms/loadbalancer/LoadBalancer.php @@ -20,8 +20,26 @@ * @file * @ingroup Database */ +namespace Wikimedia\Rdbms; + use Psr\Log\LoggerInterface; +use Psr\Log\NullLogger; use Wikimedia\ScopedCallback; +use Database; +use BagOStuff; +use EmptyBagOStuff; +use WANObjectCache; +use ArrayUtils; +use DBError; +use DBAccessError; +use DBExpectedError; +use DBUnexpectedError; +use DBTransactionError; +use DBTransactionSizeError; +use DBConnectionError; +use InvalidArgumentException; +use RuntimeException; +use Exception; /** * Database connection, tracking, load balancing, and transaction manager for a cluster @@ -31,7 +49,7 @@ use Wikimedia\ScopedCallback; class LoadBalancer implements ILoadBalancer { /** @var array[] Map of (server index => server config array) */ private $mServers; - /** @var IDatabase[][][] Map of local/foreignUsed/foreignFree => server index => IDatabase array */ + /** @var \Database[][][] Map of local/foreignUsed/foreignFree => server index => IDatabase array */ private $mConns; /** @var float[] Map of (server index => weight) */ private $mLoads; @@ -48,6 +66,8 @@ class LoadBalancer implements ILoadBalancer { /** @var ILoadMonitor */ private $loadMonitor; + /** @var ChronologyProtector|null */ + private $chronProt; /** @var BagOStuff */ private $srvCache; /** @var BagOStuff */ @@ -67,8 +87,8 @@ class LoadBalancer implements ILoadBalancer { /** @var LoggerInterface */ protected $perfLogger; - /** @var bool|IDatabase Database connection that caused a problem */ - private $mErrorConnection; + /** @var \Database Database connection that caused a problem */ + private $errorConnection; /** @var integer The generic (not query grouped) replica DB index (of $mServers) */ private $mReadIndex; /** @var bool|DBMasterPos False if not set */ @@ -103,6 +123,8 @@ class LoadBalancer implements ILoadBalancer { /** @var boolean */ private $disabled = false; + /** @var boolean */ + private $chronProtInitialized = false; /** @var integer Warn when this many connection are held */ const CONN_HELD_WARN_THRESHOLD = 10; @@ -140,7 +162,6 @@ class LoadBalancer implements ILoadBalancer { ]; $this->mLoads = []; $this->mWaitForPos = false; - $this->mErrorConnection = false; $this->mAllowLagged = false; if ( isset( $params['readOnlyReason'] ) && is_string( $params['readOnlyReason'] ) ) { @@ -194,7 +215,7 @@ class LoadBalancer implements ILoadBalancer { }; foreach ( [ 'replLogger', 'connLogger', 'queryLogger', 'perfLogger' ] as $key ) { - $this->$key = isset( $params[$key] ) ? $params[$key] : new \Psr\Log\NullLogger(); + $this->$key = isset( $params[$key] ) ? $params[$key] : new NullLogger(); } $this->host = isset( $params['hostname'] ) @@ -202,6 +223,10 @@ class LoadBalancer implements ILoadBalancer { : ( gethostname() ?: 'unknown' ); $this->cliMode = isset( $params['cliMode'] ) ? $params['cliMode'] : PHP_SAPI === 'cli'; $this->agent = isset( $params['agent'] ) ? $params['agent'] : ''; + + if ( isset( $params['chronologyProtector'] ) ) { + $this->chronProt = $params['chronologyProtector']; + } } /** @@ -211,7 +236,17 @@ class LoadBalancer implements ILoadBalancer { */ private function getLoadMonitor() { if ( !isset( $this->loadMonitor ) ) { + $compat = [ + 'LoadMonitor' => LoadMonitor::class, + 'LoadMonitorNull' => LoadMonitorNull::class, + 'LoadMonitorMySQL' => LoadMonitorMySQL::class, + ]; + $class = $this->loadMonitorConfig['class']; + if ( isset( $compat[$class] ) ) { + $class = $compat[$class]; + } + $this->loadMonitor = new $class( $this, $this->srvCache, $this->memCache, $this->loadMonitorConfig ); $this->loadMonitor->setLogger( $this->replLogger ); @@ -394,21 +429,24 @@ class LoadBalancer implements ILoadBalancer { return $i; } - /** - * @param DBMasterPos|false $pos - */ public function waitFor( $pos ) { + $oldPos = $this->mWaitForPos; $this->mWaitForPos = $pos; - $i = $this->mReadIndex; + // If a generic reader connection was already established, then wait now + $i = $this->mReadIndex; if ( $i > 0 ) { if ( !$this->doWait( $i ) ) { $this->laggedReplicaMode = true; } } + + // Restore the older position if it was higher + $this->setWaitForPositionIfHigher( $oldPos ); } public function waitForOne( $pos, $timeout = null ) { + $oldPos = $this->mWaitForPos; $this->mWaitForPos = $pos; $i = $this->mReadIndex; @@ -426,10 +464,14 @@ class LoadBalancer implements ILoadBalancer { $ok = true; // no applicable loads } + // Restore the older position if it was higher + $this->setWaitForPositionIfHigher( $oldPos ); + return $ok; } public function waitForAll( $pos, $timeout = null ) { + $oldPos = $this->mWaitForPos; $this->mWaitForPos = $pos; $serverCount = count( $this->mServers ); @@ -440,10 +482,26 @@ class LoadBalancer implements ILoadBalancer { } } + // Restore the older position if it was higher + $this->setWaitForPositionIfHigher( $oldPos ); + return $ok; } /** + * @param DBMasterPos|bool $pos + */ + private function setWaitForPositionIfHigher( $pos ) { + if ( !$pos ) { + return; + } + + if ( !$this->mWaitForPos || $pos->hasReached( $this->mWaitForPos ) ) { + $this->mWaitForPos = $pos; + } + } + + /** * @param int $i * @return IDatabase|bool */ @@ -472,10 +530,13 @@ class LoadBalancer implements ILoadBalancer { // Check if we already know that the DB has reached this point $server = $this->getServerName( $index ); - $key = $this->srvCache->makeGlobalKey( __CLASS__, 'last-known-pos', $server ); + $key = $this->srvCache->makeGlobalKey( __CLASS__, 'last-known-pos', $server, 'v1' ); /** @var DBMasterPos $knownReachedPos */ $knownReachedPos = $this->srvCache->get( $key ); - if ( $knownReachedPos && $knownReachedPos->hasReached( $this->mWaitForPos ) ) { + if ( + $knownReachedPos instanceof DBMasterPos && + $knownReachedPos->hasReached( $this->mWaitForPos ) + ) { $this->replLogger->debug( __METHOD__ . ": replica DB $server known to be caught up (pos >= $knownReachedPos)." ); return true; @@ -685,6 +746,13 @@ class LoadBalancer implements ILoadBalancer { $domain = false; // local connection requested } + if ( !$this->chronProtInitialized && $this->chronProt ) { + $this->connLogger->debug( __METHOD__ . ': calling initLB() before first connection.' ); + // Load CP positions before connecting so that doWait() triggers later if needed + $this->chronProtInitialized = true; + $this->chronProt->initLB( $this ); + } + if ( $domain !== false ) { $conn = $this->openForeignConnection( $i, $domain ); } elseif ( isset( $this->mConns['local'][$i][0] ) ) { @@ -703,17 +771,17 @@ class LoadBalancer implements ILoadBalancer { $this->mConns['local'][$i][0] = $conn; } else { $this->connLogger->warning( "Failed to connect to database $i at '$serverName'." ); - $this->mErrorConnection = $conn; + $this->errorConnection = $conn; $conn = false; } } - if ( $conn && !$conn->isOpen() ) { + if ( $conn instanceof IDatabase && !$conn->isOpen() ) { // Connection was made but later unrecoverably lost for some reason. // Do not return a handle that will just throw exceptions on use, // but let the calling code (e.g. getReaderIndex) try another server. // See DatabaseMyslBase::ping() for how this can happen. - $this->mErrorConnection = $conn; + $this->errorConnection = $conn; $conn = false; } @@ -732,7 +800,7 @@ class LoadBalancer implements ILoadBalancer { * it has been freed first with reuseConnection(). * * On error, returns false, and the connection which caused the - * error will be available via $this->mErrorConnection. + * error will be available via $this->errorConnection. * * @note If disable() was called on this LoadBalancer, this method will throw a DBAccessError. * @@ -764,7 +832,7 @@ class LoadBalancer implements ILoadBalancer { if ( strlen( $dbName ) && !$conn->selectDB( $dbName ) ) { $this->mLastError = "Error selecting database '$dbName' on server " . $conn->getServer() . " from client host {$this->host}"; - $this->mErrorConnection = $conn; + $this->errorConnection = $conn; $conn = false; } else { $conn->tablePrefix( $prefix ); @@ -785,7 +853,7 @@ class LoadBalancer implements ILoadBalancer { $conn = $this->reallyOpenConnection( $server, $dbName ); if ( !$conn->isOpen() ) { $this->connLogger->warning( __METHOD__ . ": connection error for $i/$domain" ); - $this->mErrorConnection = $conn; + $this->errorConnection = $conn; $conn = false; } else { $conn->tablePrefix( $prefix ); @@ -795,7 +863,7 @@ class LoadBalancer implements ILoadBalancer { } // Increment reference count - if ( $conn ) { + if ( $conn instanceof IDatabase ) { $refCount = $conn->getLBInfo( 'foreignPoolRefCount' ); $conn->setLBInfo( 'foreignPoolRefCount', $refCount + 1 ); } @@ -893,22 +961,13 @@ class LoadBalancer implements ILoadBalancer { * @throws DBConnectionError */ private function reportConnectionError() { - $conn = $this->mErrorConnection; // the connection which caused the error + $conn = $this->errorConnection; // the connection which caused the error $context = [ 'method' => __METHOD__, 'last_error' => $this->mLastError, ]; - if ( !is_object( $conn ) ) { - // No last connection, probably due to all servers being too busy - $this->connLogger->error( - "LB failure with no last connection. Connection error: {last_error}", - $context - ); - - // If all servers were busy, mLastError will contain something sensible - throw new DBConnectionError( null, $this->mLastError ); - } else { + if ( $conn instanceof IDatabase ) { $context['db_server'] = $conn->getServer(); $this->connLogger->warning( "Connection error: {last_error} ({db_server})", @@ -917,6 +976,15 @@ class LoadBalancer implements ILoadBalancer { // throws DBConnectionError $conn->reportConnectionError( "{$this->mLastError} ({$context['db_server']})" ); + } else { + // No last connection, probably due to all servers being too busy + $this->connLogger->error( + "LB failure with no last connection. Connection error: {last_error}", + $context + ); + + // If all servers were busy, mLastError will contain something sensible + throw new DBConnectionError( null, $this->mLastError ); } } @@ -1336,7 +1404,7 @@ class LoadBalancer implements ILoadBalancer { /** * @param string $domain Domain ID, or false for the current domain - * @param IDatabase|null DB master connectionl used to avoid loops [optional] + * @param IDatabase|null $conn DB master connectionl used to avoid loops [optional] * @return bool */ private function masterRunningReadOnly( $domain, IDatabase $conn = null ) { @@ -1471,8 +1539,9 @@ class LoadBalancer implements ILoadBalancer { /** * @param IDatabase $conn - * @param DBMasterPos|false $pos + * @param DBMasterPos|bool $pos * @param int $timeout + * @return bool */ public function safeWaitForMasterPos( IDatabase $conn, $pos = false, $timeout = 10 ) { if ( $this->getServerCount() <= 1 || !$conn->getLBInfo( 'replica' ) ) { @@ -1571,3 +1640,5 @@ class LoadBalancer implements ILoadBalancer { $this->disable(); } } + +class_alias( LoadBalancer::class, 'LoadBalancer' ); diff --git a/includes/libs/rdbms/loadbalancer/LoadBalancerSingle.php b/includes/libs/rdbms/loadbalancer/LoadBalancerSingle.php index 0a0520250438..79d250f6a063 100644 --- a/includes/libs/rdbms/loadbalancer/LoadBalancerSingle.php +++ b/includes/libs/rdbms/loadbalancer/LoadBalancerSingle.php @@ -21,6 +21,10 @@ * @ingroup Database */ +namespace Wikimedia\Rdbms; + +use InvalidArgumentException; + /** * Trivial LoadBalancer that always returns an injected connection handle */ @@ -72,3 +76,5 @@ class LoadBalancerSingle extends LoadBalancer { return $this->db; } } + +class_alias( 'Wikimedia\Rdbms\LoadBalancerSingle', 'LoadBalancerSingle' ); diff --git a/includes/libs/rdbms/loadmonitor/ILoadMonitor.php b/includes/libs/rdbms/loadmonitor/ILoadMonitor.php index 14a52c5a8cfb..4f6701fa138f 100644 --- a/includes/libs/rdbms/loadmonitor/ILoadMonitor.php +++ b/includes/libs/rdbms/loadmonitor/ILoadMonitor.php @@ -20,7 +20,11 @@ * @file * @ingroup Database */ + +namespace Wikimedia\Rdbms; + use Psr\Log\LoggerAwareInterface; +use BagOStuff; /** * An interface for database load monitoring diff --git a/includes/libs/rdbms/loadmonitor/LoadMonitor.php b/includes/libs/rdbms/loadmonitor/LoadMonitor.php index da4909d77fde..d120b6f3d23b 100644 --- a/includes/libs/rdbms/loadmonitor/LoadMonitor.php +++ b/includes/libs/rdbms/loadmonitor/LoadMonitor.php @@ -19,8 +19,12 @@ * @ingroup Database */ +namespace Wikimedia\Rdbms; + use Psr\Log\LoggerInterface; +use Psr\Log\NullLogger; use Wikimedia\ScopedCallback; +use BagOStuff; /** * Basic DB load monitor with no external dependencies @@ -49,7 +53,7 @@ class LoadMonitor implements ILoadMonitor { $this->parent = $lb; $this->srvCache = $srvCache; $this->mainCache = $cache; - $this->replLogger = new \Psr\Log\NullLogger(); + $this->replLogger = new NullLogger(); $this->movingAveRatio = isset( $options['movingAveRatio'] ) ? $options['movingAveRatio'] diff --git a/includes/libs/rdbms/loadmonitor/LoadMonitorMySQL.php b/includes/libs/rdbms/loadmonitor/LoadMonitorMySQL.php index e3747943c16f..ff72dbc96f8f 100644 --- a/includes/libs/rdbms/loadmonitor/LoadMonitorMySQL.php +++ b/includes/libs/rdbms/loadmonitor/LoadMonitorMySQL.php @@ -19,6 +19,10 @@ * @ingroup Database */ +namespace Wikimedia\Rdbms; + +use BagOStuff; + /** * Basic MySQL load monitor with no external dependencies * Uses memcached to cache the replication lag for a short time diff --git a/includes/libs/rdbms/loadmonitor/LoadMonitorNull.php b/includes/libs/rdbms/loadmonitor/LoadMonitorNull.php index c4e25dc24a7b..613dac527827 100644 --- a/includes/libs/rdbms/loadmonitor/LoadMonitorNull.php +++ b/includes/libs/rdbms/loadmonitor/LoadMonitorNull.php @@ -18,7 +18,11 @@ * @file * @ingroup Database */ + +namespace Wikimedia\Rdbms; + use Psr\Log\LoggerInterface; +use BagOStuff; class LoadMonitorNull implements ILoadMonitor { public function __construct( diff --git a/includes/libs/replacers/Replacer.php b/includes/libs/replacers/Replacer.php index 3b978357ede4..655e77108788 100644 --- a/includes/libs/replacers/Replacer.php +++ b/includes/libs/replacers/Replacer.php @@ -27,7 +27,7 @@ abstract class Replacer { * @return array */ public function cb() { - return [ &$this, 'replace' ]; + return [ $this, 'replace' ]; } /** diff --git a/includes/libs/time/ConvertibleTimestamp.php b/includes/libs/time/ConvertibleTimestamp.php deleted file mode 100644 index c830b4eb642c..000000000000 --- a/includes/libs/time/ConvertibleTimestamp.php +++ /dev/null @@ -1,269 +0,0 @@ -<?php -/** - * Creation, parsing, and conversion of timestamps. - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, write to the Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - * http://www.gnu.org/copyleft/gpl.html - * - * @file - * @since 1.20 - * @author Tyler Romeo, 2012 - */ - -/** - * Library for creating, parsing, and converting timestamps. Based on the JS - * library that does the same thing. - * - * @since 1.28 - */ -class ConvertibleTimestamp { - /** - * Standard gmdate() formats for the different timestamp types. - */ - private static $formats = [ - TS_UNIX => 'U', - TS_MW => 'YmdHis', - TS_DB => 'Y-m-d H:i:s', - TS_ISO_8601 => 'Y-m-d\TH:i:s\Z', - TS_ISO_8601_BASIC => 'Ymd\THis\Z', - TS_EXIF => 'Y:m:d H:i:s', // This shouldn't ever be used, but is included for completeness - TS_RFC2822 => 'D, d M Y H:i:s', - TS_ORACLE => 'd-m-Y H:i:s.000000', // Was 'd-M-y h.i.s A' . ' +00:00' before r51500 - TS_POSTGRES => 'Y-m-d H:i:s', - ]; - - /** - * The actual timestamp being wrapped (DateTime object). - * @var DateTime - */ - public $timestamp; - - /** - * Make a new timestamp and set it to the specified time, - * or the current time if unspecified. - * - * @param bool|string|int|float|DateTime $timestamp Timestamp to set, or false for current time - */ - public function __construct( $timestamp = false ) { - if ( $timestamp instanceof DateTime ) { - $this->timestamp = $timestamp; - } else { - $this->setTimestamp( $timestamp ); - } - } - - /** - * Set the timestamp to the specified time, or the current time if unspecified. - * - * Parse the given timestamp into either a DateTime object or a Unix timestamp, - * and then store it. - * - * @param string|bool $ts Timestamp to store, or false for now - * @throws TimestampException - */ - public function setTimestamp( $ts = false ) { - $m = []; - $da = []; - $strtime = ''; - - // We want to catch 0, '', null... but not date strings starting with a letter. - if ( !$ts || $ts === "\0\0\0\0\0\0\0\0\0\0\0\0\0\0" ) { - $uts = time(); - $strtime = "@$uts"; - } elseif ( preg_match( '/^(\d{4})\-(\d\d)\-(\d\d) (\d\d):(\d\d):(\d\d)$/D', $ts, $da ) ) { - # TS_DB - } elseif ( preg_match( '/^(\d{4}):(\d\d):(\d\d) (\d\d):(\d\d):(\d\d)$/D', $ts, $da ) ) { - # TS_EXIF - } elseif ( preg_match( '/^(\d{4})(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)$/D', $ts, $da ) ) { - # TS_MW - } elseif ( preg_match( '/^(-?\d{1,13})(\.\d+)?$/D', $ts, $m ) ) { - # TS_UNIX - $strtime = "@{$m[1]}"; // https://secure.php.net/manual/en/datetime.formats.compound.php - } elseif ( preg_match( '/^\d{2}-\d{2}-\d{4} \d{2}:\d{2}:\d{2}.\d{6}$/', $ts ) ) { - # TS_ORACLE // session altered to DD-MM-YYYY HH24:MI:SS.FF6 - $strtime = preg_replace( '/(\d\d)\.(\d\d)\.(\d\d)(\.(\d+))?/', "$1:$2:$3", - str_replace( '+00:00', 'UTC', $ts ) ); - } elseif ( preg_match( - '/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.*\d*)?Z?$/', - $ts, - $da - ) ) { - # TS_ISO_8601 - } elseif ( preg_match( - '/^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})(?:\.*\d*)?Z?$/', - $ts, - $da - ) ) { - # TS_ISO_8601_BASIC - } elseif ( preg_match( - '/^(\d{4})\-(\d\d)\-(\d\d) (\d\d):(\d\d):(\d\d)\.*\d*[\+\- ](\d\d)$/', - $ts, - $da - ) ) { - # TS_POSTGRES - } elseif ( preg_match( - '/^(\d{4})\-(\d\d)\-(\d\d) (\d\d):(\d\d):(\d\d)\.*\d* GMT$/', - $ts, - $da - ) ) { - # TS_POSTGRES - } elseif ( preg_match( - # Day of week - '/^[ \t\r\n]*([A-Z][a-z]{2},[ \t\r\n]*)?' . - # dd Mon yyyy - '\d\d?[ \t\r\n]*[A-Z][a-z]{2}[ \t\r\n]*\d{2}(?:\d{2})?' . - # hh:mm:ss - '[ \t\r\n]*\d\d[ \t\r\n]*:[ \t\r\n]*\d\d[ \t\r\n]*:[ \t\r\n]*\d\d/S', - $ts - ) ) { - # TS_RFC2822, accepting a trailing comment. - # See http://www.squid-cache.org/mail-archive/squid-users/200307/0122.html / r77171 - # The regex is a superset of rfc2822 for readability - $strtime = strtok( $ts, ';' ); - } elseif ( preg_match( '/^[A-Z][a-z]{5,8}, \d\d-[A-Z][a-z]{2}-\d{2} \d\d:\d\d:\d\d/', $ts ) ) { - # TS_RFC850 - $strtime = $ts; - } elseif ( preg_match( '/^[A-Z][a-z]{2} [A-Z][a-z]{2} +\d{1,2} \d\d:\d\d:\d\d \d{4}/', $ts ) ) { - # asctime - $strtime = $ts; - } else { - throw new TimestampException( __METHOD__ . ": Invalid timestamp - $ts" ); - } - - if ( !$strtime ) { - $da = array_map( 'intval', $da ); - $da[0] = "%04d-%02d-%02dT%02d:%02d:%02d.00+00:00"; - $strtime = call_user_func_array( "sprintf", $da ); - } - - try { - $final = new DateTime( $strtime, new DateTimeZone( 'GMT' ) ); - } catch ( Exception $e ) { - throw new TimestampException( __METHOD__ . ': Invalid timestamp format.', $e->getCode(), $e ); - } - - if ( $final === false ) { - throw new TimestampException( __METHOD__ . ': Invalid timestamp format.' ); - } - - $this->timestamp = $final; - } - - /** - * Convert a timestamp string to a given format. - * - * @param int $style Constant Output format for timestamp - * @param string $ts Timestamp - * @return string|bool Formatted timestamp or false on failure - */ - public static function convert( $style = TS_UNIX, $ts ) { - try { - $ct = new static( $ts ); - return $ct->getTimestamp( $style ); - } catch ( TimestampException $e ) { - return false; - } - } - - /** - * Get the current time in the given format - * - * @param int $style Constant Output format for timestamp - * @return string - */ - public static function now( $style = TS_MW ) { - return static::convert( $style, time() ); - } - - /** - * Get the timestamp represented by this object in a certain form. - * - * Convert the internal timestamp to the specified format and then - * return it. - * - * @param int $style Constant Output format for timestamp - * @throws TimestampException - * @return string The formatted timestamp - */ - public function getTimestamp( $style = TS_UNIX ) { - if ( !isset( self::$formats[$style] ) ) { - throw new TimestampException( __METHOD__ . ': Illegal timestamp output type.' ); - } - - $output = $this->timestamp->format( self::$formats[$style] ); - - if ( ( $style == TS_RFC2822 ) || ( $style == TS_POSTGRES ) ) { - $output .= ' GMT'; - } - - if ( $style == TS_MW && strlen( $output ) !== 14 ) { - throw new TimestampException( __METHOD__ . ': The timestamp cannot be represented in ' . - 'the specified format' ); - } - - return $output; - } - - /** - * @return string - */ - public function __toString() { - return $this->getTimestamp(); - } - - /** - * Calculate the difference between two ConvertibleTimestamp objects. - * - * @param ConvertibleTimestamp $relativeTo Base time to calculate difference from - * @return DateInterval|bool The DateInterval object representing the - * difference between the two dates or false on failure - */ - public function diff( ConvertibleTimestamp $relativeTo ) { - return $this->timestamp->diff( $relativeTo->timestamp ); - } - - /** - * Set the timezone of this timestamp to the specified timezone. - * - * @param string $timezone Timezone to set - * @throws TimestampException - */ - public function setTimezone( $timezone ) { - try { - $this->timestamp->setTimezone( new DateTimeZone( $timezone ) ); - } catch ( Exception $e ) { - throw new TimestampException( __METHOD__ . ': Invalid timezone.', $e->getCode(), $e ); - } - } - - /** - * Get the timezone of this timestamp. - * - * @return DateTimeZone The timezone - */ - public function getTimezone() { - return $this->timestamp->getTimezone(); - } - - /** - * Format the timestamp in a given format. - * - * @param string $format Pattern to format in - * @return string The formatted timestamp - */ - public function format( $format ) { - return $this->timestamp->format( $format ); - } -} diff --git a/includes/libs/time/TimestampException.php b/includes/libs/time/TimestampException.php deleted file mode 100644 index 36ffdeeaf9bf..000000000000 --- a/includes/libs/time/TimestampException.php +++ /dev/null @@ -1,7 +0,0 @@ -<?php - -/** - * @since 1.20 - */ -class TimestampException extends Exception { -} diff --git a/includes/libs/time/defines.php b/includes/libs/time/defines.php deleted file mode 100644 index ff4dde86ad7a..000000000000 --- a/includes/libs/time/defines.php +++ /dev/null @@ -1,52 +0,0 @@ -<?php - -/** - * Unix time - the number of seconds since 1970-01-01 00:00:00 UTC - */ -define( 'TS_UNIX', 0 ); - -/** - * MediaWiki concatenated string timestamp (YYYYMMDDHHMMSS) - */ -define( 'TS_MW', 1 ); - -/** - * MySQL DATETIME (YYYY-MM-DD HH:MM:SS) - */ -define( 'TS_DB', 2 ); - -/** - * RFC 2822 format, for E-mail and HTTP headers - */ -define( 'TS_RFC2822', 3 ); - -/** - * ISO 8601 format with no timezone: 1986-02-09T20:00:00Z - * - * This is used by Special:Export - */ -define( 'TS_ISO_8601', 4 ); - -/** - * An Exif timestamp (YYYY:MM:DD HH:MM:SS) - * - * @see http://exif.org/Exif2-2.PDF The Exif 2.2 spec, see page 28 for the - * DateTime tag and page 36 for the DateTimeOriginal and - * DateTimeDigitized tags. - */ -define( 'TS_EXIF', 5 ); - -/** - * Oracle format time. - */ -define( 'TS_ORACLE', 6 ); - -/** - * Postgres format time. - */ -define( 'TS_POSTGRES', 7 ); - -/** - * ISO 8601 basic format with no timezone: 19860209T200000Z. This is used by ResourceLoader - */ -define( 'TS_ISO_8601_BASIC', 9 ); diff --git a/includes/libs/virtualrest/VirtualRESTService.php b/includes/libs/virtualrest/VirtualRESTService.php index ccb14db0204f..2f160787e1b8 100644 --- a/includes/libs/virtualrest/VirtualRESTService.php +++ b/includes/libs/virtualrest/VirtualRESTService.php @@ -51,8 +51,7 @@ abstract class VirtualRESTService { * @return string The name of the service behind this VRS object. */ public function getName() { - return isset( $this->params['name'] ) ? $this->params['name'] : - get_class( $this ); + return isset( $this->params['name'] ) ? $this->params['name'] : static::class; } /** diff --git a/includes/libs/xmp/XMP.php b/includes/libs/xmp/XMP.php index a657a33f2c89..f1df7f19dde1 100644 --- a/includes/libs/xmp/XMP.php +++ b/includes/libs/xmp/XMP.php @@ -647,7 +647,7 @@ class XMPReader implements LoggerAwareInterface { private function endElementNested( $elm ) { /* cur item must be the same as $elm, unless if in MODE_STRUCT - in which case it could also be rdf:Description */ + * in which case it could also be rdf:Description */ if ( $this->curItem[0] !== $elm && !( $elm === self::NS_RDF . ' Description' && $this->mode[0] === self::MODE_STRUCT ) @@ -895,7 +895,7 @@ class XMPReader implements LoggerAwareInterface { if ( $elm === self::NS_RDF . ' Seq' ) { array_unshift( $this->mode, self::MODE_LI ); } elseif ( $elm === self::NS_RDF . ' Bag' ) { - # bug 27105 + # T29105 $this->logger->info( __METHOD__ . ' Expected an rdf:Seq, but got an rdf:Bag. Pretending' . ' it is a Seq, since some buggy software is known to screw this up.' ); array_unshift( $this->mode, self::MODE_LI ); @@ -1086,7 +1086,7 @@ class XMPReader implements LoggerAwareInterface { } } else { array_unshift( $this->mode, self::MODE_IGNORE ); - array_unshift( $this->curItem, $elm ); + array_unshift( $this->curItem, $ns . ' ' . $tag ); return; } diff --git a/includes/libs/xmp/XMPInfo.php b/includes/libs/xmp/XMPInfo.php index 052be33a4be8..5211a2cd3987 100644 --- a/includes/libs/xmp/XMPInfo.php +++ b/includes/libs/xmp/XMPInfo.php @@ -650,7 +650,7 @@ class XMPInfo { 'choices' => [ '1' => true, '2' => true ], ], /******** - * Disable extracting this property (bug 31944) + * Disable extracting this property (T33944) * Several files have a string instead of a Seq * for this property. XMPReader doesn't handle * mismatched types very gracefully (it marks @@ -658,7 +658,7 @@ class XMPInfo { * the relavent prop). Since this prop * doesn't communicate all that useful information * just disable this prop for now, until such - * XMPReader is more graceful (bug 32172) + * XMPReader is more graceful (T34172) * 'YCbCrSubSampling' => array( * 'map_group' => 'exif', * 'mode' => XMPReader::MODE_SEQ, diff --git a/includes/libs/xmp/XMPValidate.php b/includes/libs/xmp/XMPValidate.php index 31eaa3baa76c..76ae279f32c9 100644 --- a/includes/libs/xmp/XMPValidate.php +++ b/includes/libs/xmp/XMPValidate.php @@ -23,6 +23,7 @@ use Psr\Log\LoggerInterface; use Psr\Log\LoggerAwareInterface; +use Wikimedia\Timestamp\ConvertibleTimestamp; /** * This contains some static methods for diff --git a/includes/logging/BlockLogFormatter.php b/includes/logging/BlockLogFormatter.php index c3902326c607..a0bfb59345d8 100644 --- a/includes/logging/BlockLogFormatter.php +++ b/includes/logging/BlockLogFormatter.php @@ -59,9 +59,15 @@ class BlockLogFormatter extends LogFormatter { // The lrm is needed to make sure that the number // is shown on the correct side of the tooltip text. $durationTooltip = '‎' . htmlspecialchars( $params[4] ); - $params[4] = Message::rawParam( "<span class='blockExpiry' title='$durationTooltip'>" . - $this->context->getLanguage()->translateBlockExpiry( $params[4], - $this->context->getUser() ) . '</span>' ); + $params[4] = Message::rawParam( + "<span class='blockExpiry' title='$durationTooltip'>" . + $this->context->getLanguage()->translateBlockExpiry( + $params[4], + $this->context->getUser(), + wfTimestamp( TS_UNIX, $this->entry->getTimestamp() ) + ) . + '</span>' + ); $params[5] = isset( $params[5] ) ? self::formatBlockFlags( $params[5], $this->context->getLanguage() ) : ''; } diff --git a/includes/logging/DeleteLogFormatter.php b/includes/logging/DeleteLogFormatter.php index 05973df32543..ceb00520bf11 100644 --- a/includes/logging/DeleteLogFormatter.php +++ b/includes/logging/DeleteLogFormatter.php @@ -39,6 +39,12 @@ class DeleteLogFormatter extends LogFormatter { // logentry-suppress-event-legacy, logentry-suppress-revision-legacy return "$key-legacy"; } + } elseif ( $this->entry->getSubtype() === 'restore' ) { + $rawParams = $this->entry->getParameters(); + if ( !isset( $rawParams[':assoc:count'] ) ) { + // Message: logentry-delete-restore-nocount + return $key . '-nocount'; + } } return $key; @@ -97,6 +103,19 @@ class DeleteLogFormatter extends LogFormatter { $this->parsedParametersDeleteLog = array_slice( $params, 0, 3 ); return $this->parsedParametersDeleteLog; } + } elseif ( $subtype === 'restore' ) { + $rawParams = $this->entry->getParameters(); + if ( isset( $rawParams[':assoc:count'] ) ) { + $countList = []; + foreach ( $rawParams[':assoc:count'] as $type => $count ) { + if ( $count ) { + // Messages: restore-count-revisions, restore-count-files + $countList[] = $this->context->msg( 'restore-count-' . $type ) + ->numParams( $count )->plain(); + } + } + $params[3] = $this->context->getLanguage()->listToText( $countList ); + } } $this->parsedParametersDeleteLog = $params; @@ -276,6 +295,11 @@ class DeleteLogFormatter extends LogFormatter { $params[':assoc:old'][$key] = (bool)( $old & $bit ); $params[':assoc:new'][$key] = (bool)( $new & $bit ); } + } elseif ( $subtype === 'restore' ) { + $rawParams = $entry->getParameters(); + if ( isset( $rawParams[':assoc:count'] ) ) { + $params[':assoc:count'] = $rawParams[':assoc:count']; + } } return $params; diff --git a/includes/logging/LogEntry.php b/includes/logging/LogEntry.php index c9f13457de3d..1c5899ba85fa 100644 --- a/includes/logging/LogEntry.php +++ b/includes/logging/LogEntry.php @@ -28,6 +28,8 @@ * @since 1.19 */ +use Wikimedia\Rdbms\IDatabase; + /** * Interface for log entries. Every log entry has these methods. * diff --git a/includes/logging/LogEventsList.php b/includes/logging/LogEventsList.php index 6665336aa77a..317652a3b7aa 100644 --- a/includes/logging/LogEventsList.php +++ b/includes/logging/LogEventsList.php @@ -24,6 +24,7 @@ */ use MediaWiki\MediaWikiServices; +use Wikimedia\Rdbms\IDatabase; class LogEventsList extends ContextSource { const NO_ACTION_LINK = 1; @@ -544,7 +545,8 @@ class LogEventsList extends ContextSource { * @param string $user The user who made the log entries * @param array $param Associative Array with the following additional options: * - lim Integer Limit of items to show, default is 50 - * - conds Array Extra conditions for the query (e.g. "log_action != 'revision'") + * - conds Array Extra conditions for the query + * (e.g. 'log_action != ' . $dbr->addQuotes( 'revision' )) * - showIfEmpty boolean Set to false if you don't want any output in case the loglist is empty * if set to true (default), "No matching items in log" is displayed if loglist is empty * - msgKey Array If you want a nice box with a message, set this to the key of the message. diff --git a/includes/logging/LogFormatter.php b/includes/logging/LogFormatter.php index a64fee1e5073..68404bfcea32 100644 --- a/includes/logging/LogFormatter.php +++ b/includes/logging/LogFormatter.php @@ -167,7 +167,7 @@ class LogFormatter { /** * Even uglier hack to maintain backwards compatibilty with IRC bots - * (bug 34508). + * (T36508). * @see getActionText() * @return string Text */ @@ -188,7 +188,7 @@ class LogFormatter { /** * Even uglier hack to maintain backwards compatibilty with IRC bots - * (bug 34508). + * (T36508). * @see getActionText() * @return string Text */ @@ -353,7 +353,11 @@ class LogFormatter { $rawDuration = $parameters['5::duration']; $rawFlags = $parameters['6::flags']; } - $duration = $wgContLang->translateBlockExpiry( $rawDuration ); + $duration = $wgContLang->translateBlockExpiry( + $rawDuration, + null, + wfTimestamp( TS_UNIX, $entry->getTimestamp() ) + ); $flags = BlockLogFormatter::formatBlockFlags( $rawFlags, $wgContLang ); $text = wfMessage( 'blocklogentry' ) ->rawParams( $target, $duration, $flags )->inContentLanguage()->escaped(); @@ -363,7 +367,11 @@ class LogFormatter { ->rawParams( $target )->inContentLanguage()->escaped(); break; case 'reblock': - $duration = $wgContLang->translateBlockExpiry( $parameters['5::duration'] ); + $duration = $wgContLang->translateBlockExpiry( + $parameters['5::duration'], + null, + wfTimestamp( TS_UNIX, $entry->getTimestamp() ) + ); $flags = BlockLogFormatter::formatBlockFlags( $parameters['6::flags'], $wgContLang ); $text = wfMessage( 'reblock-logentry' ) ->rawParams( $target, $duration, $flags )->inContentLanguage()->escaped(); diff --git a/includes/logging/LogPager.php b/includes/logging/LogPager.php index 68163c13e514..ea28ff202e86 100644 --- a/includes/logging/LogPager.php +++ b/includes/logging/LogPager.php @@ -181,7 +181,7 @@ class LogPager extends ReverseChronologicalPager { } else { $this->mConds['log_user'] = $userid; } - // Paranoia: avoid brute force searches (bug 17342) + // Paranoia: avoid brute force searches (T19342) $user = $this->getUser(); if ( !$user->isAllowed( 'deletedhistory' ) ) { $this->mConds[] = $this->mDb->bitAnd( 'log_deleted', LogPage::DELETED_USER ) . ' = 0'; @@ -256,7 +256,7 @@ class LogPager extends ReverseChronologicalPager { } else { $this->mConds['log_title'] = $title->getDBkey(); } - // Paranoia: avoid brute force searches (bug 17342) + // Paranoia: avoid brute force searches (T19342) $user = $this->getUser(); if ( !$user->isAllowed( 'deletedhistory' ) ) { $this->mConds[] = $db->bitAnd( 'log_deleted', LogPage::DELETED_ACTION ) . ' = 0'; diff --git a/includes/logging/RightsLogFormatter.php b/includes/logging/RightsLogFormatter.php index be73c86495d7..791330c160a1 100644 --- a/includes/logging/RightsLogFormatter.php +++ b/includes/logging/RightsLogFormatter.php @@ -70,7 +70,7 @@ class RightsLogFormatter extends LogFormatter { protected function getMessageParameters() { $params = parent::getMessageParameters(); - // Really old entries + // Really old entries that lack old/new groups if ( !isset( $params[3] ) && !isset( $params[4] ) ) { return $params; } @@ -81,25 +81,29 @@ class RightsLogFormatter extends LogFormatter { $userName = $this->entry->getTarget()->getText(); if ( !$this->plaintext && count( $oldGroups ) ) { foreach ( $oldGroups as &$group ) { - $group = User::getGroupMember( $group, $userName ); + $group = UserGroupMembership::getGroupMemberName( $group, $userName ); } } if ( !$this->plaintext && count( $newGroups ) ) { foreach ( $newGroups as &$group ) { - $group = User::getGroupMember( $group, $userName ); + $group = UserGroupMembership::getGroupMemberName( $group, $userName ); } } - $lang = $this->context->getLanguage(); + // fetch the metadata about each group membership + $allParams = $this->entry->getParameters(); + if ( count( $oldGroups ) ) { - $params[3] = $lang->listToText( $oldGroups ); + $params[3] = [ 'raw' => $this->formatRightsList( $oldGroups, + isset( $allParams['oldmetadata'] ) ? $allParams['oldmetadata'] : [] ) ]; } else { $params[3] = $this->msg( 'rightsnone' )->text(); } if ( count( $newGroups ) ) { // Array_values is used here because of T44211 // see use of array_unique in UserrightsPage::doSaveUserGroups on $newGroups. - $params[4] = $lang->listToText( array_values( $newGroups ) ); + $params[4] = [ 'raw' => $this->formatRightsList( array_values( $newGroups ), + isset( $allParams['newmetadata'] ) ? $allParams['newmetadata'] : [] ) ]; } else { $params[4] = $this->msg( 'rightsnone' )->text(); } @@ -109,6 +113,42 @@ class RightsLogFormatter extends LogFormatter { return $params; } + protected function formatRightsList( $groups, $serializedUGMs = [] ) { + $uiLanguage = $this->context->getLanguage(); + $uiUser = $this->context->getUser(); + // separate arrays of temporary and permanent memberships + $tempList = $permList = []; + + reset( $groups ); + reset( $serializedUGMs ); + while ( current( $groups ) ) { + $group = current( $groups ); + + if ( current( $serializedUGMs ) && + isset( current( $serializedUGMs )['expiry'] ) && + current( $serializedUGMs )['expiry'] + ) { + // there is an expiry date; format the group and expiry into a friendly string + $expiry = current( $serializedUGMs )['expiry']; + $expiryFormatted = $uiLanguage->userTimeAndDate( $expiry, $uiUser ); + $expiryFormattedD = $uiLanguage->userDate( $expiry, $uiUser ); + $expiryFormattedT = $uiLanguage->userTime( $expiry, $uiUser ); + $tempList[] = $this->msg( 'rightslogentry-temporary-group' )->params( $group, + $expiryFormatted, $expiryFormattedD, $expiryFormattedT )->parse(); + } else { + // the right does not expire; just insert the group name + $permList[] = $group; + } + + next( $groups ); + next( $serializedUGMs ); + } + + // place all temporary memberships first, to avoid the ambiguity of + // "adinistrator, bureaucrat and importer (temporary, until X time)" + return $uiLanguage->listToText( array_merge( $tempList, $permList ) ); + } + protected function getParametersForApi() { $entry = $this->entry; $params = $entry->getParameters(); @@ -126,12 +166,44 @@ class RightsLogFormatter extends LogFormatter { } } - // Really old entries does not have log params + // Really old entries do not have log params, so form them from whatever info + // we have. + // Also walk through the parallel arrays of groups and metadata, combining each + // metadata array with the name of the group it pertains to if ( isset( $params['4:array:oldgroups'] ) ) { $params['4:array:oldgroups'] = $this->makeGroupArray( $params['4:array:oldgroups'] ); + + $oldmetadata =& $params['oldmetadata']; + // unset old metadata entry to ensure metadata goes at the end of the params array + unset( $params['oldmetadata'] ); + $params['oldmetadata'] = array_map( function( $index ) use ( $params, $oldmetadata ) { + $result = [ 'group' => $params['4:array:oldgroups'][$index] ]; + if ( isset( $oldmetadata[$index] ) ) { + $result += $oldmetadata[$index]; + } + $result['expiry'] = ApiResult::formatExpiry( isset( $result['expiry'] ) ? + $result['expiry'] : null ); + + return $result; + }, array_keys( $params['4:array:oldgroups'] ) ); } + if ( isset( $params['5:array:newgroups'] ) ) { $params['5:array:newgroups'] = $this->makeGroupArray( $params['5:array:newgroups'] ); + + $newmetadata =& $params['newmetadata']; + // unset old metadata entry to ensure metadata goes at the end of the params array + unset( $params['newmetadata'] ); + $params['newmetadata'] = array_map( function( $index ) use ( $params, $newmetadata ) { + $result = [ 'group' => $params['5:array:newgroups'][$index] ]; + if ( isset( $newmetadata[$index] ) ) { + $result += $newmetadata[$index]; + } + $result['expiry'] = ApiResult::formatExpiry( isset( $result['expiry'] ) ? + $result['expiry'] : null ); + + return $result; + }, array_keys( $params['5:array:newgroups'] ) ); } return $params; @@ -145,6 +217,14 @@ class RightsLogFormatter extends LogFormatter { if ( isset( $ret['newgroups'] ) ) { ApiResult::setIndexedTagName( $ret['newgroups'], 'g' ); } + if ( isset( $ret['oldmetadata'] ) ) { + ApiResult::setArrayType( $ret['oldmetadata'], 'array' ); + ApiResult::setIndexedTagName( $ret['oldmetadata'], 'g' ); + } + if ( isset( $ret['newmetadata'] ) ) { + ApiResult::setArrayType( $ret['newmetadata'], 'array' ); + ApiResult::setIndexedTagName( $ret['newmetadata'], 'g' ); + } return $ret; } diff --git a/includes/mail/EmailNotification.php b/includes/mail/EmailNotification.php index 1d0bdf6d779c..d66e7e37b647 100644 --- a/includes/mail/EmailNotification.php +++ b/includes/mail/EmailNotification.php @@ -316,7 +316,7 @@ class EmailNotification { $pageTitle = $this->title->getPrefixedText(); if ( $this->oldid ) { - // Always show a link to the diff which triggered the mail. See bug 32210. + // Always show a link to the diff which triggered the mail. See T34210. $keys['$NEWPAGE'] = "\n\n" . wfMessage( 'enotif_lastdiff', $this->title->getCanonicalURL( [ 'diff' => 'next', 'oldid' => $this->oldid ] ) ) ->inContentLanguage()->text(); @@ -363,7 +363,7 @@ class EmailNotification { Skin::makeInternalOrExternalUrl( wfMessage( 'helppage' )->inContentLanguage()->text() ) ); - # Replace this after transforming the message, bug 35019 + # Replace this after transforming the message, T37019 $postTransformKeys['$PAGESUMMARY'] = $this->summary == '' ? ' - ' : $this->summary; // Now build message's subject and body diff --git a/includes/mail/UserMailer.php b/includes/mail/UserMailer.php index 21effa0e02d7..3858f27566ef 100644 --- a/includes/mail/UserMailer.php +++ b/includes/mail/UserMailer.php @@ -103,9 +103,9 @@ class UserMailer { * @param string $subject Email's subject. * @param string $body Email's text or Array of two strings to be the text and html bodies * @param array $options: - * 'replyTo' MailAddress - * 'contentType' string default 'text/plain; charset=UTF-8' - * 'headers' array Extra headers to set + * 'replyTo' MailAddress + * 'contentType' string default 'text/plain; charset=UTF-8' + * 'headers' array Extra headers to set * * @throws MWException * @throws Exception @@ -197,9 +197,9 @@ class UserMailer { * @param string $subject Email's subject. * @param string $body Email's text or Array of two strings to be the text and html bodies * @param array $options: - * 'replyTo' MailAddress - * 'contentType' string default 'text/plain; charset=UTF-8' - * 'headers' array Extra headers to set + * 'replyTo' MailAddress + * 'contentType' string default 'text/plain; charset=UTF-8' + * 'headers' array Extra headers to set * * @throws MWException * @throws Exception diff --git a/includes/media/Bitmap.php b/includes/media/Bitmap.php index ac0564d2e829..0f0b074a677a 100644 --- a/includes/media/Bitmap.php +++ b/includes/media/Bitmap.php @@ -150,7 +150,7 @@ class BitmapHandler extends TransformationalImageHandler { if ( $params['interlace'] ) { $animation_post = [ '-interlace', 'JPEG' ]; } - # Sharpening, see bug 6193 + # Sharpening, see T8193 if ( ( $params['physicalWidth'] + $params['physicalHeight'] ) / ( $params['srcWidth'] + $params['srcHeight'] ) < $wgSharpenReductionThreshold @@ -178,10 +178,10 @@ class BitmapHandler extends TransformationalImageHandler { // be a total drag. :P $scene = 0; } elseif ( $this->isAnimatedImage( $image ) ) { - // Coalesce is needed to scale animated GIFs properly (bug 1017). + // Coalesce is needed to scale animated GIFs properly (T3017). $animation_pre = [ '-coalesce' ]; // We optimize the output, but -optimize is broken, - // use optimizeTransparency instead (bug 11822) + // use optimizeTransparency instead (T13822) if ( version_compare( $this->getMagickVersion(), "6.3.5" ) >= 0 ) { $animation_post = [ '-fuzz', '5%', '-layers', 'optimizeTransparency' ]; } @@ -211,7 +211,7 @@ class BitmapHandler extends TransformationalImageHandler { && $xcfMeta['colorType'] === 'greyscale-alpha' && version_compare( $this->getMagickVersion(), "6.8.9-3" ) < 0 ) { - // bug 66323 - Greyscale images not rendered properly. + // T68323 - Greyscale images not rendered properly. // So only take the "red" channel. $channelOnly = [ '-channel', 'R', '-separate' ]; $animation_pre = array_merge( $animation_pre, $channelOnly ); @@ -283,7 +283,7 @@ class BitmapHandler extends TransformationalImageHandler { $im->readImage( $params['srcPath'] ); if ( $params['mimeType'] == 'image/jpeg' ) { - // Sharpening, see bug 6193 + // Sharpening, see T8193 if ( ( $params['physicalWidth'] + $params['physicalHeight'] ) / ( $params['srcWidth'] + $params['srcHeight'] ) < $wgSharpenReductionThreshold @@ -312,7 +312,7 @@ class BitmapHandler extends TransformationalImageHandler { // be a total drag. :P $im->setImageScene( 0 ); } elseif ( $this->isAnimatedImage( $image ) ) { - // Coalesce is needed to scale animated GIFs properly (bug 1017). + // Coalesce is needed to scale animated GIFs properly (T3017). $im = $im->coalesceImages(); } // GIF interlacing is only available since 6.3.4 diff --git a/includes/media/DjVuImage.php b/includes/media/DjVuImage.php index 5e8f8c8f1d07..57b5b36c1421 100644 --- a/includes/media/DjVuImage.php +++ b/includes/media/DjVuImage.php @@ -277,9 +277,9 @@ class DjVuImage { $reg = <<<EOR /\(page\s[\d-]*\s[\d-]*\s[\d-]*\s[\d-]*\s*" ((?> # Text to match is composed of atoms of either: - \\\\. # - any escaped character - | # - any character different from " and \ - [^"\\\\]+ + \\\\. # - any escaped character + | # - any character different from " and \ + [^"\\\\]+ )*?) "\s*\) | # Or page can be empty ; in this case, djvutxt dumps () diff --git a/includes/media/FormatMetadata.php b/includes/media/FormatMetadata.php index 51a0135859ef..f2372874baf0 100644 --- a/includes/media/FormatMetadata.php +++ b/includes/media/FormatMetadata.php @@ -24,6 +24,7 @@ * @see http://exif.org/Exif2-2.PDF The Exif 2.2 specification * @file */ +use Wikimedia\Timestamp\TimestampException; /** * Format Image metadata values into a human readable form. diff --git a/includes/media/IPTC.php b/includes/media/IPTC.php index f93b1b59a5fa..b7ebfc93a5f5 100644 --- a/includes/media/IPTC.php +++ b/includes/media/IPTC.php @@ -97,7 +97,7 @@ class IPTC { case '2#025': /* keywords */ $data['Keywords'] = self::convIPTC( $val, $c ); break; - case '2#101': /* Country (shown)*/ + case '2#101': /* Country (shown) */ $data['CountryDest'] = self::convIPTC( $val, $c ); break; case '2#095': /* state/province (shown) */ @@ -115,7 +115,7 @@ class IPTC { case '2#040': /* special instructions */ $data['SpecialInstructions'] = self::convIPTC( $val, $c ); break; - case '2#105': /* headline*/ + case '2#105': /* headline */ $data['Headline'] = self::convIPTC( $val, $c ); break; case '2#110': /* credit */ diff --git a/includes/media/Jpeg.php b/includes/media/Jpeg.php index 6c857a85653a..c9f0dfab10ee 100644 --- a/includes/media/Jpeg.php +++ b/includes/media/Jpeg.php @@ -112,8 +112,8 @@ class JpegHandler extends ExifBitmapHandler { wfDebug( __METHOD__ . ': ' . $e->getMessage() . "\n" ); /* This used to use 0 (ExifBitmapHandler::OLD_BROKEN_FILE) for the cases - * * No metadata in the file - * * Something is broken in the file. + * * No metadata in the file + * * Something is broken in the file. * However, if the metadata support gets expanded then you can't tell if the 0 is from * a broken file, or just no props found. A broken file is likely to stay broken, but * a file which had no props could have props once the metadata support is improved. diff --git a/includes/media/MediaHandler.php b/includes/media/MediaHandler.php index 2a735a217785..6a23bd6022d4 100644 --- a/includes/media/MediaHandler.php +++ b/includes/media/MediaHandler.php @@ -762,7 +762,7 @@ abstract class MediaHandler { * @param string $cmd */ protected function logErrorForExternalProcess( $retval, $err, $cmd ) { - # Keep error output limited (bug 57985) + # Keep error output limited (T59985) $errMessage = trim( substr( $err, 0, self::MAX_ERR_LOG_SIZE ) ); wfDebugLog( 'thumbnail', diff --git a/includes/media/SVGMetadataExtractor.php b/includes/media/SVGMetadataExtractor.php index 6a974c782989..4087fb3d5b38 100644 --- a/includes/media/SVGMetadataExtractor.php +++ b/includes/media/SVGMetadataExtractor.php @@ -86,13 +86,13 @@ class SVGReader { } // Expand entities, since Adobe Illustrator uses them for xmlns - // attributes (bug 31719). Note that libxml2 has some protection + // attributes (T33719). Note that libxml2 has some protection // against large recursive entity expansions so this is not as // insecure as it might appear to be. However, it is still extremely // insecure. It's necessary to wrap any read() calls with // libxml_disable_entity_loader() to avoid arbitrary local file // inclusion, or even arbitrary code execution if the expect - // extension is installed (bug 46859). + // extension is installed (T48859). $oldDisable = libxml_disable_entity_loader( true ); $this->reader->setParserProperty( XMLReader::SUBST_ENTITIES, true ); diff --git a/includes/media/TransformationalImageHandler.php b/includes/media/TransformationalImageHandler.php index 60aec4572985..1ab0f369dbd8 100644 --- a/includes/media/TransformationalImageHandler.php +++ b/includes/media/TransformationalImageHandler.php @@ -569,7 +569,7 @@ abstract class TransformationalImageHandler extends ImageHandler { */ public function rotate( $file, $params ) { return new MediaTransformError( 'thumbnail_error', 0, 0, - get_class( $this ) . ' rotation not implemented' ); + static::class . ' rotation not implemented' ); } /** diff --git a/includes/objectcache/ObjectCache.php b/includes/objectcache/ObjectCache.php index 0a4f0ed3c527..3370e5b9d1a7 100644 --- a/includes/objectcache/ObjectCache.php +++ b/includes/objectcache/ObjectCache.php @@ -246,8 +246,14 @@ class ObjectCache { global $wgMainCacheType, $wgMessageCacheType, $wgParserCacheType; $candidates = [ $wgMainCacheType, $wgMessageCacheType, $wgParserCacheType ]; foreach ( $candidates as $candidate ) { + $cache = false; if ( $candidate !== CACHE_NONE && $candidate !== CACHE_ANYTHING ) { - return self::getInstance( $candidate ); + $cache = self::getInstance( $candidate ); + // CACHE_ACCEL might default to nothing if no APCu + // See includes/ServiceWiring.php + if ( !( $cache instanceof EmptyBagOStuff ) ) { + return $cache; + } } } @@ -359,7 +365,7 @@ class ObjectCache { * * @since 1.26 * @return WANObjectCache - * @deprecated Since 1.28 Use MediaWikiServices::getMainWANCache() + * @deprecated Since 1.28 Use MediaWikiServices::getMainWANObjectCache() */ public static function getMainWANInstance() { return MediaWikiServices::getInstance()->getMainWANObjectCache(); diff --git a/includes/objectcache/SqlBagOStuff.php b/includes/objectcache/SqlBagOStuff.php index de49fc3458a5..8f94374dd03e 100644 --- a/includes/objectcache/SqlBagOStuff.php +++ b/includes/objectcache/SqlBagOStuff.php @@ -21,8 +21,11 @@ * @ingroup Cache */ +use Wikimedia\Rdbms\IDatabase; use \MediaWiki\MediaWikiServices; use \Wikimedia\WaitConditionLoop; +use \Wikimedia\Rdbms\TransactionProfiler; +use Wikimedia\Rdbms\LoadBalancer; /** * Class to store objects in the database @@ -403,7 +406,7 @@ class SqlBagOStuff extends BagOStuff { $exptime = $this->convertExpiry( $exptime ); $encExpiry = $db->timestamp( $exptime ); } - // (bug 24425) use a replace if the db supports it instead of + // (T26425) use a replace if the db supports it instead of // delete/insert to avoid clashes with conflicting keynames $db->update( $tableName, @@ -478,7 +481,7 @@ class SqlBagOStuff extends BagOStuff { ], __METHOD__, 'IGNORE' ); if ( $db->affectedRows() == 0 ) { - // Race condition. See bug 28611 + // Race condition. See T30611 $newValue = null; } } catch ( DBError $e ) { diff --git a/includes/page/Article.php b/includes/page/Article.php index a33c84f7e079..ee0ff225ca84 100644 --- a/includes/page/Article.php +++ b/includes/page/Article.php @@ -19,6 +19,7 @@ * * @file */ +use MediaWiki\MediaWikiServices; /** * Class for viewing MediaWiki article and history. @@ -198,24 +199,6 @@ class Article implements Page { } /** - * Note that getContent does not follow redirects anymore. - * If you need to fetch redirectable content easily, try - * the shortcut in WikiPage::getRedirectTarget() - * - * This function has side effects! Do not use this function if you - * only want the real revision text if any. - * - * @deprecated since 1.21; use WikiPage::getContent() instead - * - * @return string Return the text of this revision - */ - public function getContent() { - wfDeprecated( __METHOD__, '1.21' ); - $content = $this->getContentObject(); - return ContentHandler::getContentText( $content ); - } - - /** * Returns a Content object representing the pages effective display content, * not necessarily the revision's content! * @@ -512,7 +495,7 @@ class Article implements Page { $useParserCache = $this->mPage->shouldCheckParserCache( $parserOptions, $oldid ); wfDebug( 'Article::view using parser cache: ' . ( $useParserCache ? 'yes' : 'no' ) . "\n" ); if ( $user->getStubThreshold() ) { - $this->getContext()->getStats()->increment( 'pcache_miss_stub' ); + MediaWikiServices::getInstance()->getStatsdDataFactory()->increment( 'pcache_miss_stub' ); } $this->showRedirectedFromHeader(); @@ -744,7 +727,7 @@ class Article implements Page { $ns = $this->getTitle()->getNamespace(); - # Don't index user and user talk pages for blocked users (bug 11443) + # Don't index user and user talk pages for blocked users (T13443) if ( ( $ns == NS_USER || $ns == NS_USER_TALK ) && !$this->getTitle()->isSubpage() ) { $specificTarget = null; $vagueTarget = null; @@ -802,7 +785,7 @@ class Article implements Page { } if ( isset( $wgArticleRobotPolicies[$this->getTitle()->getPrefixedText()] ) ) { - # (bug 14900) site config can override user-defined __INDEX__ or __NOINDEX__ + # (T16900) site config can override user-defined __INDEX__ or __NOINDEX__ $policy = array_merge( $policy, self::formatRobotPolicy( $wgArticleRobotPolicies[$this->getTitle()->getPrefixedText()] ) @@ -1150,7 +1133,7 @@ class Article implements Page { || $title->getNamespace() == NS_USER_TALK ) { $rootPart = explode( '/', $title->getText() )[0]; - $user = User::newFromName( $rootPart, false /* allow IP users*/ ); + $user = User::newFromName( $rootPart, false /* allow IP users */ ); $ip = User::isIP( $rootPart ); $block = Block::newFromTarget( $user, $user ); @@ -1189,7 +1172,10 @@ class Article implements Page { $loggedIn = $this->getContext()->getUser()->isLoggedIn(); if ( $loggedIn || $cache->get( $key ) ) { $logTypes = [ 'delete', 'move' ]; - $conds = [ "log_action != 'revision'" ]; + + $dbr = wfGetDB( DB_REPLICA ); + + $conds = [ 'log_action != ' . $dbr->addQuotes( 'revision' ) ]; // Give extensions a chance to hide their (unrelated) log entries Hooks::run( 'Article::MissingArticleConditions', [ &$conds, $logTypes ] ); LogEventsList::showLogExtract( @@ -1672,15 +1658,6 @@ class Article implements Page { $title = $this->getTitle(); $ctx = $this->getContext(); $outputPage = $ctx->getOutput(); - if ( !wfMessage( 'deletereason-dropdown' )->inContentLanguage()->isDisabled() ) { - $reasonsList = Xml::getArrayFromWikiTextList( - wfMessage( 'deletereason-dropdown' )->inContentLanguage()->text() - ); - $outputPage->addModules( 'mediawiki.reasonSuggest' ); - $outputPage->addJsConfigVars( [ - 'reasons' => $reasonsList - ] ); - } $useMediaWikiUIEverywhere = $ctx->getConfig()->get( 'UseMediaWikiUIEverywhere' ); $outputPage->setPageTitle( wfMessage( 'delete-confirm', $title->getPrefixedText() ) ); $outputPage->addBacklinkSubtitle( $title ); @@ -2043,22 +2020,13 @@ class Article implements Page { /** * Call to WikiPage function for backwards compatibility. - * @see WikiPage::doEdit - * - * @deprecated since 1.21: use doEditContent() instead. - */ - public function doEdit( $text, $summary, $flags = 0, $baseRevId = false, $user = null ) { - wfDeprecated( __METHOD__, '1.21' ); - return $this->mPage->doEdit( $text, $summary, $flags, $baseRevId, $user ); - } - - /** - * Call to WikiPage function for backwards compatibility. + * @deprecated since 1.29. Use WikiPage::doEditContent() directly instead * @see WikiPage::doEditContent */ public function doEditContent( Content $content, $summary, $flags = 0, $baseRevId = false, User $user = null, $serialFormat = null ) { + wfDeprecated( __METHOD__, '1.29' ); return $this->mPage->doEditContent( $content, $summary, $flags, $baseRevId, $user, $serialFormat ); @@ -2075,16 +2043,20 @@ class Article implements Page { /** * Call to WikiPage function for backwards compatibility. * @see WikiPage::doPurge + * @note In 1.28 (and only 1.28), this took a $flags parameter that + * controlled how much purging was done. */ - public function doPurge( $flags = WikiPage::PURGE_ALL ) { - return $this->mPage->doPurge( $flags ); + public function doPurge() { + return $this->mPage->doPurge(); } /** * Call to WikiPage function for backwards compatibility. * @see WikiPage::getLastPurgeTimestamp + * @deprecated since 1.29 */ public function getLastPurgeTimestamp() { + wfDeprecated( __METHOD__, '1.29' ); return $this->mPage->getLastPurgeTimestamp(); } diff --git a/includes/page/ImageHistoryPseudoPager.php b/includes/page/ImageHistoryPseudoPager.php index 58f1666842eb..4785ef1ad419 100644 --- a/includes/page/ImageHistoryPseudoPager.php +++ b/includes/page/ImageHistoryPseudoPager.php @@ -32,9 +32,27 @@ class ImageHistoryPseudoPager extends ReverseChronologicalPager { protected $mTitle; /** + * @since 1.14 + * @var ImagePage + */ + public $mImagePage; + + /** + * @since 1.14 + * @var File[] + */ + public $mHist; + + /** + * @since 1.14 + * @var int[] + */ + public $mRange; + + /** * @param ImagePage $imagePage */ - function __construct( $imagePage ) { + public function __construct( $imagePage ) { parent::__construct( $imagePage->getContext() ); $this->mImagePage = $imagePage; $this->mTitle = clone $imagePage->getTitle(); @@ -53,18 +71,18 @@ class ImageHistoryPseudoPager extends ReverseChronologicalPager { /** * @return Title */ - function getTitle() { + public function getTitle() { return $this->mTitle; } - function getQueryInfo() { + public function getQueryInfo() { return false; } /** * @return string */ - function getIndexField() { + public function getIndexField() { return ''; } @@ -72,14 +90,14 @@ class ImageHistoryPseudoPager extends ReverseChronologicalPager { * @param object $row * @return string */ - function formatRow( $row ) { + public function formatRow( $row ) { return ''; } /** * @return string */ - function getBody() { + public function getBody() { $s = ''; $this->doQuery(); if ( count( $this->mHist ) ) { @@ -113,7 +131,7 @@ class ImageHistoryPseudoPager extends ReverseChronologicalPager { return $s; } - function doQuery() { + public function doQuery() { if ( $this->mQueryDone ) { return; } diff --git a/includes/page/ImagePage.php b/includes/page/ImagePage.php index b60b0108a11c..f8202a6cb27a 100644 --- a/includes/page/ImagePage.php +++ b/includes/page/ImagePage.php @@ -20,6 +20,8 @@ * @file */ +use Wikimedia\Rdbms\ResultWrapper; + /** * Class for viewing MediaWiki file description pages * @@ -213,7 +215,7 @@ class ImagePage extends Article { } $out->addModuleStyles( [ - 'filepage', // always show the local local Filepage.css, bug 29277 + 'filepage', // always show the local local Filepage.css, T31277 'mediawiki.action.view.filepage', // Add MediaWiki styles for a file page ] ); } @@ -336,7 +338,7 @@ class ImagePage extends Article { $filename = wfEscapeWikiText( $this->displayImg->getName() ); $linktext = $filename; - // Use of &$this in hooks triggers warnings in PHP 7.1 + // Avoid PHP 7.1 warning from passing $this by reference $imagePage = $this; Hooks::run( 'ImageOpenShowImageInlineBefore', [ &$imagePage, &$out ] ); @@ -534,7 +536,7 @@ class ImagePage extends Article { // this will get messy. // The dirmark, however, must not be immediately adjacent // to the filename, because it can get copied with it. - // See bug 25277. + // See T27277. // @codingStandardsIgnoreStart Ignore long line $out->addWikiText( <<<EOT <div class="fullMedia"><span class="dangerousLink">{$medialink}</span> $dirmark<span class="fileInfo">$longDesc</span></div> @@ -585,6 +587,8 @@ EOT } else { # Image does not exist if ( !$this->getId() ) { + $dbr = wfGetDB( DB_REPLICA ); + # No article exists either # Show deletion log to be consistent with normal articles LogEventsList::showLogExtract( @@ -593,7 +597,7 @@ EOT $this->getTitle()->getPrefixedText(), '', [ 'lim' => 10, - 'conds' => [ "log_action != 'revision'" ], + 'conds' => [ 'log_action != ' . $dbr->addQuotes( 'revision' ) ], 'showIfEmpty' => false, 'msgKey' => [ 'moveddeleted-notice' ] ] diff --git a/includes/page/PageArchive.php b/includes/page/PageArchive.php new file mode 100644 index 000000000000..11e1a30db86d --- /dev/null +++ b/includes/page/PageArchive.php @@ -0,0 +1,790 @@ +<?php +/** + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + */ + +use MediaWiki\MediaWikiServices; +use Wikimedia\Rdbms\ResultWrapper; +use Wikimedia\Rdbms\IDatabase; + +/** + * Used to show archived pages and eventually restore them. + */ +class PageArchive { + /** @var Title */ + protected $title; + + /** @var Status */ + protected $fileStatus; + + /** @var Status */ + protected $revisionStatus; + + /** @var Config */ + protected $config; + + public function __construct( $title, Config $config = null ) { + if ( is_null( $title ) ) { + throw new MWException( __METHOD__ . ' given a null title.' ); + } + $this->title = $title; + if ( $config === null ) { + wfDebug( __METHOD__ . ' did not have a Config object passed to it' ); + $config = MediaWikiServices::getInstance()->getMainConfig(); + } + $this->config = $config; + } + + public function doesWrites() { + return true; + } + + /** + * List all deleted pages recorded in the archive table. Returns result + * wrapper with (ar_namespace, ar_title, count) fields, ordered by page + * namespace/title. + * + * @return ResultWrapper + */ + public static function listAllPages() { + $dbr = wfGetDB( DB_REPLICA ); + + return self::listPages( $dbr, '' ); + } + + /** + * List deleted pages recorded in the archive matching the + * given term, using search engine archive. + * Returns result wrapper with (ar_namespace, ar_title, count) fields. + * + * @param string $term Search term + * @return ResultWrapper + */ + public static function listPagesBySearch( $term ) { + $title = Title::newFromText( $term ); + if ( $title ) { + $ns = $title->getNamespace(); + $termMain = $title->getText(); + $termDb = $title->getDBkey(); + } else { + // Prolly won't work too good + // @todo handle bare namespace names cleanly? + $ns = 0; + $termMain = $termDb = $term; + } + + // Try search engine first + $engine = MediaWikiServices::getInstance()->newSearchEngine(); + $engine->setLimitOffset( 100 ); + $engine->setNamespaces( [ $ns ] ); + $results = $engine->searchArchiveTitle( $termMain ); + if ( !$results->isOK() ) { + $results = []; + } else { + $results = $results->getValue(); + } + + if ( !$results ) { + // Fall back to regular prefix search + return self::listPagesByPrefix( $term ); + } + + $dbr = wfGetDB( DB_REPLICA ); + $condTitles = array_unique( array_map( function ( Title $t ) { + return $t->getDBkey(); + }, $results ) ); + $conds = [ + 'ar_namespace' => $ns, + $dbr->makeList( [ 'ar_title' => $condTitles ], LIST_OR ) . " OR ar_title " . + $dbr->buildLike( $termDb, $dbr->anyString() ) + ]; + + return self::listPages( $dbr, $conds ); + } + + /** + * List deleted pages recorded in the archive table matching the + * given title prefix. + * Returns result wrapper with (ar_namespace, ar_title, count) fields. + * + * @param string $prefix Title prefix + * @return ResultWrapper + */ + public static function listPagesByPrefix( $prefix ) { + $dbr = wfGetDB( DB_REPLICA ); + + $title = Title::newFromText( $prefix ); + if ( $title ) { + $ns = $title->getNamespace(); + $prefix = $title->getDBkey(); + } else { + // Prolly won't work too good + // @todo handle bare namespace names cleanly? + $ns = 0; + } + + $conds = [ + 'ar_namespace' => $ns, + 'ar_title' . $dbr->buildLike( $prefix, $dbr->anyString() ), + ]; + + return self::listPages( $dbr, $conds ); + } + + /** + * @param IDatabase $dbr + * @param string|array $condition + * @return bool|ResultWrapper + */ + protected static function listPages( $dbr, $condition ) { + return $dbr->select( + [ 'archive' ], + [ + 'ar_namespace', + 'ar_title', + 'count' => 'COUNT(*)' + ], + $condition, + __METHOD__, + [ + 'GROUP BY' => [ 'ar_namespace', 'ar_title' ], + 'ORDER BY' => [ 'ar_namespace', 'ar_title' ], + 'LIMIT' => 100, + ] + ); + } + + /** + * List the revisions of the given page. Returns result wrapper with + * (ar_minor_edit, ar_timestamp, ar_user, ar_user_text, ar_comment) fields. + * + * @return ResultWrapper + */ + public function listRevisions() { + $dbr = wfGetDB( DB_REPLICA ); + + $tables = [ 'archive' ]; + + $fields = [ + 'ar_minor_edit', 'ar_timestamp', 'ar_user', 'ar_user_text', + 'ar_comment', 'ar_len', 'ar_deleted', 'ar_rev_id', 'ar_sha1', + 'ar_page_id' + ]; + + if ( $this->config->get( 'ContentHandlerUseDB' ) ) { + $fields[] = 'ar_content_format'; + $fields[] = 'ar_content_model'; + } + + $conds = [ 'ar_namespace' => $this->title->getNamespace(), + 'ar_title' => $this->title->getDBkey() ]; + + $options = [ 'ORDER BY' => 'ar_timestamp DESC' ]; + + $join_conds = []; + + ChangeTags::modifyDisplayQuery( + $tables, + $fields, + $conds, + $join_conds, + $options, + '' + ); + + return $dbr->select( $tables, + $fields, + $conds, + __METHOD__, + $options, + $join_conds + ); + } + + /** + * List the deleted file revisions for this page, if it's a file page. + * Returns a result wrapper with various filearchive fields, or null + * if not a file page. + * + * @return ResultWrapper + * @todo Does this belong in Image for fuller encapsulation? + */ + public function listFiles() { + if ( $this->title->getNamespace() != NS_FILE ) { + return null; + } + + $dbr = wfGetDB( DB_REPLICA ); + return $dbr->select( + 'filearchive', + ArchivedFile::selectFields(), + [ 'fa_name' => $this->title->getDBkey() ], + __METHOD__, + [ 'ORDER BY' => 'fa_timestamp DESC' ] + ); + } + + /** + * Return a Revision object containing data for the deleted revision. + * Note that the result *may* or *may not* have a null page ID. + * + * @param string $timestamp + * @return Revision|null + */ + public function getRevision( $timestamp ) { + $dbr = wfGetDB( DB_REPLICA ); + + $fields = [ + 'ar_rev_id', + 'ar_text', + 'ar_comment', + 'ar_user', + 'ar_user_text', + 'ar_timestamp', + 'ar_minor_edit', + 'ar_flags', + 'ar_text_id', + 'ar_deleted', + 'ar_len', + 'ar_sha1', + ]; + + if ( $this->config->get( 'ContentHandlerUseDB' ) ) { + $fields[] = 'ar_content_format'; + $fields[] = 'ar_content_model'; + } + + $row = $dbr->selectRow( 'archive', + $fields, + [ 'ar_namespace' => $this->title->getNamespace(), + 'ar_title' => $this->title->getDBkey(), + 'ar_timestamp' => $dbr->timestamp( $timestamp ) ], + __METHOD__ ); + + if ( $row ) { + return Revision::newFromArchiveRow( $row, [ 'title' => $this->title ] ); + } + + return null; + } + + /** + * Return the most-previous revision, either live or deleted, against + * the deleted revision given by timestamp. + * + * May produce unexpected results in case of history merges or other + * unusual time issues. + * + * @param string $timestamp + * @return Revision|null Null when there is no previous revision + */ + public function getPreviousRevision( $timestamp ) { + $dbr = wfGetDB( DB_REPLICA ); + + // Check the previous deleted revision... + $row = $dbr->selectRow( 'archive', + 'ar_timestamp', + [ 'ar_namespace' => $this->title->getNamespace(), + 'ar_title' => $this->title->getDBkey(), + 'ar_timestamp < ' . + $dbr->addQuotes( $dbr->timestamp( $timestamp ) ) ], + __METHOD__, + [ + 'ORDER BY' => 'ar_timestamp DESC', + 'LIMIT' => 1 ] ); + $prevDeleted = $row ? wfTimestamp( TS_MW, $row->ar_timestamp ) : false; + + $row = $dbr->selectRow( [ 'page', 'revision' ], + [ 'rev_id', 'rev_timestamp' ], + [ + 'page_namespace' => $this->title->getNamespace(), + 'page_title' => $this->title->getDBkey(), + 'page_id = rev_page', + 'rev_timestamp < ' . + $dbr->addQuotes( $dbr->timestamp( $timestamp ) ) ], + __METHOD__, + [ + 'ORDER BY' => 'rev_timestamp DESC', + 'LIMIT' => 1 ] ); + $prevLive = $row ? wfTimestamp( TS_MW, $row->rev_timestamp ) : false; + $prevLiveId = $row ? intval( $row->rev_id ) : null; + + if ( $prevLive && $prevLive > $prevDeleted ) { + // Most prior revision was live + return Revision::newFromId( $prevLiveId ); + } elseif ( $prevDeleted ) { + // Most prior revision was deleted + return $this->getRevision( $prevDeleted ); + } + + // No prior revision on this page. + return null; + } + + /** + * Get the text from an archive row containing ar_text, ar_flags and ar_text_id + * + * @param object $row Database row + * @return string + */ + public function getTextFromRow( $row ) { + if ( is_null( $row->ar_text_id ) ) { + // An old row from MediaWiki 1.4 or previous. + // Text is embedded in this row in classic compression format. + return Revision::getRevisionText( $row, 'ar_' ); + } + + // New-style: keyed to the text storage backend. + $dbr = wfGetDB( DB_REPLICA ); + $text = $dbr->selectRow( 'text', + [ 'old_text', 'old_flags' ], + [ 'old_id' => $row->ar_text_id ], + __METHOD__ ); + + return Revision::getRevisionText( $text ); + } + + /** + * Fetch (and decompress if necessary) the stored text of the most + * recently edited deleted revision of the page. + * + * If there are no archived revisions for the page, returns NULL. + * + * @return string|null + */ + public function getLastRevisionText() { + $dbr = wfGetDB( DB_REPLICA ); + $row = $dbr->selectRow( 'archive', + [ 'ar_text', 'ar_flags', 'ar_text_id' ], + [ 'ar_namespace' => $this->title->getNamespace(), + 'ar_title' => $this->title->getDBkey() ], + __METHOD__, + [ 'ORDER BY' => 'ar_timestamp DESC' ] ); + + if ( $row ) { + return $this->getTextFromRow( $row ); + } + + return null; + } + + /** + * Quick check if any archived revisions are present for the page. + * + * @return bool + */ + public function isDeleted() { + $dbr = wfGetDB( DB_REPLICA ); + $n = $dbr->selectField( 'archive', 'COUNT(ar_title)', + [ 'ar_namespace' => $this->title->getNamespace(), + 'ar_title' => $this->title->getDBkey() ], + __METHOD__ + ); + + return ( $n > 0 ); + } + + /** + * Restore the given (or all) text and file revisions for the page. + * Once restored, the items will be removed from the archive tables. + * The deletion log will be updated with an undeletion notice. + * + * This also sets Status objects, $this->fileStatus and $this->revisionStatus + * (depending what operations are attempted). + * + * @param array $timestamps Pass an empty array to restore all revisions, + * otherwise list the ones to undelete. + * @param string $comment + * @param array $fileVersions + * @param bool $unsuppress + * @param User $user User performing the action, or null to use $wgUser + * @param string|string[] $tags Change tags to add to log entry + * ($user should be able to add the specified tags before this is called) + * @return array|bool array(number of file revisions restored, number of image revisions + * restored, log message) on success, false on failure. + */ + public function undelete( $timestamps, $comment = '', $fileVersions = [], + $unsuppress = false, User $user = null, $tags = null + ) { + // If both the set of text revisions and file revisions are empty, + // restore everything. Otherwise, just restore the requested items. + $restoreAll = empty( $timestamps ) && empty( $fileVersions ); + + $restoreText = $restoreAll || !empty( $timestamps ); + $restoreFiles = $restoreAll || !empty( $fileVersions ); + + if ( $restoreFiles && $this->title->getNamespace() == NS_FILE ) { + $img = wfLocalFile( $this->title ); + $img->load( File::READ_LATEST ); + $this->fileStatus = $img->restore( $fileVersions, $unsuppress ); + if ( !$this->fileStatus->isOK() ) { + return false; + } + $filesRestored = $this->fileStatus->successCount; + } else { + $filesRestored = 0; + } + + if ( $restoreText ) { + $this->revisionStatus = $this->undeleteRevisions( $timestamps, $unsuppress, $comment ); + if ( !$this->revisionStatus->isOK() ) { + return false; + } + + $textRestored = $this->revisionStatus->getValue(); + } else { + $textRestored = 0; + } + + // Touch the log! + + if ( !$textRestored && !$filesRestored ) { + wfDebug( "Undelete: nothing undeleted...\n" ); + + return false; + } + + if ( $user === null ) { + global $wgUser; + $user = $wgUser; + } + + $logEntry = new ManualLogEntry( 'delete', 'restore' ); + $logEntry->setPerformer( $user ); + $logEntry->setTarget( $this->title ); + $logEntry->setComment( $comment ); + $logEntry->setTags( $tags ); + $logEntry->setParameters( [ + ':assoc:count' => [ + 'revisions' => $textRestored, + 'files' => $filesRestored, + ], + ] ); + + Hooks::run( 'ArticleUndeleteLogEntry', [ $this, &$logEntry, $user ] ); + + $logid = $logEntry->insert(); + $logEntry->publish( $logid ); + + return [ $textRestored, $filesRestored, $comment ]; + } + + /** + * This is the meaty bit -- It restores archived revisions of the given page + * to the revision table. + * + * @param array $timestamps Pass an empty array to restore all revisions, + * otherwise list the ones to undelete. + * @param bool $unsuppress Remove all ar_deleted/fa_deleted restrictions of seletected revs + * @param string $comment + * @throws ReadOnlyError + * @return Status Status object containing the number of revisions restored on success + */ + private function undeleteRevisions( $timestamps, $unsuppress = false, $comment = '' ) { + if ( wfReadOnly() ) { + throw new ReadOnlyError(); + } + + $dbw = wfGetDB( DB_MASTER ); + $dbw->startAtomic( __METHOD__ ); + + $restoreAll = empty( $timestamps ); + + # Does this page already exist? We'll have to update it... + $article = WikiPage::factory( $this->title ); + # Load latest data for the current page (T33179) + $article->loadPageData( 'fromdbmaster' ); + $oldcountable = $article->isCountable(); + + $page = $dbw->selectRow( 'page', + [ 'page_id', 'page_latest' ], + [ 'page_namespace' => $this->title->getNamespace(), + 'page_title' => $this->title->getDBkey() ], + __METHOD__, + [ 'FOR UPDATE' ] // lock page + ); + + if ( $page ) { + $makepage = false; + # Page already exists. Import the history, and if necessary + # we'll update the latest revision field in the record. + + # Get the time span of this page + $previousTimestamp = $dbw->selectField( 'revision', 'rev_timestamp', + [ 'rev_id' => $page->page_latest ], + __METHOD__ ); + + if ( $previousTimestamp === false ) { + wfDebug( __METHOD__ . ": existing page refers to a page_latest that does not exist\n" ); + + $status = Status::newGood( 0 ); + $status->warning( 'undeleterevision-missing' ); + $dbw->endAtomic( __METHOD__ ); + + return $status; + } + } else { + # Have to create a new article... + $makepage = true; + $previousTimestamp = 0; + } + + $oldWhere = [ + 'ar_namespace' => $this->title->getNamespace(), + 'ar_title' => $this->title->getDBkey(), + ]; + if ( !$restoreAll ) { + $oldWhere['ar_timestamp'] = array_map( [ &$dbw, 'timestamp' ], $timestamps ); + } + + $fields = [ + 'ar_id', + 'ar_rev_id', + 'rev_id', + 'ar_text', + 'ar_comment', + 'ar_user', + 'ar_user_text', + 'ar_timestamp', + 'ar_minor_edit', + 'ar_flags', + 'ar_text_id', + 'ar_deleted', + 'ar_page_id', + 'ar_len', + 'ar_sha1' + ]; + + if ( $this->config->get( 'ContentHandlerUseDB' ) ) { + $fields[] = 'ar_content_format'; + $fields[] = 'ar_content_model'; + } + + /** + * Select each archived revision... + */ + $result = $dbw->select( + [ 'archive', 'revision' ], + $fields, + $oldWhere, + __METHOD__, + /* options */ + [ 'ORDER BY' => 'ar_timestamp' ], + [ 'revision' => [ 'LEFT JOIN', 'ar_rev_id=rev_id' ] ] + ); + + $rev_count = $result->numRows(); + if ( !$rev_count ) { + wfDebug( __METHOD__ . ": no revisions to restore\n" ); + + $status = Status::newGood( 0 ); + $status->warning( "undelete-no-results" ); + $dbw->endAtomic( __METHOD__ ); + + return $status; + } + + // We use ar_id because there can be duplicate ar_rev_id even for the same + // page. In this case, we may be able to restore the first one. + $restoreFailedArIds = []; + + // Map rev_id to the ar_id that is allowed to use it. When checking later, + // if it doesn't match, the current ar_id can not be restored. + + // Value can be an ar_id or -1 (-1 means no ar_id can use it, since the + // rev_id is taken before we even start the restore). + $allowedRevIdToArIdMap = []; + + $latestRestorableRow = null; + + foreach ( $result as $row ) { + if ( $row->ar_rev_id ) { + // rev_id is taken even before we start restoring. + if ( $row->ar_rev_id === $row->rev_id ) { + $restoreFailedArIds[] = $row->ar_id; + $allowedRevIdToArIdMap[$row->ar_rev_id] = -1; + } else { + // rev_id is not taken yet in the DB, but it might be taken + // by a prior revision in the same restore operation. If + // not, we need to reserve it. + if ( isset( $allowedRevIdToArIdMap[$row->ar_rev_id] ) ) { + $restoreFailedArIds[] = $row->ar_id; + } else { + $allowedRevIdToArIdMap[$row->ar_rev_id] = $row->ar_id; + $latestRestorableRow = $row; + } + } + } else { + // If ar_rev_id is null, there can't be a collision, and a + // rev_id will be chosen automatically. + $latestRestorableRow = $row; + } + } + + $result->seek( 0 ); // move back + + $oldPageId = 0; + if ( $latestRestorableRow !== null ) { + $oldPageId = (int)$latestRestorableRow->ar_page_id; // pass this to ArticleUndelete hook + + // grab the content to check consistency with global state before restoring the page. + $revision = Revision::newFromArchiveRow( $latestRestorableRow, + [ + 'title' => $article->getTitle(), // used to derive default content model + ] + ); + $user = User::newFromName( $revision->getUserText( Revision::RAW ), false ); + $content = $revision->getContent( Revision::RAW ); + + // NOTE: article ID may not be known yet. prepareSave() should not modify the database. + $status = $content->prepareSave( $article, 0, -1, $user ); + if ( !$status->isOK() ) { + $dbw->endAtomic( __METHOD__ ); + + return $status; + } + } + + $newid = false; // newly created page ID + $restored = 0; // number of revisions restored + /** @var Revision $revision */ + $revision = null; + $restoredPages = []; + // If there are no restorable revisions, we can skip most of the steps. + if ( $latestRestorableRow === null ) { + $failedRevisionCount = $rev_count; + } else { + if ( $makepage ) { + // Check the state of the newest to-be version... + if ( !$unsuppress + && ( $latestRestorableRow->ar_deleted & Revision::DELETED_TEXT ) + ) { + $dbw->endAtomic( __METHOD__ ); + + return Status::newFatal( "undeleterevdel" ); + } + // Safe to insert now... + $newid = $article->insertOn( $dbw, $latestRestorableRow->ar_page_id ); + if ( $newid === false ) { + // The old ID is reserved; let's pick another + $newid = $article->insertOn( $dbw ); + } + $pageId = $newid; + } else { + // Check if a deleted revision will become the current revision... + if ( $latestRestorableRow->ar_timestamp > $previousTimestamp ) { + // Check the state of the newest to-be version... + if ( !$unsuppress + && ( $latestRestorableRow->ar_deleted & Revision::DELETED_TEXT ) + ) { + $dbw->endAtomic( __METHOD__ ); + + return Status::newFatal( "undeleterevdel" ); + } + } + + $newid = false; + $pageId = $article->getId(); + } + + foreach ( $result as $row ) { + // Check for key dupes due to needed archive integrity. + if ( $row->ar_rev_id && $allowedRevIdToArIdMap[$row->ar_rev_id] !== $row->ar_id ) { + continue; + } + // Insert one revision at a time...maintaining deletion status + // unless we are specifically removing all restrictions... + $revision = Revision::newFromArchiveRow( $row, + [ + 'page' => $pageId, + 'title' => $this->title, + 'deleted' => $unsuppress ? 0 : $row->ar_deleted + ] ); + + $revision->insertOn( $dbw ); + $restored++; + + Hooks::run( 'ArticleRevisionUndeleted', + [ &$this->title, $revision, $row->ar_page_id ] ); + $restoredPages[$row->ar_page_id] = true; + } + + // Now that it's safely stored, take it out of the archive + // Don't delete rows that we failed to restore + $toDeleteConds = $oldWhere; + $failedRevisionCount = count( $restoreFailedArIds ); + if ( $failedRevisionCount > 0 ) { + $toDeleteConds[] = 'ar_id NOT IN ( ' . $dbw->makeList( $restoreFailedArIds ) . ' )'; + } + + $dbw->delete( 'archive', + $toDeleteConds, + __METHOD__ ); + } + + $status = Status::newGood( $restored ); + + if ( $failedRevisionCount > 0 ) { + $status->warning( + wfMessage( 'undeleterevision-duplicate-revid', $failedRevisionCount ) ); + } + + // Was anything restored at all? + if ( $restored ) { + $created = (bool)$newid; + // Attach the latest revision to the page... + $wasnew = $article->updateIfNewerOn( $dbw, $revision ); + if ( $created || $wasnew ) { + // Update site stats, link tables, etc + $article->doEditUpdates( + $revision, + User::newFromName( $revision->getUserText( Revision::RAW ), false ), + [ + 'created' => $created, + 'oldcountable' => $oldcountable, + 'restored' => true + ] + ); + } + + Hooks::run( 'ArticleUndelete', + [ &$this->title, $created, $comment, $oldPageId, $restoredPages ] ); + if ( $this->title->getNamespace() == NS_FILE ) { + DeferredUpdates::addUpdate( new HTMLCacheUpdate( $this->title, 'imagelinks' ) ); + } + } + + $dbw->endAtomic( __METHOD__ ); + + return $status; + } + + /** + * @return Status + */ + public function getFileStatus() { + return $this->fileStatus; + } + + /** + * @return Status + */ + public function getRevisionStatus() { + return $this->revisionStatus; + } +} diff --git a/includes/page/WikiFilePage.php b/includes/page/WikiFilePage.php index 1fa4bfa9812f..66fadf5eedad 100644 --- a/includes/page/WikiFilePage.php +++ b/includes/page/WikiFilePage.php @@ -20,6 +20,8 @@ * @file */ +use Wikimedia\Rdbms\FakeResultWrapper; + /** * Special handling for file pages * @@ -162,9 +164,12 @@ class WikiFilePage extends WikiPage { return $this->mDupes; } - public function doPurge( $flags = self::PURGE_ALL ) { + /** + * Override handling of action=purge + * @return bool + */ + public function doPurge() { $this->loadFile(); - if ( $this->mFile->exists() ) { wfDebug( 'ImagePage::doPurge purging ' . $this->mFile->getName() . "\n" ); DeferredUpdates::addUpdate( new HTMLCacheUpdate( $this->mTitle, 'imagelinks' ) ); @@ -180,8 +185,7 @@ class WikiFilePage extends WikiPage { // Purge redirect cache $this->mRepo->invalidateImageRedirect( $this->mTitle ); } - - return parent::doPurge( $flags ); + return parent::doPurge(); } /** diff --git a/includes/page/WikiPage.php b/includes/page/WikiPage.php index 0f1efe71dad2..7044e6a9421a 100644 --- a/includes/page/WikiPage.php +++ b/includes/page/WikiPage.php @@ -22,6 +22,8 @@ use \MediaWiki\Logger\LoggerFactory; use \MediaWiki\MediaWikiServices; +use Wikimedia\Rdbms\FakeResultWrapper; +use Wikimedia\Rdbms\IDatabase; /** * Class representing a MediaWiki article and history. @@ -83,9 +85,10 @@ class WikiPage implements Page, IDBAccessObject { */ protected $mLinksUpdated = '19700101000000'; - const PURGE_CDN_CACHE = 1; // purge CDN cache for page variant URLs - const PURGE_CLUSTER_PCACHE = 2; // purge parser cache in the local datacenter - const PURGE_GLOBAL_PCACHE = 4; // set page_touched to clear parser cache in all datacenters + /** @deprecated since 1.29. Added in 1.28 for partial purging, no longer used. */ + const PURGE_CDN_CACHE = 1; + const PURGE_CLUSTER_PCACHE = 2; + const PURGE_GLOBAL_PCACHE = 4; const PURGE_ALL = 7; /** @@ -151,7 +154,7 @@ class WikiPage implements Page, IDBAccessObject { * @return WikiPage|null */ public static function newFromID( $id, $from = 'fromdb' ) { - // page id's are never 0 or negative, see bug 61166 + // page ids are never 0 or negative, see T63166 if ( $id < 1 ) { return null; } @@ -257,7 +260,7 @@ class WikiPage implements Page, IDBAccessObject { $this->mTimestamp = ''; $this->mIsRedirect = false; $this->mLatest = false; - // Bug 57026: do not clear mPreparedEdit since prepareTextForEdit() already checks + // T59026: do not clear mPreparedEdit since prepareTextForEdit() already checks // the requested rev ID and content against the cached one for equality. For most // content types, the output should not change during the lifetime of this cache. // Clearing it can cause extra parses on edit for no reason. @@ -323,7 +326,7 @@ class WikiPage implements Page, IDBAccessObject { $row = $dbr->selectRow( 'page', $fields, $conditions, __METHOD__, $options ); - Hooks::run( 'ArticlePageDataAfter', [ &$this, &$row ] ); + Hooks::run( 'ArticlePageDataAfter', [ &$wikiPage, &$row ] ); return $row; } @@ -424,7 +427,7 @@ class WikiPage implements Page, IDBAccessObject { $this->mLinksUpdated = wfTimestampOrNull( TS_MW, $data->page_links_updated ); $this->mIsRedirect = intval( $data->page_is_redirect ); $this->mLatest = intval( $data->page_latest ); - // Bug 37225: $latest may no longer match the cached latest Revision object. + // T39225: $latest may no longer match the cached latest Revision object. // Double-check the ID of any cached latest Revision object for consistency. if ( $this->mLastRevision && $this->mLastRevision->getId() != $this->mLatest ) { $this->mLastRevision = null; @@ -573,37 +576,12 @@ class WikiPage implements Page, IDBAccessObject { * @return Revision|null */ public function getOldestRevision() { - // Try using the replica DB first, then try the master - $continue = 2; - $db = wfGetDB( DB_REPLICA ); - $revSelectFields = Revision::selectFields(); - - $row = null; - while ( $continue ) { - $row = $db->selectRow( - [ 'page', 'revision' ], - $revSelectFields, - [ - 'page_namespace' => $this->mTitle->getNamespace(), - 'page_title' => $this->mTitle->getDBkey(), - 'rev_page = page_id' - ], - __METHOD__, - [ - 'ORDER BY' => 'rev_timestamp ASC' - ] - ); - - if ( $row ) { - $continue = 0; - } else { - $db = wfGetDB( DB_MASTER ); - $continue--; - } + $rev = $this->mTitle->getFirstRevision(); + if ( !$rev ) { + $rev = $this->mTitle->getFirstRevision( Title::GAID_FOR_UPDATE ); } - - return $row ? Revision::newFromRow( $row ) : null; + return $rev; } /** @@ -621,7 +599,7 @@ class WikiPage implements Page, IDBAccessObject { } if ( $this->mDataLoadedFrom == self::READ_LOCKING ) { - // Bug 37225: if session S1 loads the page row FOR UPDATE, the result always + // T39225: if session S1 loads the page row FOR UPDATE, the result always // includes the latest changes committed. This is true even within REPEATABLE-READ // transactions, where S1 normally only sees changes committed before the first S1 // SELECT. Thus we need S1 to also gets the revision row FOR UPDATE; otherwise, it @@ -1120,10 +1098,11 @@ class WikiPage implements Page, IDBAccessObject { /** * Perform the actions of a page purging - * @param integer $flags Bitfield of WikiPage::PURGE_* constants * @return bool + * @note In 1.28 (and only 1.28), this took a $flags parameter that + * controlled how much purging was done. */ - public function doPurge( $flags = self::PURGE_ALL ) { + public function doPurge() { // Avoid PHP 7.1 warning of passing $this by reference $wikiPage = $this; @@ -1131,30 +1110,15 @@ class WikiPage implements Page, IDBAccessObject { return false; } - if ( ( $flags & self::PURGE_GLOBAL_PCACHE ) == self::PURGE_GLOBAL_PCACHE ) { - // Set page_touched in the database to invalidate all DC caches - $this->mTitle->invalidateCache(); - } elseif ( ( $flags & self::PURGE_CLUSTER_PCACHE ) == self::PURGE_CLUSTER_PCACHE ) { - // Delete the parser options key in the local cluster to invalidate the DC cache - ParserCache::singleton()->deleteOptionsKey( $this ); - // Avoid sending HTTP 304s in ViewAction to the client who just issued the purge - $cache = ObjectCache::getLocalClusterInstance(); - $cache->set( - $cache->makeKey( 'page', 'last-dc-purge', $this->getId() ), - wfTimestamp( TS_MW ), - $cache::TTL_HOUR - ); - } + $this->mTitle->invalidateCache(); - if ( ( $flags & self::PURGE_CDN_CACHE ) == self::PURGE_CDN_CACHE ) { - // Clear any HTML file cache - HTMLFileCache::clearFileCache( $this->getTitle() ); - // Send purge after any page_touched above update was committed - DeferredUpdates::addUpdate( - new CdnCacheUpdate( $this->mTitle->getCdnUrls() ), - DeferredUpdates::PRESEND - ); - } + // Clear file cache + HTMLFileCache::clearFileCache( $this->getTitle() ); + // Send purge after above page_touched update was committed + DeferredUpdates::addUpdate( + new CdnCacheUpdate( $this->mTitle->getCdnUrls() ), + DeferredUpdates::PRESEND + ); if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) { $messageCache = MessageCache::singleton(); @@ -1169,11 +1133,11 @@ class WikiPage implements Page, IDBAccessObject { * * @return string|bool TS_MW timestamp or false * @since 1.28 + * @deprecated since 1.29. It will always return false. */ public function getLastPurgeTimestamp() { - $cache = ObjectCache::getLocalClusterInstance(); - - return $cache->get( $cache->makeKey( 'page', 'last-dc-purge', $this->getId() ) ); + wfDeprecated( __METHOD__, '1.29' ); + return false; } /** @@ -1462,7 +1426,7 @@ class WikiPage implements Page, IDBAccessObject { $this->getContentHandler()->getModelID() ); } - // Bug 30711: always use current version when adding a new section + // T32711: always use current version when adding a new section if ( is_null( $baseRevId ) || $sectionId === 'new' ) { $oldContent = $this->getContent(); } else { @@ -1508,68 +1472,6 @@ class WikiPage implements Page, IDBAccessObject { * Change an existing article or create a new article. Updates RC and all necessary caches, * optionally via the deferred update array. * - * @param string $text New text - * @param string $summary Edit summary - * @param int $flags Bitfield: - * EDIT_NEW - * Article is known or assumed to be non-existent, create a new one - * EDIT_UPDATE - * Article is known or assumed to be pre-existing, update it - * EDIT_MINOR - * Mark this edit minor, if the user is allowed to do so - * EDIT_SUPPRESS_RC - * Do not log the change in recentchanges - * EDIT_FORCE_BOT - * Mark the edit a "bot" edit regardless of user rights - * EDIT_AUTOSUMMARY - * Fill in blank summaries with generated text where possible - * EDIT_INTERNAL - * Signal that the page retrieve/save cycle happened entirely in this request. - * - * If neither EDIT_NEW nor EDIT_UPDATE is specified, the status of the - * article will be detected. If EDIT_UPDATE is specified and the article - * doesn't exist, the function will return an edit-gone-missing error. If - * EDIT_NEW is specified and the article does exist, an edit-already-exists - * error will be returned. These two conditions are also possible with - * auto-detection due to MediaWiki's performance-optimised locking strategy. - * - * @param bool|int $baseRevId The revision ID this edit was based off, if any. - * This is not the parent revision ID, rather the revision ID for older - * content used as the source for a rollback, for example. - * @param User $user The user doing the edit - * - * @throws MWException - * @return Status Possible errors: - * edit-hook-aborted: The ArticleSave hook aborted the edit but didn't - * set the fatal flag of $status - * edit-gone-missing: In update mode, but the article didn't exist. - * edit-conflict: In update mode, the article changed unexpectedly. - * edit-no-change: Warning that the text was the same as before. - * edit-already-exists: In creation mode, but the article already exists. - * - * Extensions may define additional errors. - * - * $return->value will contain an associative array with members as follows: - * new: Boolean indicating if the function attempted to create a new article. - * revision: The revision object for the inserted revision, or null. - * - * Compatibility note: this function previously returned a boolean value - * indicating success/failure - * - * @deprecated since 1.21: use doEditContent() instead. - */ - public function doEdit( $text, $summary, $flags = 0, $baseRevId = false, $user = null ) { - wfDeprecated( __METHOD__, '1.21' ); - - $content = ContentHandler::makeContent( $text, $this->getTitle() ); - - return $this->doEditContent( $content, $summary, $flags, $baseRevId, $user ); - } - - /** - * Change an existing article or create a new article. Updates RC and all necessary caches, - * optionally via the deferred update array. - * * @param Content $content New content * @param string $summary Edit summary * @param int $flags Bitfield: @@ -1663,9 +1565,7 @@ class WikiPage implements Page, IDBAccessObject { $hook_args = [ &$wikiPage, &$user, &$content, &$summary, $flags & EDIT_MINOR, null, null, &$flags, &$hookStatus ]; // Check if the hook rejected the attempted save - if ( !Hooks::run( 'PageContentSave', $hook_args ) - || !ContentHandler::runLegacyHooks( 'ArticleSave', $hook_args, '1.21' ) - ) { + if ( !Hooks::run( 'PageContentSave', $hook_args ) ) { if ( $hookStatus->isOK() ) { // Hook returned false but didn't call fatal(); use generic message $hookStatus->fatal( 'edit-hook-aborted' ); @@ -1761,7 +1661,7 @@ class WikiPage implements Page, IDBAccessObject { return $status; } elseif ( !$oldContent ) { - // Sanity check for bug 37225 + // Sanity check for T39225 throw new MWException( "Could not find text for current revision {$oldid}." ); } @@ -1849,7 +1749,7 @@ class WikiPage implements Page, IDBAccessObject { $dbw->endAtomic( __METHOD__ ); $this->mTimestamp = $now; } else { - // Bug 32948: revision ID must be set to page {{REVISIONID}} and + // T34948: revision ID must be set to page {{REVISIONID}} and // related variables correctly. Likewise for {{REVISIONUSER}} (T135261). $revision->setId( $this->getLatest() ); $revision->setUserIdAndName( @@ -1893,7 +1793,6 @@ class WikiPage implements Page, IDBAccessObject { $params = [ &$wikiPage, &$user, $content, $summary, $flags & EDIT_MINOR, null, null, &$flags, $revision, &$status, $meta['baseRevId'], $meta['undidRevId'] ]; - ContentHandler::runLegacyHooks( 'ArticleSaveComplete', $params ); Hooks::run( 'PageContentSaveComplete', $params ); } ), @@ -2015,13 +1914,10 @@ class WikiPage implements Page, IDBAccessObject { // Trigger post-create hook $params = [ &$wikiPage, &$user, $content, $summary, $flags & EDIT_MINOR, null, null, &$flags, $revision ]; - ContentHandler::runLegacyHooks( 'ArticleInsertComplete', $params, '1.21' ); Hooks::run( 'PageContentInsertComplete', $params ); // Trigger post-save hook $params = array_merge( $params, [ &$status, $meta['baseRevId'] ] ); - ContentHandler::runLegacyHooks( 'ArticleSaveComplete', $params, '1.21' ); Hooks::run( 'PageContentSaveComplete', $params ); - } ), DeferredUpdates::PRESEND @@ -2093,7 +1989,7 @@ class WikiPage implements Page, IDBAccessObject { $user = is_null( $user ) ? $wgUser : $user; // XXX: check $user->getId() here??? - // Use a sane default for $serialFormat, see bug 57026 + // Use a sane default for $serialFormat, see T59026 if ( $serialFormat === null ) { $serialFormat = $content->getContentHandler()->getDefaultFormat(); } @@ -2156,8 +2052,12 @@ class WikiPage implements Page, IDBAccessObject { ); } else { // Try to avoid a second parse if {{REVISIONID}} is used - $edit->popts->setSpeculativeRevIdCallback( function () { - return 1 + (int)wfGetDB( DB_MASTER )->selectField( + $dbIndex = ( $this->mDataLoadedFrom & self::READ_LATEST ) === self::READ_LATEST + ? DB_MASTER // use the best possible guess + : DB_REPLICA; // T154554 + + $edit->popts->setSpeculativeRevIdCallback( function () use ( $dbIndex ) { + return 1 + (int)wfGetDB( $dbIndex )->selectField( 'revision', 'MAX(rev_id)', [], @@ -2261,7 +2161,7 @@ class WikiPage implements Page, IDBAccessObject { // Update the links tables and other secondary data if ( $content ) { - $recursive = $options['changed']; // bug 50785 + $recursive = $options['changed']; // T52785 $updates = $content->getSecondaryDataUpdates( $this->getTitle(), null, $recursive, $editInfo->output ); @@ -2363,7 +2263,7 @@ class WikiPage implements Page, IDBAccessObject { if ( $options['created'] ) { self::onArticleCreate( $this->mTitle ); - } elseif ( $options['changed'] ) { // bug 50785 + } elseif ( $options['changed'] ) { // T52785 self::onArticleEdit( $this->mTitle, $revision ); } @@ -2977,7 +2877,7 @@ class WikiPage implements Page, IDBAccessObject { $dbw->onTransactionPreCommitOrIdle( function () use ( $dbw, $logEntry, $logid ) { - // Bug 56776: avoid deadlocks (especially from FileDeleteForm) + // T58776: avoid deadlocks (especially from FileDeleteForm) $logEntry->publish( $logid ); }, __METHOD__ @@ -3257,7 +3157,7 @@ class WikiPage implements Page, IDBAccessObject { ); // Set patrolling and bot flag on the edits, which gets rollbacked. - // This is done even on edit failure to have patrolling in that case (bug 62157). + // This is done even on edit failure to have patrolling in that case (T64157). $set = []; if ( $bot && $guser->isAllowed( 'markbotedits' ) ) { // Mark all reverted edits as bot @@ -3716,4 +3616,15 @@ class WikiPage implements Page, IDBAccessObject { public function getSourceURL() { return $this->getTitle()->getCanonicalURL(); } + + /* + * @param WANObjectCache $cache + * @return string[] + * @since 1.28 + */ + public function getMutableCacheKeys( WANObjectCache $cache ) { + $linkCache = MediaWikiServices::getInstance()->getLinkCache(); + + return $linkCache->getMutableCacheKeys( $cache, $this->getTitle()->getTitleValue() ); + } } diff --git a/includes/pager/IndexPager.php b/includes/pager/IndexPager.php index 395cee5b8065..0b867ef00e91 100644 --- a/includes/pager/IndexPager.php +++ b/includes/pager/IndexPager.php @@ -21,6 +21,9 @@ * @ingroup Pager */ +use Wikimedia\Rdbms\ResultWrapper; +use Wikimedia\Rdbms\IDatabase; + /** * IndexPager is an efficient pager which uses a (roughly unique) index in the * data set to implement paging, rather than a "LIMIT offset,limit" clause. @@ -195,7 +198,7 @@ abstract class IndexPager extends ContextSource implements Pager { */ public function doQuery() { # Use the child class name for profiling - $fname = __METHOD__ . ' (' . get_class( $this ) . ')'; + $fname = __METHOD__ . ' (' . static::class . ')'; $section = Profiler::instance()->scopedProfileIn( $fname ); // @todo This should probably compare to DIR_DESCENDING and DIR_ASCENDING constants @@ -346,7 +349,7 @@ abstract class IndexPager extends ContextSource implements Pager { * @return string */ function getSqlComment() { - return get_class( $this ); + return static::class; } /** diff --git a/includes/pager/ReverseChronologicalPager.php b/includes/pager/ReverseChronologicalPager.php index 6f325c967c34..76f347023e88 100644 --- a/includes/pager/ReverseChronologicalPager.php +++ b/includes/pager/ReverseChronologicalPager.php @@ -20,6 +20,7 @@ * @file * @ingroup Pager */ +use Wikimedia\Timestamp\TimestampException; /** * IndexPager with a formatted navigation bar diff --git a/includes/parser/BlockLevelPass.php b/includes/parser/BlockLevelPass.php index cbacd34811f6..2023d134452a 100644 --- a/includes/parser/BlockLevelPass.php +++ b/includes/parser/BlockLevelPass.php @@ -38,6 +38,7 @@ class BlockLevelPass { const COLON_STATE_COMMENT = 5; const COLON_STATE_COMMENTDASH = 6; const COLON_STATE_COMMENTDASHDASH = 7; + const COLON_STATE_LC = 8; /** * Make lists from lines starting with ':', '*', '#', etc. @@ -298,7 +299,7 @@ class BlockLevelPass { if ( $openMatch || $closeMatch ) { $pendingPTag = false; - # @todo bug 5718: paragraph closed + # @todo T7718: paragraph closed $output .= $this->closeParagraph(); if ( $preOpenMatch && !$preCloseMatch ) { $this->inPre = true; @@ -352,7 +353,7 @@ class BlockLevelPass { } } } - # somewhere above we forget to get out of pre block (bug 785) + # somewhere above we forget to get out of pre block (T2785) if ( $preCloseMatch && $this->inPre ) { $this->inPre = false; } @@ -389,15 +390,14 @@ class BlockLevelPass { * @return string The position of the ':', or false if none found */ private function findColonNoLinks( $str, &$before, &$after ) { - $colonPos = strpos( $str, ':' ); - if ( $colonPos === false ) { + if ( !preg_match( '/:|<|-\{/', $str, $m, PREG_OFFSET_CAPTURE ) ) { # Nothing to find! return false; } - $ltPos = strpos( $str, '<' ); - if ( $ltPos === false || $ltPos > $colonPos ) { + if ( $m[0][0] === ':' ) { # Easy; no tag nesting to worry about + $colonPos = $m[0][1]; $before = substr( $str, 0, $colonPos ); $after = substr( $str, $colonPos + 1 ); return $colonPos; @@ -405,9 +405,10 @@ class BlockLevelPass { # Ugly state machine to walk through avoiding tags. $state = self::COLON_STATE_TEXT; - $level = 0; + $ltLevel = 0; + $lcLevel = 0; $len = strlen( $str ); - for ( $i = 0; $i < $len; $i++ ) { + for ( $i = $m[0][1]; $i < $len; $i++ ) { $c = $str[$i]; switch ( $state ) { @@ -418,7 +419,7 @@ class BlockLevelPass { $state = self::COLON_STATE_TAGSTART; break; case ":": - if ( $level === 0 ) { + if ( $ltLevel === 0 ) { # We found it! $before = substr( $str, 0, $i ); $after = substr( $str, $i + 1 ); @@ -428,35 +429,44 @@ class BlockLevelPass { break; default: # Skip ahead looking for something interesting - $colonPos = strpos( $str, ':', $i ); - if ( $colonPos === false ) { + if ( !preg_match( '/:|<|-\{/', $str, $m, PREG_OFFSET_CAPTURE, $i ) ) { # Nothing else interesting return false; } - $ltPos = strpos( $str, '<', $i ); - if ( $level === 0 ) { - if ( $ltPos === false || $colonPos < $ltPos ) { - # We found it! - $before = substr( $str, 0, $colonPos ); - $after = substr( $str, $colonPos + 1 ); - return $i; - } + if ( $m[0][0] === '-{' ) { + $state = self::COLON_STATE_LC; + $lcLevel++; + $i = $m[0][1] + 1; + } else { + # Skip ahead to next interesting character. + $i = $m[0][1] - 1; } - if ( $ltPos === false ) { - # Nothing else interesting to find; abort! - # We're nested, but there's no close tags left. Abort! - break 2; + break; + } + break; + case self::COLON_STATE_LC: + # In language converter markup -{ ... }- + if ( !preg_match( '/-\{|\}-/', $str, $m, PREG_OFFSET_CAPTURE, $i ) ) { + # Nothing else interesting to find; abort! + # We're nested in language converter markup, but there + # are no close tags left. Abort! + break 2; + } elseif ( $m[0][0] === '-{' ) { + $i = $m[0][1] + 1; + $lcLevel++; + } elseif ( $m[0][0] === '}-' ) { + $i = $m[0][1] + 1; + $lcLevel--; + if ( $lcLevel === 0 ) { + $state = self::COLON_STATE_TEXT; } - # Skip ahead to next tag start - $i = $ltPos; - $state = self::COLON_STATE_TAGSTART; } break; case self::COLON_STATE_TAG: # In a <tag> switch ( $c ) { case ">": - $level++; + $ltLevel++; $state = self::COLON_STATE_TEXT; break; case "/": @@ -486,10 +496,12 @@ class BlockLevelPass { case self::COLON_STATE_CLOSETAG: # In a </tag> if ( $c === ">" ) { - $level--; - if ( $level < 0 ) { + if ( $ltLevel > 0 ) { + $ltLevel--; + } else { + # ignore the excess close tag, but keep looking for + # colons. (This matches Parsoid behavior.) wfDebug( __METHOD__ . ": Invalid input; too many close tags\n" ); - return false; } $state = self::COLON_STATE_TEXT; } @@ -526,8 +538,11 @@ class BlockLevelPass { throw new MWException( "State machine error in " . __METHOD__ ); } } - if ( $level > 0 ) { - wfDebug( __METHOD__ . ": Invalid input; not enough close tags (level $level, state $state)\n" ); + if ( $ltLevel > 0 || $lcLevel > 0 ) { + wfDebug( + __METHOD__ . ": Invalid input; not enough close tags " . + "(level $ltLevel/$lcLevel, state $state)\n" + ); return false; } return false; diff --git a/includes/parser/CoreParserFunctions.php b/includes/parser/CoreParserFunctions.php index 6aa3accebe4c..e34d10b6a398 100644 --- a/includes/parser/CoreParserFunctions.php +++ b/includes/parser/CoreParserFunctions.php @@ -157,7 +157,7 @@ class CoreParserFunctions { } /** - * urlencodes a string according to one of three patterns: (bug 22474) + * urlencodes a string according to one of three patterns: (T24474) * * By default (for HTTP "query" strings), spaces are encoded as '+'. * Or to encode a value for the HTTP "path", spaces are encoded as '%20'. diff --git a/includes/parser/CoreTagHooks.php b/includes/parser/CoreTagHooks.php index c943b7c98637..438603a84165 100644 --- a/includes/parser/CoreTagHooks.php +++ b/includes/parser/CoreTagHooks.php @@ -79,12 +79,25 @@ class CoreTagHooks { * @param array $attributes * @param Parser $parser * @throws MWException - * @return array + * @return array|string Output of tag hook */ public static function html( $content, $attributes, $parser ) { global $wgRawHtml; if ( $wgRawHtml ) { - return [ $content, 'markerType' => 'nowiki' ]; + if ( $parser->getOptions()->getAllowUnsafeRawHtml() ) { + return [ $content, 'markerType' => 'nowiki' ]; + } else { + // In a system message where raw html is + // not allowed (but it is allowed in other + // contexts). + return Html::rawElement( + 'span', + [ 'class' => 'error' ], + // Using ->text() not ->parse() as + // a paranoia measure against a loop. + wfMessage( 'rawhtml-notallowed' )->escaped() + ); + } } else { throw new MWException( '<html> extension tag encountered unexpectedly' ); } diff --git a/includes/parser/DateFormatter.php b/includes/parser/DateFormatter.php index 40da3685ab4a..605a873b7dbb 100644 --- a/includes/parser/DateFormatter.php +++ b/includes/parser/DateFormatter.php @@ -27,13 +27,19 @@ * @ingroup Parser */ class DateFormatter { - public $mSource, $mTarget; - public $monthNames = '', $rxDM, $rxMD, $rxDMY, $rxYDM, $rxMDY, $rxYMD; + private $mSource, $mTarget; + private $monthNames = ''; - public $regexes, $pDays, $pMonths, $pYears; - public $rules, $xMonths, $preferences; + private $regexes; + private $rules, $xMonths, $preferences; - protected $lang, $mLinked; + private $lang, $mLinked; + + /** @var string[] */ + private $keys; + + /** @var string[] */ + private $targets; const ALL = -1; const NONE = 0; @@ -101,7 +107,7 @@ class DateFormatter { $this->targets[self::ISO2] = '[[y-m-d]]'; # Rules - # pref source target + # pref source target $this->rules[self::DMY][self::MD] = self::DM; $this->rules[self::ALL][self::MD] = self::MD; $this->rules[self::MDY][self::DM] = self::MD; @@ -121,7 +127,7 @@ class DateFormatter { * Get a DateFormatter object * * @param Language|string|null $lang In which language to format the date - * Defaults to the site content language + * Defaults to the site content language * @return DateFormatter */ public static function getInstance( $lang = null ) { @@ -191,17 +197,19 @@ class DateFormatter { // Another horrible hack $this->mLinked = $linked; - $text = preg_replace_callback( $regex, [ &$this, 'replace' ], $text ); + $text = preg_replace_callback( $regex, [ $this, 'replace' ], $text ); unset( $this->mLinked ); } return $text; } /** + * Regexp replacement callback + * * @param array $matches * @return string */ - public function replace( $matches ) { + private function replace( $matches ) { # Extract information from $matches $linked = true; if ( isset( $this->mLinked ) ) { @@ -217,15 +225,17 @@ class DateFormatter { } } - return $this->formatDate( $bits, $linked ); + return $this->formatDate( $bits, $matches[0], $linked ); } /** * @param array $bits + * @param string $orig Original input string, to be returned + * on formatting failure. * @param bool $link * @return string */ - public function formatDate( $bits, $link = true ) { + private function formatDate( $bits, $orig, $link = true ) { $format = $this->targets[$this->mTarget]; if ( !$link ) { @@ -300,8 +310,9 @@ class DateFormatter { } } if ( $fail ) { - /** @todo FIXME: $matches doesn't exist here, what's expected? */ - $text = $matches[0]; + // This occurs when parsing a date with day or month outside the bounds + // of possibilities. + $text = $orig; } $isoBits = []; @@ -323,7 +334,7 @@ class DateFormatter { * Return a regex that can be used to find month names in string * @return string regex to find the months with */ - public function getMonthRegex() { + private function getMonthRegex() { $names = []; for ( $i = 1; $i <= 12; $i++ ) { $names[] = $this->lang->getMonthName( $i ); @@ -337,7 +348,7 @@ class DateFormatter { * @param string $monthName Month name * @return string ISO month name */ - public function makeIsoMonth( $monthName ) { + private function makeIsoMonth( $monthName ) { $n = $this->xMonths[$this->lang->lc( $monthName )]; return sprintf( '%02d', $n ); } @@ -347,7 +358,7 @@ class DateFormatter { * @param string $year Year name * @return string ISO year name */ - public function makeIsoYear( $year ) { + private function makeIsoYear( $year ) { # Assumes the year is in a nice format, as enforced by the regex if ( substr( $year, -2 ) == 'BC' ) { $num = intval( substr( $year, 0, -3 ) ) - 1; @@ -366,7 +377,7 @@ class DateFormatter { * @return int|string int representing year number in case of AD dates, or string containing * year number and 'BC' at the end otherwise. */ - public function makeNormalYear( $iso ) { + private function makeNormalYear( $iso ) { if ( $iso[0] == '-' ) { $text = ( intval( substr( $iso, 1 ) ) + 1 ) . ' BC'; } else { diff --git a/includes/parser/LinkHolderArray.php b/includes/parser/LinkHolderArray.php index e7712f2b74c3..d2a0a1a6d457 100644 --- a/includes/parser/LinkHolderArray.php +++ b/includes/parser/LinkHolderArray.php @@ -613,7 +613,7 @@ class LinkHolderArray { public function replaceText( $text ) { $text = preg_replace_callback( '/<!--(LINK|IWLINK) (.*?)-->/', - [ &$this, 'replaceTextCallback' ], + [ $this, 'replaceTextCallback' ], $text ); return $text; diff --git a/includes/parser/MWTidy.php b/includes/parser/MWTidy.php index 5e5461587b9d..01bf2d0d651f 100644 --- a/includes/parser/MWTidy.php +++ b/includes/parser/MWTidy.php @@ -138,6 +138,9 @@ class MWTidy { case 'Html5Internal': $instance = new MediaWiki\Tidy\Html5Internal( $config ); break; + case 'RemexHtml': + $instance = new MediaWiki\Tidy\RemexDriver( $config ); + break; case 'disabled': return false; default: diff --git a/includes/parser/Parser.php b/includes/parser/Parser.php index 79fc1722f98e..953f021c2bcd 100644 --- a/includes/parser/Parser.php +++ b/includes/parser/Parser.php @@ -89,13 +89,15 @@ class Parser { # Everything except bracket, space, or control characters # \p{Zs} is unicode 'separator, space' category. It covers the space 0x20 # as well as U+3000 is IDEOGRAPHIC SPACE for T21052 - const EXT_LINK_URL_CLASS = '[^][<>"\\x00-\\x20\\x7F\p{Zs}]'; + # \x{FFFD} is the Unicode replacement character, which Preprocessor_DOM + # uses to replace invalid HTML characters. + const EXT_LINK_URL_CLASS = '[^][<>"\\x00-\\x20\\x7F\p{Zs}\x{FFFD}]'; # Simplified expression to match an IPv4 or IPv6 address, or # at least one character of a host name (embeds EXT_LINK_URL_CLASS) - const EXT_LINK_ADDR = '(?:[0-9.]+|\\[(?i:[0-9a-f:.]+)\\]|[^][<>"\\x00-\\x20\\x7F\p{Zs}])'; + const EXT_LINK_ADDR = '(?:[0-9.]+|\\[(?i:[0-9a-f:.]+)\\]|[^][<>"\\x00-\\x20\\x7F\p{Zs}\x{FFFD}])'; # RegExp to make image URLs (embeds IPv6 part of EXT_LINK_ADDR) // @codingStandardsIgnoreStart Generic.Files.LineLength - const EXT_IMAGE_REGEX = '/^(http:\/\/|https:\/\/)((?:\\[(?i:[0-9a-f:.]+)\\])?[^][<>"\\x00-\\x20\\x7F\p{Zs}]+) + const EXT_IMAGE_REGEX = '/^(http:\/\/|https:\/\/)((?:\\[(?i:[0-9a-f:.]+)\\])?[^][<>"\\x00-\\x20\\x7F\p{Zs}\x{FFFD}]+) \\/([A-Za-z0-9_.,~%\\-+&;#*?!=()@\\x80-\\xFF]+)\\.((?i)gif|png|jpg|jpeg)$/Sxu'; // @codingStandardsIgnoreEnd @@ -264,7 +266,7 @@ class Parser { $this->mUrlProtocols = wfUrlProtocols(); $this->mExtLinkBracketedRegex = '/\[(((?i)' . $this->mUrlProtocols . ')' . self::EXT_LINK_ADDR . - self::EXT_LINK_URL_CLASS . '*)\p{Zs}*([^\]\\x00-\\x08\\x0a-\\x1F]*?)\]/Su'; + self::EXT_LINK_URL_CLASS . '*)\p{Zs}*([^\]\\x00-\\x08\\x0a-\\x1F\\x{FFFD}]*?)\]/Su'; if ( isset( $conf['preprocessorClass'] ) ) { $this->mPreprocessorClass = $conf['preprocessorClass']; } elseif ( defined( 'HPHP_VERSION' ) ) { @@ -330,7 +332,9 @@ class Parser { CoreTagHooks::register( $this ); $this->initialiseVariables(); - Hooks::run( 'ParserFirstCallInit', [ &$this ] ); + // Avoid PHP 7.1 warning from passing $this by reference + $parser = $this; + Hooks::run( 'ParserFirstCallInit', [ &$parser ] ); } /** @@ -381,7 +385,9 @@ class Parser { $this->mProfiler = new SectionProfiler(); - Hooks::run( 'ParserClearState', [ &$this ] ); + // Avoid PHP 7.1 warning from passing $this by reference + $parser = $this; + Hooks::run( 'ParserClearState', [ &$parser ] ); } /** @@ -413,6 +419,8 @@ class Parser { $text = strtr( $text, "\x7f", "?" ); $magicScopeVariable = $this->lock(); } + // Strip U+0000 NULL (T159174) + $text = str_replace( "\000", '', $text ); $this->startParse( $title, $options, self::OT_HTML, $clearState ); @@ -435,11 +443,13 @@ class Parser { $this->mRevisionSize = null; } - Hooks::run( 'ParserBeforeStrip', [ &$this, &$text, &$this->mStripState ] ); + // Avoid PHP 7.1 warning from passing $this by reference + $parser = $this; + Hooks::run( 'ParserBeforeStrip', [ &$parser, &$text, &$this->mStripState ] ); # No more strip! - Hooks::run( 'ParserAfterStrip', [ &$this, &$text, &$this->mStripState ] ); + Hooks::run( 'ParserAfterStrip', [ &$parser, &$text, &$this->mStripState ] ); $text = $this->internalParse( $text ); - Hooks::run( 'ParserAfterParse', [ &$this, &$text, &$this->mStripState ] ); + Hooks::run( 'ParserAfterParse', [ &$parser, &$text, &$this->mStripState ] ); $text = $this->internalParseHalfParsed( $text, true, $linestart ); @@ -615,8 +625,10 @@ class Parser { * @return string UNSAFE half-parsed HTML */ public function recursiveTagParse( $text, $frame = false ) { - Hooks::run( 'ParserBeforeStrip', [ &$this, &$text, &$this->mStripState ] ); - Hooks::run( 'ParserAfterStrip', [ &$this, &$text, &$this->mStripState ] ); + // Avoid PHP 7.1 warning from passing $this by reference + $parser = $this; + Hooks::run( 'ParserBeforeStrip', [ &$parser, &$text, &$this->mStripState ] ); + Hooks::run( 'ParserAfterStrip', [ &$parser, &$text, &$this->mStripState ] ); $text = $this->internalParse( $text, false, $frame ); return $text; } @@ -663,8 +675,10 @@ class Parser { if ( $revid !== null ) { $this->mRevisionId = $revid; } - Hooks::run( 'ParserBeforeStrip', [ &$this, &$text, &$this->mStripState ] ); - Hooks::run( 'ParserAfterStrip', [ &$this, &$text, &$this->mStripState ] ); + // Avoid PHP 7.1 warning from passing $this by reference + $parser = $this; + Hooks::run( 'ParserBeforeStrip', [ &$parser, &$text, &$this->mStripState ] ); + Hooks::run( 'ParserAfterStrip', [ &$parser, &$text, &$this->mStripState ] ); $text = $this->replaceVariables( $text, $frame ); $text = $this->mStripState->unstripBoth( $text ); return $text; @@ -1259,8 +1273,11 @@ class Parser { $origText = $text; + // Avoid PHP 7.1 warning from passing $this by reference + $parser = $this; + # Hook to suspend the parser in this state - if ( !Hooks::run( 'ParserBeforeInternalParse', [ &$this, &$text, &$this->mStripState ] ) ) { + if ( !Hooks::run( 'ParserBeforeInternalParse', [ &$parser, &$text, &$this->mStripState ] ) ) { return $text; } @@ -1280,16 +1297,16 @@ class Parser { $text = $this->replaceVariables( $text ); } - Hooks::run( 'InternalParseBeforeSanitize', [ &$this, &$text, &$this->mStripState ] ); + Hooks::run( 'InternalParseBeforeSanitize', [ &$parser, &$text, &$this->mStripState ] ); $text = Sanitizer::removeHTMLtags( $text, - [ &$this, 'attributeStripCallback' ], + [ $this, 'attributeStripCallback' ], false, array_keys( $this->mTransparentTagHooks ), [], - [ &$this, 'addTrackingCategory' ] + [ $this, 'addTrackingCategory' ] ); - Hooks::run( 'InternalParseBeforeLinks', [ &$this, &$text, &$this->mStripState ] ); + Hooks::run( 'InternalParseBeforeLinks', [ &$parser, &$text, &$this->mStripState ] ); # Tables need to come after variable replacement for things to work # properly; putting them before other transformations should keep @@ -1328,8 +1345,11 @@ class Parser { private function internalParseHalfParsed( $text, $isMain = true, $linestart = true ) { $text = $this->mStripState->unstripGeneral( $text ); + // Avoid PHP 7.1 warning from passing $this by reference + $parser = $this; + if ( $isMain ) { - Hooks::run( 'ParserAfterUnstrip', [ &$this, &$text ] ); + Hooks::run( 'ParserAfterUnstrip', [ &$parser, &$text ] ); } # Clean up special characters, only run once, next-to-last before doBlockLevels @@ -1368,7 +1388,7 @@ class Parser { $text = $this->mStripState->unstripNoWiki( $text ); if ( $isMain ) { - Hooks::run( 'ParserBeforeTidy', [ &$this, &$text ] ); + Hooks::run( 'ParserBeforeTidy', [ &$parser, &$text ] ); } $text = $this->replaceTransparentTags( $text ); @@ -1409,7 +1429,7 @@ class Parser { } if ( $isMain ) { - Hooks::run( 'ParserAfterTidy', [ &$this, &$text ] ); + Hooks::run( 'ParserAfterTidy', [ &$parser, &$text ] ); } return $text; @@ -1434,20 +1454,21 @@ class Parser { $spdash = "(?:-|$space)"; # a dash or a non-newline space $spaces = "$space++"; # possessive match of 1 or more spaces $text = preg_replace_callback( - '!(?: # Start cases - (<a[ \t\r\n>].*?</a>) | # m[1]: Skip link text - (<.*?>) | # m[2]: Skip stuff inside - # HTML elements' . " - (\b(?i:$prots)($addr$urlChar*)) | # m[3]: Free external links - # m[4]: Post-protocol path - \b(?:RFC|PMID) $spaces # m[5]: RFC or PMID, capture number + '!(?: # Start cases + (<a[ \t\r\n>].*?</a>) | # m[1]: Skip link text + (<.*?>) | # m[2]: Skip stuff inside HTML elements' . " + (\b # m[3]: Free external links + (?i:$prots) + ($addr$urlChar*) # m[4]: Post-protocol path + ) | + \b(?:RFC|PMID) $spaces # m[5]: RFC or PMID, capture number ([0-9]+)\b | - \bISBN $spaces ( # m[6]: ISBN, capture number + \bISBN $spaces ( # m[6]: ISBN, capture number (?: 97[89] $spdash? )? # optional 13-digit ISBN prefix (?: [0-9] $spdash? ){9} # 9 digits with opt. delimiters [0-9Xx] # check digit )\b - )!xu", [ &$this, 'magicLinkCallback' ], $text ); + )!xu", [ $this, 'magicLinkCallback' ], $text ); return $text; } @@ -1589,9 +1610,7 @@ class Parser { true, 'free', $this->getExternalLinkAttribs( $url ), $this->mTitle ); # Register it in the output object... - # Replace unnecessary URL escape codes with their equivalent characters - $pasteurized = self::normalizeLinkUrl( $url ); - $this->mOutput->addExternalLink( $pasteurized ); + $this->mOutput->addExternalLink( $url ); } return $text . $trail; } @@ -1887,10 +1906,7 @@ class Parser { $this->getExternalLinkAttribs( $url ), $this->mTitle ) . $dtrail . $trail; # Register link in the output object. - # Replace unnecessary URL escape codes with the referenced character - # This prevents spammers from hiding links from the filters - $pasteurized = self::normalizeLinkUrl( $url ); - $this->mOutput->addExternalLink( $pasteurized ); + $this->mOutput->addExternalLink( $url ); } return $s; @@ -1950,18 +1966,6 @@ class Parser { /** * Replace unusual escape codes in a URL with their equivalent characters * - * @deprecated since 1.24, use normalizeLinkUrl - * @param string $url - * @return string - */ - public static function replaceUnusualEscapes( $url ) { - wfDeprecated( __METHOD__, '1.24' ); - return self::normalizeLinkUrl( $url ); - } - - /** - * Replace unusual escape codes in a URL with their equivalent characters - * * This generally follows the syntax defined in RFC 3986, with special * consideration for HTTP query strings. * @@ -2213,7 +2217,7 @@ class Parser { continue; } - $origLink = $m[1]; + $origLink = ltrim( $m[1], ' ' ); # Don't allow internal links to pages containing # PROTO: where PROTO is a valid URL protocol; these @@ -2479,7 +2483,7 @@ class Parser { * * @private * - * @param int $index + * @param string $index Magic variable identifier as mapped in MagicWord::$mVariableIDs * @param bool|PPFrame $frame * * @throws MWException @@ -2498,18 +2502,21 @@ class Parser { . ' called while parsing (no title set)' ); } + // Avoid PHP 7.1 warning from passing $this by reference + $parser = $this; + /** * Some of these require message or data lookups and can be * expensive to check many times. */ - if ( Hooks::run( 'ParserGetVariableValueVarCache', [ &$this, &$this->mVarCache ] ) ) { + if ( Hooks::run( 'ParserGetVariableValueVarCache', [ &$parser, &$this->mVarCache ] ) ) { if ( isset( $this->mVarCache[$index] ) ) { return $this->mVarCache[$index]; } } $ts = wfTimestamp( TS_UNIX, $this->mOptions->getTimestamp() ); - Hooks::run( 'ParserGetVariableValueTs', [ &$this, &$ts ] ); + Hooks::run( 'ParserGetVariableValueTs', [ &$parser, &$ts ] ); $pageLang = $this->getFunctionLang(); @@ -2822,7 +2829,7 @@ class Parser { $ret = null; Hooks::run( 'ParserGetVariableValueSwitch', - [ &$this, &$this->mVarCache, &$index, &$ret, &$frame ] + [ &$parser, &$this->mVarCache, &$index, &$ret, &$frame ] ); return $ret; @@ -3245,6 +3252,7 @@ class Parser { $text = '<span class="error">' . wfMessage( 'parser-template-loop-warning', $titleText )->inContentLanguage()->text() . '</span>'; + $this->addTrackingCategory( 'template-loop-category' ); wfDebug( __METHOD__ . ": template loop broken at '$titleText'\n" ); } } @@ -3366,7 +3374,10 @@ class Parser { throw new MWException( "Tag hook for $function is not callable\n" ); } - $allArgs = [ &$this ]; + // Avoid PHP 7.1 warning from passing $this by reference + $parser = $this; + + $allArgs = [ &$parser ]; if ( $flags & self::SFH_OBJECT_ARGS ) { # Convert arguments to PPNodes and collect for appending to $allArgs $funcArgs = []; @@ -3875,7 +3886,9 @@ class Parser { throw new MWException( "Tag hook for $name is not callable\n" ); } - $output = call_user_func_array( $callback, [ &$this, $frame, $content, $attributes ] ); + // Avoid PHP 7.1 warning from passing $this by reference + $parser = $this; + $output = call_user_func_array( $callback, [ &$parser, $frame, $content, $attributes ] ); } else { $output = '<span class="error">Invalid tag extension name: ' . htmlspecialchars( $name ) . '</span>'; @@ -4450,6 +4463,9 @@ class Parser { $this->startParse( $title, $options, self::OT_WIKI, $clearState ); $this->setUser( $user ); + // Strip U+0000 NULL (T159174) + $text = str_replace( "\000", '', $text ); + // We still normalize line endings for backwards-compatibility // with other code that just calls PST, but this should already // be handled in TextContent subclasses @@ -4954,7 +4970,7 @@ class Parser { $ig->setShowFilename( false ); $ig->setParser( $this ); $ig->setHideBadImages(); - $ig->setAttributes( Sanitizer::validateTagAttributes( $params, 'table' ) ); + $ig->setAttributes( Sanitizer::validateTagAttributes( $params, 'ul' ) ); if ( isset( $params['showfilename'] ) ) { $ig->setShowFilename( true ); @@ -4978,7 +4994,9 @@ class Parser { } $ig->setAdditionalOptions( $params ); - Hooks::run( 'BeforeParserrenderImageGallery', [ &$this, &$ig ] ); + // Avoid PHP 7.1 warning from passing $this by reference + $parser = $this; + Hooks::run( 'BeforeParserrenderImageGallery', [ &$parser, &$ig ] ); $lines = StringUtils::explode( "\n", $text ); foreach ( $lines as $line ) { @@ -5063,9 +5081,11 @@ class Parser { } if ( preg_match( "/^($prots)$addr$chars*$/u", $linkValue ) ) { $link = $linkValue; + $this->mOutput->addExternalLink( $link ); } else { $localLinkTitle = Title::newFromText( $linkValue ); if ( $localLinkTitle !== null ) { + $this->mOutput->addLink( $localLinkTitle ); $link = $localLinkTitle->getLinkURL(); } } diff --git a/includes/parser/ParserCache.php b/includes/parser/ParserCache.php index 9e9654097595..f76c0b5dec17 100644 --- a/includes/parser/ParserCache.php +++ b/includes/parser/ParserCache.php @@ -223,7 +223,7 @@ class ParserCache { // The edit section preference may not be the appropiate one in // the ParserOutput, as we are not storing it in the parsercache - // key. Force it here. See bug 31445. + // key. Force it here. See T33445. $value->setEditSectionTokens( $popts->getEditSection() ); $wikiPage = method_exists( $article, 'getPage' ) diff --git a/includes/parser/ParserOptions.php b/includes/parser/ParserOptions.php index 2900f41a9768..2cdd8c7075d5 100644 --- a/includes/parser/ParserOptions.php +++ b/includes/parser/ParserOptions.php @@ -243,6 +243,21 @@ class ParserOptions { */ private $redirectTarget = null; + /** + * If the wiki is configured to allow raw html ($wgRawHtml = true) + * is it allowed in the specific case of parsing this page. + * + * This is meant to disable unsafe parser tags in cases where + * a malicious user may control the input to the parser. + * + * @note This is expected to be true for normal pages even if the + * wiki has $wgRawHtml disabled in general. The setting only + * signifies that raw html would be unsafe in the current context + * provided that raw html is allowed at all. + * @var boolean + */ + private $allowUnsafeRawHtml = true; + public function getInterwikiMagic() { return $this->mInterwikiMagic; } @@ -409,7 +424,7 @@ class ParserOptions { * when the page is rendered based on the language of the user. * * @note When saving, this will return the default language instead of the user's. - * {{int: }} uses this which used to produce inconsistent link tables (bug 14404). + * {{int: }} uses this which used to produce inconsistent link tables (T16404). * * @return Language * @since 1.19 @@ -457,6 +472,15 @@ class ParserOptions { public function getMagicRFCLinks() { return $this->mMagicRFCLinks; } + + /** + * @since 1.29 + * @return bool + */ + public function getAllowUnsafeRawHtml() { + return $this->allowUnsafeRawHtml; + } + public function setInterwikiMagic( $x ) { return wfSetVar( $this->mInterwikiMagic, $x ); } @@ -597,6 +621,15 @@ class ParserOptions { } /** + * @param bool|null Value to set or null to get current value + * @return bool Current value for allowUnsafeRawHtml + * @since 1.29 + */ + public function setAllowUnsafeRawHtml( $x ) { + return wfSetVar( $this->allowUnsafeRawHtml, $x ); + } + + /** * Set the redirect target. * * Note that setting or changing this does not *make* the page a redirect diff --git a/includes/parser/ParserOutput.php b/includes/parser/ParserOutput.php index 7bf848fb9092..7de3b304f1d4 100644 --- a/includes/parser/ParserOutput.php +++ b/includes/parser/ParserOutput.php @@ -535,6 +535,10 @@ class ParserOutput extends CacheTime { # We don't register links pointing to our own server, unless... :-) global $wgServer, $wgRegisterInternalExternals; + # Replace unnecessary URL escape codes with the referenced character + # This prevents spammers from hiding links from the filters + $url = parser::normalizeLinkUrl( $url ); + $registerExternalLink = true; if ( !$wgRegisterInternalExternals ) { $registerExternalLink = !self::isLinkInternal( $wgServer, $url ); @@ -696,6 +700,8 @@ class ParserOutput extends CacheTime { * to SpecialTrackingCategories::$coreTrackingCategories, and extensions * should add to "TrackingCategories" in their extension.json. * + * @todo Migrate some code to TrackingCategories + * * @param string $msg Message key * @param Title $title title of the page which is being tracked * @return bool Whether the addition was successful @@ -707,7 +713,7 @@ class ParserOutput extends CacheTime { return false; } - // Important to parse with correct title (bug 31469) + // Important to parse with correct title (T33469) $cat = wfMessage( $msg ) ->title( $title ) ->inContentLanguage() diff --git a/includes/parser/Preprocessor_DOM.php b/includes/parser/Preprocessor_DOM.php index 661318bea19c..b93c6173ea64 100644 --- a/includes/parser/Preprocessor_DOM.php +++ b/includes/parser/Preprocessor_DOM.php @@ -134,7 +134,7 @@ class Preprocessor_DOM extends Preprocessor { * is to assume a direct page view. * * The generated DOM tree must depend only on the input text and the flags. - * The DOM tree must be the same in OT_HTML and OT_WIKI mode, to avoid a regression of bug 4899. + * The DOM tree must be the same in OT_HTML and OT_WIKI mode, to avoid a regression of T6899. * * Any flag added to the $flags parameter here, or any other parameter liable to cause a * change in the DOM tree for a given text, must be passed through the section identifier diff --git a/includes/parser/Preprocessor_Hash.php b/includes/parser/Preprocessor_Hash.php index 2666c93f8703..b2e9531ddd3c 100644 --- a/includes/parser/Preprocessor_Hash.php +++ b/includes/parser/Preprocessor_Hash.php @@ -107,7 +107,7 @@ class Preprocessor_Hash extends Preprocessor { * included. Default is to assume a direct page view. * * The generated DOM tree must depend only on the input text and the flags. - * The DOM tree must be the same in OT_HTML and OT_WIKI mode, to avoid a regression of bug 4899. + * The DOM tree must be the same in OT_HTML and OT_WIKI mode, to avoid a regression of T6899. * * Any flag added to the $flags parameter here, or any other parameter liable to cause a * change in the DOM tree for a given text, must be passed through the section identifier diff --git a/includes/poolcounter/PoolWorkArticleView.php b/includes/poolcounter/PoolWorkArticleView.php index 534e86b6bead..1f1add7924e4 100644 --- a/includes/poolcounter/PoolWorkArticleView.php +++ b/includes/poolcounter/PoolWorkArticleView.php @@ -129,7 +129,7 @@ class PoolWorkArticleView extends PoolCounterWork { return false; } - // Reduce effects of race conditions for slow parses (bug 46014) + // Reduce effects of race conditions for slow parses (T48014) $cacheTime = wfTimestampNow(); $time = - microtime( true ); diff --git a/includes/profiler/Profiler.php b/includes/profiler/Profiler.php index 8b4f01a3064f..252a227a1d3f 100644 --- a/includes/profiler/Profiler.php +++ b/includes/profiler/Profiler.php @@ -22,6 +22,7 @@ * @defgroup Profiler Profiler */ use Wikimedia\ScopedCallback; +use Wikimedia\Rdbms\TransactionProfiler; /** * Profiler base class that defines the interface and some trivial diff --git a/includes/profiler/output/ProfilerOutputDb.php b/includes/profiler/output/ProfilerOutputDb.php index 088721c0f77b..264ec0c63718 100644 --- a/includes/profiler/output/ProfilerOutputDb.php +++ b/includes/profiler/output/ProfilerOutputDb.php @@ -41,7 +41,7 @@ class ProfilerOutputDb extends ProfilerOutput { } public function canUse() { - # Do not log anything if database is readonly (bug 5375) + # Do not log anything if database is readonly (T7375) return !wfReadOnly(); } diff --git a/includes/profiler/output/ProfilerOutputStats.php b/includes/profiler/output/ProfilerOutputStats.php index 52aa54acca66..bb8655183c4a 100644 --- a/includes/profiler/output/ProfilerOutputStats.php +++ b/includes/profiler/output/ProfilerOutputStats.php @@ -21,6 +21,7 @@ * @file * @ingroup Profiler */ +use MediaWiki\MediaWikiServices; /** * ProfilerOutput class that flushes profiling data to the profiling @@ -38,7 +39,7 @@ class ProfilerOutputStats extends ProfilerOutput { */ public function log( array $stats ) { $prefix = isset( $this->params['prefix'] ) ? $this->params['prefix'] : ''; - $contextStats = $this->collector->getContext()->getStats(); + $contextStats = MediaWikiServices::getInstance()->getStatsdDataFactory(); foreach ( $stats as $stat ) { $key = "{$prefix}.{$stat['name']}"; diff --git a/includes/rcfeed/FormattedRCFeed.php b/includes/rcfeed/FormattedRCFeed.php new file mode 100644 index 000000000000..48a9f946a5b6 --- /dev/null +++ b/includes/rcfeed/FormattedRCFeed.php @@ -0,0 +1,68 @@ +<?php +/** + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + */ + +/** + * Base class for RC feed engines that send messages in a freely configurable + * format to a uri-addressed engine set in $wgRCEngines. + * @since 1.29 + */ +abstract class FormattedRCFeed extends RCFeed { + private $params; + + /** + * @param array $params + * - 'uri' + * - 'formatter' + * @see $wgRCFeeds + */ + public function __construct( array $params = [] ) { + $this->params = $params; + } + + /** + * Send some text to the specified feed. + * + * @param array $feed The feed, as configured in an associative array + * @param string $line The text to send + * @return bool Success + */ + abstract public function send( array $feed, $line ); + + /** + * @param RecentChange $rc + * @param string|null $actionComment + * @return bool Success + */ + public function notify( RecentChange $rc, $actionComment = null ) { + $params = $this->params; + /** @var $formatter RCFeedFormatter */ + $formatter = is_object( $params['formatter'] ) ? $params['formatter'] : new $params['formatter']; + + $line = $formatter->getLine( $params, $rc, $actionComment ); + if ( !$line ) { + // @codeCoverageIgnoreStart + // T109544 - If a feed formatter returns null, this will otherwise cause an + // error in at least RedisPubSubFeedEngine. Not sure best to handle this. + return; + // @codeCoverageIgnoreEnd + } + return $this->send( $params, $line ); + } +} diff --git a/includes/rcfeed/RCFeed.php b/includes/rcfeed/RCFeed.php new file mode 100644 index 000000000000..284f68a2da2d --- /dev/null +++ b/includes/rcfeed/RCFeed.php @@ -0,0 +1,59 @@ +<?php +/** + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + */ + +/** + * @see $wgRCFeeds + * @since 1.29 + */ +abstract class RCFeed { + /** + * @param array $params + */ + public function __construct( array $params = [] ) { + } + + /** + * Dispatch the recent changes notification. + * + * @param RecentChange $rc + * @param string|null $actionComment + * @return bool Success + */ + abstract public function notify( RecentChange $rc, $actionComment = null ); + + /** + * @param array $params + * @return RCFeed + * @throws Exception + */ + final public static function factory( array $params ) { + if ( !isset( $params['class'] ) ) { + if ( !isset( $params['uri'] ) ) { + throw new Exception( "RCFeeds must have a 'class' or 'uri' set." ); + } + return RecentChange::getEngine( $params['uri'], $params ); + } + $class = $params['class']; + if ( !class_exists( $class ) ) { + throw new Exception( "Unknown class '$class'." ); + } + return new $class( $params ); + } +} diff --git a/includes/rcfeed/RCFeedEngine.php b/includes/rcfeed/RCFeedEngine.php index 0b0cd86951ac..49436fa17177 100644 --- a/includes/rcfeed/RCFeedEngine.php +++ b/includes/rcfeed/RCFeedEngine.php @@ -1,5 +1,4 @@ <?php - /** * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -20,18 +19,9 @@ */ /** - * Interface for RC feed engines, which send formatted notifications - * + * Backward-compatibility alias. * @since 1.22 + * @deprecated since 1.29 Use FormattedRCFeed instead */ -interface RCFeedEngine { - /** - * Sends some text to the specified live feed. - * - * @see IRCColourfulRCFeedFormatter::cleanupForIRC - * @param array $feed The feed, as configured in an associative array - * @param string $line The text to send - * @return bool Success - */ - public function send( array $feed, $line ); +abstract class RCFeedEngine extends FormattedRCFeed { } diff --git a/includes/rcfeed/RedisPubSubFeedEngine.php b/includes/rcfeed/RedisPubSubFeedEngine.php index c10e959fa247..4c011be2cd69 100644 --- a/includes/rcfeed/RedisPubSubFeedEngine.php +++ b/includes/rcfeed/RedisPubSubFeedEngine.php @@ -1,5 +1,4 @@ <?php - /** * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -20,7 +19,7 @@ */ /** - * Emit a recent change notification via Redis Pub/Sub + * Send recent change notifications via Redis Pub/Sub * * If the feed URI contains a path component, it will be used to generate a * channel name by stripping the leading slash and replacing any remaining @@ -36,10 +35,10 @@ * * @since 1.22 */ -class RedisPubSubFeedEngine implements RCFeedEngine { +class RedisPubSubFeedEngine extends RCFeedEngine { /** - * @see RCFeedEngine::send + * @see FormattedRCFeed::send */ public function send( array $feed, $line ) { $parsed = wfParseUrl( $feed['uri'] ); diff --git a/includes/rcfeed/UDPRCFeedEngine.php b/includes/rcfeed/UDPRCFeedEngine.php index 9afae661a057..61ced5f440c2 100644 --- a/includes/rcfeed/UDPRCFeedEngine.php +++ b/includes/rcfeed/UDPRCFeedEngine.php @@ -1,5 +1,4 @@ <?php - /** * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -20,11 +19,10 @@ */ /** - * Sends the notification to the specified host in a UDP packet. + * Send recent change notifications in a UDP packet. * @since 1.22 */ - -class UDPRCFeedEngine implements RCFeedEngine { +class UDPRCFeedEngine extends RCFeedEngine { /** * @see RCFeedEngine::send */ diff --git a/includes/registration/Processor.php b/includes/registration/Processor.php index a4100bbc07aa..210deb1bdea8 100644 --- a/includes/registration/Processor.php +++ b/includes/registration/Processor.php @@ -23,11 +23,11 @@ interface Processor { /** * @return array With following keys: - * 'globals' - variables to be set to $GLOBALS - * 'defines' - constants to define - * 'callbacks' - functions to be executed by the registry - * 'credits' - metadata to be stored by registry - * 'attributes' - registration info which isn't a global variable + * 'globals' - variables to be set to $GLOBALS + * 'defines' - constants to define + * 'callbacks' - functions to be executed by the registry + * 'credits' - metadata to be stored by registry + * 'attributes' - registration info which isn't a global variable */ public function getExtractedInfo(); diff --git a/includes/registration/VersionChecker.php b/includes/registration/VersionChecker.php index 5aaaa1b8ab9e..a31551c36142 100644 --- a/includes/registration/VersionChecker.php +++ b/includes/registration/VersionChecker.php @@ -87,17 +87,17 @@ class VersionChecker { * installed extensions in the $credits array. * * Example $extDependencies: - * { - * 'FooBar' => { - * 'MediaWiki' => '>= 1.25.0', - * 'extensions' => { - * 'FooBaz' => '>= 1.25.0' - * }, - * 'skins' => { - * 'BazBar' => '>= 1.0.0' - * } - * } - * } + * { + * 'FooBar' => { + * 'MediaWiki' => '>= 1.25.0', + * 'extensions' => { + * 'FooBaz' => '>= 1.25.0' + * }, + * 'skins' => { + * 'BazBar' => '>= 1.0.0' + * } + * } + * } * * @param array $extDependencies All extensions that depend on other ones * @return array diff --git a/includes/resourceloader/ResourceLoader.php b/includes/resourceloader/ResourceLoader.php index f0b48d544f03..e72eaf298861 100644 --- a/includes/resourceloader/ResourceLoader.php +++ b/includes/resourceloader/ResourceLoader.php @@ -185,7 +185,7 @@ class ResourceLoader implements LoggerAwareInterface { return self::applyFilter( $filter, $data ); } - $stats = RequestContext::getMain()->getStats(); + $stats = MediaWikiServices::getInstance()->getStatsdDataFactory(); $cache = ObjectCache::getLocalServerInstance( CACHE_ANYTHING ); $key = $cache->makeGlobalKey( @@ -255,7 +255,10 @@ class ResourceLoader implements LoggerAwareInterface { $this->register( include "$IP/resources/ResourcesOOUI.php" ); // Register extension modules $this->register( $config->get( 'ResourceModules' ) ); - Hooks::run( 'ResourceLoaderRegisterModules', [ &$this ] ); + + // Avoid PHP 7.1 warning from passing $this by reference + $rl = $this; + Hooks::run( 'ResourceLoaderRegisterModules', [ &$rl ] ); if ( $config->get( 'EnableJavaScriptTest' ) === true ) { $this->registerTestModules(); @@ -404,7 +407,9 @@ class ResourceLoader implements LoggerAwareInterface { $testModules = []; $testModules['qunit'] = []; // Get other test suites (e.g. from extensions) - Hooks::run( 'ResourceLoaderTestModules', [ &$testModules, &$this ] ); + // Avoid PHP 7.1 warning from passing $this by reference + $rl = $this; + Hooks::run( 'ResourceLoaderTestModules', [ &$testModules, &$rl ] ); // Add the testrunner (which configures QUnit) to the dependencies. // Since it must be ready before any of the test suites are executed. @@ -709,7 +714,7 @@ class ResourceLoader implements LoggerAwareInterface { $module = $this->getModule( $name ); if ( $module ) { // Do not allow private modules to be loaded from the web. - // This is a security issue, see bug 34907. + // This is a security issue, see T36907. if ( $module->getGroup() === 'private' ) { $this->logger->debug( "Request for private module '$name' denied" ); $this->errors[] = "Cannot show private module \"$name\""; @@ -813,6 +818,7 @@ class ResourceLoader implements LoggerAwareInterface { * @return void */ protected function sendResponseHeaders( ResourceLoaderContext $context, $etag, $errors ) { + \MediaWiki\HeaderCallback::warnIfHeadersSent(); $rlMaxage = $this->config->get( 'ResourceLoaderMaxage' ); // Use a short cache expiry so that updates propagate to clients quickly, if: // - No version specified (shared resources, e.g. stylesheets) @@ -1211,7 +1217,7 @@ MESSAGE; $styles = (array)$styles; foreach ( $styles as $style ) { $style = trim( $style ); - // Don't output an empty "@media print { }" block (bug 40498) + // Don't output an empty "@media print { }" block (T42498) if ( $style !== '' ) { // Transform the media type based on request params and config // The way that this relies on $wgRequest to propagate request params is slightly evil @@ -1628,7 +1634,7 @@ MESSAGE; */ public function getLessCompiler( $extraVars = [] ) { // When called from the installer, it is possible that a required PHP extension - // is missing (at least for now; see bug 47564). If this is the case, throw an + // is missing (at least for now; see T49564). If this is the case, throw an // exception (caught by the installer) to prevent a fatal error later on. if ( !class_exists( 'Less_Parser' ) ) { throw new MWException( 'MediaWiki requires the less.php parser' ); diff --git a/includes/resourceloader/ResourceLoaderClientHtml.php b/includes/resourceloader/ResourceLoaderClientHtml.php index ef2827c9b6bf..8c792ad3c13a 100644 --- a/includes/resourceloader/ResourceLoaderClientHtml.php +++ b/includes/resourceloader/ResourceLoaderClientHtml.php @@ -365,7 +365,11 @@ class ResourceLoaderClientHtml { $rl = $mainContext->getResourceLoader(); $chunks = []; + // Sort module names so requests are more uniform + sort( $modules ); + if ( $mainContext->getDebug() && count( $modules ) > 1 ) { + $chunks = []; // Recursively call us for every item foreach ( $modules as $name ) { @@ -374,8 +378,6 @@ class ResourceLoaderClientHtml { return new WrappedStringList( "\n", $chunks ); } - // Sort module names so requests are more uniform - sort( $modules ); // Create keyed-by-source and then keyed-by-group list of module objects from modules list $sortedModules = []; foreach ( $modules as $name ) { @@ -415,7 +417,7 @@ class ResourceLoaderClientHtml { // Special handling for the user group; because users might change their stuff // on-wiki like user pages, or user preferences; we need to find the highest // timestamp of these user-changeable modules so we can ensure cache misses on change - // This should NOT be done for the site group (bug 27564) because anons get that too + // This should NOT be done for the site group (T29564) because anons get that too // and we shouldn't be putting timestamps in CDN-cached HTML if ( $group === 'user' ) { // Must setModules() before makeVersionQuery() diff --git a/includes/resourceloader/ResourceLoaderContext.php b/includes/resourceloader/ResourceLoaderContext.php index a1a89cb6202d..8955b8c2a0fe 100644 --- a/includes/resourceloader/ResourceLoaderContext.php +++ b/includes/resourceloader/ResourceLoaderContext.php @@ -197,7 +197,7 @@ class ResourceLoaderContext { if ( $this->direction === null ) { $this->direction = $this->getRequest()->getRawVal( 'dir' ); if ( !$this->direction ) { - // Determine directionality based on user language (bug 6100) + // Determine directionality based on user language (T8100) $this->direction = Language::factory( $this->getLanguage() )->getDir(); } } diff --git a/includes/resourceloader/ResourceLoaderImage.php b/includes/resourceloader/ResourceLoaderImage.php index 2503b22e5902..19d54714f54a 100644 --- a/includes/resourceloader/ResourceLoaderImage.php +++ b/includes/resourceloader/ResourceLoaderImage.php @@ -67,23 +67,27 @@ class ResourceLoaderImage { } } } + // Remove 'deprecated' key + if ( is_array( $this->descriptor ) ) { + unset( $this->descriptor[ 'deprecated' ] ); + } // Ensure that all files have common extension. $extensions = []; - $descriptor = (array)$descriptor; + $descriptor = (array)$this->descriptor; array_walk_recursive( $descriptor, function ( $path ) use ( &$extensions ) { $extensions[] = pathinfo( $path, PATHINFO_EXTENSION ); } ); $extensions = array_unique( $extensions ); if ( count( $extensions ) !== 1 ) { throw new InvalidArgumentException( - "File type for different image files of '$name' not the same" + "File type for different image files of '$name' not the same in module '$module'" ); } $ext = $extensions[0]; if ( !isset( self::$fileTypes[$ext] ) ) { throw new InvalidArgumentException( - "Invalid file type for image files of '$name' (valid: svg, png, gif, jpg)" + "Invalid file type for image files of '$name' (valid: svg, png, gif, jpg) in module '$module'" ); } $this->extension = $ext; @@ -176,6 +180,7 @@ class ResourceLoaderImage { 'variant' => $variant, 'format' => $format, 'lang' => $context->getLanguage(), + 'skin' => $context->getSkin(), 'version' => $context->getVersion(), ]; diff --git a/includes/resourceloader/ResourceLoaderImageModule.php b/includes/resourceloader/ResourceLoaderImageModule.php index ff1b7b1a8893..d26c96181c82 100644 --- a/includes/resourceloader/ResourceLoaderImageModule.php +++ b/includes/resourceloader/ResourceLoaderImageModule.php @@ -70,7 +70,8 @@ class ResourceLoaderImageModule extends ResourceLoaderModule { * 'selectorWithVariant' => [CSS selector template, variables: {prefix} {name} {variant}], * // List of variants that may be used for the image files * 'variants' => [ - * [theme name] => [ + * // This level of nesting can be omitted if you use the same images for every skin + * [skin name (or 'default')] => [ * [variant name] => [ * 'color' => [color string, e.g. '#ffff00'], * 'global' => [boolean, if true, this variant is available @@ -82,7 +83,8 @@ class ResourceLoaderImageModule extends ResourceLoaderModule { * ], * // List of image files and their options * 'images' => [ - * [theme name] => [ + * // This level of nesting can be omitted if you use the same images for every skin + * [skin name (or 'default')] => [ * [icon name] => [ * 'file' => [file path string or array whose values are file path strings * and whose keys are 'default', 'ltr', 'rtl', a single @@ -315,11 +317,7 @@ class ResourceLoaderImageModule extends ResourceLoaderModule { $selectors = $this->getSelectors(); foreach ( $this->getImages( $context ) as $name => $image ) { - $declarations = $this->getCssDeclarations( - $image->getDataUri( $context, null, 'original' ), - $image->getUrl( $context, $script, null, 'rasterized' ) - ); - $declarations = implode( "\n\t", $declarations ); + $declarations = $this->getStyleDeclarations( $context, $image, $script ); $selector = strtr( $selectors['selectorWithoutVariant'], [ @@ -331,11 +329,7 @@ class ResourceLoaderImageModule extends ResourceLoaderModule { $rules[] = "$selector {\n\t$declarations\n}"; foreach ( $image->getVariants() as $variant ) { - $declarations = $this->getCssDeclarations( - $image->getDataUri( $context, $variant, 'original' ), - $image->getUrl( $context, $script, $variant, 'rasterized' ) - ); - $declarations = implode( "\n\t", $declarations ); + $declarations = $this->getStyleDeclarations( $context, $image, $script, $variant ); $selector = strtr( $selectors['selectorWithVariant'], [ @@ -353,6 +347,28 @@ class ResourceLoaderImageModule extends ResourceLoaderModule { } /** + * @param ResourceLoaderContext $context + * @param ResourceLoaderImage $image Image to get the style for + * @param string $script URL to load.php + * @param string|null $variant Variant to get the style for + * @return string + */ + private function getStyleDeclarations( + ResourceLoaderContext $context, + ResourceLoaderImage $image, + $script, + $variant = null + ) { + $imageDataUri = $image->getDataUri( $context, $variant, 'original' ); + $primaryUrl = $imageDataUri ?: $image->getUrl( $context, $script, $variant, 'original' ); + $declarations = $this->getCssDeclarations( + $primaryUrl, + $image->getUrl( $context, $script, $variant, 'rasterized' ) + ); + return implode( "\n\t", $declarations ); + } + + /** * SVG support using a transparent gradient to guarantee cross-browser * compatibility (browsers able to understand gradient syntax support also SVG). * http://pauginer.tumblr.com/post/36614680636/invisible-gradient-technique diff --git a/includes/resourceloader/ResourceLoaderJqueryMsgModule.php b/includes/resourceloader/ResourceLoaderJqueryMsgModule.php index a3b059b7bfec..1704481224bb 100644 --- a/includes/resourceloader/ResourceLoaderJqueryMsgModule.php +++ b/includes/resourceloader/ResourceLoaderJqueryMsgModule.php @@ -43,9 +43,26 @@ class ResourceLoaderJqueryMsgModule extends ResourceLoaderFileModule { ) ); - $dataScript = Xml::encodeJsCall( 'mw.jqueryMsg.setParserDefaults', [ $parserDefaults ] ); + $mainDataScript = Xml::encodeJsCall( 'mw.jqueryMsg.setParserDefaults', [ $parserDefaults ] ); - return $fileScript . $dataScript; + // Associative array mapping magic words (e.g. SITENAME) + // to their values. + $magicWords = [ + 'SITENAME' => $this->getConfig()->get( 'Sitename' ), + ]; + + Hooks::run( 'ResourceLoaderJqueryMsgModuleMagicWords', [ $context, &$magicWords ] ); + + $magicWordExtendData = [ + 'magic' => $magicWords, + ]; + + $magicWordDataScript = Xml::encodeJsCall( 'mw.jqueryMsg.setParserDefaults', [ + $magicWordExtendData, + /* deep= */ true + ] ); + + return $fileScript . $mainDataScript . $magicWordDataScript; } /** diff --git a/includes/resourceloader/ResourceLoaderModule.php b/includes/resourceloader/ResourceLoaderModule.php index 8124f3398b22..5404e0fb9bfc 100644 --- a/includes/resourceloader/ResourceLoaderModule.php +++ b/includes/resourceloader/ResourceLoaderModule.php @@ -147,8 +147,8 @@ abstract class ResourceLoaderModule implements LoggerAwareInterface { if ( $deprecationInfo ) { $name = $this->getName(); $warning = 'This page is using the deprecated ResourceLoader module "' . $name . '".'; - if ( !is_bool( $deprecationInfo ) && isset( $deprecationInfo['message'] ) ) { - $warning .= "\n" . $deprecationInfo['message']; + if ( is_string( $deprecationInfo ) ) { + $warning .= "\n" . $deprecationInfo; } return Xml::encodeJsCall( 'mw.log.warn', @@ -461,29 +461,47 @@ abstract class ResourceLoaderModule implements LoggerAwareInterface { * @param array $localFileRefs List of files */ protected function saveFileDependencies( ResourceLoaderContext $context, $localFileRefs ) { - // Normalise array - $localFileRefs = array_values( array_unique( $localFileRefs ) ); - sort( $localFileRefs ); try { + // Related bugs and performance considerations: + // 1. Don't needlessly change the database value with the same list in a + // different order or with duplicates. + // 2. Use relative paths to avoid ghost entries when $IP changes. (T111481) + // 3. Don't needlessly replace the database with the same value + // just because $IP changed (e.g. when upgrading a wiki). + // 4. Don't create an endless replace loop on every request for this + // module when '../' is used anywhere. Even though both are expanded + // (one expanded by getFileDependencies from the DB, the other is + // still raw as originally read by RL), the latter has not + // been normalized yet. + + // Normalise + $localFileRefs = array_values( array_unique( $localFileRefs ) ); + sort( $localFileRefs ); + $localPaths = self::getRelativePaths( $localFileRefs ); + + $storedPaths = self::getRelativePaths( $this->getFileDependencies( $context ) ); // If the list has been modified since last time we cached it, update the cache - if ( $localFileRefs !== $this->getFileDependencies( $context ) ) { + if ( $localPaths !== $storedPaths ) { + $vary = $context->getSkin() . '|' . $context->getLanguage(); $cache = ObjectCache::getLocalClusterInstance(); - $key = $cache->makeKey( __METHOD__, $this->getName() ); + $key = $cache->makeKey( __METHOD__, $this->getName(), $vary ); $scopeLock = $cache->getScopedLock( $key, 0 ); if ( !$scopeLock ) { return; // T124649; avoid write slams } - $vary = $context->getSkin() . '|' . $context->getLanguage(); + $deps = FormatJson::encode( $localPaths ); $dbw = wfGetDB( DB_MASTER ); - $dbw->replace( 'module_deps', - [ [ 'md_module', 'md_skin' ] ], + $dbw->upsert( 'module_deps', [ 'md_module' => $this->getName(), 'md_skin' => $vary, - // Use relative paths to avoid ghost entries when $IP changes (T111481) - 'md_deps' => FormatJson::encode( self::getRelativePaths( $localFileRefs ) ), + 'md_deps' => $deps, + ], + [ 'md_module', 'md_skin' ], + [ + 'md_deps' => $deps, ] ); @@ -606,7 +624,7 @@ abstract class ResourceLoaderModule implements LoggerAwareInterface { */ final protected function buildContent( ResourceLoaderContext $context ) { $rl = $context->getResourceLoader(); - $stats = RequestContext::getMain()->getStats(); + $stats = MediaWikiServices::getInstance()->getStatsdDataFactory(); $statStart = microtime( true ); // Only include properties that are relevant to this context (e.g. only=scripts) @@ -633,7 +651,7 @@ abstract class ResourceLoaderModule implements LoggerAwareInterface { && substr( rtrim( $scripts ), -1 ) !== ';' ) { // Append semicolon to prevent weird bugs caused by files not - // terminating their statements right (bug 27054) + // terminating their statements right (T29054) $scripts .= ";\n"; } } @@ -644,7 +662,7 @@ abstract class ResourceLoaderModule implements LoggerAwareInterface { if ( $context->shouldIncludeStyles() ) { $styles = []; // Don't create empty stylesheets like [ '' => '' ] for modules - // that don't *have* any stylesheets (bug 38024). + // that don't *have* any stylesheets (T40024). $stylePairs = $this->getStyles( $context ); if ( count( $stylePairs ) ) { // If we are in debug mode without &only= set, we'll want to return an array of URLs @@ -825,7 +843,7 @@ abstract class ResourceLoaderModule implements LoggerAwareInterface { */ public function getDefinitionSummary( ResourceLoaderContext $context ) { return [ - '_class' => get_class( $this ), + '_class' => static::class, '_cacheEpoch' => $this->getConfig()->get( 'CacheEpoch' ), ]; } diff --git a/includes/resourceloader/ResourceLoaderOOUIImageModule.php b/includes/resourceloader/ResourceLoaderOOUIImageModule.php index 52aa39250d1c..14e5c2691e91 100644 --- a/includes/resourceloader/ResourceLoaderOOUIImageModule.php +++ b/includes/resourceloader/ResourceLoaderOOUIImageModule.php @@ -26,6 +26,7 @@ class ResourceLoaderOOUIImageModule extends ResourceLoaderImageModule { protected function loadFromDefinition() { if ( $this->definition === null ) { + // Do nothing if definition was already processed return; } @@ -38,39 +39,48 @@ class ResourceLoaderOOUIImageModule extends ResourceLoaderImageModule { $definition = []; foreach ( $themes as $skin => $theme ) { + // Find the path to the JSON file which contains the actual image definitions for this theme // TODO Allow extensions to specify this path somehow - $dataPath = $this->localBasePath . '/' . $rootPath . '/' . $theme . '/' . $name . '.json'; + $dataPath = $rootPath . '/' . strtolower( $theme ) . '/' . $name . '.json'; + $localDataPath = $this->localBasePath . '/' . $dataPath; - if ( file_exists( $dataPath ) ) { - $data = json_decode( file_get_contents( $dataPath ), true ); - $fixPath = function ( &$path ) use ( $rootPath, $theme ) { - // TODO Allow extensions to specify this path somehow - $path = $rootPath . '/' . $theme . '/' . $path; - }; - array_walk( $data['images'], function ( &$value ) use ( $fixPath ) { - if ( is_string( $value['file'] ) ) { - $fixPath( $value['file'] ); - } elseif ( is_array( $value['file'] ) ) { - array_walk_recursive( $value['file'], $fixPath ); - } - } ); - } else { - $data = []; + // If there's no file for this module of this theme, that's okay, it will just use the defaults + if ( !file_exists( $localDataPath ) ) { + continue; } + $data = json_decode( file_get_contents( $localDataPath ), true ); + // Expand the paths to images (since they are relative to the JSON file that defines them, not + // our base directory) + $fixPath = function ( &$path ) use ( $dataPath ) { + $path = dirname( $dataPath ) . '/' . $path; + }; + array_walk( $data['images'], function ( &$value ) use ( $fixPath ) { + if ( is_string( $value['file'] ) ) { + $fixPath( $value['file'] ); + } elseif ( is_array( $value['file'] ) ) { + array_walk_recursive( $value['file'], $fixPath ); + } + } ); + + // Convert into a definition compatible with the parent vanilla ResourceLoaderImageModule foreach ( $data as $key => $value ) { switch ( $key ) { + // Images and color variants are defined per-theme, here converted to per-skin case 'images': case 'variants': $definition[$key][$skin] = $data[$key]; break; + // Other options must be identical for each theme (or only defined in the default one) default: if ( !isset( $definition[$key] ) ) { $definition[$key] = $data[$key]; } elseif ( $definition[$key] !== $data[$key] ) { throw new Exception( - "Mismatched OOUI theme definitions are not supported: trying to load $key of $theme theme" + "Mismatched OOUI theme images definition: " . + "key '$key' of theme '$theme' " . + "does not match other themes" ); } break; @@ -78,7 +88,7 @@ class ResourceLoaderOOUIImageModule extends ResourceLoaderImageModule { } } - // Fields from definition silently override keys from JSON files + // Fields from module definition silently override keys from JSON files $this->definition += $definition; parent::loadFromDefinition(); diff --git a/includes/resourceloader/ResourceLoaderSkinModule.php b/includes/resourceloader/ResourceLoaderSkinModule.php index 91e63e70aa76..7d37944f0ee6 100644 --- a/includes/resourceloader/ResourceLoaderSkinModule.php +++ b/includes/resourceloader/ResourceLoaderSkinModule.php @@ -23,8 +23,6 @@ class ResourceLoaderSkinModule extends ResourceLoaderFileModule { - /* Methods */ - /** * @param ResourceLoaderContext $context * @return array @@ -42,6 +40,8 @@ class ResourceLoaderSkinModule extends ResourceLoaderFileModule { $styles['all'][] = '.mw-wiki-logo { background-image: ' . CSSMin::buildUrlValue( $logo1 ) . '; }'; + // Only 1.5x and 2x are supported + // Note: Keep in sync with OutputPage::addLogoPreloadLinkHeaders() if ( $logoHD ) { if ( isset( $logoHD['1.5x'] ) ) { $styles[ @@ -77,13 +77,12 @@ class ResourceLoaderSkinModule extends ResourceLoaderFileModule { return false; } - /** - * @param ResourceLoaderContext $context - * @return string: Hash - */ - public function getModifiedHash( ResourceLoaderContext $context ) { - $logo = $this->getConfig()->get( 'Logo' ); - $logoHD = $this->getConfig()->get( 'LogoHD' ); - return md5( parent::getModifiedHash( $context ) . $logo . json_encode( $logoHD ) ); + public function getDefinitionSummary( ResourceLoaderContext $context ) { + $summary = parent::getDefinitionSummary( $context ); + $summary[] = [ + 'logo' => $this->getConfig()->get( 'Logo' ), + 'logoHD' => $this->getConfig()->get( 'LogoHD' ), + ]; + return $summary; } } diff --git a/includes/resourceloader/ResourceLoaderSpecialCharacterDataModule.php b/includes/resourceloader/ResourceLoaderSpecialCharacterDataModule.php index 44371bbee22f..a0061e3531e6 100644 --- a/includes/resourceloader/ResourceLoaderSpecialCharacterDataModule.php +++ b/includes/resourceloader/ResourceLoaderSpecialCharacterDataModule.php @@ -93,6 +93,7 @@ class ResourceLoaderSpecialCharacterDataModule extends ResourceLoaderModule { 'special-characters-group-thai', 'special-characters-group-lao', 'special-characters-group-khmer', + 'special-characters-group-canadianaboriginal', 'special-characters-title-endash', 'special-characters-title-emdash', 'special-characters-title-minus' diff --git a/includes/resourceloader/ResourceLoaderStartUpModule.php b/includes/resourceloader/ResourceLoaderStartUpModule.php index a99305ca6142..04b2f72d5d34 100644 --- a/includes/resourceloader/ResourceLoaderStartUpModule.php +++ b/includes/resourceloader/ResourceLoaderStartUpModule.php @@ -82,7 +82,7 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule { 'wgSearchType' => $conf->get( 'SearchType' ), 'wgVariantArticlePath' => $conf->get( 'VariantArticlePath' ), // Force object to avoid "empty" associative array from - // becoming [] instead of {} in JS (bug 34604) + // becoming [] instead of {} in JS (T36604) 'wgActionPaths' => (object)$conf->get( 'ActionPaths' ), 'wgServer' => $conf->get( 'Server' ), 'wgServerName' => $conf->get( 'ServerName' ), @@ -342,7 +342,7 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule { /** * @param ResourceLoaderContext $context - * @return string + * @return string JavaScript code */ public function getScript( ResourceLoaderContext $context ) { global $IP; diff --git a/includes/resourceloader/ResourceLoaderUserDefaultsModule.php b/includes/resourceloader/ResourceLoaderUserDefaultsModule.php index 66320457a517..b9dc098268c6 100644 --- a/includes/resourceloader/ResourceLoaderUserDefaultsModule.php +++ b/includes/resourceloader/ResourceLoaderUserDefaultsModule.php @@ -37,7 +37,7 @@ class ResourceLoaderUserDefaultsModule extends ResourceLoaderModule { /** * @param ResourceLoaderContext $context - * @return string + * @return string JavaScript code */ public function getScript( ResourceLoaderContext $context ) { return Xml::encodeJsCall( diff --git a/includes/resourceloader/ResourceLoaderUserOptionsModule.php b/includes/resourceloader/ResourceLoaderUserOptionsModule.php index b3b3f16832e4..0c332cff8d35 100644 --- a/includes/resourceloader/ResourceLoaderUserOptionsModule.php +++ b/includes/resourceloader/ResourceLoaderUserOptionsModule.php @@ -48,7 +48,7 @@ class ResourceLoaderUserOptionsModule extends ResourceLoaderModule { /** * @param ResourceLoaderContext $context - * @return string + * @return string JavaScript code */ public function getScript( ResourceLoaderContext $context ) { return Xml::encodeJsCall( 'mw.user.options.set', diff --git a/includes/resourceloader/ResourceLoaderUserTokensModule.php b/includes/resourceloader/ResourceLoaderUserTokensModule.php index cea1f3940b13..bfa7326d9c60 100644 --- a/includes/resourceloader/ResourceLoaderUserTokensModule.php +++ b/includes/resourceloader/ResourceLoaderUserTokensModule.php @@ -57,7 +57,7 @@ class ResourceLoaderUserTokensModule extends ResourceLoaderModule { * Add FILTER_NOMIN annotation to prevent needless minification and caching (T84960). * * @param ResourceLoaderContext $context - * @return string + * @return string JavaScript code */ public function getScript( ResourceLoaderContext $context ) { return Xml::encodeJsCall( diff --git a/includes/resourceloader/ResourceLoaderWikiModule.php b/includes/resourceloader/ResourceLoaderWikiModule.php index 14d6e056e064..3eac5df9a70f 100644 --- a/includes/resourceloader/ResourceLoaderWikiModule.php +++ b/includes/resourceloader/ResourceLoaderWikiModule.php @@ -22,6 +22,8 @@ * @author Roan Kattouw */ +use Wikimedia\Rdbms\IDatabase; + /** * Abstraction for ResourceLoader modules which pull from wiki pages * @@ -146,7 +148,16 @@ class ResourceLoaderWikiModule extends ResourceLoaderModule { protected function getContent( $titleText ) { $title = Title::newFromText( $titleText ); if ( !$title ) { - return null; + return null; // Bad title + } + + // If the page is a redirect, follow the redirect. + if ( $title->isRedirect() ) { + $content = $this->getContentObj( $title ); + $title = $content ? $content->getUltimateRedirectTarget() : null; + if ( !$title ) { + return null; // Dead redirect + } } $handler = ContentHandler::getForTitle( $title ); @@ -155,9 +166,22 @@ class ResourceLoaderWikiModule extends ResourceLoaderModule { } elseif ( $handler->isSupportedFormat( CONTENT_FORMAT_JAVASCRIPT ) ) { $format = CONTENT_FORMAT_JAVASCRIPT; } else { - return null; + return null; // Bad content model } + $content = $this->getContentObj( $title ); + if ( !$content ) { + return null; // No content found + } + + return $content->serialize( $format ); + } + + /** + * @param Title $title + * @return Content|null + */ + protected function getContentObj( Title $title ) { $revision = Revision::newKnownCurrent( wfGetDB( DB_REPLICA ), $title->getArticleID(), $title->getLatestRevID() ); if ( !$revision ) { @@ -165,18 +189,16 @@ class ResourceLoaderWikiModule extends ResourceLoaderModule { } $revision->setTitle( $title ); $content = $revision->getContent( Revision::RAW ); - if ( !$content ) { wfDebugLog( 'resourceloader', __METHOD__ . ': failed to load content of JS/CSS page!' ); return null; } - - return $content->serialize( $format ); + return $content; } /** * @param ResourceLoaderContext $context - * @return string + * @return string JavaScript code */ public function getScript( ResourceLoaderContext $context ) { $scripts = ''; @@ -268,7 +290,7 @@ class ResourceLoaderWikiModule extends ResourceLoaderModule { return true; } - // Bug 68488: For other modules (i.e. ones that are called in cached html output) only check + // T70488: For other modules (i.e. ones that are called in cached html output) only check // page existance. This ensures that, if some pages in a module are temporarily blanked, // we don't end omit the module's script or link tag on some pages. return count( $revisions ) === 0; @@ -357,6 +379,11 @@ class ResourceLoaderWikiModule extends ResourceLoaderModule { } } + if ( !$wikiModules ) { + // Nothing to preload + return; + } + $pageNames = array_keys( $allPages ); sort( $pageNames ); $hash = sha1( implode( '|', $pageNames ) ); diff --git a/includes/revisiondelete/RevDelArchiveList.php b/includes/revisiondelete/RevDelArchiveList.php index ad9259b3077c..9afaf404c81b 100644 --- a/includes/revisiondelete/RevDelArchiveList.php +++ b/includes/revisiondelete/RevDelArchiveList.php @@ -19,6 +19,8 @@ * @ingroup RevisionDelete */ +use Wikimedia\Rdbms\IDatabase; + /** * List for archive table items, i.e. revisions deleted via action=delete */ diff --git a/includes/revisiondelete/RevDelArchivedFileList.php b/includes/revisiondelete/RevDelArchivedFileList.php index afee6374c9a2..1d80d8696cf9 100644 --- a/includes/revisiondelete/RevDelArchivedFileList.php +++ b/includes/revisiondelete/RevDelArchivedFileList.php @@ -19,6 +19,8 @@ * @ingroup RevisionDelete */ +use Wikimedia\Rdbms\IDatabase; + /** * List for filearchive table items */ diff --git a/includes/revisiondelete/RevDelFileItem.php b/includes/revisiondelete/RevDelFileItem.php index 9beafc9893b4..62bafe948530 100644 --- a/includes/revisiondelete/RevDelFileItem.php +++ b/includes/revisiondelete/RevDelFileItem.php @@ -19,6 +19,8 @@ * @ingroup RevisionDelete */ +use Wikimedia\Rdbms\IDatabase; + /** * Item class for an oldimage table row */ diff --git a/includes/revisiondelete/RevDelFileList.php b/includes/revisiondelete/RevDelFileList.php index 00cb2e147ce4..77cf97676206 100644 --- a/includes/revisiondelete/RevDelFileList.php +++ b/includes/revisiondelete/RevDelFileList.php @@ -19,6 +19,8 @@ * @ingroup RevisionDelete */ +use Wikimedia\Rdbms\IDatabase; + /** * List for oldimage table items */ diff --git a/includes/revisiondelete/RevDelList.php b/includes/revisiondelete/RevDelList.php index 833e38b2dc0e..64a6aec806d5 100644 --- a/includes/revisiondelete/RevDelList.php +++ b/includes/revisiondelete/RevDelList.php @@ -19,6 +19,8 @@ * @ingroup RevisionDelete */ +use MediaWiki\MediaWikiServices; + /** * Abstract base class for a list of deletable items. The list class * needs to be able to make a query from a set of identifiers to pull @@ -255,7 +257,8 @@ abstract class RevDelList extends RevisionListBase { $status->merge( $this->doPreCommitUpdates() ); if ( !$status->isOK() ) { // Fatal error, such as no configured archive directory or I/O failures - wfGetLBFactory()->rollbackMasterChanges( __METHOD__ ); + $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory(); + $lbFactory->rollbackMasterChanges( __METHOD__ ); return $status; } diff --git a/includes/revisiondelete/RevDelLogList.php b/includes/revisiondelete/RevDelLogList.php index ff1d2eda1ed3..19327781150f 100644 --- a/includes/revisiondelete/RevDelLogList.php +++ b/includes/revisiondelete/RevDelLogList.php @@ -19,6 +19,8 @@ * @ingroup RevisionDelete */ +use Wikimedia\Rdbms\IDatabase; + /** * List for logging table items */ diff --git a/includes/revisiondelete/RevDelRevisionList.php b/includes/revisiondelete/RevDelRevisionList.php index f0b1907d837f..1ea6a381b596 100644 --- a/includes/revisiondelete/RevDelRevisionList.php +++ b/includes/revisiondelete/RevDelRevisionList.php @@ -19,6 +19,9 @@ * @ingroup RevisionDelete */ +use Wikimedia\Rdbms\FakeResultWrapper; +use Wikimedia\Rdbms\IDatabase; + /** * List for revision table items * diff --git a/includes/revisiondelete/RevisionDeleteUser.php b/includes/revisiondelete/RevisionDeleteUser.php index 7f41eb2a1a93..7812fb9819ef 100644 --- a/includes/revisiondelete/RevisionDeleteUser.php +++ b/includes/revisiondelete/RevisionDeleteUser.php @@ -21,6 +21,8 @@ * @ingroup RevisionDelete */ +use Wikimedia\Rdbms\IDatabase; + /** * Backend functions for suppressing and unsuppressing all references to a given user, * used when blocking with HideUser enabled. This was spun out of SpecialBlockip.php diff --git a/includes/search/SearchDatabase.php b/includes/search/SearchDatabase.php index 38c60d0bec74..d51e525b6068 100644 --- a/includes/search/SearchDatabase.php +++ b/includes/search/SearchDatabase.php @@ -21,6 +21,8 @@ * @ingroup Search */ +use Wikimedia\Rdbms\IDatabase; + /** * Base search engine base class for database-backed searches * @ingroup Search diff --git a/includes/search/SearchEngine.php b/includes/search/SearchEngine.php index 0bcb07a5a67f..6bb4e5aebafc 100644 --- a/includes/search/SearchEngine.php +++ b/includes/search/SearchEngine.php @@ -73,6 +73,21 @@ abstract class SearchEngine { } /** + * Perform a title search in the article archive. + * NOTE: these results still should be filtered by + * matching against PageArchive, permissions checks etc + * The results returned by this methods are only sugegstions and + * may not end up being shown to the user. + * + * @param string $term Raw search term + * @return Status<Title[]> + * @since 1.29 + */ + function searchArchiveTitle( $term ) { + return Status::newGood( [] ); + } + + /** * Perform a title-only search query and return a result set. * If title searches are not supported or disabled, return null. * STUB @@ -110,6 +125,20 @@ abstract class SearchEngine { } /** + * Way to retrieve custom data set by setFeatureData + * or by the engine itself. + * @since 1.29 + * @param string $feature feature name + * @return mixed the feature value or null if unset + */ + public function getFeatureData( $feature ) { + if ( isset ( $this->features[$feature] ) ) { + return $this->features[$feature]; + } + return null; + } + + /** * When overridden in derived class, performs database-specific conversions * on text to be used for searching or updating search index. * Default implementation does nothing (simply returns $string). @@ -706,8 +735,21 @@ abstract class SearchEngine { public function getSearchIndexFields() { $models = ContentHandler::getContentModels(); $fields = []; + $seenHandlers = new SplObjectStorage(); foreach ( $models as $model ) { - $handler = ContentHandler::getForModelID( $model ); + try { + $handler = ContentHandler::getForModelID( $model ); + } + catch ( MWUnknownContentModelException $e ) { + // If we can find no handler, ignore it + continue; + } + // Several models can have the same handler, so avoid processing it repeatedly + if ( $seenHandlers->contains( $handler ) ) { + // We already did this one + continue; + } + $seenHandlers->attach( $handler ); $handlerFields = $handler->getFieldsForSearchIndex( $this ); foreach ( $handlerFields as $fieldName => $fieldData ) { if ( empty( $fields[$fieldName] ) ) { diff --git a/includes/search/SearchEngineFactory.php b/includes/search/SearchEngineFactory.php index a767bc313097..613d33ca093f 100644 --- a/includes/search/SearchEngineFactory.php +++ b/includes/search/SearchEngineFactory.php @@ -1,5 +1,7 @@ <?php +use Wikimedia\Rdbms\IDatabase; + /** * Factory class for SearchEngine. * Allows to create engine of the specific type. diff --git a/includes/search/SearchHighlighter.php b/includes/search/SearchHighlighter.php index d0e3a240d6df..cebdb40dbbda 100644 --- a/includes/search/SearchHighlighter.php +++ b/includes/search/SearchHighlighter.php @@ -29,6 +29,10 @@ class SearchHighlighter { protected $mCleanWikitext = true; + /** + * @warning If you pass false to this constructor, then + * the caller is responsible for HTML escaping. + */ function __construct( $cleanupWikitext = true ) { $this->mCleanWikitext = $cleanupWikitext; } @@ -456,6 +460,10 @@ class SearchHighlighter { $text = preg_replace( "/('''|<\/?[iIuUbB]>)/", "", $text ); $text = preg_replace( "/''/", "", $text ); + // Note, the previous /<\/?[^>]+>/ is insufficient + // for XSS safety as the HTML tag can span multiple + // search results (T144845). + $text = Sanitizer::escapeHtmlAllowEntities( $text ); return $text; } diff --git a/includes/search/SearchIndexFieldDefinition.php b/includes/search/SearchIndexFieldDefinition.php index 8a06b65ed7bd..04344fdadd7e 100644 --- a/includes/search/SearchIndexFieldDefinition.php +++ b/includes/search/SearchIndexFieldDefinition.php @@ -34,6 +34,11 @@ abstract class SearchIndexFieldDefinition implements SearchIndexField { protected $subfields = []; /** + * @var callable + */ + private $mergeCallback; + + /** * SearchIndexFieldDefinition constructor. * @param string $name Field name * @param int $type Index type @@ -91,9 +96,12 @@ abstract class SearchIndexFieldDefinition implements SearchIndexField { * @return SearchIndexField|false New definition or false if not mergeable. */ public function merge( SearchIndexField $that ) { + if ( !empty( $this->mergeCallback ) ) { + return call_user_func( $this->mergeCallback, $this, $that ); + } // TODO: which definitions may be compatible? if ( ( $that instanceof self ) && $this->type === $that->type && - $this->flags === $that->flags && $this->type !== self::INDEX_TYPE_NESTED + $this->flags === $that->flags && $this->type !== self::INDEX_TYPE_NESTED ) { return $that; } @@ -125,4 +133,11 @@ abstract class SearchIndexFieldDefinition implements SearchIndexField { */ abstract public function getMapping( SearchEngine $engine ); + /** + * Set field-specific merge strategy. + * @param callable $callback + */ + public function setMergeCallback( $callback ) { + $this->mergeCallback = $callback; + } } diff --git a/includes/search/SqlSearchResultSet.php b/includes/search/SqlSearchResultSet.php index c3985d1852f1..53d09e82b101 100644 --- a/includes/search/SqlSearchResultSet.php +++ b/includes/search/SqlSearchResultSet.php @@ -1,4 +1,7 @@ <?php + +use Wikimedia\Rdbms\ResultWrapper; + /** * This class is used for different SQL-based search engines shipped with MediaWiki * @ingroup Search diff --git a/includes/session/SessionManager.php b/includes/session/SessionManager.php index 0041450f8654..7cc850948618 100644 --- a/includes/session/SessionManager.php +++ b/includes/session/SessionManager.php @@ -773,7 +773,8 @@ final class SessionManager implements SessionManagerInterface { return $failHandler(); } } elseif ( !$info->getUserInfo()->isVerified() ) { - $this->logger->warning( + // probably just a session timeout + $this->logger->info( 'Session "{session}": Unverified user provided and no metadata to auth it', [ 'session' => $info, diff --git a/includes/session/SessionManagerInterface.php b/includes/session/SessionManagerInterface.php index 3ab0f431835e..c6990fefe760 100644 --- a/includes/session/SessionManagerInterface.php +++ b/includes/session/SessionManagerInterface.php @@ -91,9 +91,9 @@ interface SessionManagerInterface extends LoggerAwareInterface { * * The return value is such that someone could theoretically do this: * @code - * foreach ( $provider->getVaryHeaders() as $header => $options ) { - * $outputPage->addVaryHeader( $header, $options ); - * } + * foreach ( $provider->getVaryHeaders() as $header => $options ) { + * $outputPage->addVaryHeader( $header, $options ); + * } * @endcode * * @return array diff --git a/includes/session/SessionProvider.php b/includes/session/SessionProvider.php index 61c7500d05c4..3cf69b7b3333 100644 --- a/includes/session/SessionProvider.php +++ b/includes/session/SessionProvider.php @@ -397,9 +397,9 @@ abstract class SessionProvider implements SessionProviderInterface, LoggerAwareI * * The return value is such that someone could theoretically do this: * @code - * foreach ( $provider->getVaryHeaders() as $header => $options ) { - * $outputPage->addVaryHeader( $header, $options ); - * } + * foreach ( $provider->getVaryHeaders() as $header => $options ) { + * $outputPage->addVaryHeader( $header, $options ); + * } * @endcode * * @protected For use by \MediaWiki\Session\SessionManager only @@ -455,7 +455,7 @@ abstract class SessionProvider implements SessionProviderInterface, LoggerAwareI * @return string */ public function __toString() { - return get_class( $this ); + return static::class; } /** @@ -475,7 +475,7 @@ abstract class SessionProvider implements SessionProviderInterface, LoggerAwareI */ protected function describeMessage() { return wfMessage( - 'sessionprovider-' . str_replace( '\\', '-', strtolower( get_class( $this ) ) ) + 'sessionprovider-' . str_replace( '\\', '-', strtolower( static::class ) ) ); } diff --git a/includes/site/DBSiteStore.php b/includes/site/DBSiteStore.php index e5247f2fc1ca..e106f37ecc15 100644 --- a/includes/site/DBSiteStore.php +++ b/includes/site/DBSiteStore.php @@ -1,5 +1,7 @@ <?php +use Wikimedia\Rdbms\LoadBalancer; + /** * Represents the site configuration of a wiki. * Holds a list of sites (ie SiteList), stored in the database. diff --git a/includes/site/Site.php b/includes/site/Site.php index 6a97a502099b..28f19f9abfa9 100644 --- a/includes/site/Site.php +++ b/includes/site/Site.php @@ -463,6 +463,9 @@ class Site implements Serializable { * @param string $languageCode */ public function setLanguageCode( $languageCode ) { + if ( !Language::isValidCode( $languageCode ) ) { + throw new InvalidArgumentException( "$languageCode is not a valid language code." ); + } $this->languageCode = $languageCode; } diff --git a/includes/skins/BaseTemplate.php b/includes/skins/BaseTemplate.php index 65eb9b776ef7..5868904d2029 100644 --- a/includes/skins/BaseTemplate.php +++ b/includes/skins/BaseTemplate.php @@ -112,7 +112,9 @@ abstract class BaseTemplate extends QuickTemplate { $toolbox['info']['id'] = 't-info'; } - Hooks::run( 'BaseTemplateToolbox', [ &$this, &$toolbox ] ); + // Avoid PHP 7.1 warning from passing $this by reference + $template = $this; + Hooks::run( 'BaseTemplateToolbox', [ &$template, &$toolbox ] ); return $toolbox; } @@ -227,7 +229,9 @@ abstract class BaseTemplate extends QuickTemplate { ob_start(); // We pass an extra 'true' at the end so extensions using BaseTemplateToolbox // can abort and avoid outputting double toolbox links - Hooks::run( 'SkinTemplateToolboxEnd', [ &$this, true ] ); + // Avoid PHP 7.1 warning from passing $this by reference + $template = $this; + Hooks::run( 'SkinTemplateToolboxEnd', [ &$template, true ] ); $hookContents = ob_get_contents(); ob_end_clean(); if ( !trim( $hookContents ) ) { @@ -283,12 +287,31 @@ abstract class BaseTemplate extends QuickTemplate { * @param string $name */ protected function renderAfterPortlet( $name ) { + echo $this->getAfterPortlet( $name ); + } + + /** + * Allows extensions to hook into known portlets and add stuff to them + * + * @param string $name + * + * @return string html + * @since 1.29 + */ + protected function getAfterPortlet( $name ) { + $html = ''; $content = ''; Hooks::run( 'BaseTemplateAfterPortlet', [ $this, $name, &$content ] ); if ( $content !== '' ) { - echo "<div class='after-portlet after-portlet-$name'>$content</div>"; + $html = Html::rawElement( + 'div', + [ 'class' => [ 'after-portlet', 'after-portlet-' . $name ] ], + $content + ); } + + return $html; } /** @@ -320,12 +343,12 @@ abstract class BaseTemplate extends QuickTemplate { * * If a "data" key is present, it must be an array, where the keys represent * the data-xxx properties with their provided values. For example, - * $item['data'] = [ - * 'foo' => 1, - * 'bar' => 'baz', - * ]; + * $item['data'] = [ + * 'foo' => 1, + * 'bar' => 'baz', + * ]; * will render as element properties: - * data-foo='1' data-bar='baz' + * data-foo='1' data-bar='baz' * * @param array $options Can be used to affect the output of a link. * Possible options are: @@ -629,6 +652,69 @@ abstract class BaseTemplate extends QuickTemplate { } /** + * Renderer for getFooterIcons and getFooterLinks + * + * @param string $iconStyle $option for getFooterIcons: "icononly", "nocopyright" + * @param string $linkStyle $option for getFooterLinks: "flat" + * + * @return string html + * @since 1.29 + */ + protected function getFooter( $iconStyle = 'icononly', $linkStyle = 'flat' ) { + $validFooterIcons = $this->getFooterIcons( $iconStyle ); + $validFooterLinks = $this->getFooterLinks( $linkStyle ); + + $html = ''; + + if ( count( $validFooterIcons ) + count( $validFooterLinks ) > 0 ) { + $html .= Html::openElement( 'div', [ + 'id' => 'footer-bottom', + 'role' => 'contentinfo', + 'lang' => $this->get( 'userlang' ), + 'dir' => $this->get( 'dir' ) + ] ); + $footerEnd = Html::closeElement( 'div' ); + } else { + $footerEnd = ''; + } + foreach ( $validFooterIcons as $blockName => $footerIcons ) { + $html .= Html::openElement( 'div', [ + 'id' => 'f-' . Sanitizer::escapeId( $blockName ) . 'ico', + 'class' => 'footer-icons' + ] ); + foreach ( $footerIcons as $icon ) { + $html .= $this->getSkin()->makeFooterIcon( $icon ); + } + $html .= Html::closeElement( 'div' ); + } + if ( count( $validFooterLinks ) > 0 ) { + $html .= Html::openElement( 'ul', [ 'id' => 'f-list', 'class' => 'footer-places' ] ); + foreach ( $validFooterLinks as $aLink ) { + $html .= Html::rawElement( + 'li', + [ 'id' => Sanitizer::escapeId( $aLink ) ], + $this->get( $aLink ) + ); + } + $html .= Html::closeElement( 'ul' ); + } + + $html .= $this->getClear() . $footerEnd; + + return $html; + } + + /** + * Get a div with the core visualClear class, for clearing floats + * + * @return string html + * @since 1.29 + */ + protected function getClear() { + return Html::element( 'div', [ 'class' => 'visualClear' ] ); + } + + /** * Get the suggested HTML for page status indicators: icons (or short text snippets) usually * displayed in the top-right corner of the page, outside of the main content. * @@ -644,7 +730,7 @@ abstract class BaseTemplate extends QuickTemplate { * @since 1.25 */ public function getIndicators() { - $out = "<div class=\"mw-indicators\">\n"; + $out = "<div class=\"mw-indicators mw-body-content\">\n"; foreach ( $this->data['indicators'] as $id => $content ) { $out .= Html::rawElement( 'div', @@ -660,15 +746,25 @@ abstract class BaseTemplate extends QuickTemplate { } /** - * Output the basic end-page trail including bottomscripts, reporttime, and + * Output getTrail + */ + function printTrail() { + echo $this->getTrail(); + } + + /** + * Get the basic end-page trail including bottomscripts, reporttime, and * debug stuff. This should be called right before outputting the closing * body and html tags. + * + * @return string + * @since 1.29 */ - function printTrail() { -?> -<?php echo MWDebug::getDebugHTML( $this->getSkin()->getContext() ); ?> -<?php $this->html( 'bottomscripts' ); /* JS call to runBodyOnloadHook */ ?> -<?php $this->html( 'reporttime' ) ?> -<?php + function getTrail() { + $html = MWDebug::getDebugHTML( $this->getSkin()->getContext() ); + $html .= $this->get( 'bottomscripts' ); + $html .= $this->get( 'reporttime' ); + + return $html; } } diff --git a/includes/skins/Skin.php b/includes/skins/Skin.php index 96812ea23fd9..52678d4ea35d 100644 --- a/includes/skins/Skin.php +++ b/includes/skins/Skin.php @@ -149,6 +149,9 @@ abstract class Skin extends ContextSource { * Defines the ResourceLoader modules that should be added to the skin * It is recommended that skins wishing to override call parent::getDefaultModules() * and substitute out any modules they wish to change by using a key to look them up + * + * For style modules, use setupSkinUserCss() instead. + * * @return array Array of modules with helper keys for easy overriding */ public function getDefaultModules() { @@ -171,6 +174,16 @@ abstract class Skin extends ContextSource { 'user' => [], ]; + // Preload jquery.tablesorter for mediawiki.page.ready + if ( strpos( $out->getHTML(), 'sortable' ) !== false ) { + $modules['content'][] = 'jquery.tablesorter'; + } + + // Preload jquery.makeCollapsible for mediawiki.page.ready + if ( strpos( $out->getHTML(), 'mw-collapsible' ) !== false ) { + $modules['content'][] = 'jquery.makeCollapsible'; + } + // Add various resources if required if ( $wgUseAjax && $wgEnableAPI ) { if ( $wgEnableWriteAPI && $user->isLoggedIn() @@ -381,7 +394,7 @@ abstract class Skin extends ContextSource { if ( $title->isSpecialPage() ) { $type = 'ns-special'; - // bug 23315: provide a class based on the canonical special page name without subpages + // T25315: provide a class based on the canonical special page name without subpages list( $canonicalName ) = SpecialPageFactory::resolveAlias( $title->getDBkey() ); if ( $canonicalName ) { $type .= ' ' . Sanitizer::escapeClass( "mw-special-$canonicalName" ); @@ -906,7 +919,10 @@ abstract class Skin extends ContextSource { $html = htmlspecialchars( $icon["alt"] ); } if ( $url ) { - $html = Html::rawElement( 'a', [ "href" => $url ], $html ); + global $wgExternalLinkTarget; + $html = Html::rawElement( 'a', + [ "href" => $url, "target" => $wgExternalLinkTarget ], + $html ); } } return $html; @@ -1035,7 +1051,7 @@ abstract class Skin extends ContextSource { global $wgStylePath, $wgStyleVersion; if ( $this->stylename === null ) { - $class = get_class( $this ); + $class = static::class; throw new MWException( "$class::\$stylename must be set to use getSkinStylePath()" ); } @@ -1283,7 +1299,7 @@ abstract class Skin extends ContextSource { $line = array_map( 'trim', explode( '|', $line, 2 ) ); if ( count( $line ) !== 2 ) { // Second sanity check, could be hit by people doing - // funky stuff with parserfuncs... (bug 33321) + // funky stuff with parserfuncs... (T35321) continue; } @@ -1538,7 +1554,7 @@ abstract class Skin extends ContextSource { $attribs = []; if ( !is_null( $tooltip ) ) { - # Bug 25462: undo double-escaping. + # T27462: undo double-escaping. $tooltip = Sanitizer::decodeCharReferences( $tooltip ); $attribs['title'] = wfMessage( 'editsectionhint' )->rawParams( $tooltip ) ->inLanguage( $lang )->text(); diff --git a/includes/skins/SkinTemplate.php b/includes/skins/SkinTemplate.php index 575a9acf7731..61dbf2b1c101 100644 --- a/includes/skins/SkinTemplate.php +++ b/includes/skins/SkinTemplate.php @@ -61,7 +61,7 @@ class SkinTemplate extends Skin { * * @param OutputPage $out */ - function setupSkinUserCss( OutputPage $out ) { + public function setupSkinUserCss( OutputPage $out ) { $moduleStyles = [ 'mediawiki.legacy.shared', 'mediawiki.legacy.commonPrint', @@ -344,7 +344,7 @@ class SkinTemplate extends Skin { $tpl->set( 'charset', 'UTF-8' ); $tpl->setRef( 'wgScript', $wgScript ); $tpl->setRef( 'skinname', $this->skinname ); - $tpl->set( 'skinclass', get_class( $this ) ); + $tpl->set( 'skinclass', static::class ); $tpl->setRef( 'skin', $this ); $tpl->setRef( 'stylename', $this->stylename ); $tpl->set( 'printable', $out->isPrintable() ); @@ -582,7 +582,7 @@ class SkinTemplate extends Skin { /* set up the default links for the personal toolbar */ $personal_urls = []; - # Due to bug 32276, if a user does not have read permissions, + # Due to T34276, if a user does not have read permissions, # $this->getTitle() will just give Special:Badtitle, which is # not especially useful as a returnto parameter. Use the title # from the request instead, if there was one. @@ -663,7 +663,7 @@ class SkinTemplate extends Skin { 'text' => $this->msg( 'pt-userlogout' )->text(), 'href' => self::makeSpecialUrl( 'Userlogout', // userlogout link must always contain an & character, otherwise we might not be able - // to detect a buggy precaching proxy (bug 17790) + // to detect a buggy precaching proxy (T19790) $title->isSpecial( 'Preferences' ) ? 'noreturnto' : $returnto ), 'active' => false ]; @@ -1120,7 +1120,7 @@ class SkinTemplate extends Skin { $content_navigation['namespaces']['special'] = [ 'class' => 'selected', 'text' => $this->msg( 'nstab-special' )->text(), - 'href' => $request->getRequestURL(), // @see: bug 2457, bug 2510 + 'href' => $request->getRequestURL(), // @see: T4457, T4510 'context' => 'subject' ]; @@ -1283,7 +1283,7 @@ class SkinTemplate extends Skin { 'href' => $this->getTitle()->getLocalURL( "action=info" ) ]; - if ( $this->getTitle()->exists() ) { + if ( $this->getTitle()->exists() || $this->getTitle()->inNamespace( NS_CATEGORY ) ) { $nav_urls['recentchangeslinked'] = [ 'href' => SpecialPage::getTitleFor( 'Recentchangeslinked', $this->thispage )->getLocalURL() ]; @@ -1322,11 +1322,11 @@ class SkinTemplate extends Skin { if ( !$user->isAnon() ) { $sur = new UserrightsPage; $sur->setContext( $this->getContext() ); - $canChange = $sur->userCanChangeRights( $this->getUser(), false ); + $canChange = $sur->userCanChangeRights( $user ); $nav_urls['userrights'] = [ 'text' => $this->msg( $canChange ? 'tool-link-userrights' : 'tool-link-userrights-readonly', - $this->getUser()->getName() + $rootUser )->text(), 'href' => self::makeSpecialUrlSubpage( 'Userrights', $rootUser ) ]; diff --git a/includes/specialpage/ChangesListSpecialPage.php b/includes/specialpage/ChangesListSpecialPage.php index 00efeae1e22e..ad9a248ec60a 100644 --- a/includes/specialpage/ChangesListSpecialPage.php +++ b/includes/specialpage/ChangesListSpecialPage.php @@ -20,6 +20,9 @@ * @file * @ingroup SpecialPage */ +use MediaWiki\Logger\LoggerFactory; +use Wikimedia\Rdbms\ResultWrapper; +use Wikimedia\Rdbms\IDatabase; /** * Special page which uses a ChangesList to show query results. @@ -37,6 +40,388 @@ abstract class ChangesListSpecialPage extends SpecialPage { /** @var array */ protected $customFilters; + // Order of both groups and filters is significant; first is top-most priority, + // descending from there. + // 'showHideSuffix' is a shortcut to and avoid spelling out + // details specific to subclasses here. + /** + * Definition information for the filters and their groups + * + * The value is $groupDefinition, a parameter to the ChangesListFilterGroup constructor. + * However, priority is dynamically added for the core groups, to ease maintenance. + * + * Groups are displayed to the user in the structured UI. However, if necessary, + * all of the filters in a group can be configured to only display on the + * unstuctured UI, in which case you don't need a group title. This is done in + * getFilterGroupDefinitionFromLegacyCustomFilters, for example. + * + * @var array $filterGroupDefinitions + */ + private $filterGroupDefinitions; + + // Same format as filterGroupDefinitions, but for a single group (reviewStatus) + // that is registered conditionally. + private $reviewStatusFilterGroupDefinition; + + // Single filter registered conditionally + private $hideCategorizationFilterDefinition; + + /** + * Filter groups, and their contained filters + * This is an associative array (with group name as key) of ChangesListFilterGroup objects. + * + * @var array $filterGroups + */ + protected $filterGroups = []; + + public function __construct( $name, $restriction ) { + parent::__construct( $name, $restriction ); + + $this->filterGroupDefinitions = [ + [ + 'name' => 'registration', + 'title' => 'rcfilters-filtergroup-registration', + 'class' => ChangesListBooleanFilterGroup::class, + 'filters' => [ + [ + 'name' => 'hideliu', + 'label' => 'rcfilters-filter-registered-label', + 'description' => 'rcfilters-filter-registered-description', + // rcshowhideliu-show, rcshowhideliu-hide, + // wlshowhideliu + 'showHideSuffix' => 'showhideliu', + 'default' => false, + 'queryCallable' => function ( $specialClassName, $ctx, $dbr, &$tables, &$fields, &$conds, + &$query_options, &$join_conds ) { + + $conds[] = 'rc_user = 0'; + }, + 'cssClassSuffix' => 'liu', + 'isRowApplicableCallable' => function ( $ctx, $rc ) { + return $rc->getAttribute( 'rc_user' ); + }, + + ], + [ + 'name' => 'hideanons', + 'label' => 'rcfilters-filter-unregistered-label', + 'description' => 'rcfilters-filter-unregistered-description', + // rcshowhideanons-show, rcshowhideanons-hide, + // wlshowhideanons + 'showHideSuffix' => 'showhideanons', + 'default' => false, + 'queryCallable' => function ( $specialClassName, $ctx, $dbr, &$tables, &$fields, &$conds, + &$query_options, &$join_conds ) { + + $conds[] = 'rc_user != 0'; + }, + 'cssClassSuffix' => 'anon', + 'isRowApplicableCallable' => function ( $ctx, $rc ) { + return !$rc->getAttribute( 'rc_user' ); + }, + ] + ], + ], + + [ + 'name' => 'userExpLevel', + 'title' => 'rcfilters-filtergroup-userExpLevel', + 'class' => ChangesListStringOptionsFilterGroup::class, + // Excludes unregistered users + 'isFullCoverage' => false, + 'filters' => [ + [ + 'name' => 'newcomer', + 'label' => 'rcfilters-filter-user-experience-level-newcomer-label', + 'description' => 'rcfilters-filter-user-experience-level-newcomer-description', + 'cssClassSuffix' => 'user-newcomer', + 'isRowApplicableCallable' => function ( $ctx, $rc ) { + $performer = $rc->getPerformer(); + return $performer && $performer->isLoggedIn() && + $performer->getExperienceLevel() === 'newcomer'; + } + ], + [ + 'name' => 'learner', + 'label' => 'rcfilters-filter-user-experience-level-learner-label', + 'description' => 'rcfilters-filter-user-experience-level-learner-description', + 'cssClassSuffix' => 'user-learner', + 'isRowApplicableCallable' => function ( $ctx, $rc ) { + $performer = $rc->getPerformer(); + return $performer && $performer->isLoggedIn() && + $performer->getExperienceLevel() === 'learner'; + }, + ], + [ + 'name' => 'experienced', + 'label' => 'rcfilters-filter-user-experience-level-experienced-label', + 'description' => 'rcfilters-filter-user-experience-level-experienced-description', + 'cssClassSuffix' => 'user-experienced', + 'isRowApplicableCallable' => function ( $ctx, $rc ) { + $performer = $rc->getPerformer(); + return $performer && $performer->isLoggedIn() && + $performer->getExperienceLevel() === 'experienced'; + }, + ] + ], + 'default' => ChangesListStringOptionsFilterGroup::NONE, + 'queryCallable' => [ $this, 'filterOnUserExperienceLevel' ], + ], + + [ + 'name' => 'authorship', + 'title' => 'rcfilters-filtergroup-authorship', + 'class' => ChangesListBooleanFilterGroup::class, + 'filters' => [ + [ + 'name' => 'hidemyself', + 'label' => 'rcfilters-filter-editsbyself-label', + 'description' => 'rcfilters-filter-editsbyself-description', + // rcshowhidemine-show, rcshowhidemine-hide, + // wlshowhidemine + 'showHideSuffix' => 'showhidemine', + 'default' => false, + 'queryCallable' => function ( $specialClassName, $ctx, $dbr, &$tables, &$fields, &$conds, + &$query_options, &$join_conds ) { + + $user = $ctx->getUser(); + $conds[] = 'rc_user_text != ' . $dbr->addQuotes( $user->getName() ); + }, + 'cssClassSuffix' => 'self', + 'isRowApplicableCallable' => function ( $ctx, $rc ) { + return $ctx->getUser()->equals( $rc->getPerformer() ); + }, + ], + [ + 'name' => 'hidebyothers', + 'label' => 'rcfilters-filter-editsbyother-label', + 'description' => 'rcfilters-filter-editsbyother-description', + 'default' => false, + 'queryCallable' => function ( $specialClassName, $ctx, $dbr, &$tables, &$fields, &$conds, + &$query_options, &$join_conds ) { + + $user = $ctx->getUser(); + $conds[] = 'rc_user_text = ' . $dbr->addQuotes( $user->getName() ); + }, + 'cssClassSuffix' => 'others', + 'isRowApplicableCallable' => function ( $ctx, $rc ) { + return !$ctx->getUser()->equals( $rc->getPerformer() ); + }, + ] + ] + ], + + [ + 'name' => 'automated', + 'title' => 'rcfilters-filtergroup-automated', + 'class' => ChangesListBooleanFilterGroup::class, + 'filters' => [ + [ + 'name' => 'hidebots', + 'label' => 'rcfilters-filter-bots-label', + 'description' => 'rcfilters-filter-bots-description', + // rcshowhidebots-show, rcshowhidebots-hide, + // wlshowhidebots + 'showHideSuffix' => 'showhidebots', + 'default' => false, + 'queryCallable' => function ( $specialClassName, $ctx, $dbr, &$tables, &$fields, &$conds, + &$query_options, &$join_conds ) { + + $conds[] = 'rc_bot = 0'; + }, + 'cssClassSuffix' => 'bot', + 'isRowApplicableCallable' => function ( $ctx, $rc ) { + return $rc->getAttribute( 'rc_bot' ); + }, + ], + [ + 'name' => 'hidehumans', + 'label' => 'rcfilters-filter-humans-label', + 'description' => 'rcfilters-filter-humans-description', + 'default' => false, + 'queryCallable' => function ( $specialClassName, $ctx, $dbr, &$tables, &$fields, &$conds, + &$query_options, &$join_conds ) { + + $conds[] = 'rc_bot = 1'; + }, + 'cssClassSuffix' => 'human', + 'isRowApplicableCallable' => function ( $ctx, $rc ) { + return !$rc->getAttribute( 'rc_bot' ); + }, + ] + ] + ], + + // reviewStatus (conditional) + + [ + 'name' => 'significance', + 'title' => 'rcfilters-filtergroup-significance', + 'class' => ChangesListBooleanFilterGroup::class, + 'priority' => -6, + 'filters' => [ + [ + 'name' => 'hideminor', + 'label' => 'rcfilters-filter-minor-label', + 'description' => 'rcfilters-filter-minor-description', + // rcshowhideminor-show, rcshowhideminor-hide, + // wlshowhideminor + 'showHideSuffix' => 'showhideminor', + 'default' => false, + 'queryCallable' => function ( $specialClassName, $ctx, $dbr, &$tables, &$fields, &$conds, + &$query_options, &$join_conds ) { + + $conds[] = 'rc_minor = 0'; + }, + 'cssClassSuffix' => 'minor', + 'isRowApplicableCallable' => function ( $ctx, $rc ) { + return $rc->getAttribute( 'rc_minor' ); + } + ], + [ + 'name' => 'hidemajor', + 'label' => 'rcfilters-filter-major-label', + 'description' => 'rcfilters-filter-major-description', + 'default' => false, + 'queryCallable' => function ( $specialClassName, $ctx, $dbr, &$tables, &$fields, &$conds, + &$query_options, &$join_conds ) { + + $conds[] = 'rc_minor = 1'; + }, + 'cssClassSuffix' => 'major', + 'isRowApplicableCallable' => function ( $ctx, $rc ) { + return !$rc->getAttribute( 'rc_minor' ); + } + ] + ] + ], + + // With extensions, there can be change types that will not be hidden by any of these. + [ + 'name' => 'changeType', + 'title' => 'rcfilters-filtergroup-changetype', + 'class' => ChangesListBooleanFilterGroup::class, + 'filters' => [ + [ + 'name' => 'hidepageedits', + 'label' => 'rcfilters-filter-pageedits-label', + 'description' => 'rcfilters-filter-pageedits-description', + 'default' => false, + 'priority' => -2, + 'queryCallable' => function ( $specialClassName, $ctx, $dbr, &$tables, &$fields, &$conds, + &$query_options, &$join_conds ) { + + $conds[] = 'rc_type != ' . $dbr->addQuotes( RC_EDIT ); + }, + 'cssClassSuffix' => 'src-mw-edit', + 'isRowApplicableCallable' => function ( $ctx, $rc ) { + return $rc->getAttribute( 'rc_source' ) === RecentChange::SRC_EDIT; + }, + ], + [ + 'name' => 'hidenewpages', + 'label' => 'rcfilters-filter-newpages-label', + 'description' => 'rcfilters-filter-newpages-description', + 'default' => false, + 'priority' => -3, + 'queryCallable' => function ( $specialClassName, $ctx, $dbr, &$tables, &$fields, &$conds, + &$query_options, &$join_conds ) { + + $conds[] = 'rc_type != ' . $dbr->addQuotes( RC_NEW ); + }, + 'cssClassSuffix' => 'src-mw-new', + 'isRowApplicableCallable' => function ( $ctx, $rc ) { + return $rc->getAttribute( 'rc_source' ) === RecentChange::SRC_NEW; + }, + ], + + // hidecategorization + + [ + 'name' => 'hidelog', + 'label' => 'rcfilters-filter-logactions-label', + 'description' => 'rcfilters-filter-logactions-description', + 'default' => false, + 'priority' => -5, + 'queryCallable' => function ( $specialClassName, $ctx, $dbr, &$tables, &$fields, &$conds, + &$query_options, &$join_conds ) { + + $conds[] = 'rc_type != ' . $dbr->addQuotes( RC_LOG ); + }, + 'cssClassSuffix' => 'src-mw-log', + 'isRowApplicableCallable' => function ( $ctx, $rc ) { + return $rc->getAttribute( 'rc_source' ) === RecentChange::SRC_LOG; + } + ], + ], + ], + ]; + + $this->reviewStatusFilterGroupDefinition = [ + [ + 'name' => 'reviewStatus', + 'title' => 'rcfilters-filtergroup-reviewstatus', + 'class' => ChangesListBooleanFilterGroup::class, + 'priority' => -5, + 'filters' => [ + [ + 'name' => 'hidepatrolled', + 'label' => 'rcfilters-filter-patrolled-label', + 'description' => 'rcfilters-filter-patrolled-description', + // rcshowhidepatr-show, rcshowhidepatr-hide + // wlshowhidepatr + 'showHideSuffix' => 'showhidepatr', + 'default' => false, + 'queryCallable' => function ( $specialClassName, $ctx, $dbr, &$tables, &$fields, &$conds, + &$query_options, &$join_conds ) { + + $conds[] = 'rc_patrolled = 0'; + }, + 'cssClassSuffix' => 'patrolled', + 'isRowApplicableCallable' => function ( $ctx, $rc ) { + return $rc->getAttribute( 'rc_patrolled' ); + }, + ], + [ + 'name' => 'hideunpatrolled', + 'label' => 'rcfilters-filter-unpatrolled-label', + 'description' => 'rcfilters-filter-unpatrolled-description', + 'default' => false, + 'queryCallable' => function ( $specialClassName, $ctx, $dbr, &$tables, &$fields, &$conds, + &$query_options, &$join_conds ) { + + $conds[] = 'rc_patrolled = 1'; + }, + 'cssClassSuffix' => 'unpatrolled', + 'isRowApplicableCallable' => function ( $ctx, $rc ) { + return !$rc->getAttribute( 'rc_patrolled' ); + }, + ], + ], + ] + ]; + + $this->hideCategorizationFilterDefinition = [ + 'name' => 'hidecategorization', + 'label' => 'rcfilters-filter-categorization-label', + 'description' => 'rcfilters-filter-categorization-description', + // rcshowhidecategorization-show, rcshowhidecategorization-hide. + // wlshowhidecategorization + 'showHideSuffix' => 'showhidecategorization', + 'default' => false, + 'priority' => -4, + 'queryCallable' => function ( $specialClassName, $ctx, $dbr, &$tables, &$fields, &$conds, + &$query_options, &$join_conds ) { + + $conds[] = 'rc_type != ' . $dbr->addQuotes( RC_CATEGORIZE ); + }, + 'cssClassSuffix' => 'src-mw-categorize', + 'isRowApplicableCallable' => function ( $ctx, $rc ) { + return $rc->getAttribute( 'rc_source' ) === RecentChange::SRC_CATEGORIZE; + }, + ]; + } + /** * Main execution point * @@ -54,6 +439,7 @@ abstract class ChangesListSpecialPage extends SpecialPage { if ( $rows === false ) { if ( !$this->including() ) { $this->doHeader( $opts, 0 ); + $this->outputNoResults(); $this->getOutput()->setStatusCode( 404 ); } @@ -73,10 +459,28 @@ abstract class ChangesListSpecialPage extends SpecialPage { } } $batch->execute(); - $this->webOutput( $rows, $opts ); $rows->free(); + + if ( $this->getConfig()->get( 'EnableWANCacheReaper' ) ) { + // Clean up any bad page entries for titles showing up in RC + DeferredUpdates::addUpdate( new WANCacheReapUpdate( + $this->getDB(), + LoggerFactory::getInstance( 'objectcache' ) + ) ); + } + } + + /** + * Add the "no results" message to the output + */ + protected function outputNoResults() { + $this->getOutput()->addHTML( + '<div class="mw-changeslist-empty">' . + $this->msg( 'recentchanges-noresult' )->parse() . + '</div>' + ); } /** @@ -86,9 +490,15 @@ abstract class ChangesListSpecialPage extends SpecialPage { */ public function getRows() { $opts = $this->getOptions(); - $conds = $this->buildMainQueryConds( $opts ); - return $this->doMainQuery( $conds, $opts ); + $tables = []; + $fields = []; + $conds = []; + $query_options = []; + $join_conds = []; + $this->buildQuery( $tables, $fields, $conds, $query_options, $join_conds, $opts ); + + return $this->doMainQuery( $tables, $fields, $conds, $query_options, $join_conds, $opts ); } /** @@ -105,17 +515,173 @@ abstract class ChangesListSpecialPage extends SpecialPage { } /** - * Create a FormOptions object with options as specified by the user + * Register all filters and their groups (including those from hooks), plus handle + * conflicts and defaults. + * + * You might want to customize these in the same method, in subclasses. You can + * call getFilterGroup to access a group, and (on the group) getFilter to access a + * filter, then make necessary modfications to the filter or group (e.g. with + * setDefault). + */ + protected function registerFilters() { + $this->registerFiltersFromDefinitions( $this->filterGroupDefinitions ); + + // Make sure this is not being transcluded (we don't want to show this + // information to all users just because the user that saves the edit can + // patrol) + if ( !$this->including() && $this->getUser()->useRCPatrol() ) { + $this->registerFiltersFromDefinitions( $this->reviewStatusFilterGroupDefinition ); + } + + $changeTypeGroup = $this->getFilterGroup( 'changeType' ); + + if ( $this->getConfig()->get( 'RCWatchCategoryMembership' ) ) { + $transformedHideCategorizationDef = $this->transformFilterDefinition( + $this->hideCategorizationFilterDefinition + ); + + $transformedHideCategorizationDef['group'] = $changeTypeGroup; + + $hideCategorization = new ChangesListBooleanFilter( + $transformedHideCategorizationDef + ); + } + + Hooks::run( 'ChangesListSpecialPageStructuredFilters', [ $this ] ); + + $unstructuredGroupDefinition = + $this->getFilterGroupDefinitionFromLegacyCustomFilters( + $this->getCustomFilters() + ); + $this->registerFiltersFromDefinitions( [ $unstructuredGroupDefinition ] ); + + $userExperienceLevel = $this->getFilterGroup( 'userExpLevel' ); + + $registration = $this->getFilterGroup( 'registration' ); + $anons = $registration->getFilter( 'hideanons' ); + + // This means there is a conflict between any item in user experience level + // being checked and only anons being *shown* (hideliu=1&hideanons=0 in the + // URL, or equivalent). + $userExperienceLevel->conflictsWith( + $anons, + 'rcfilters-filtergroup-user-experience-level-conflicts-unregistered-global', + 'rcfilters-filtergroup-user-experience-level-conflicts-unregistered', + 'rcfilters-filter-unregistered-conflicts-user-experience-level' + ); + + $categoryFilter = $changeTypeGroup->getFilter( 'hidecategorization' ); + $logactionsFilter = $changeTypeGroup->getFilter( 'hidelog' ); + $pagecreationFilter = $changeTypeGroup->getFilter( 'hidenewpages' ); + + $significanceTypeGroup = $this->getFilterGroup( 'significance' ); + $hideMinorFilter = $significanceTypeGroup->getFilter( 'hideminor' ); + + // categoryFilter is conditional; see registerFilters + if ( $categoryFilter !== null ) { + $hideMinorFilter->conflictsWith( + $categoryFilter, + 'rcfilters-hideminor-conflicts-typeofchange-global', + 'rcfilters-hideminor-conflicts-typeofchange', + 'rcfilters-typeofchange-conflicts-hideminor' + ); + } + $hideMinorFilter->conflictsWith( + $logactionsFilter, + 'rcfilters-hideminor-conflicts-typeofchange-global', + 'rcfilters-hideminor-conflicts-typeofchange', + 'rcfilters-typeofchange-conflicts-hideminor' + ); + $hideMinorFilter->conflictsWith( + $pagecreationFilter, + 'rcfilters-hideminor-conflicts-typeofchange-global', + 'rcfilters-hideminor-conflicts-typeofchange', + 'rcfilters-typeofchange-conflicts-hideminor' + ); + } + + /** + * Transforms filter definition to prepare it for constructor. + * + * See overrides of this method as well. + * + * @param array $filterDefinition Original filter definition + * + * @return array Transformed definition + */ + protected function transformFilterDefinition( array $filterDefinition ) { + return $filterDefinition; + } + + /** + * Register filters from a definition object + * + * Array specifying groups and their filters; see Filter and + * ChangesListFilterGroup constructors. + * + * There is light processing to simplify core maintenance. + */ + protected function registerFiltersFromDefinitions( array $definition ) { + $autoFillPriority = -1; + foreach ( $definition as $groupDefinition ) { + if ( !isset( $groupDefinition['priority'] ) ) { + $groupDefinition['priority'] = $autoFillPriority; + } else { + // If it's explicitly specified, start over the auto-fill + $autoFillPriority = $groupDefinition['priority']; + } + + $autoFillPriority--; + + $className = $groupDefinition['class']; + unset( $groupDefinition['class'] ); + + foreach ( $groupDefinition['filters'] as &$filterDefinition ) { + $filterDefinition = $this->transformFilterDefinition( $filterDefinition ); + } + + $this->registerFilterGroup( new $className( $groupDefinition ) ); + } + } + + /** + * Get filter group definition from legacy custom filters + * + * @param array Custom filters from legacy hooks + * @return array Group definition + */ + protected function getFilterGroupDefinitionFromLegacyCustomFilters( $customFilters ) { + // Special internal unstructured group + $unstructuredGroupDefinition = [ + 'name' => 'unstructured', + 'class' => ChangesListBooleanFilterGroup::class, + 'priority' => -1, // Won't display in structured + 'filters' => [], + ]; + + foreach ( $customFilters as $name => $params ) { + $unstructuredGroupDefinition['filters'][] = [ + 'name' => $name, + 'showHide' => $params['msg'], + 'default' => $params['default'], + ]; + } + + return $unstructuredGroupDefinition; + } + + /** + * Register all the filters, including legacy hook-driven ones. + * Then create a FormOptions object with options as specified by the user * * @param array $parameters * * @return FormOptions */ public function setup( $parameters ) { + $this->registerFilters(); + $opts = $this->getDefaultOptions(); - foreach ( $this->getCustomFilters() as $key => $params ) { - $opts->add( $key, $params['default'] ); - } $opts = $this->fetchOptionsFromRequest( $opts ); @@ -130,8 +696,11 @@ abstract class ChangesListSpecialPage extends SpecialPage { } /** - * Get a FormOptions object containing the default options. By default returns some basic options, - * you might want to not call parent method and discard them, or to override default values. + * Get a FormOptions object containing the default options. By default, returns + * some basic options. The filters listed explicitly here are overriden in this + * method, in subclasses, but most filters (e.g. hideminor, userExpLevel filters, + * and more) are structured. Structured filters are overriden in registerFilters. + * not here. * * @return FormOptions */ @@ -139,23 +708,18 @@ abstract class ChangesListSpecialPage extends SpecialPage { $config = $this->getConfig(); $opts = new FormOptions(); - $opts->add( 'hideminor', false ); - $opts->add( 'hidemajor', false ); - $opts->add( 'hidebots', false ); - $opts->add( 'hidehumans', false ); - $opts->add( 'hideanons', false ); - $opts->add( 'hideliu', false ); - $opts->add( 'hidepatrolled', false ); - $opts->add( 'hideunpatrolled', false ); - $opts->add( 'hidemyself', false ); - $opts->add( 'hidebyothers', false ); - - if ( $config->get( 'RCWatchCategoryMembership' ) ) { - $opts->add( 'hidecategorization', false ); + // Add all filters + foreach ( $this->filterGroups as $filterGroup ) { + // URL parameters can be per-group, like 'userExpLevel', + // or per-filter, like 'hideminor'. + if ( $filterGroup->isPerGroupRequestParameter() ) { + $opts->add( $filterGroup->getName(), $filterGroup->getDefault() ); + } else { + foreach ( $filterGroup->getFilters() as $filter ) { + $opts->add( $filter->getName(), $filter->getDefault() ); + } + } } - $opts->add( 'hidepageedits', false ); - $opts->add( 'hidenewpages', false ); - $opts->add( 'hidelog', false ); $opts->add( 'namespace', '', FormOptions::INTNULL ); $opts->add( 'invert', false ); @@ -165,14 +729,88 @@ abstract class ChangesListSpecialPage extends SpecialPage { } /** - * Get custom show/hide filters + * Register a structured changes list filter group + * + * @param ChangesListFilterGroup $group + */ + public function registerFilterGroup( ChangesListFilterGroup $group ) { + $groupName = $group->getName(); + + $this->filterGroups[$groupName] = $group; + } + + /** + * Gets the currently registered filters groups + * + * @return array Associative array of ChangesListFilterGroup objects, with group name as key + */ + protected function getFilterGroups() { + return $this->filterGroups; + } + + /** + * Gets a specified ChangesListFilterGroup by name + * + * @param string $groupName Name of group + * + * @return ChangesListFilterGroup|null Group, or null if not registered + */ + public function getFilterGroup( $groupName ) { + return isset( $this->filterGroups[$groupName] ) ? + $this->filterGroups[$groupName] : + null; + } + + // Currently, this intentionally only includes filters that display + // in the structured UI. This can be changed easily, though, if we want + // to include data on filters that use the unstructured UI. messageKeys is a + // special top-level value, with the value being an array of the message keys to + // send to the client. + /** + * Gets structured filter information needed by JS + * + * @return array Associative array + * * array $return['groups'] Group data + * * array $return['messageKeys'] Array of message keys + */ + public function getStructuredFilterJsData() { + $output = [ + 'groups' => [], + 'messageKeys' => [], + ]; + + $context = $this->getContext(); + + usort( $this->filterGroups, function ( $a, $b ) { + return $b->getPriority() - $a->getPriority(); + } ); + + foreach ( $this->filterGroups as $groupName => $group ) { + $groupOutput = $group->getJsData( $this ); + if ( $groupOutput !== null ) { + $output['messageKeys'] = array_merge( + $output['messageKeys'], + $groupOutput['messageKeys'] + ); + + unset( $groupOutput['messageKeys'] ); + $output['groups'][] = $groupOutput; + } + } + + return $output; + } + + /** + * Get custom show/hide filters using deprecated ChangesListSpecialPageFilters + * hook. * * @return array Map of filter URL param names to properties (msg/default) */ protected function getCustomFilters() { if ( $this->customFilters === null ) { $this->customFilters = []; - Hooks::run( 'ChangesListSpecialPageFilters', [ $this, &$this->customFilters ] ); + Hooks::run( 'ChangesListSpecialPageFilters', [ $this, &$this->customFilters ], '1.29' ); } return $this->customFilters; @@ -199,7 +837,37 @@ abstract class ChangesListSpecialPage extends SpecialPage { * @param FormOptions $opts */ public function parseParameters( $par, FormOptions $opts ) { - // nothing by default + $stringParameterNameSet = []; + $hideParameterNameSet = []; + + // URL parameters can be per-group, like 'userExpLevel', + // or per-filter, like 'hideminor'. + + foreach ( $this->filterGroups as $filterGroup ) { + if ( $filterGroup->isPerGroupRequestParameter() ) { + $stringParameterNameSet[$filterGroup->getName()] = true; + } elseif ( $filterGroup->getType() === ChangesListBooleanFilterGroup::TYPE ) { + foreach ( $filterGroup->getFilters() as $filter ) { + $hideParameterNameSet[$filter->getName()] = true; + } + } + } + + $bits = preg_split( '/\s*,\s*/', trim( $par ) ); + foreach ( $bits as $bit ) { + $m = []; + if ( isset( $hideParameterNameSet[$bit] ) ) { + // hidefoo => hidefoo=true + $opts[$bit] = true; + } elseif ( isset( $hideParameterNameSet["hide$bit"] ) ) { + // foo => hidefoo=false + $opts["hide$bit"] = false; + } elseif ( preg_match( '/^(.*)=(.*)$/', $bit, $m ) ) { + if ( isset( $stringParameterNameSet[$m[1]] ) ) { + $opts[$m[1]] = $m[2]; + } + } + } } /** @@ -212,97 +880,46 @@ abstract class ChangesListSpecialPage extends SpecialPage { } /** - * Return an array of conditions depending of options set in $opts + * Sets appropriate tables, fields, conditions, etc. depending on which filters + * the user requested. * + * @param array &$tables Array of tables; see IDatabase::select $table + * @param array &$fields Array of fields; see IDatabase::select $vars + * @param array &$conds Array of conditions; see IDatabase::select $conds + * @param array &$query_options Array of query options; see IDatabase::select $options + * @param array &$join_conds Array of join conditions; see IDatabase::select $join_conds * @param FormOptions $opts - * @return array */ - public function buildMainQueryConds( FormOptions $opts ) { + protected function buildQuery( &$tables, &$fields, &$conds, &$query_options, + &$join_conds, FormOptions $opts ) { + $dbr = $this->getDB(); $user = $this->getUser(); - $conds = []; - // It makes no sense to hide both anons and logged-in users. When this occurs, try a guess on - // what the user meant and either show only bots or force anons to be shown. - $botsonly = false; - $hideanons = $opts['hideanons']; - if ( $opts['hideanons'] && $opts['hideliu'] ) { - if ( $opts['hidebots'] ) { - $hideanons = false; - } else { - $botsonly = true; - } - } - - // Toggles - if ( $opts['hideminor'] ) { - $conds[] = 'rc_minor = 0'; - } - if ( $opts['hidemajor'] ) { - $conds[] = 'rc_minor = 1'; - } - if ( $opts['hidebots'] ) { - $conds['rc_bot'] = 0; - } - if ( $opts['hidehumans'] ) { - $conds[] = 'rc_bot = 1'; - } - if ( $user->useRCPatrol() ) { - if ( $opts['hidepatrolled'] ) { - $conds[] = 'rc_patrolled = 0'; - } - if ( $opts['hideunpatrolled'] ) { - $conds[] = 'rc_patrolled = 1'; - } - } - if ( $botsonly ) { - $conds['rc_bot'] = 1; - } else { - if ( $opts['hideliu'] ) { - $conds[] = 'rc_user = 0'; - } - if ( $hideanons ) { - $conds[] = 'rc_user != 0'; - } - } - - if ( $opts['hidemyself'] ) { - if ( $user->getId() ) { - $conds[] = 'rc_user != ' . $dbr->addQuotes( $user->getId() ); - } else { - $conds[] = 'rc_user_text != ' . $dbr->addQuotes( $user->getName() ); - } - } - if ( $opts['hidebyothers'] ) { - if ( $user->getId() ) { - $conds[] = 'rc_user = ' . $dbr->addQuotes( $user->getId() ); + $context = $this->getContext(); + foreach ( $this->filterGroups as $filterGroup ) { + // URL parameters can be per-group, like 'userExpLevel', + // or per-filter, like 'hideminor'. + if ( $filterGroup->isPerGroupRequestParameter() ) { + $filterGroup->modifyQuery( $dbr, $this, $tables, $fields, $conds, + $query_options, $join_conds, $opts[$filterGroup->getName()] ); } else { - $conds[] = 'rc_user_text = ' . $dbr->addQuotes( $user->getName() ); + foreach ( $filterGroup->getFilters() as $filter ) { + if ( $opts[$filter->getName()] ) { + $filter->modifyQuery( $dbr, $this, $tables, $fields, $conds, + $query_options, $join_conds ); + } + } } } - if ( $this->getConfig()->get( 'RCWatchCategoryMembership' ) - && $opts['hidecategorization'] === true - ) { - $conds[] = 'rc_type != ' . $dbr->addQuotes( RC_CATEGORIZE ); - } - if ( $opts['hidepageedits'] ) { - $conds[] = 'rc_type != ' . $dbr->addQuotes( RC_EDIT ); - } - if ( $opts['hidenewpages'] ) { - $conds[] = 'rc_type != ' . $dbr->addQuotes( RC_NEW ); - } - if ( $opts['hidelog'] ) { - $conds[] = 'rc_type != ' . $dbr->addQuotes( RC_LOG ); - } - // Namespace filtering if ( $opts['namespace'] !== '' ) { $selectedNS = $dbr->addQuotes( $opts['namespace'] ); $operator = $opts['invert'] ? '!=' : '='; $boolean = $opts['invert'] ? 'AND' : 'OR'; - // Namespace association (bug 2429) + // Namespace association (T4429) if ( !$opts['associated'] ) { $condition = "rc_namespace $operator $selectedNS"; } else { @@ -317,22 +934,24 @@ abstract class ChangesListSpecialPage extends SpecialPage { $conds[] = $condition; } - - return $conds; } /** * Process the query * - * @param array $conds + * @param array $tables Array of tables; see IDatabase::select $table + * @param array $fields Array of fields; see IDatabase::select $vars + * @param array $conds Array of conditions; see IDatabase::select $conds + * @param array $query_options Array of query options; see IDatabase::select $options + * @param array $join_conds Array of join conditions; see IDatabase::select $join_conds * @param FormOptions $opts * @return bool|ResultWrapper Result or false */ - public function doMainQuery( $conds, $opts ) { - $tables = [ 'recentchanges' ]; - $fields = RecentChange::selectFields(); - $query_options = []; - $join_conds = []; + protected function doMainQuery( $tables, $fields, $conds, + $query_options, $join_conds, FormOptions $opts ) { + + $tables[] = 'recentchanges'; + $fields = array_merge( RecentChange::selectFields(), $fields ); ChangeTags::modifyDisplayQuery( $tables, @@ -343,6 +962,15 @@ abstract class ChangesListSpecialPage extends SpecialPage { '' ); + // It makes no sense to hide both anons and logged-in users. When this occurs, try a guess on + // what the user meant and either show only bots or force anons to be shown. + + // ------- + + // XXX: We're no longer doing this handling. To preserve back-compat, we need to complete + // T151873 (particularly the hideanons/hideliu/hidebots/hidehumans part) in conjunction + // with merging this. + if ( !$this->runMainQueryHook( $tables, $fields, $conds, $query_options, $join_conds, $opts ) ) { @@ -446,7 +1074,8 @@ abstract class ChangesListSpecialPage extends SpecialPage { /** * Get options to be displayed in a form * @todo This should handle options returned by getDefaultOptions(). - * @todo Not called by anything, should be called by something… doHeader() maybe? + * @todo Not called by anything in this class (but is in subclasses), should be + * called by something… doHeader() maybe? * * @param FormOptions $opts * @return array @@ -523,21 +1152,78 @@ abstract class ChangesListSpecialPage extends SpecialPage { } /** - * Get filters that can be rendered. + * Filter on users' experience levels; this will not be called if nothing is + * selected. * - * Filters with 'msg' => false can be used to filter data but won't - * be presented as show/hide toggles in the UI. They are not returned - * by this function. - * - * @param array $allFilters Map of filter URL param names to properties (msg/default) - * @return array Map of filter URL param names to properties (msg/default) + * @param string $specialPageClassName Class name of current special page + * @param IContextSource $context Context, for e.g. user + * @param IDatabase $dbr Database, for addQuotes, makeList, and similar + * @param array &$tables Array of tables; see IDatabase::select $table + * @param array &$fields Array of fields; see IDatabase::select $vars + * @param array &$conds Array of conditions; see IDatabase::select $conds + * @param array &$query_options Array of query options; see IDatabase::select $options + * @param array &$join_conds Array of join conditions; see IDatabase::select $join_conds + * @param array $selectedExpLevels The allowed active values, sorted */ - protected function getRenderableCustomFilters( $allFilters ) { - return array_filter( - $allFilters, - function( $filter ) { - return isset( $filter['msg'] ) && ( $filter['msg'] !== false ); - } + public function filterOnUserExperienceLevel( $specialPageClassName, $context, $dbr, + &$tables, &$fields, &$conds, &$query_options, &$join_conds, $selectedExpLevels ) { + + global $wgLearnerEdits, + $wgExperiencedUserEdits, + $wgLearnerMemberSince, + $wgExperiencedUserMemberSince; + + $LEVEL_COUNT = 3; + + // If all levels are selected, all logged-in users are included (but no + // anons), so we can short-circuit. + if ( count( $selectedExpLevels ) === $LEVEL_COUNT ) { + $conds[] = 'rc_user != 0'; + return; + } + + $tables[] = 'user'; + $join_conds['user'] = [ 'LEFT JOIN', 'rc_user = user_id' ]; + + $now = time(); + $secondsPerDay = 86400; + $learnerCutoff = $now - $wgLearnerMemberSince * $secondsPerDay; + $experiencedUserCutoff = $now - $wgExperiencedUserMemberSince * $secondsPerDay; + + $aboveNewcomer = $dbr->makeList( + [ + 'user_editcount >= ' . intval( $wgLearnerEdits ), + 'user_registration <= ' . $dbr->timestamp( $learnerCutoff ), + ], + IDatabase::LIST_AND + ); + + $aboveLearner = $dbr->makeList( + [ + 'user_editcount >= ' . intval( $wgExperiencedUserEdits ), + 'user_registration <= ' . $dbr->timestamp( $experiencedUserCutoff ), + ], + IDatabase::LIST_AND ); + + if ( $selectedExpLevels === [ 'newcomer' ] ) { + $conds[] = "NOT ( $aboveNewcomer )"; + } elseif ( $selectedExpLevels === [ 'learner' ] ) { + $conds[] = $dbr->makeList( + [ $aboveNewcomer, "NOT ( $aboveLearner )" ], + IDatabase::LIST_AND + ); + } elseif ( $selectedExpLevels === [ 'experienced' ] ) { + $conds[] = $aboveLearner; + } elseif ( $selectedExpLevels === [ 'learner', 'newcomer' ] ) { + $conds[] = "NOT ( $aboveLearner )"; + } elseif ( $selectedExpLevels === [ 'experienced', 'newcomer' ] ) { + $conds[] = $dbr->makeList( + [ "NOT ( $aboveNewcomer )", $aboveLearner ], + IDatabase::LIST_OR + ); + } elseif ( $selectedExpLevels === [ 'experienced', 'learner' ] ) { + $conds[] = $aboveNewcomer; + } } } diff --git a/includes/specialpage/ImageQueryPage.php b/includes/specialpage/ImageQueryPage.php index c4e53eef0506..59abefd83e9f 100644 --- a/includes/specialpage/ImageQueryPage.php +++ b/includes/specialpage/ImageQueryPage.php @@ -21,6 +21,9 @@ * @ingroup SpecialPage */ +use Wikimedia\Rdbms\ResultWrapper; +use Wikimedia\Rdbms\IDatabase; + /** * Variant of QueryPage which uses a gallery to output results, thus * suited for reports generating images diff --git a/includes/specialpage/LoginSignupSpecialPage.php b/includes/specialpage/LoginSignupSpecialPage.php index c3ee32120b28..5c048a20785c 100644 --- a/includes/specialpage/LoginSignupSpecialPage.php +++ b/includes/specialpage/LoginSignupSpecialPage.php @@ -177,7 +177,7 @@ abstract class LoginSignupSpecialPage extends AuthManagerSpecialPage { # 1. When switching accounts, it sucks to get automatically logged out # 2. Do not return to PasswordReset after a successful password change - # but goto Wiki start page (Main_Page) instead ( bug 33997 ) + # but goto Wiki start page (Main_Page) instead ( T35997 ) $returnToTitle = Title::newFromText( $this->mReturnTo ); if ( is_object( $returnToTitle ) && ( $returnToTitle->isSpecial( 'Userlogout' ) @@ -702,7 +702,7 @@ abstract class LoginSignupSpecialPage extends AuthManagerSpecialPage { */ protected function getFakeTemplate( $msg, $msgType ) { global $wgAuth, $wgEnableEmail, $wgHiddenPrefs, $wgEmailConfirmToEdit, $wgEnableUserEmail, - $wgSecureLogin, $wgPasswordResetRoutes; + $wgSecureLogin, $wgPasswordResetRoutes; // make a best effort to get the value of fields which used to be fixed in the old login // template but now might or might not exist depending on what providers are used @@ -727,7 +727,7 @@ abstract class LoginSignupSpecialPage extends AuthManagerSpecialPage { $user = $this->getUser(); $template = new FakeAuthTemplate(); - // Pre-fill username (if not creating an account, bug 44775). + // Pre-fill username (if not creating an account, T46775). if ( $data->mUsername == '' && $this->isSignup() ) { if ( $user->isLoggedIn() ) { $data->mUsername = $user->getName(); @@ -772,7 +772,7 @@ abstract class LoginSignupSpecialPage extends AuthManagerSpecialPage { $resetLink = $this->isSignup() ? null : is_array( $wgPasswordResetRoutes ) - && in_array( true, array_values( $wgPasswordResetRoutes ), true ); + && in_array( true, array_values( $wgPasswordResetRoutes ), true ); $template->set( 'header', '' ); $template->set( 'formheader', '' ); diff --git a/includes/specialpage/PageQueryPage.php b/includes/specialpage/PageQueryPage.php index 3bb3f8515f1a..76b1535fdead 100644 --- a/includes/specialpage/PageQueryPage.php +++ b/includes/specialpage/PageQueryPage.php @@ -21,6 +21,9 @@ * @ingroup SpecialPage */ +use Wikimedia\Rdbms\ResultWrapper; +use Wikimedia\Rdbms\IDatabase; + /** * Variant of QueryPage which formats the result as a simple link to the page * diff --git a/includes/specialpage/QueryPage.php b/includes/specialpage/QueryPage.php index 359250005574..68d2d3073eac 100644 --- a/includes/specialpage/QueryPage.php +++ b/includes/specialpage/QueryPage.php @@ -21,6 +21,9 @@ * @ingroup SpecialPage */ +use Wikimedia\Rdbms\ResultWrapper; +use Wikimedia\Rdbms\IDatabase; + /** * This is a class for doing query pages; since they're almost all the same, * we factor out some of the functionality into a superclass, and let @@ -302,7 +305,7 @@ abstract class QueryPage extends SpecialPage { return 0; } - $fname = get_class( $this ) . '::recache'; + $fname = static::class . '::recache'; $dbw = wfGetDB( DB_MASTER ); if ( !$dbw ) { return false; @@ -322,7 +325,7 @@ abstract class QueryPage extends SpecialPage { $value = wfTimestamp( TS_UNIX, $row->value ); } else { - $value = intval( $row->value ); // @bug 14414 + $value = intval( $row->value ); // T16414 } } else { $value = 0; @@ -387,7 +390,7 @@ abstract class QueryPage extends SpecialPage { * @since 1.18 */ public function reallyDoQuery( $limit, $offset = false ) { - $fname = get_class( $this ) . "::reallyDoQuery"; + $fname = static::class . '::reallyDoQuery'; $dbr = $this->getRecacheDB(); $query = $this->getQueryInfo(); $order = $this->getOrderFields(); @@ -405,7 +408,7 @@ abstract class QueryPage extends SpecialPage { $options = isset( $query['options'] ) ? (array)$query['options'] : []; $join_conds = isset( $query['join_conds'] ) ? (array)$query['join_conds'] : []; - if ( count( $order ) ) { + if ( $order ) { $options['ORDER BY'] = $order; } @@ -455,30 +458,50 @@ abstract class QueryPage extends SpecialPage { public function fetchFromCache( $limit, $offset = false ) { $dbr = wfGetDB( DB_REPLICA ); $options = []; + if ( $limit !== false ) { $options['LIMIT'] = intval( $limit ); } + if ( $offset !== false ) { $options['OFFSET'] = intval( $offset ); } + + $order = $this->getCacheOrderFields(); if ( $this->sortDescending() ) { - $options['ORDER BY'] = 'qc_value DESC'; - } else { - $options['ORDER BY'] = 'qc_value ASC'; + foreach ( $order as &$field ) { + $field .= " DESC"; + } + } + if ( $order ) { + $options['ORDER BY'] = $order; } - return $dbr->select( 'querycache', [ 'qc_type', + + return $dbr->select( 'querycache', + [ 'qc_type', 'namespace' => 'qc_namespace', 'title' => 'qc_title', 'value' => 'qc_value' ], [ 'qc_type' => $this->getName() ], - __METHOD__, $options + __METHOD__, + $options ); } + /** + * Return the order fields for fetchFromCache. Default is to always use + * "ORDER BY value" which was the default prior to this function. + * @return array + * @since 1.29 + */ + function getCacheOrderFields() { + return [ 'value' ]; + } + public function getCachedTimestamp() { if ( is_null( $this->cachedTimestamp ) ) { $dbr = wfGetDB( DB_REPLICA ); - $fname = get_class( $this ) . '::getCachedTimestamp'; + $fname = static::class . '::getCachedTimestamp'; $this->cachedTimestamp = $dbr->selectField( 'querycache_info', 'qci_timestamp', [ 'qci_type' => $this->getName() ], $fname ); } diff --git a/includes/specialpage/RedirectSpecialPage.php b/includes/specialpage/RedirectSpecialPage.php index ea7d78314802..9b5d5f463d49 100644 --- a/includes/specialpage/RedirectSpecialPage.php +++ b/includes/specialpage/RedirectSpecialPage.php @@ -41,7 +41,7 @@ abstract class RedirectSpecialPage extends UnlistedSpecialPage { $query = $this->getRedirectQuery(); // Redirect to a page title with possible query parameters if ( $redirect instanceof Title ) { - $url = $redirect->getFullURL( $query ); + $url = $redirect->getFullUrlForRedirect( $query ); $this->getOutput()->redirect( $url ); return $redirect; @@ -52,7 +52,7 @@ abstract class RedirectSpecialPage extends UnlistedSpecialPage { return $redirect; } else { - $class = get_class( $this ); + $class = static::class; throw new MWException( "RedirectSpecialPage $class doesn't redirect!" ); } } diff --git a/includes/specialpage/SpecialPageFactory.php b/includes/specialpage/SpecialPageFactory.php index daabdedf2b5e..84d3b08095d3 100644 --- a/includes/specialpage/SpecialPageFactory.php +++ b/includes/specialpage/SpecialPageFactory.php @@ -96,6 +96,7 @@ class SpecialPageFactory { 'Block' => 'SpecialBlock', 'Unblock' => 'SpecialUnblock', 'BlockList' => 'SpecialBlockList', + 'AutoblockList' => 'SpecialAutoblockList', 'ChangePassword' => 'SpecialChangePassword', 'BotPasswords' => 'SpecialBotPasswords', 'PasswordReset' => 'SpecialPasswordReset', @@ -144,6 +145,7 @@ class SpecialPageFactory { 'RandomInCategory' => 'SpecialRandomInCategory', 'Randomredirect' => 'SpecialRandomredirect', 'Randomrootpage' => 'SpecialRandomrootpage', + 'GoToInterwiki' => 'SpecialGoToInterwiki', // High use pages 'Mostlinkedcategories' => 'MostlinkedCategoriesPage', @@ -346,7 +348,7 @@ class SpecialPageFactory { return [ null, null ]; } - if ( !isset( $bits[1] ) ) { // bug 2087 + if ( !isset( $bits[1] ) ) { // T4087 $par = null; } else { $par = $bits[1]; @@ -504,7 +506,7 @@ class SpecialPageFactory { * @param bool $including Bool output is being captured for use in {{special:whatever}} * @param LinkRenderer|null $linkRenderer (since 1.28) * - * @return bool + * @return bool|Title */ public static function executePath( Title &$title, IContextSource &$context, $including = false, LinkRenderer $linkRenderer = null @@ -512,7 +514,7 @@ class SpecialPageFactory { // @todo FIXME: Redirects broken due to this call $bits = explode( '/', $title->getDBkey(), 2 ); $name = $bits[0]; - if ( !isset( $bits[1] ) ) { // bug 2087 + if ( !isset( $bits[1] ) ) { // T4087 $par = null; } else { $par = $bits[1]; diff --git a/includes/specialpage/WantedQueryPage.php b/includes/specialpage/WantedQueryPage.php index 00fca12c7829..d788f2bbcdbb 100644 --- a/includes/specialpage/WantedQueryPage.php +++ b/includes/specialpage/WantedQueryPage.php @@ -21,6 +21,9 @@ * @ingroup SpecialPage */ +use Wikimedia\Rdbms\ResultWrapper; +use Wikimedia\Rdbms\IDatabase; + /** * Class definition for a wanted query page like * WantedPages, WantedTemplates, etc @@ -117,4 +120,37 @@ abstract class WantedQueryPage extends QueryPage { $label = $this->msg( 'nlinks' )->numParams( $result->value )->escaped(); return Linker::link( $wlh, $label ); } + + /** + * Order by title for pages with the same number of links to them + * + * @return array + * @since 1.29 + */ + function getOrderFields() { + return [ 'value DESC', 'namespace', 'title' ]; + } + + /** + * Do not order descending for all order fields. We will use DESC only on one field, see + * getOrderFields above. This overwrites sortDescending from QueryPage::getOrderFields(). + * Do NOT change this to true unless you remove the phrase DESC in getOrderFiels above. + * If you do a database error will be thrown due to double adding DESC to query! + * + * @return bool + * @since 1.29 + */ + function sortDescending() { + return false; + } + + /** + * Also use the order fields returned by getOrderFields when fetching from the cache. + * @return array + * @since 1.29 + */ + function getCacheOrderFields() { + return $this->getOrderFields(); + } + } diff --git a/includes/specials/SpecialActiveusers.php b/includes/specials/SpecialActiveusers.php index a01e9b2675d3..e7030c56e53a 100644 --- a/includes/specials/SpecialActiveusers.php +++ b/includes/specials/SpecialActiveusers.php @@ -86,7 +86,7 @@ class SpecialActiveUsers extends SpecialPage { $groups = User::getAllGroups(); foreach ( $groups as $group ) { - $msg = htmlspecialchars( User::getGroupName( $group ) ); + $msg = htmlspecialchars( UserGroupMembership::getGroupName( $group ) ); $options[$msg] = $group; } diff --git a/includes/specials/SpecialAllMessages.php b/includes/specials/SpecialAllMessages.php index 49ca9f45de9f..405670921b51 100644 --- a/includes/specials/SpecialAllMessages.php +++ b/includes/specials/SpecialAllMessages.php @@ -67,8 +67,6 @@ class SpecialAllMessages extends SpecialPage { wfGetLangObj( $request->getVal( 'lang', $par ) ) ); - $this->langcode = $this->table->lang->getCode(); - $out->addHTML( $this->table->buildForm() ); $out->addParserOutputContent( $this->table->getFullOutput() ); } diff --git a/includes/specials/SpecialAllPages.php b/includes/specials/SpecialAllPages.php index 4b8446a795d9..17f6cca4ac21 100644 --- a/includes/specials/SpecialAllPages.php +++ b/includes/specials/SpecialAllPages.php @@ -69,7 +69,11 @@ class SpecialAllPages extends IncludableSpecialPage { $from = $request->getVal( 'from', null ); $to = $request->getVal( 'to', null ); $namespace = $request->getInt( 'namespace' ); - $hideredirects = $request->getBool( 'hideredirects', false ); + + $miserMode = (bool)$this->getConfig()->get( 'MiserMode' ); + + // Redirects filter is disabled in MiserMode + $hideredirects = $request->getBool( 'hideredirects', false ) && !$miserMode; $namespaces = $this->getLanguage()->getNamespaces(); @@ -100,6 +104,7 @@ class SpecialAllPages extends IncludableSpecialPage { protected function outputHTMLForm( $namespace = NS_MAIN, $from = '', $to = '', $hideRedirects = false ) { + $miserMode = (bool)$this->getConfig()->get( 'MiserMode' ); $fields = [ 'from' => [ 'type' => 'text', @@ -133,6 +138,11 @@ class SpecialAllPages extends IncludableSpecialPage { 'value' => $hideRedirects, ], ]; + + if ( $miserMode ) { + unset( $fields['hideredirects'] ); + } + $form = HTMLForm::factory( 'table', $fields, $this->getContext() ); $form->setMethod( 'get' ) ->setWrapperLegendMsg( 'allpages' ) diff --git a/includes/specials/SpecialAutoblockList.php b/includes/specials/SpecialAutoblockList.php new file mode 100644 index 000000000000..dcb2444e18b8 --- /dev/null +++ b/includes/specials/SpecialAutoblockList.php @@ -0,0 +1,152 @@ +<?php +/** + * Implements Special:AutoblockList + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @ingroup SpecialPage + */ + +/** + * A special page that lists autoblocks + * + * @since 1.29 + * @ingroup SpecialPage + */ +class SpecialAutoblockList extends SpecialPage { + + function __construct() { + parent::__construct( 'AutoblockList' ); + } + + /** + * Main execution point + * + * @param string $par Title fragment + */ + public function execute( $par ) { + $this->setHeaders(); + $this->outputHeader(); + $out = $this->getOutput(); + $lang = $this->getLanguage(); + $out->setPageTitle( $this->msg( 'autoblocklist' ) ); + $this->addHelpLink( 'Autoblock' ); + $out->addModuleStyles( [ 'mediawiki.special' ] ); + + # setup BlockListPager here to get the actual default Limit + $pager = $this->getBlockListPager(); + + # Just show the block list + $fields = [ + 'Limit' => [ + 'type' => 'limitselect', + 'label-message' => 'table_pager_limit_label', + 'options' => [ + $lang->formatNum( 20 ) => 20, + $lang->formatNum( 50 ) => 50, + $lang->formatNum( 100 ) => 100, + $lang->formatNum( 250 ) => 250, + $lang->formatNum( 500 ) => 500, + ], + 'name' => 'limit', + 'default' => $pager->getLimit(), + ] + ]; + + $context = new DerivativeContext( $this->getContext() ); + $context->setTitle( $this->getPageTitle() ); // Remove subpage + $form = HTMLForm::factory( 'ooui', $fields, $context ); + $form->setMethod( 'get' ) + ->setFormIdentifier( 'blocklist' ) + ->setWrapperLegendMsg( 'autoblocklist-legend' ) + ->setSubmitTextMsg( 'autoblocklist-submit' ) + ->setSubmitProgressive() + ->prepareForm() + ->displayForm( false ); + + $this->showList( $pager ); + } + + /** + * Setup a new BlockListPager instance. + * @return BlockListPager + */ + protected function getBlockListPager() { + $conds = [ + 'ipb_parent_block_id IS NOT NULL' + ]; + # Is the user allowed to see hidden blocks? + if ( !$this->getUser()->isAllowed( 'hideuser' ) ) { + $conds['ipb_deleted'] = 0; + } + + return new BlockListPager( $this, $conds ); + } + + /** + * Show the list of blocked accounts matching the actual filter. + * @param BlockListPager $pager The BlockListPager instance for this page + */ + protected function showList( BlockListPager $pager ) { + $out = $this->getOutput(); + + # Check for other blocks, i.e. global/tor blocks + $otherAutoblockLink = []; + Hooks::run( 'OtherAutoblockLogLink', [ &$otherAutoblockLink ] ); + + # Show additional header for the local block only when other blocks exists. + # Not necessary in a standard installation without such extensions enabled + if ( count( $otherAutoblockLink ) ) { + $out->addHTML( + Html::element( 'h2', [], $this->msg( 'autoblocklist-localblocks', + $pager->getNumRows() )->parse() ) + . "\n" + ); + } + + if ( $pager->getNumRows() ) { + $out->addParserOutputContent( $pager->getFullOutput() ); + } else { + $out->addWikiMsg( 'autoblocklist-empty' ); + } + + if ( count( $otherAutoblockLink ) ) { + $out->addHTML( + Html::rawElement( + 'h2', + [], + $this->msg( 'autoblocklist-otherblocks', count( $otherAutoblockLink ) )->parse() + ) . "\n" + ); + $list = ''; + foreach ( $otherAutoblockLink as $link ) { + $list .= Html::rawElement( 'li', [], $link ) . "\n"; + } + $out->addHTML( + Html::rawElement( + 'ul', + [ 'class' => 'mw-autoblocklist-otherblocks' ], + $list + ) . "\n" + ); + } + } + + protected function getGroupName() { + return 'users'; + } +} diff --git a/includes/specials/SpecialBlock.php b/includes/specials/SpecialBlock.php index 82f7d0843f6d..04c04b239c67 100644 --- a/includes/specials/SpecialBlock.php +++ b/includes/specials/SpecialBlock.php @@ -64,7 +64,7 @@ class SpecialBlock extends FormSpecialPage { protected function checkExecutePermissions( User $user ) { parent::checkExecutePermissions( $user ); - # bug 15810: blocked admins should have limited access here + # T17810: blocked admins should have limited access here $status = self::checkUnblockSelf( $this->target, $user ); if ( $status !== true ) { throw new ErrorPageError( 'badaccess', $status ); @@ -127,15 +127,7 @@ class SpecialBlock extends FormSpecialPage { */ protected function getFormFields() { global $wgBlockAllowsUTEdit; - if ( !wfMessage( 'ipbreason-dropdown' )->inContentLanguage()->isDisabled() ) { - $reasonsList = Xml::getArrayFromWikiTextList( - wfMessage( 'ipbreason-dropdown' )->inContentLanguage()->text() - ); - $this->getOutput()->addModules( 'mediawiki.reasonSuggest' ); - $this->getOutput()->addJsConfigVars( [ - 'reasons' => $reasonsList - ] ); - } + $user = $this->getUser(); $suggestedDurations = self::getSuggestedDurations(); @@ -283,7 +275,7 @@ class SpecialBlock extends FormSpecialPage { } // If the username was hidden (ipb_deleted == 1), don't show the reason - // unless this user also has rights to hideuser: Bug 35839 + // unless this user also has rights to hideuser: T37839 if ( !$block->mHideName || $this->getUser()->isAllowed( 'hideuser' ) ) { $fields['Reason']['default'] = $block->mReason; } else { @@ -752,7 +744,7 @@ class SpecialBlock extends FormSpecialPage { $blockNotConfirmed = !$data['Confirm'] || ( array_key_exists( 'PreviousTarget', $data ) && $data['PreviousTarget'] !== $target ); - # Special case for API - bug 32434 + # Special case for API - T34434 $reblockNotAllowed = ( array_key_exists( 'Reblock', $data ) && !$data['Reblock'] ); # Show form unless the user is already aware of this... @@ -832,12 +824,12 @@ class SpecialBlock extends FormSpecialPage { $logEntry->setComment( $data['Reason'][0] ); $logEntry->setPerformer( $performer ); $logEntry->setParameters( $logParams ); - # Relate log ID to block IDs (bug 25763) + # Relate log ID to block IDs (T27763) $blockIds = array_merge( [ $status['id'] ], $status['autoIds'] ); $logEntry->setRelations( [ 'ipb_id' => $blockIds ] ); $logId = $logEntry->insert(); - if ( count( $data['Tags'] ) ) { + if ( !empty( $data['Tags'] ) ) { $logEntry->setTags( $data['Tags'] ); } @@ -910,7 +902,7 @@ class SpecialBlock extends FormSpecialPage { } /** - * bug 15810: blocked admins should not be able to block/unblock + * T17810: blocked admins should not be able to block/unblock * others, and probably shouldn't be able to unblock themselves * either. * @param User|int|string $user diff --git a/includes/specials/SpecialBrokenRedirects.php b/includes/specials/SpecialBrokenRedirects.php index b730ecd789d1..cd9345d1bf74 100644 --- a/includes/specials/SpecialBrokenRedirects.php +++ b/includes/specials/SpecialBrokenRedirects.php @@ -21,6 +21,9 @@ * @ingroup SpecialPage */ +use Wikimedia\Rdbms\ResultWrapper; +use Wikimedia\Rdbms\IDatabase; + /** * A special page listing redirects to non existent page. Those should be * fixed to point to an existing page. diff --git a/includes/specials/SpecialChangeCredentials.php b/includes/specials/SpecialChangeCredentials.php index b81ca3d50f4c..970a2e29f2a8 100644 --- a/includes/specials/SpecialChangeCredentials.php +++ b/includes/specials/SpecialChangeCredentials.php @@ -126,7 +126,27 @@ class SpecialChangeCredentials extends AuthManagerSpecialPage { if ( !static::$loadUserData ) { return []; } else { - return parent::getAuthFormDescriptor( $requests, $action ); + $descriptor = parent::getAuthFormDescriptor( $requests, $action ); + + $any = false; + foreach ( $descriptor as &$field ) { + if ( $field['type'] === 'password' && $field['name'] !== 'retype' ) { + $any = true; + if ( isset( $field['cssclass'] ) ) { + $field['cssclass'] .= ' mw-changecredentials-validate-password'; + } else { + $field['cssclass'] = 'mw-changecredentials-validate-password'; + } + } + } + + if ( $any ) { + $this->getOutput()->addModules( [ + 'mediawiki.special.changecredentials.js' + ] ); + } + + return $descriptor; } } @@ -238,7 +258,7 @@ class SpecialChangeCredentials extends AuthManagerSpecialPage { } $title = Title::newFromText( $returnTo ); - return $title->getFullURL( $returnToQuery ); + return $title->getFullUrlForRedirect( $returnToQuery ); } protected function getRequestBlacklist() { diff --git a/includes/specials/SpecialChangeEmail.php b/includes/specials/SpecialChangeEmail.php index 785447f7f983..eb98fe76a757 100644 --- a/includes/specials/SpecialChangeEmail.php +++ b/includes/specials/SpecialChangeEmail.php @@ -136,7 +136,7 @@ class SpecialChangeEmail extends FormSpecialPage { $query = $request->getVal( 'returntoquery' ); if ( $this->status->value === true ) { - $this->getOutput()->redirect( $titleObj->getFullURL( $query ) ); + $this->getOutput()->redirect( $titleObj->getFullUrlForRedirect( $query ) ); } elseif ( $this->status->value === 'eauth' ) { # Notify user that a confirmation email has been sent... $this->getOutput()->wrapWikiMsg( "<div class='error' style='clear: both;'>\n$1\n</div>", diff --git a/includes/specials/SpecialContributions.php b/includes/specials/SpecialContributions.php index 40277ca40f3a..1028002a230a 100644 --- a/includes/specials/SpecialContributions.php +++ b/includes/specials/SpecialContributions.php @@ -359,7 +359,7 @@ class SpecialContributions extends IncludableSpecialPage { [ 'page' => $userpage->getPrefixedText() ] ); - # Suppression log link (bug 59120) + # Suppression log link (T61120) if ( $sp->getUser()->isAllowed( 'suppressionlog' ) ) { $tools['log-suppression'] = $linkRenderer->makeKnownLink( SpecialPage::getTitleFor( 'Log', 'suppress' ), @@ -529,7 +529,6 @@ class SpecialContributions extends IncludableSpecialPage { 'text', [ 'size' => '40', - 'required' => '', 'class' => [ 'mw-input', 'mw-ui-input-inline', diff --git a/includes/specials/SpecialDoubleRedirects.php b/includes/specials/SpecialDoubleRedirects.php index 9140bf1426b7..d7e99db817ac 100644 --- a/includes/specials/SpecialDoubleRedirects.php +++ b/includes/specials/SpecialDoubleRedirects.php @@ -21,6 +21,9 @@ * @ingroup SpecialPage */ +use Wikimedia\Rdbms\ResultWrapper; +use Wikimedia\Rdbms\IDatabase; + /** * A special page listing redirects to redirecting page. * The software will automatically not follow double redirects, to prevent loops. @@ -75,7 +78,7 @@ class DoubleRedirectsPage extends QueryPage { 'conds' => [ 'ra.rd_from = pa.page_id', - // Filter out redirects where the target goes interwiki (bug 40353). + // Filter out redirects where the target goes interwiki (T42353). // This isn't an optimization, it is required for correct results, // otherwise a non-double redirect like Bar -> w:Foo will show up // like "Bar -> Foo -> w:Foo". diff --git a/includes/specials/SpecialEditWatchlist.php b/includes/specials/SpecialEditWatchlist.php index b44727110377..e1ecfe8cd516 100644 --- a/includes/specials/SpecialEditWatchlist.php +++ b/includes/specials/SpecialEditWatchlist.php @@ -793,7 +793,7 @@ class EditWatchlistCheckboxSeriesField extends HTMLMultiSelectField { * HTMLMultiSelectField throws validation errors if we get input data * that doesn't match the data set in the form setup. This causes * problems if something gets removed from the watchlist while the - * form is open (bug 32126), but we know that invalid items will + * form is open (T34126), but we know that invalid items will * be harmless so we can override it here. * * @param string $value The value the field was submitted with diff --git a/includes/specials/SpecialEmailuser.php b/includes/specials/SpecialEmailuser.php index 085b68d6d82c..a69406cb8d22 100644 --- a/includes/specials/SpecialEmailuser.php +++ b/includes/specials/SpecialEmailuser.php @@ -231,14 +231,15 @@ class SpecialEmailUser extends UnlistedSpecialPage { return 'usermaildisabled'; } - if ( !$user->isAllowed( 'sendemail' ) ) { - return 'badaccess'; - } - + // Run this before $user->isAllowed, to show appropriate message to anons (T160309) if ( !$user->isEmailConfirmed() ) { return 'mailnologin'; } + if ( !$user->isAllowed( 'sendemail' ) ) { + return 'badaccess'; + } + if ( $user->isBlockedFromEmailuser() ) { wfDebug( "User is blocked from sending e-mail.\n" ); diff --git a/includes/specials/SpecialExpandTemplates.php b/includes/specials/SpecialExpandTemplates.php index ca0d13930e6f..560d75a6b1d3 100644 --- a/includes/specials/SpecialExpandTemplates.php +++ b/includes/specials/SpecialExpandTemplates.php @@ -263,7 +263,7 @@ class SpecialExpandTemplates extends SpecialPage { $user = $this->getUser(); // To prevent cross-site scripting attacks, don't show the preview if raw HTML is - // allowed and a valid edit token is not provided (bug 71111). However, MediaWiki + // allowed and a valid edit token is not provided (T73111). However, MediaWiki // does not currently provide logged-out users with CSRF protection; in that case, // do not show the preview unless anonymous editing is allowed. if ( $user->isAnon() && !$user->isAllowed( 'edit' ) ) { diff --git a/includes/specials/SpecialExport.php b/includes/specials/SpecialExport.php index bf535a6f3ae7..f5e9e49b6985 100644 --- a/includes/specials/SpecialExport.php +++ b/includes/specials/SpecialExport.php @@ -23,6 +23,8 @@ * @ingroup SpecialPage */ +use Mediawiki\MediaWikiServices; + /** * A special page that allows users to export pages in a XML file * @@ -359,7 +361,7 @@ class SpecialExport extends SpecialPage { $pages = array_keys( $pageSet ); - // Normalize titles to the same format and remove dupes, see bug 17374 + // Normalize titles to the same format and remove dupes, see T19374 foreach ( $pages as $k => $v ) { $pages[$k] = str_replace( " ", "_", $v ); } @@ -374,7 +376,7 @@ class SpecialExport extends SpecialPage { $buffer = WikiExporter::BUFFER; } else { // Use an unbuffered query; histories may be very long! - $lb = wfGetLBFactory()->newMainLB(); + $lb = MediaWikiServices::getInstance()->getDBLoadBalancerFactory()->newMainLB(); $db = $lb->getConnection( DB_REPLICA ); $buffer = WikiExporter::STREAM; @@ -392,7 +394,7 @@ class SpecialExport extends SpecialPage { $exporter->allPages(); } else { foreach ( $pages as $page ) { - # Bug 8824: Only export pages the user can read + # T10824: Only export pages the user can read $title = Title::newFromText( $page ); if ( is_null( $title ) ) { // @todo Perhaps output an <error> tag or something. diff --git a/includes/specials/SpecialGoToInterwiki.php b/includes/specials/SpecialGoToInterwiki.php new file mode 100644 index 000000000000..809a14aac39e --- /dev/null +++ b/includes/specials/SpecialGoToInterwiki.php @@ -0,0 +1,79 @@ +<?php +/** + * Implements Special:GoToInterwiki + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @ingroup SpecialPage + */ + +/** + * Landing page for non-local interwiki links. + * + * Meant to warn people that the site they're visiting + * is not the local wiki (In case of phishing tricks). + * Only meant to be used for things that directly + * redirect from url (e.g. Special:Search/google:foo ) + * Not meant for general interwiki linking (e.g. + * [[google:foo]] should still directly link) + * + * @ingroup SpecialPage + */ +class SpecialGoToInterwiki extends UnlistedSpecialPage { + public function __construct( $name = 'GoToInterwiki' ) { + parent::__construct( $name ); + } + + public function execute( $par ) { + $this->setHeaders(); + $target = Title::newFromText( $par ); + // Disallow special pages as a precaution against + // possible redirect loops. + if ( !$target || $target->isSpecialPage() ) { + $this->getOutput()->setStatusCode( 404 ); + $this->getOutput()->addWikiMsg( 'gotointerwiki-invalid' ); + return; + } + + $url = $target->getFullURL(); + if ( !$target->isExternal() || $target->isLocal() ) { + // Either a normal page, or a local interwiki. + // just redirect. + $this->getOutput()->redirect( $url, '301' ); + } else { + $this->getOutput()->addWikiMsg( + 'gotointerwiki-external', + $url, + $target->getFullText() + ); + } + } + + /** + * @return bool + */ + public function requiresWrite() { + return false; + } + + /** + * @return String + */ + protected function getGroupName() { + return 'redirects'; + } +} diff --git a/includes/specials/SpecialJavaScriptTest.php b/includes/specials/SpecialJavaScriptTest.php index 0e2e7db04621..dc6a61975066 100644 --- a/includes/specials/SpecialJavaScriptTest.php +++ b/includes/specials/SpecialJavaScriptTest.php @@ -137,7 +137,9 @@ class SpecialJavaScriptTest extends SpecialPage { $code .= '(function () {' . 'var start = window.__karma__ ? window.__karma__.start : QUnit.start;' . 'try {' - . 'mw.loader.using( ' . Xml::encodeJsVar( $modules ) . ' ).always( start );' + . 'mw.loader.using( ' . Xml::encodeJsVar( $modules ) . ' )' + . '.always( start )' + . '.fail( function ( e ) { throw e; } );' . '} catch ( e ) { start(); throw e; }' . '}());'; diff --git a/includes/specials/SpecialLinkSearch.php b/includes/specials/SpecialLinkSearch.php index a2fa8447ca5b..dae60744dce9 100644 --- a/includes/specials/SpecialLinkSearch.php +++ b/includes/specials/SpecialLinkSearch.php @@ -22,6 +22,9 @@ * @author Brion Vibber */ +use Wikimedia\Rdbms\ResultWrapper; +use Wikimedia\Rdbms\IDatabase; + /** * Special:LinkSearch to search the external-links table. * @ingroup SpecialPage diff --git a/includes/specials/SpecialListDuplicatedFiles.php b/includes/specials/SpecialListDuplicatedFiles.php index dbe5c2fff048..d5fb0018cafd 100644 --- a/includes/specials/SpecialListDuplicatedFiles.php +++ b/includes/specials/SpecialListDuplicatedFiles.php @@ -24,6 +24,9 @@ * @author Brian Wolff */ +use Wikimedia\Rdbms\ResultWrapper; +use Wikimedia\Rdbms\IDatabase; + /** * Special:ListDuplicatedFiles Lists all files where the current version is * a duplicate of the current version of some other file. diff --git a/includes/specials/SpecialListgrouprights.php b/includes/specials/SpecialListgrouprights.php index f3d3a776e602..7a25e55d5835 100644 --- a/includes/specials/SpecialListgrouprights.php +++ b/includes/specials/SpecialListgrouprights.php @@ -273,12 +273,14 @@ class SpecialListGroupRights extends SpecialPage { } elseif ( is_array( $changeGroup ) ) { $changeGroup = array_intersect( array_values( array_unique( $changeGroup ) ), $allGroups ); if ( count( $changeGroup ) ) { + $groupLinks = []; + foreach ( $changeGroup as $group ) { + $groupLinks[] = UserGroupMembership::getLink( $group, $this->getContext(), 'wiki' ); + } // For grep: listgrouprights-addgroup, listgrouprights-removegroup, // listgrouprights-addgroup-self, listgrouprights-removegroup-self $r[] = $this->msg( 'listgrouprights-' . $messageKey, - $lang->listToText( array_map( [ 'User', 'makeGroupLinkWiki' ], $changeGroup ) ), - count( $changeGroup ) - )->parse(); + $lang->listToText( $groupLinks ), count( $changeGroup ) )->parse(); } } } diff --git a/includes/specials/SpecialListredirects.php b/includes/specials/SpecialListredirects.php index d034a6ca7743..5f3862973be5 100644 --- a/includes/specials/SpecialListredirects.php +++ b/includes/specials/SpecialListredirects.php @@ -24,6 +24,9 @@ * @author Rob Church <robchur@gmail.com> */ +use Wikimedia\Rdbms\ResultWrapper; +use Wikimedia\Rdbms\IDatabase; + /** * Special:Listredirects - Lists all the redirects on the wiki. * @ingroup SpecialPage diff --git a/includes/specials/SpecialLog.php b/includes/specials/SpecialLog.php index 533a33179a75..195d08b1c548 100644 --- a/includes/specials/SpecialLog.php +++ b/includes/specials/SpecialLog.php @@ -96,7 +96,7 @@ class SpecialLog extends SpecialPage { # Some log types are only for a 'User:' title but we might have been given # only the username instead of the full title 'User:username'. This part try - # to lookup for a user by that name and eventually fix user input. See bug 1697. + # to lookup for a user by that name and eventually fix user input. See T3697. if ( in_array( $opts->getValue( 'type' ), self::getLogTypesOnUser() ) ) { # ok we have a type of log which expect a user title. $target = Title::newFromText( $opts->getValue( 'page' ) ); diff --git a/includes/specials/SpecialMIMEsearch.php b/includes/specials/SpecialMIMEsearch.php index 15696bcbd855..52cb30a1bcdc 100644 --- a/includes/specials/SpecialMIMEsearch.php +++ b/includes/specials/SpecialMIMEsearch.php @@ -111,7 +111,8 @@ class MIMEsearchPage extends QueryPage { function getPageHeader() { $formDescriptor = [ 'mime' => [ - 'type' => 'text', + 'type' => 'combobox', + 'options' => $this->getSuggestionsForTypes(), 'name' => 'mime', 'label-message' => 'mimetype', 'required' => true, @@ -127,6 +128,33 @@ class MIMEsearchPage extends QueryPage { ->displayForm( false ); } + protected function getSuggestionsForTypes() { + $dbr = wfGetDB( DB_REPLICA ); + $lastMajor = null; + $suggestions = []; + $result = $dbr->select( + [ 'image' ], + // We ignore img_media_type, but using it in the query is needed for MySQL to choose a + // sensible execution plan + [ 'img_media_type', 'img_major_mime', 'img_minor_mime' ], + [], + __METHOD__, + [ 'GROUP BY' => [ 'img_media_type', 'img_major_mime', 'img_minor_mime' ] ] + ); + foreach ( $result as $row ) { + $major = $row->img_major_mime; + $minor = $row->img_minor_mime; + $suggestions[ "$major/$minor" ] = "$major/$minor"; + if ( $lastMajor === $major ) { + // If there are at least two with the same major mime type, also include the wildcard + $suggestions[ "$major/*" ] = "$major/*"; + } + $lastMajor = $major; + } + ksort( $suggestions ); + return $suggestions; + } + public function execute( $par ) { $this->mime = $par ? $par : $this->getRequest()->getText( 'mime' ); $this->mime = trim( $this->mime ); diff --git a/includes/specials/SpecialMediaStatistics.php b/includes/specials/SpecialMediaStatistics.php index 1cb654969621..7c4b49093015 100644 --- a/includes/specials/SpecialMediaStatistics.php +++ b/includes/specials/SpecialMediaStatistics.php @@ -22,6 +22,9 @@ * @author Brian Wolff */ +use Wikimedia\Rdbms\ResultWrapper; +use Wikimedia\Rdbms\IDatabase; + /** * @ingroup SpecialPage */ diff --git a/includes/specials/SpecialMostcategories.php b/includes/specials/SpecialMostcategories.php index 6095412ac7e9..bebed12e381c 100644 --- a/includes/specials/SpecialMostcategories.php +++ b/includes/specials/SpecialMostcategories.php @@ -24,6 +24,9 @@ * @author Ævar Arnfjörð Bjarmason <avarab@gmail.com> */ +use Wikimedia\Rdbms\ResultWrapper; +use Wikimedia\Rdbms\IDatabase; + /** * A special page that list pages that have highest category count * diff --git a/includes/specials/SpecialMostinterwikis.php b/includes/specials/SpecialMostinterwikis.php index 210c4a2808e2..c140ee9633fe 100644 --- a/includes/specials/SpecialMostinterwikis.php +++ b/includes/specials/SpecialMostinterwikis.php @@ -24,6 +24,9 @@ * @author Umherirrender */ +use Wikimedia\Rdbms\ResultWrapper; +use Wikimedia\Rdbms\IDatabase; + /** * A special page that listed pages that have highest interwiki count * diff --git a/includes/specials/SpecialMostlinked.php b/includes/specials/SpecialMostlinked.php index 712574cf14e3..fbfaa73831f2 100644 --- a/includes/specials/SpecialMostlinked.php +++ b/includes/specials/SpecialMostlinked.php @@ -25,6 +25,9 @@ * @author Rob Church <robchur@gmail.com> */ +use Wikimedia\Rdbms\ResultWrapper; +use Wikimedia\Rdbms\IDatabase; + /** * A special page to show pages ordered by the number of pages linking to them. * diff --git a/includes/specials/SpecialMostlinkedcategories.php b/includes/specials/SpecialMostlinkedcategories.php index 41678cb34d60..956207f883a5 100644 --- a/includes/specials/SpecialMostlinkedcategories.php +++ b/includes/specials/SpecialMostlinkedcategories.php @@ -24,6 +24,9 @@ * @author Ævar Arnfjörð Bjarmason <avarab@gmail.com> */ +use Wikimedia\Rdbms\ResultWrapper; +use Wikimedia\Rdbms\IDatabase; + /** * A querypage to show categories ordered in descending order by the pages in them * diff --git a/includes/specials/SpecialMostlinkedtemplates.php b/includes/specials/SpecialMostlinkedtemplates.php index d10279163abe..dee1c8ec5b9a 100644 --- a/includes/specials/SpecialMostlinkedtemplates.php +++ b/includes/specials/SpecialMostlinkedtemplates.php @@ -22,6 +22,9 @@ * @author Rob Church <robchur@gmail.com> */ +use Wikimedia\Rdbms\ResultWrapper; +use Wikimedia\Rdbms\IDatabase; + /** * Special page lists templates with a large number of * transclusion links, i.e. "most used" templates diff --git a/includes/specials/SpecialMovepage.php b/includes/specials/SpecialMovepage.php index 298d6c4edb41..7d8a493a8d41 100644 --- a/includes/specials/SpecialMovepage.php +++ b/includes/specials/SpecialMovepage.php @@ -77,7 +77,7 @@ class MovePageForm extends UnlistedSpecialPage { $request = $this->getRequest(); $target = !is_null( $par ) ? $par : $request->getVal( 'target' ); - // Yes, the use of getVal() and getText() is wanted, see bug 20365 + // Yes, the use of getVal() and getText() is wanted, see T22365 $oldTitleText = $request->getVal( 'wpOldTitle', $target ); $this->oldTitle = Title::newFromText( $oldTitleText ); @@ -620,7 +620,7 @@ class MovePageForm extends UnlistedSpecialPage { // a redirect to the new title. This is not safe, but what we did before was // even worse: we just determined whether a redirect should have been created, // and reported that it was created if it should have, without any checks. - // Also note that isRedirect() is unreliable because of bug 37209. + // Also note that isRedirect() is unreliable because of T39209. $msgName = 'movepage-moved-redirect'; } else { $msgName = 'movepage-moved-noredirect'; @@ -630,7 +630,9 @@ class MovePageForm extends UnlistedSpecialPage { $newLink )->params( $oldText, $newText )->parseAsBlock() ); $out->addWikiMsg( $msgName ); - Hooks::run( 'SpecialMovepageAfterMove', [ &$this, &$ot, &$nt ] ); + // Avoid PHP 7.1 warning from passing $this by reference + $movePage = $this; + Hooks::run( 'SpecialMovepageAfterMove', [ &$movePage, &$ot, &$nt ] ); # Now we move extra pages we've been asked to move: subpages and talk # pages. First, if the old page or the new page is a talk page, we @@ -706,7 +708,7 @@ class MovePageForm extends UnlistedSpecialPage { $newPageName = preg_replace( '#^' . preg_quote( $ot->getDBkey(), '#' ) . '#', - StringUtils::escapeRegexReplacement( $nt->getDBkey() ), # bug 21234 + StringUtils::escapeRegexReplacement( $nt->getDBkey() ), # T23234 $oldSubpage->getDBkey() ); @@ -719,7 +721,7 @@ class MovePageForm extends UnlistedSpecialPage { $newNs = $nt->getSubjectPage()->getNamespace(); } - # Bug 14385: we need makeTitleSafe because the new page names may + # T16385: we need makeTitleSafe because the new page names may # be longer than 255 characters. $newSubpage = Title::makeTitleSafe( $newNs, $newPageName ); if ( !$newSubpage ) { diff --git a/includes/specials/SpecialNewimages.php b/includes/specials/SpecialNewimages.php index 9e3a7509bcac..12dae8b8db78 100644 --- a/includes/specials/SpecialNewimages.php +++ b/includes/specials/SpecialNewimages.php @@ -39,6 +39,7 @@ class SpecialNewFiles extends IncludableSpecialPage { $opts = new FormOptions(); $opts->add( 'like', '' ); + $opts->add( 'user', '' ); $opts->add( 'showbots', false ); $opts->add( 'hidepatrolled', false ); $opts->add( 'limit', 50 ); @@ -75,6 +76,12 @@ class SpecialNewFiles extends IncludableSpecialPage { 'name' => 'like', ], + 'user' => [ + 'type' => 'text', + 'label-message' => 'newimages-user', + 'name' => 'user', + ], + 'showbots' => [ 'type' => 'check', 'label-message' => 'newimages-showbots', diff --git a/includes/specials/SpecialNewpages.php b/includes/specials/SpecialNewpages.php index be8ad8fb40f5..be8ad8fb40f5 100755..100644 --- a/includes/specials/SpecialNewpages.php +++ b/includes/specials/SpecialNewpages.php diff --git a/includes/specials/SpecialPageLanguage.php b/includes/specials/SpecialPageLanguage.php index db05ebe587de..2943fd4e3d69 100644 --- a/includes/specials/SpecialPageLanguage.php +++ b/includes/specials/SpecialPageLanguage.php @@ -136,7 +136,7 @@ class SpecialPageLanguage extends FormSpecialPage { } // Url to redirect to after the operation - $this->goToUrl = $title->getFullURL( + $this->goToUrl = $title->getFullUrlForRedirect( $title->isRedirect() ? [ 'redirect' => 'no' ] : [] ); diff --git a/includes/specials/SpecialPreferences.php b/includes/specials/SpecialPreferences.php index eee5b641a36c..40b50ea5bf46 100644 --- a/includes/specials/SpecialPreferences.php +++ b/includes/specials/SpecialPreferences.php @@ -148,7 +148,7 @@ class SpecialPreferences extends SpecialPage { // Set session data for the success message $this->getRequest()->getSession()->set( 'specialPreferencesSaveSuccess', 1 ); - $url = $this->getPageTitle()->getFullURL(); + $url = $this->getPageTitle()->getFullUrlForRedirect(); $this->getOutput()->redirect( $url ); return true; diff --git a/includes/specials/SpecialPrefixindex.php b/includes/specials/SpecialPrefixindex.php index 46715918b548..34ffa07363e1 100644 --- a/includes/specials/SpecialPrefixindex.php +++ b/includes/specials/SpecialPrefixindex.php @@ -83,7 +83,7 @@ class SpecialPrefixindex extends SpecialAllPages { $showme = $from; } - // Bug 27864: if transcluded, show all pages instead of the form. + // T29864: if transcluded, show all pages instead of the form. if ( $this->including() || $showme != '' || $ns !== null ) { $this->showPrefixChunk( $namespace, $showme, $from ); } else { diff --git a/includes/specials/SpecialProtectedpages.php b/includes/specials/SpecialProtectedpages.php index 5bdae159ebf9..8e20d88372bc 100644 --- a/includes/specials/SpecialProtectedpages.php +++ b/includes/specials/SpecialProtectedpages.php @@ -21,8 +21,6 @@ * @ingroup SpecialPage */ -use MediaWiki\Linker\LinkRenderer; - /** * A special page that lists protected pages * @@ -273,317 +271,3 @@ class SpecialProtectedpages extends SpecialPage { return 'maintenance'; } } - -/** - * @todo document - * @ingroup Pager - */ -class ProtectedPagesPager extends TablePager { - public $mForm, $mConds; - private $type, $level, $namespace, $sizetype, $size, $indefonly, $cascadeonly, $noredirect; - - /** - * @var LinkRenderer - */ - private $linkRenderer; - - /** - * @param SpecialProtectedpages $form - * @param array $conds - * @param $type - * @param $level - * @param $namespace - * @param string $sizetype - * @param int $size - * @param bool $indefonly - * @param bool $cascadeonly - * @param bool $noredirect - * @param LinkRenderer $linkRenderer - */ - function __construct( $form, $conds = [], $type, $level, $namespace, - $sizetype = '', $size = 0, $indefonly = false, $cascadeonly = false, $noredirect = false, - LinkRenderer $linkRenderer - ) { - $this->mForm = $form; - $this->mConds = $conds; - $this->type = ( $type ) ? $type : 'edit'; - $this->level = $level; - $this->namespace = $namespace; - $this->sizetype = $sizetype; - $this->size = intval( $size ); - $this->indefonly = (bool)$indefonly; - $this->cascadeonly = (bool)$cascadeonly; - $this->noredirect = (bool)$noredirect; - $this->linkRenderer = $linkRenderer; - parent::__construct( $form->getContext() ); - } - - function preprocessResults( $result ) { - # Do a link batch query - $lb = new LinkBatch; - $userids = []; - - foreach ( $result as $row ) { - $lb->add( $row->page_namespace, $row->page_title ); - // field is nullable, maybe null on old protections - if ( $row->log_user !== null ) { - $userids[] = $row->log_user; - } - } - - // fill LinkBatch with user page and user talk - if ( count( $userids ) ) { - $userCache = UserCache::singleton(); - $userCache->doQuery( $userids, [], __METHOD__ ); - foreach ( $userids as $userid ) { - $name = $userCache->getProp( $userid, 'name' ); - if ( $name !== false ) { - $lb->add( NS_USER, $name ); - $lb->add( NS_USER_TALK, $name ); - } - } - } - - $lb->execute(); - } - - function getFieldNames() { - static $headers = null; - - if ( $headers == [] ) { - $headers = [ - 'log_timestamp' => 'protectedpages-timestamp', - 'pr_page' => 'protectedpages-page', - 'pr_expiry' => 'protectedpages-expiry', - 'log_user' => 'protectedpages-performer', - 'pr_params' => 'protectedpages-params', - 'log_comment' => 'protectedpages-reason', - ]; - foreach ( $headers as $key => $val ) { - $headers[$key] = $this->msg( $val )->text(); - } - } - - return $headers; - } - - /** - * @param string $field - * @param string $value - * @return string HTML - * @throws MWException - */ - function formatValue( $field, $value ) { - /** @var $row object */ - $row = $this->mCurrentRow; - - switch ( $field ) { - case 'log_timestamp': - // when timestamp is null, this is a old protection row - if ( $value === null ) { - $formatted = Html::rawElement( - 'span', - [ 'class' => 'mw-protectedpages-unknown' ], - $this->msg( 'protectedpages-unknown-timestamp' )->escaped() - ); - } else { - $formatted = htmlspecialchars( $this->getLanguage()->userTimeAndDate( - $value, $this->getUser() ) ); - } - break; - - case 'pr_page': - $title = Title::makeTitleSafe( $row->page_namespace, $row->page_title ); - if ( !$title ) { - $formatted = Html::element( - 'span', - [ 'class' => 'mw-invalidtitle' ], - Linker::getInvalidTitleDescription( - $this->getContext(), - $row->page_namespace, - $row->page_title - ) - ); - } else { - $formatted = $this->linkRenderer->makeLink( $title ); - } - if ( !is_null( $row->page_len ) ) { - $formatted .= $this->getLanguage()->getDirMark() . - ' ' . Html::rawElement( - 'span', - [ 'class' => 'mw-protectedpages-length' ], - Linker::formatRevisionSize( $row->page_len ) - ); - } - break; - - case 'pr_expiry': - $formatted = htmlspecialchars( $this->getLanguage()->formatExpiry( - $value, /* User preference timezone */true ) ); - $title = Title::makeTitleSafe( $row->page_namespace, $row->page_title ); - if ( $this->getUser()->isAllowed( 'protect' ) && $title ) { - $changeProtection = $this->linkRenderer->makeKnownLink( - $title, - $this->msg( 'protect_change' )->text(), - [], - [ 'action' => 'unprotect' ] - ); - $formatted .= ' ' . Html::rawElement( - 'span', - [ 'class' => 'mw-protectedpages-actions' ], - $this->msg( 'parentheses' )->rawParams( $changeProtection )->escaped() - ); - } - break; - - case 'log_user': - // when timestamp is null, this is a old protection row - if ( $row->log_timestamp === null ) { - $formatted = Html::rawElement( - 'span', - [ 'class' => 'mw-protectedpages-unknown' ], - $this->msg( 'protectedpages-unknown-performer' )->escaped() - ); - } else { - $username = UserCache::singleton()->getProp( $value, 'name' ); - if ( LogEventsList::userCanBitfield( - $row->log_deleted, - LogPage::DELETED_USER, - $this->getUser() - ) ) { - if ( $username === false ) { - $formatted = htmlspecialchars( $value ); - } else { - $formatted = Linker::userLink( $value, $username ) - . Linker::userToolLinks( $value, $username ); - } - } else { - $formatted = $this->msg( 'rev-deleted-user' )->escaped(); - } - if ( LogEventsList::isDeleted( $row, LogPage::DELETED_USER ) ) { - $formatted = '<span class="history-deleted">' . $formatted . '</span>'; - } - } - break; - - case 'pr_params': - $params = []; - // Messages: restriction-level-sysop, restriction-level-autoconfirmed - $params[] = $this->msg( 'restriction-level-' . $row->pr_level )->escaped(); - if ( $row->pr_cascade ) { - $params[] = $this->msg( 'protect-summary-cascade' )->escaped(); - } - $formatted = $this->getLanguage()->commaList( $params ); - break; - - case 'log_comment': - // when timestamp is null, this is an old protection row - if ( $row->log_timestamp === null ) { - $formatted = Html::rawElement( - 'span', - [ 'class' => 'mw-protectedpages-unknown' ], - $this->msg( 'protectedpages-unknown-reason' )->escaped() - ); - } else { - if ( LogEventsList::userCanBitfield( - $row->log_deleted, - LogPage::DELETED_COMMENT, - $this->getUser() - ) ) { - $formatted = Linker::formatComment( $value !== null ? $value : '' ); - } else { - $formatted = $this->msg( 'rev-deleted-comment' )->escaped(); - } - if ( LogEventsList::isDeleted( $row, LogPage::DELETED_COMMENT ) ) { - $formatted = '<span class="history-deleted">' . $formatted . '</span>'; - } - } - break; - - default: - throw new MWException( "Unknown field '$field'" ); - } - - return $formatted; - } - - function getQueryInfo() { - $conds = $this->mConds; - $conds[] = 'pr_expiry > ' . $this->mDb->addQuotes( $this->mDb->timestamp() ) . - ' OR pr_expiry IS NULL'; - $conds[] = 'page_id=pr_page'; - $conds[] = 'pr_type=' . $this->mDb->addQuotes( $this->type ); - - if ( $this->sizetype == 'min' ) { - $conds[] = 'page_len>=' . $this->size; - } elseif ( $this->sizetype == 'max' ) { - $conds[] = 'page_len<=' . $this->size; - } - - if ( $this->indefonly ) { - $infinity = $this->mDb->addQuotes( $this->mDb->getInfinity() ); - $conds[] = "pr_expiry = $infinity OR pr_expiry IS NULL"; - } - if ( $this->cascadeonly ) { - $conds[] = 'pr_cascade = 1'; - } - if ( $this->noredirect ) { - $conds[] = 'page_is_redirect = 0'; - } - - if ( $this->level ) { - $conds[] = 'pr_level=' . $this->mDb->addQuotes( $this->level ); - } - if ( !is_null( $this->namespace ) ) { - $conds[] = 'page_namespace=' . $this->mDb->addQuotes( $this->namespace ); - } - - return [ - 'tables' => [ 'page', 'page_restrictions', 'log_search', 'logging' ], - 'fields' => [ - 'pr_id', - 'page_namespace', - 'page_title', - 'page_len', - 'pr_type', - 'pr_level', - 'pr_expiry', - 'pr_cascade', - 'log_timestamp', - 'log_user', - 'log_comment', - 'log_deleted', - ], - 'conds' => $conds, - 'join_conds' => [ - 'log_search' => [ - 'LEFT JOIN', [ - 'ls_field' => 'pr_id', 'ls_value = ' . $this->mDb->buildStringCast( 'pr_id' ) - ] - ], - 'logging' => [ - 'LEFT JOIN', [ - 'ls_log_id = log_id' - ] - ] - ] - ]; - } - - protected function getTableClass() { - return parent::getTableClass() . ' mw-protectedpages'; - } - - function getIndexField() { - return 'pr_id'; - } - - function getDefaultSort() { - return 'pr_id'; - } - - function isFieldSortable( $field ) { - // no index for sorting exists - return false; - } -} diff --git a/includes/specials/SpecialRecentchanges.php b/includes/specials/SpecialRecentchanges.php index b2e56742f328..f88f09c60eec 100644 --- a/includes/specials/SpecialRecentchanges.php +++ b/includes/specials/SpecialRecentchanges.php @@ -22,6 +22,8 @@ */ use MediaWiki\MediaWikiServices; +use Wikimedia\Rdbms\ResultWrapper; +use Wikimedia\Rdbms\FakeResultWrapper; /** * A special page that lists last changes made to the wiki @@ -52,7 +54,8 @@ class SpecialRecentChanges extends ChangesListSpecialPage { } // 10 seconds server-side caching max - $this->getOutput()->setCdnMaxage( 10 ); + $out = $this->getOutput(); + $out->setCdnMaxage( 10 ); // Check if the client has a cached version $lastmod = $this->checkLastModified(); if ( $lastmod === false ) { @@ -64,6 +67,65 @@ class SpecialRecentChanges extends ChangesListSpecialPage { true ); parent::execute( $subpage ); + + if ( $this->isStructuredFilterUiEnabled() ) { + $jsData = $this->getStructuredFilterJsData(); + + $messages = []; + foreach ( $jsData['messageKeys'] as $key ){ + $messages[$key] = $this->msg( $key )->plain(); + } + + $out->addHTML( + ResourceLoader::makeInlineScript( + ResourceLoader::makeMessageSetScript( $messages ) + ) + ); + + $out->addJsConfigVars( 'wgStructuredChangeFilters', $jsData['groups'] ); + } + } + + /** + * @inheritdoc + */ + protected function transformFilterDefinition( array $filterDefinition ) { + if ( isset( $filterDefinition['showHideSuffix'] ) ) { + $filterDefinition['showHide'] = 'rc' . $filterDefinition['showHideSuffix']; + } + + return $filterDefinition; + } + + /** + * @inheritdoc + */ + protected function registerFilters() { + parent::registerFilters(); + + $user = $this->getUser(); + + $significance = $this->getFilterGroup( 'significance' ); + $hideMinor = $significance->getFilter( 'hideminor' ); + $hideMinor->setDefault( $user->getBoolOption( 'hideminor' ) ); + + $automated = $this->getFilterGroup( 'automated' ); + $hideBots = $automated->getFilter( 'hidebots' ); + $hideBots->setDefault( true ); + + $reviewStatus = $this->getFilterGroup( 'reviewStatus' ); + if ( $reviewStatus !== null ) { + // Conditional on feature being available and rights + $hidePatrolled = $reviewStatus->getFilter( 'hidepatrolled' ); + $hidePatrolled->setDefault( $user->getBoolOption( 'hidepatrolled' ) ); + } + + $changeType = $this->getFilterGroup( 'changeType' ); + $hideCategorization = $changeType->getFilter( 'hidecategorization' ); + if ( $hideCategorization !== null ) { + // Conditional on feature being available + $hideCategorization->setDefault( $user->getBoolOption( 'hidecategorization' ) ); + } } /** @@ -79,20 +141,10 @@ class SpecialRecentChanges extends ChangesListSpecialPage { $opts->add( 'limit', $user->getIntOption( 'rclimit' ) ); $opts->add( 'from', '' ); - $opts->add( 'hideminor', $user->getBoolOption( 'hideminor' ) ); - $opts->add( 'hidebots', true ); - $opts->add( 'hideanons', false ); - $opts->add( 'hideliu', false ); - $opts->add( 'hidepatrolled', $user->getBoolOption( 'hidepatrolled' ) ); - $opts->add( 'hidemyself', false ); - $opts->add( 'hidecategorization', $user->getBoolOption( 'hidecategorization' ) ); - $opts->add( 'categories', '' ); $opts->add( 'categories_any', false ); $opts->add( 'tagfilter', '' ); - $opts->add( 'userExpLevel', 'all' ); - return $opts; } @@ -117,36 +169,10 @@ class SpecialRecentChanges extends ChangesListSpecialPage { * @param FormOptions $opts */ public function parseParameters( $par, FormOptions $opts ) { + parent::parseParameters( $par, $opts ); + $bits = preg_split( '/\s*,\s*/', trim( $par ) ); foreach ( $bits as $bit ) { - if ( 'hidebots' === $bit ) { - $opts['hidebots'] = true; - } - if ( 'bots' === $bit ) { - $opts['hidebots'] = false; - } - if ( 'hideminor' === $bit ) { - $opts['hideminor'] = true; - } - if ( 'minor' === $bit ) { - $opts['hideminor'] = false; - } - if ( 'hideliu' === $bit ) { - $opts['hideliu'] = true; - } - if ( 'hidepatrolled' === $bit ) { - $opts['hidepatrolled'] = true; - } - if ( 'hideanons' === $bit ) { - $opts['hideanons'] = true; - } - if ( 'hidemyself' === $bit ) { - $opts['hidemyself'] = true; - } - if ( 'hidecategorization' === $bit ) { - $opts['hidecategorization'] = true; - } - if ( is_numeric( $bit ) ) { $opts['limit'] = $bit; } @@ -173,14 +199,14 @@ class SpecialRecentChanges extends ChangesListSpecialPage { } /** - * Return an array of conditions depending of options set in $opts - * - * @param FormOptions $opts - * @return array + * @inheritdoc */ - public function buildMainQueryConds( FormOptions $opts ) { + protected function buildQuery( &$tables, &$fields, &$conds, + &$query_options, &$join_conds, FormOptions $opts ) { + $dbr = $this->getDB(); - $conds = parent::buildMainQueryConds( $opts ); + parent::buildQuery( $tables, $fields, $conds, + $query_options, $join_conds, $opts ); // Calculate cutoff $cutoff_unixtime = time() - ( $opts['days'] * 86400 ); @@ -195,25 +221,19 @@ class SpecialRecentChanges extends ChangesListSpecialPage { } $conds[] = 'rc_timestamp >= ' . $dbr->addQuotes( $cutoff ); - - return $conds; } /** - * Process the query - * - * @param array $conds - * @param FormOptions $opts - * @return bool|ResultWrapper Result or false (for Recentchangeslinked only) + * @inheritdoc */ - public function doMainQuery( $conds, $opts ) { + protected function doMainQuery( $tables, $fields, $conds, $query_options, + $join_conds, FormOptions $opts ) { + $dbr = $this->getDB(); $user = $this->getUser(); - $tables = [ 'recentchanges' ]; - $fields = RecentChange::selectFields(); - $query_options = []; - $join_conds = []; + $tables[] = 'recentchanges'; + $fields = array_merge( RecentChange::selectFields(), $fields ); // JOIN on watchlist for users if ( $user->getId() && $user->isAllowed( 'viewmywatchlist' ) ) { @@ -242,8 +262,6 @@ class SpecialRecentChanges extends ChangesListSpecialPage { $opts['tagfilter'] ); - $this->filterOnUserExperienceLevel( $tables, $conds, $join_conds, $opts ); - if ( !$this->runMainQueryHook( $tables, $fields, $conds, $query_options, $join_conds, $opts ) ) { @@ -330,7 +348,7 @@ class SpecialRecentChanges extends ChangesListSpecialPage { $dbr = $this->getDB(); $counter = 1; - $list = ChangesList::newFromContext( $this->getContext() ); + $list = ChangesList::newFromContext( $this->getContext(), $this->filterGroups ); $list->initChangesListRows( $rows ); $userShowHiddenCats = $this->getUser()->getBoolOption( 'showhiddencats' ); @@ -380,11 +398,7 @@ class SpecialRecentChanges extends ChangesListSpecialPage { $rclistOutput .= $list->endRecentChangesList(); if ( $rows->numRows() === 0 ) { - $this->getOutput()->addHTML( - '<div class="mw-changeslist-empty">' . - $this->msg( 'recentchanges-noresult' )->parse() . - '</div>' - ); + $this->outputNoResults(); if ( !$this->including() ) { $this->getOutput()->setStatusCode( 404 ); } @@ -455,14 +469,31 @@ class SpecialRecentChanges extends ChangesListSpecialPage { $panel[] = $form; $panelString = implode( "\n", $panel ); - $this->getOutput()->addHTML( - Xml::fieldset( - $this->msg( 'recentchanges-legend' )->text(), - $panelString, - [ 'class' => 'rcoptions' ] - ) + $rcoptions = Xml::fieldset( + $this->msg( 'recentchanges-legend' )->text(), + $panelString, + [ 'class' => 'rcoptions' ] ); + // Insert a placeholder for RCFilters + if ( $this->getUser()->getOption( 'rcenhancedfilters' ) ) { + $rcfilterContainer = Html::element( + 'div', + [ 'class' => 'rcfilters-container' ] + ); + + // Wrap both with rcfilters-head + $this->getOutput()->addHTML( + Html::rawElement( + 'div', + [ 'class' => 'rcfilters-head' ], + $rcfilterContainer . $rcoptions + ) + ); + } else { + $this->getOutput()->addHTML( $rcoptions ); + } + $this->setBottomText( $opts ); } @@ -520,19 +551,26 @@ class SpecialRecentChanges extends ChangesListSpecialPage { } /** + * Check whether the structured filter UI is enabled + * + * @return bool + */ + protected function isStructuredFilterUiEnabled() { + return $this->getUser()->getOption( + 'rcenhancedfilters' + ); + } + + /** * Add page-specific modules. */ protected function addModules() { parent::addModules(); $out = $this->getOutput(); $out->addModules( 'mediawiki.special.recentchanges' ); - if ( $this->getUser()->getOption( - 'rcenhancedfilters', - /*default=*/ null, - /*ignoreHidden=*/ true - ) - ) { - $out->addModules( 'mediawiki.rcfilters.filters' ); + if ( $this->isStructuredFilterUiEnabled() ) { + $out->addModules( 'mediawiki.rcfilters.filters.ui' ); + $out->addModuleStyles( 'mediawiki.rcfilters.filters.base.styles' ); } } @@ -653,7 +691,7 @@ class SpecialRecentChanges extends ChangesListSpecialPage { $newrows[$k] = $rowsarr[$k]; } } - $rows = $newrows; + $rows = new FakeResultWrapper( array_values( $newrows ) ); } /** @@ -668,7 +706,7 @@ class SpecialRecentChanges extends ChangesListSpecialPage { function makeOptionsLink( $title, $override, $options, $active = false ) { $params = $override + $options; - // Bug 36524: false values have be converted to "0" otherwise + // T38524: false values have be converted to "0" otherwise // wfArrayToCgi() will omit it them. foreach ( $params as &$value ) { if ( $value === false ) { @@ -681,7 +719,10 @@ class SpecialRecentChanges extends ChangesListSpecialPage { $title = new HtmlArmor( '<strong>' . htmlspecialchars( $title ) . '</strong>' ); } - return $this->getLinkRenderer()->makeKnownLink( $this->getPageTitle(), $title, [], $params ); + return $this->getLinkRenderer()->makeKnownLink( $this->getPageTitle(), $title, [ + 'data-params' => json_encode( $override ), + 'data-keys' => implode( ',', array_keys( $override ) ), + ], $params ); } /** @@ -705,6 +746,9 @@ class SpecialRecentChanges extends ChangesListSpecialPage { $user = $this->getUser(); $config = $this->getConfig(); if ( $options['from'] ) { + $resetLink = $this->makeOptionsLink( $this->msg( 'rclistfromreset' ), + [ 'from' => '' ], $nondefaults ); + $note .= $this->msg( 'rcnotefrom' ) ->numParams( $options['limit'] ) ->params( @@ -713,7 +757,13 @@ class SpecialRecentChanges extends ChangesListSpecialPage { $lang->userTime( $options['from'], $user ) ) ->numParams( $numRows ) - ->parse() . '<br />'; + ->parse() . ' ' . + Html::rawElement( + 'span', + [ 'class' => 'rcoptions-listfromreset' ], + $this->msg( 'parentheses' )->rawParams( $resetLink )->parse() + ) . + '<br />'; } # Sort data for display and make sure it's unique after we've added user data. @@ -743,49 +793,45 @@ class SpecialRecentChanges extends ChangesListSpecialPage { } $dl = $lang->pipeList( $dl ); - // show/hide links - $filters = [ - 'hideminor' => 'rcshowhideminor', - 'hidebots' => 'rcshowhidebots', - 'hideanons' => 'rcshowhideanons', - 'hideliu' => 'rcshowhideliu', - 'hidepatrolled' => 'rcshowhidepatr', - 'hidemyself' => 'rcshowhidemine' - ]; - - if ( $config->get( 'RCWatchCategoryMembership' ) ) { - $filters['hidecategorization'] = 'rcshowhidecategorization'; - } - $showhide = [ 'show', 'hide' ]; - foreach ( $this->getRenderableCustomFilters( $this->getCustomFilters() ) as $key => $params ) { - $filters[$key] = $params['msg']; - } - - // Disable some if needed - if ( !$user->useRCPatrol() ) { - unset( $filters['hidepatrolled'] ); - } - $links = []; - foreach ( $filters as $key => $msg ) { - // The following messages are used here: - // rcshowhideminor-show, rcshowhideminor-hide, rcshowhidebots-show, rcshowhidebots-hide, - // rcshowhideanons-show, rcshowhideanons-hide, rcshowhideliu-show, rcshowhideliu-hide, - // rcshowhidepatr-show, rcshowhidepatr-hide, rcshowhidemine-show, rcshowhidemine-hide, - // rcshowhidecategorization-show, rcshowhidecategorization-hide. - $linkMessage = $this->msg( $msg . '-' . $showhide[1 - $options[$key]] ); - // Extensions can define additional filters, but don't need to define the corresponding - // messages. If they don't exist, just fall back to 'show' and 'hide'. - if ( !$linkMessage->exists() ) { - $linkMessage = $this->msg( $showhide[1 - $options[$key]] ); - } - $link = $this->makeOptionsLink( $linkMessage->text(), - [ $key => 1 - $options[$key] ], $nondefaults ); - $links[] = "<span class=\"$msg rcshowhideoption\">" - . $this->msg( $msg )->rawParams( $link )->escaped() . '</span>'; + $filterGroups = $this->getFilterGroups(); + + $context = $this->getContext(); + foreach ( $filterGroups as $groupName => $group ) { + if ( !$group->isPerGroupRequestParameter() ) { + foreach ( $group->getFilters() as $key => $filter ) { + if ( $filter->displaysOnUnstructuredUi( $this ) ) { + $msg = $filter->getShowHide(); + $linkMessage = $this->msg( $msg . '-' . $showhide[1 - $options[$key]] ); + // Extensions can define additional filters, but don't need to define the corresponding + // messages. If they don't exist, just fall back to 'show' and 'hide'. + if ( !$linkMessage->exists() ) { + $linkMessage = $this->msg( $showhide[1 - $options[$key]] ); + } + + $link = $this->makeOptionsLink( $linkMessage->text(), + [ $key => 1 - $options[$key] ], $nondefaults ); + + $attribs = [ + 'class' => "$msg rcshowhideoption", + 'data-filter-name' => $filter->getName(), + ]; + + if ( $filter->isFeatureAvailableOnStructuredUi( $this ) ) { + $attribs['data-feature-in-structured-ui'] = true; + } + + $links[] = Html::rawElement( + 'span', + $attribs, + $this->msg( $msg )->rawParams( $link )->escaped() + ); + } + } + } } // show from this onward link @@ -814,66 +860,4 @@ class SpecialRecentChanges extends ChangesListSpecialPage { protected function getCacheTTL() { return 60 * 5; } - - function filterOnUserExperienceLevel( &$tables, &$conds, &$join_conds, $opts ) { - global $wgLearnerEdits, - $wgExperiencedUserEdits, - $wgLearnerMemberSince, - $wgExperiencedUserMemberSince; - - $selectedExpLevels = explode( ',', strtolower( $opts['userExpLevel'] ) ); - // remove values that are not recognized - $selectedExpLevels = array_intersect( - $selectedExpLevels, - [ 'newcomer', 'learner', 'experienced' ] - ); - sort( $selectedExpLevels ); - - if ( $selectedExpLevels ) { - $tables[] = 'user'; - $join_conds['user'] = [ 'LEFT JOIN', 'rc_user = user_id' ]; - - $now = time(); - $secondsPerDay = 86400; - $learnerCutoff = $now - $wgLearnerMemberSince * $secondsPerDay; - $experiencedUserCutoff = $now - $wgExperiencedUserMemberSince * $secondsPerDay; - - $aboveNewcomer = $this->getDB()->makeList( - [ - 'user_editcount >= ' . intval( $wgLearnerEdits ), - 'user_registration <= ' . $this->getDB()->timestamp( $learnerCutoff ), - ], - IDatabase::LIST_AND - ); - - $aboveLearner = $this->getDB()->makeList( - [ - 'user_editcount >= ' . intval( $wgExperiencedUserEdits ), - 'user_registration <= ' . $this->getDB()->timestamp( $experiencedUserCutoff ), - ], - IDatabase::LIST_AND - ); - - if ( $selectedExpLevels === [ 'newcomer' ] ) { - $conds[] = "NOT ( $aboveNewcomer )"; - } elseif ( $selectedExpLevels === [ 'learner' ] ) { - $conds[] = $this->getDB()->makeList( - [ $aboveNewcomer, "NOT ( $aboveLearner )" ], - IDatabase::LIST_AND - ); - } elseif ( $selectedExpLevels === [ 'experienced' ] ) { - $conds[] = $aboveLearner; - } elseif ( $selectedExpLevels === [ 'learner', 'newcomer' ] ) { - $conds[] = "NOT ( $aboveLearner )"; - } elseif ( $selectedExpLevels === [ 'experienced', 'newcomer' ] ) { - $conds[] = $this->getDB()->makeList( - [ "NOT ( $aboveNewcomer )", $aboveLearner ], - IDatabase::LIST_OR - ); - } elseif ( $selectedExpLevels === [ 'experienced', 'learner' ] ) { - $conds[] = $aboveNewcomer; - } - } - } - } diff --git a/includes/specials/SpecialRecentchangeslinked.php b/includes/specials/SpecialRecentchangeslinked.php index aab0f6dcb0e5..873285b8c763 100644 --- a/includes/specials/SpecialRecentchangeslinked.php +++ b/includes/specials/SpecialRecentchangeslinked.php @@ -46,7 +46,12 @@ class SpecialRecentChangesLinked extends SpecialRecentChanges { $opts['target'] = $par; } - public function doMainQuery( $conds, $opts ) { + /** + * @inheritdoc + */ + protected function doMainQuery( $tables, $select, $conds, $query_options, + $join_conds, FormOptions $opts ) { + $target = $opts['target']; $showlinkedto = $opts['showlinkedto']; $limit = $opts['limit']; @@ -79,10 +84,8 @@ class SpecialRecentChangesLinked extends SpecialRecentChanges { $ns = $title->getNamespace(); $dbkey = $title->getDBkey(); - $tables = [ 'recentchanges' ]; - $select = RecentChange::selectFields(); - $join_conds = []; - $query_options = []; + $tables[] = 'recentchanges'; + $select = array_merge( RecentChange::selectFields(), $select ); // left join with watchlist table to highlight watched rows $uid = $this->getUser()->getId(); diff --git a/includes/specials/SpecialSearch.php b/includes/specials/SpecialSearch.php index 255e61880632..139e4f70c389 100644 --- a/includes/specials/SpecialSearch.php +++ b/includes/specials/SpecialSearch.php @@ -24,6 +24,12 @@ */ use MediaWiki\MediaWikiServices; +use MediaWiki\Widget\Search\BasicSearchResultSetWidget; +use MediaWiki\Widget\Search\FullSearchResultWidget; +use MediaWiki\Widget\Search\InterwikiSearchResultWidget; +use MediaWiki\Widget\Search\InterwikiSearchResultSetWidget; +use MediaWiki\Widget\Search\SimpleSearchResultWidget; +use MediaWiki\Widget\Search\SimpleSearchResultSetWidget; /** * implements Special:Search - Run text & title search and display the output @@ -76,12 +82,6 @@ class SpecialSearch extends SpecialPage { protected $runSuggestion = true; /** - * Names of the wikis, in format: Interwiki prefix -> caption - * @var array - */ - protected $customCaptions; - - /** * Search engine configurations. * @var SearchEngineConfig */ @@ -101,35 +101,29 @@ class SpecialSearch extends SpecialPage { */ public function execute( $par ) { $request = $this->getRequest(); + $out = $this->getOutput(); // Fetch the search term - $search = str_replace( "\n", " ", $request->getText( 'search' ) ); + $term = str_replace( "\n", " ", $request->getText( 'search' ) ); // Historically search terms have been accepted not only in the search query // parameter, but also as part of the primary url. This can have PII implications // in releasing page view data. As such issue a 301 redirect to the correct // URL. - if ( strlen( $par ) && !strlen( $search ) ) { + if ( strlen( $par ) && !strlen( $term ) ) { $query = $request->getValues(); unset( $query['title'] ); // Strip underscores from title parameter; most of the time we'll want // text form here. But don't strip underscores from actual text params! $query['search'] = str_replace( '_', ' ', $par ); - $this->getOutput()->redirect( $this->getPageTitle()->getFullURL( $query ), 301 ); + $out->redirect( $this->getPageTitle()->getFullURL( $query ), 301 ); return; } - $this->setHeaders(); - $this->outputHeader(); - $out = $this->getOutput(); - $out->allowClickjacking(); - $out->addModuleStyles( [ - 'mediawiki.special', 'mediawiki.special.search.styles', 'mediawiki.ui', 'mediawiki.ui.button', - 'mediawiki.ui.input', 'mediawiki.widgets.SearchInputWidget.styles', - ] ); - $this->addHelpLink( 'Help:Searching' ); - + // Need to load selected namespaces before handling nsRemember $this->load(); + // TODO: This performs database actions on GET request, which is going to + // be a problem for our multi-datacenter work. if ( !is_null( $request->getVal( 'nsRemember' ) ) ) { $this->saveNamespaces(); // Remove the token from the URL to prevent the user from inadvertently @@ -141,16 +135,54 @@ class SpecialSearch extends SpecialPage { return; } - $out->addJsConfigVars( [ 'searchTerm' => $search ] ); $this->searchEngineType = $request->getVal( 'srbackend' ); - - if ( $request->getVal( 'fulltext' ) - || !is_null( $request->getVal( 'offset' ) ) + if ( + !$request->getVal( 'fulltext' ) && + $request->getVal( 'offset' ) === null ) { - $this->showResults( $search ); - } else { - $this->goResult( $search ); + $url = $this->goResult( $term ); + if ( $url !== null ) { + // successful 'go' + $out->redirect( $url ); + return; + } + // No match. If it could plausibly be a title + // run the No go match hook. + $title = Title::newFromText( $term ); + if ( !is_null( $title ) ) { + Hooks::run( 'SpecialSearchNogomatch', [ &$title ] ); + } } + + $this->setupPage( $term ); + + if ( $this->getConfig()->get( 'DisableTextSearch' ) ) { + $searchForwardUrl = $this->getConfig()->get( 'SearchForwardUrl' ); + if ( $searchForwardUrl ) { + $url = str_replace( '$1', urlencode( $term ), $searchForwardUrl ); + $out->redirect( $url ); + } else { + $out->addHTML( + "<fieldset>" . + "<legend>" . + $this->msg( 'search-external' )->escaped() . + "</legend>" . + "<p class='mw-searchdisabled'>" . + $this->msg( 'searchdisabled' )->escaped() . + "</p>" . + $this->msg( 'googlesearch' )->rawParams( + htmlspecialchars( $term ), + 'UTF-8', + $this->msg( 'searchbutton' )->escaped() + )->text() . + "</fieldset>" + ); + } + + return; + } + + $this->showResults( $term ); } /** @@ -209,32 +241,25 @@ class SpecialSearch extends SpecialPage { * If an exact title match can be found, jump straight ahead to it. * * @param string $term + * @return string|null The url to redirect to, or null if no redirect. */ public function goResult( $term ) { - $this->setupPage( $term ); - # Try to go to page as entered. - $title = Title::newFromText( $term ); # If the string cannot be used to create a title - if ( is_null( $title ) ) { - $this->showResults( $term ); - - return; + if ( is_null( Title::newFromText( $term ) ) ) { + return null; } # If there's an exact or very near match, jump right there. $title = $this->getSearchEngine() ->getNearMatcher( $this->getConfig() )->getNearMatch( $term ); - - if ( !is_null( $title ) && - Hooks::run( 'SpecialSearchGoResult', [ $term, $title, &$url ] ) - ) { - if ( $url === null ) { - $url = $title->getFullURL(); - } - $this->getOutput()->redirect( $url ); - - return; + if ( is_null( $title ) ) { + return null; } - $this->showResults( $term ); + $url = null; + if ( !Hooks::run( 'SpecialSearchGoResult', [ $term, $title, &$url ] ) ) { + return null; + } + + return $url === null ? $title->getFullUrlForRedirect() : $url; } /** @@ -243,6 +268,33 @@ class SpecialSearch extends SpecialPage { public function showResults( $term ) { global $wgContLang; + if ( $this->searchEngineType !== null ) { + $this->setExtraParam( 'srbackend', $this->searchEngineType ); + } + + $out = $this->getOutput(); + $formWidget = new MediaWiki\Widget\Search\SearchFormWidget( + $this, + $this->searchConfig, + $this->getSearchProfiles() + ); + $filePrefix = $wgContLang->getFormattedNsText( NS_FILE ) . ':'; + if ( trim( $term ) === '' || $filePrefix === trim( $term ) ) { + // Empty query -- straight view of search form + if ( !Hooks::run( 'SpecialSearchResultsPrepend', [ $this, $out, $term ] ) ) { + # Hook requested termination + return; + } + $out->enableOOUI(); + // The form also contains the 'Showing results 0 - 20 of 1234' so we can + // only do the form render here for the empty $term case. Rendering + // the form when a search is provided is repeated below. + $out->addHTML( $formWidget->render( + $this->profile, $term, 0, 0, $this->offset, $this->isPowerSearch() + ) ); + return; + } + $search = $this->getSearchEngine(); $search->setFeatureData( 'rewrite', $this->runSuggestion ); $search->setLimitOffset( $this->limit, $this->offset ); @@ -251,34 +303,8 @@ class SpecialSearch extends SpecialPage { $term = $search->transformSearchTerm( $term ); Hooks::run( 'SpecialSearchSetupEngine', [ $this, $this->profile, $search ] ); - - $this->setupPage( $term ); - - $out = $this->getOutput(); - - if ( $this->getConfig()->get( 'DisableTextSearch' ) ) { - $searchFowardUrl = $this->getConfig()->get( 'SearchForwardUrl' ); - if ( $searchFowardUrl ) { - $url = str_replace( '$1', urlencode( $term ), $searchFowardUrl ); - $out->redirect( $url ); - } else { - $out->addHTML( - Xml::openElement( 'fieldset' ) . - Xml::element( 'legend', null, $this->msg( 'search-external' )->text() ) . - Xml::element( - 'p', - [ 'class' => 'mw-searchdisabled' ], - $this->msg( 'searchdisabled' )->text() - ) . - $this->msg( 'googlesearch' )->rawParams( - htmlspecialchars( $term ), - 'UTF-8', - $this->msg( 'searchbutton' )->escaped() - )->text() . - Xml::closeElement( 'fieldset' ) - ); - } - + if ( !Hooks::run( 'SpecialSearchResultsPrepend', [ $this, $out, $term ] ) ) { + # Hook requested termination return; } @@ -298,33 +324,6 @@ class SpecialSearch extends SpecialPage { $textMatches = $textStatus->getValue(); } - // did you mean... suggestions - $didYouMeanHtml = ''; - if ( $showSuggestion && $textMatches ) { - if ( $textMatches->hasRewrittenQuery() ) { - $didYouMeanHtml = $this->getDidYouMeanRewrittenHtml( $term, $textMatches ); - } elseif ( $textMatches->hasSuggestion() ) { - $didYouMeanHtml = $this->getDidYouMeanHtml( $textMatches ); - } - } - - if ( !Hooks::run( 'SpecialSearchResultsPrepend', [ $this, $out, $term ] ) ) { - # Hook requested termination - return; - } - - // start rendering the page - $out->addHTML( - Xml::openElement( - 'form', - [ - 'id' => ( $this->isPowerSearch() ? 'powersearch' : 'search' ), - 'method' => 'get', - 'action' => wfScript(), - ] - ) - ); - // Get number of results $titleMatchesNum = $textMatchesNum = $numTitleMatches = $numTextMatches = 0; if ( $titleMatches ) { @@ -334,33 +333,31 @@ class SpecialSearch extends SpecialPage { if ( $textMatches ) { $textMatchesNum = $textMatches->numRows(); $numTextMatches = $textMatches->getTotalHits(); + if ( $textMatchesNum > 0 ) { + $search->augmentSearchResults( $textMatches ); + } } $num = $titleMatchesNum + $textMatchesNum; $totalRes = $numTitleMatches + $numTextMatches; + // start rendering the page $out->enableOOUI(); - $out->addHTML( - # This is an awful awful ID name. It's not a table, but we - # named it poorly from when this was a table so now we're - # stuck with it - Xml::openElement( 'div', [ 'id' => 'mw-search-top-table' ] ) . - $this->shortDialog( $term, $num, $totalRes ) . - Xml::closeElement( 'div' ) . - $this->searchProfileTabs( $term ) . - $this->searchOptions( $term ) . - Xml::closeElement( 'form' ) . - $didYouMeanHtml - ); + $out->addHTML( $formWidget->render( + $this->profile, $term, $num, $totalRes, $this->offset, $this->isPowerSearch() + ) ); - $filePrefix = $wgContLang->getFormattedNsText( NS_FILE ) . ':'; - if ( trim( $term ) === '' || $filePrefix === trim( $term ) ) { - // Empty query -- straight view of search form - return; + // did you mean... suggestions + if ( $textMatches ) { + $dymWidget = new MediaWiki\Widget\Search\DidYouMeanWidget( $this ); + $out->addHTML( $dymWidget->render( $term, $textMatches ) ); } $out->addHTML( "<div class='searchresults'>" ); $hasErrors = $textStatus && $textStatus->getErrors(); + $hasOtherResults = $textMatches && + $textMatches->hasInterwikiResults( SearchResultSet::INLINE_RESULTS ); + if ( $hasErrors ) { list( $error, $warning ) = $textStatus->splitByErrorType(); if ( $error->getErrors() ) { @@ -379,83 +376,51 @@ class SpecialSearch extends SpecialPage { } } - // prev/next links - $prevnext = null; - if ( $num || $this->offset ) { - // Show the create link ahead - $this->showCreateLink( $title, $num, $titleMatches, $textMatches ); - if ( $totalRes > $this->limit || $this->offset ) { - if ( $this->searchEngineType !== null ) { - $this->setExtraParam( 'srbackend', $this->searchEngineType ); - } - $prevnext = $this->getLanguage()->viewPrevNext( - $this->getPageTitle(), - $this->offset, - $this->limit, - $this->powerSearchOptions() + [ 'search' => $term ], - $this->limit + $this->offset >= $totalRes - ); - } - } + // Show the create link ahead + $this->showCreateLink( $title, $num, $titleMatches, $textMatches ); + Hooks::run( 'SpecialSearchResults', [ $term, &$titleMatches, &$textMatches ] ); - $out->parserOptions()->setEditSection( false ); - if ( $titleMatches ) { - if ( $numTitleMatches > 0 ) { - $out->wrapWikiMsg( "==$1==\n", 'titlematches' ); - $out->addHTML( $this->showMatches( $titleMatches ) ); - } - $titleMatches->free(); + // If we have no results and have not already displayed an error message + if ( $num === 0 && !$hasErrors ) { + $out->wrapWikiMsg( "<p class=\"mw-search-nonefound\">\n$1</p>", [ + $hasOtherResults ? 'search-nonefound-thiswiki' : 'search-nonefound', + wfEscapeWikiText( $term ) + ] ); } - if ( $textMatches ) { - // output appropriate heading - if ( $numTextMatches > 0 && $numTitleMatches > 0 ) { - $out->addHTML( '<div class="mw-search-visualclear"></div>' ); - // if no title matches the heading is redundant - $out->wrapWikiMsg( "==$1==\n", 'textmatches' ); - } + // Although $num might be 0 there can still be secondary or inline + // results to display. + $linkRenderer = $this->getLinkRenderer(); + $mainResultWidget = new FullSearchResultWidget( $this, $linkRenderer ); - // show results - if ( $numTextMatches > 0 ) { - $search->augmentSearchResults( $textMatches ); - $out->addHTML( $this->showMatches( $textMatches ) ); - } + if ( $search->getFeatureData( 'enable-new-crossproject-page' ) ) { - // show secondary interwiki results if any - if ( $textMatches->hasInterwikiResults( SearchResultSet::SECONDARY_RESULTS ) ) { - $out->addHTML( $this->showInterwiki( $textMatches->getInterwikiResults( - SearchResultSet::SECONDARY_RESULTS ), $term ) ); - } + $sidebarResultWidget = new InterwikiSearchResultWidget( $this, $linkRenderer ); + $sidebarResultsWidget = new InterwikiSearchResultSetWidget( + $this, + $sidebarResultWidget, + $linkRenderer, + MediaWikiServices::getInstance()->getInterwikiLookup() + ); + } else { + $sidebarResultWidget = new SimpleSearchResultWidget( $this, $linkRenderer ); + $sidebarResultsWidget = new SimpleSearchResultSetWidget( + $this, + $sidebarResultWidget, + $linkRenderer, + MediaWikiServices::getInstance()->getInterwikiLookup() + ); } - $hasOtherResults = $textMatches && - $textMatches->hasInterwikiResults( SearchResultSet::INLINE_RESULTS ); + $widget = new BasicSearchResultSetWidget( $this, $mainResultWidget, $sidebarResultsWidget ); - // If we have no results and we have not already displayed an error message - if ( $num === 0 && !$hasErrors ) { - if ( !$this->offset ) { - // If we have an offset the create link was rendered earlier in this function. - // This class needs a good de-spaghettification, but for now this will - // do the job. - $this->showCreateLink( $title, $num, $titleMatches, $textMatches ); - } - $out->wrapWikiMsg( "<p class=\"mw-search-nonefound\">\n$1</p>", [ - $hasOtherResults ? 'search-nonefound-thiswiki' : 'search-nonefound', - wfEscapeWikiText( $term ) - ] ); - } + $out->addHTML( $widget->render( + $term, $this->offset, $titleMatches, $textMatches + ) ); - if ( $hasOtherResults ) { - foreach ( $textMatches->getInterwikiResults( SearchResultSet::INLINE_RESULTS ) - as $interwiki => $interwikiResult ) { - if ( $interwikiResult instanceof Status || $interwikiResult->numRows() == 0 ) { - // ignore bad interwikis for now - continue; - } - // TODO: wiki header - $out->addHTML( $this->showMatches( $interwikiResult, $interwiki ) ); - } + if ( $titleMatches ) { + $titleMatches->free(); } if ( $textMatches ) { @@ -464,115 +429,25 @@ class SpecialSearch extends SpecialPage { $out->addHTML( '<div class="mw-search-visualclear"></div>' ); - if ( $prevnext ) { + // prev/next links + if ( $totalRes > $this->limit || $this->offset ) { + $prevnext = $this->getLanguage()->viewPrevNext( + $this->getPageTitle(), + $this->offset, + $this->limit, + $this->powerSearchOptions() + [ 'search' => $term ], + $this->limit + $this->offset >= $totalRes + ); $out->addHTML( "<p class='mw-search-pager-bottom'>{$prevnext}</p>\n" ); } + // Close <div class='searchresults'> $out->addHTML( "</div>" ); Hooks::run( 'SpecialSearchResultsAppend', [ $this, $out, $term ] ); } /** - * Produce wiki header for interwiki results - * @param string $interwiki Interwiki name - * @param SearchResultSet $interwikiResult The result set - * @return string - */ - protected function interwikiHeader( $interwiki, $interwikiResult ) { - // TODO: we need to figure out how to name wikis correctly - $wikiMsg = $this->msg( 'search-interwiki-results-' . $interwiki )->parse(); - return "<p class=\"mw-search-interwiki-header mw-search-visualclear\">\n$wikiMsg</p>"; - } - - /** - * Generates HTML shown to the user when we have a suggestion about a query - * that might give more results than their current query. - */ - protected function getDidYouMeanHtml( SearchResultSet $textMatches ) { - # mirror Go/Search behavior of original request .. - $params = [ 'search' => $textMatches->getSuggestionQuery() ]; - if ( $this->fulltext === null ) { - $params['fulltext'] = 'Search'; - } else { - $params['fulltext'] = $this->fulltext; - } - $stParams = array_merge( $params, $this->powerSearchOptions() ); - - $linkRenderer = $this->getLinkRenderer(); - - $snippet = $textMatches->getSuggestionSnippet() ?: null; - if ( $snippet !== null ) { - $snippet = new HtmlArmor( $snippet ); - } - - $suggest = $linkRenderer->makeKnownLink( - $this->getPageTitle(), - $snippet, - [ 'id' => 'mw-search-DYM-suggestion' ], - $stParams - ); - - # HTML of did you mean... search suggestion link - return Html::rawElement( - 'div', - [ 'class' => 'searchdidyoumean' ], - $this->msg( 'search-suggest' )->rawParams( $suggest )->parse() - ); - } - - /** - * Generates HTML shown to user when their query has been internally rewritten, - * and the results of the rewritten query are being returned. - * - * @param string $term The users search input - * @param SearchResultSet $textMatches The response to the users initial search request - * @return string HTML linking the user to their original $term query, and the one - * suggested by $textMatches. - */ - protected function getDidYouMeanRewrittenHtml( $term, SearchResultSet $textMatches ) { - // Showing results for '$rewritten' - // Search instead for '$orig' - - $params = [ 'search' => $textMatches->getQueryAfterRewrite() ]; - if ( $this->fulltext === null ) { - $params['fulltext'] = 'Search'; - } else { - $params['fulltext'] = $this->fulltext; - } - $stParams = array_merge( $params, $this->powerSearchOptions() ); - - $linkRenderer = $this->getLinkRenderer(); - - $snippet = $textMatches->getQueryAfterRewriteSnippet() ?: null; - if ( $snippet !== null ) { - $snippet = new HtmlArmor( $snippet ); - } - - $rewritten = $linkRenderer->makeKnownLink( - $this->getPageTitle(), - $snippet, - [ 'id' => 'mw-search-DYM-rewritten' ], - $stParams - ); - - $stParams['search'] = $term; - $stParams['runsuggestion'] = 0; - $original = $linkRenderer->makeKnownLink( - $this->getPageTitle(), - $term, - [ 'id' => 'mw-search-DYM-original' ], - $stParams - ); - - return Html::rawElement( - 'div', - [ 'class' => 'searchdidyoumean' ], - $this->msg( 'search-rewritten' )->rawParams( $rewritten, $original )->escaped() - ); - } - - /** * @param Title $title * @param int $num The number of search results found * @param null|SearchResultSet $titleMatches Results from title search @@ -622,10 +497,21 @@ class SpecialSearch extends SpecialPage { } /** + * Sets up everything for the HTML output page including styles, javascript, + * page title, etc. + * * @param string $term */ protected function setupPage( $term ) { $out = $this->getOutput(); + + $this->setHeaders(); + $this->outputHeader(); + // TODO: Is this true? The namespace remember uses a user token + // on save. + $out->allowClickjacking(); + $this->addHelpLink( 'Help:Searching' ); + if ( strval( $term ) !== '' ) { $out->setPageTitle( $this->msg( 'searchresults' ) ); $out->setHTMLTitle( $this->msg( 'pagetitle' ) @@ -633,8 +519,13 @@ class SpecialSearch extends SpecialPage { ->inContentLanguage()->text() ); } - // add javascript specific to special:search + + $out->addJsConfigVars( [ 'searchTerm' => $term ] ); $out->addModules( 'mediawiki.special.search' ); + $out->addModuleStyles( [ + 'mediawiki.special', 'mediawiki.special.search.styles', 'mediawiki.ui', 'mediawiki.ui.button', + 'mediawiki.ui.input', 'mediawiki.widgets.SearchInputWidget.styles', + ] ); } /** @@ -666,17 +557,19 @@ class SpecialSearch extends SpecialPage { /** * Reconstruct the 'power search' options for links + * TODO: Instead of exposing this publicly, could we instead expose + * a function for creating search links? * * @return array */ - protected function powerSearchOptions() { + public function powerSearchOptions() { $opt = []; - if ( !$this->isPowerSearch() ) { - $opt['profile'] = $this->profile; - } else { + if ( $this->isPowerSearch() ) { foreach ( $this->namespaces as $n ) { $opt['ns' . $n] = 1; } + } else { + $opt['profile'] = $this->profile; } return $opt + $this->extraParams; @@ -720,236 +613,6 @@ class SpecialSearch extends SpecialPage { } /** - * Show whole set of results - * - * @param SearchResultSet $matches - * @param string $interwiki Interwiki name - * - * @return string - */ - protected function showMatches( $matches, $interwiki = null ) { - global $wgContLang; - - $terms = $wgContLang->convertForSearchResult( $matches->termMatches() ); - $out = ''; - $result = $matches->next(); - $pos = $this->offset; - - if ( $result && $interwiki ) { - $out .= $this->interwikiHeader( $interwiki, $matches ); - } - - $out .= "<ul class='mw-search-results'>\n"; - $widget = new \MediaWiki\Widget\Search\FullSearchResultWidget( - $this, - $this->getLinkRenderer() - ); - while ( $result ) { - $out .= $widget->render( $result, $terms, $pos++ ); - $result = $matches->next(); - } - $out .= "</ul>\n"; - - // convert the whole thing to desired language variant - $out = $wgContLang->convert( $out ); - - return $out; - } - - /** - * Extract custom captions from search-interwiki-custom message - */ - protected function getCustomCaptions() { - if ( is_null( $this->customCaptions ) ) { - $this->customCaptions = []; - // format per line <iwprefix>:<caption> - $customLines = explode( "\n", $this->msg( 'search-interwiki-custom' )->text() ); - foreach ( $customLines as $line ) { - $parts = explode( ":", $line, 2 ); - if ( count( $parts ) == 2 ) { // validate line - $this->customCaptions[$parts[0]] = $parts[1]; - } - } - } - } - - /** - * Show results from other wikis - * - * @param SearchResultSet|array $matches - * @param string $terms - * - * @return string - */ - protected function showInterwiki( $matches, $terms ) { - global $wgContLang; - - // work out custom project captions - $this->getCustomCaptions(); - - if ( !is_array( $matches ) ) { - $matches = [ $matches ]; - } - - $iwResults = []; - foreach ( $matches as $set ) { - $result = $set->next(); - while ( $result ) { - if ( !$result->isBrokenTitle() ) { - $iwResults[$result->getTitle()->getInterwiki()][] = $result; - } - $result = $set->next(); - } - } - - $out = ''; - $widget = new MediaWiki\Widget\Search\SimpleSearchResultWidget( - $this, - $this->getLinkRenderer() - ); - foreach ( $iwResults as $iwPrefix => $results ) { - $out .= $this->iwHeaderHtml( $iwPrefix, $terms ); - $out .= "<ul class='mw-search-iwresults'>"; - foreach ( $results as $result ) { - // This makes the bold asumption interwiki results are never paginated. - // That's currently true, but could change at some point? - $out .= $widget->render( $result, $terms, 0 ); - } - $out .= "</ul>"; - } - - $out = - "<div id='mw-search-interwiki'>" . - "<div id='mw-search-interwiki-caption'>" . - $this->msg( 'search-interwiki-caption' )->escaped() . - "</div>" . - $out . - "</div>"; - - // convert the whole thing to desired language variant - return $wgContLang->convert( $out ); - } - - /** - * @param string $iwPrefix The interwiki prefix to render a header for - * @param string $terms The user-provided search terms - */ - protected function iwHeaderHtml( $iwPrefix, $terms ) { - if ( isset( $this->customCaptions[$iwPrefix] ) ) { - $caption = $this->customCaptions[$iwPrefix]; - } else { - $iwLookup = MediaWiki\MediaWikiServices::getInstance()->getInterwikiLookup(); - $interwiki = $iwLookup->fetch( $iwPrefix ); - $parsed = wfParseUrl( wfExpandUrl( $interwiki ? $interwiki->getURL() : '/' ) ); - $caption = $this->msg( 'search-interwiki-default', $parsed['host'] )->text(); - } - $searchLink = Linker::linkKnown( - Title::newFromText( "$iwPrefix:Special:Search" ), - $this->msg( 'search-interwiki-more' )->text(), - [], - [ - 'search' => $terms, - 'fulltext' => 1, - ] - ); - return - "<div class='mw-search-interwiki-project'>" . - "<span class='mw-search-interwiki-more'>{$searchLink}</span>" . - $caption . - "</div>"; - } - - /** - * Generates the power search box at [[Special:Search]] - * - * @param string $term Search term - * @param array $opts - * @return string HTML form - */ - protected function powerSearchBox( $term, $opts ) { - global $wgContLang; - - // Groups namespaces into rows according to subject - $rows = []; - foreach ( $this->searchConfig->searchableNamespaces() as $namespace => $name ) { - $subject = MWNamespace::getSubject( $namespace ); - if ( !array_key_exists( $subject, $rows ) ) { - $rows[$subject] = ""; - } - - $name = $wgContLang->getConverter()->convertNamespace( $namespace ); - if ( $name == '' ) { - $name = $this->msg( 'blanknamespace' )->text(); - } - - $rows[$subject] .= - Xml::openElement( 'td' ) . - Xml::checkLabel( - $name, - "ns{$namespace}", - "mw-search-ns{$namespace}", - in_array( $namespace, $this->namespaces ) - ) . - Xml::closeElement( 'td' ); - } - - $rows = array_values( $rows ); - $numRows = count( $rows ); - - // Lays out namespaces in multiple floating two-column tables so they'll - // be arranged nicely while still accommodating different screen widths - $namespaceTables = ''; - for ( $i = 0; $i < $numRows; $i += 4 ) { - $namespaceTables .= Xml::openElement( 'table' ); - - for ( $j = $i; $j < $i + 4 && $j < $numRows; $j++ ) { - $namespaceTables .= Xml::tags( 'tr', null, $rows[$j] ); - } - - $namespaceTables .= Xml::closeElement( 'table' ); - } - - $showSections = [ 'namespaceTables' => $namespaceTables ]; - - Hooks::run( 'SpecialSearchPowerBox', [ &$showSections, $term, $opts ] ); - - $hidden = ''; - foreach ( $opts as $key => $value ) { - $hidden .= Html::hidden( $key, $value ); - } - - # Stuff to feed saveNamespaces() - $remember = ''; - $user = $this->getUser(); - if ( $user->isLoggedIn() ) { - $remember .= Xml::checkLabel( - $this->msg( 'powersearch-remember' )->text(), - 'nsRemember', - 'mw-search-powersearch-remember', - false, - // The token goes here rather than in a hidden field so it - // is only sent when necessary (not every form submission). - [ 'value' => $user->getEditToken( - 'searchnamespace', - $this->getRequest() - ) ] - ); - } - - // Return final output - return Xml::openElement( 'fieldset', [ 'id' => 'mw-searchoptions' ] ) . - Xml::element( 'legend', null, $this->msg( 'powersearch-legend' )->text() ) . - Xml::tags( 'h4', null, $this->msg( 'powersearch-ns' )->parse() ) . - Xml::element( 'div', [ 'id' => 'mw-search-togglebox' ], '', false ) . - Xml::element( 'div', [ 'class' => 'divider' ], '', false ) . - implode( Xml::element( 'div', [ 'class' => 'divider' ], '', false ), $showSections ) . - $hidden . - Xml::element( 'div', [ 'class' => 'divider' ], '', false ) . - $remember . - Xml::closeElement( 'fieldset' ); - } - - /** * @return array */ protected function getSearchProfiles() { @@ -995,169 +658,6 @@ class SpecialSearch extends SpecialPage { } /** - * @param string $term - * @return string - */ - protected function searchProfileTabs( $term ) { - $out = Html::element( 'div', [ 'class' => 'mw-search-visualclear' ] ) . - Xml::openElement( 'div', [ 'class' => 'mw-search-profile-tabs' ] ); - - $bareterm = $term; - if ( $this->startsWithImage( $term ) ) { - // Deletes prefixes - $bareterm = substr( $term, strpos( $term, ':' ) + 1 ); - } - - $profiles = $this->getSearchProfiles(); - $lang = $this->getLanguage(); - - // Outputs XML for Search Types - $out .= Xml::openElement( 'div', [ 'class' => 'search-types' ] ); - $out .= Xml::openElement( 'ul' ); - foreach ( $profiles as $id => $profile ) { - if ( !isset( $profile['parameters'] ) ) { - $profile['parameters'] = []; - } - $profile['parameters']['profile'] = $id; - - $tooltipParam = isset( $profile['namespace-messages'] ) ? - $lang->commaList( $profile['namespace-messages'] ) : null; - $out .= Xml::tags( - 'li', - [ - 'class' => $this->profile === $id ? 'current' : 'normal' - ], - $this->makeSearchLink( - $bareterm, - [], - $this->msg( $profile['message'] )->text(), - $this->msg( $profile['tooltip'], $tooltipParam )->text(), - $profile['parameters'] - ) - ); - } - $out .= Xml::closeElement( 'ul' ); - $out .= Xml::closeElement( 'div' ); - $out .= Xml::element( 'div', [ 'style' => 'clear:both' ], '', false ); - $out .= Xml::closeElement( 'div' ); - - return $out; - } - - /** - * @param string $term Search term - * @return string - */ - protected function searchOptions( $term ) { - $out = ''; - $opts = []; - $opts['profile'] = $this->profile; - - if ( $this->isPowerSearch() ) { - $out .= $this->powerSearchBox( $term, $opts ); - } else { - $form = ''; - Hooks::run( 'SpecialSearchProfileForm', [ $this, &$form, $this->profile, $term, $opts ] ); - $out .= $form; - } - - return $out; - } - - /** - * @param string $term - * @param int $resultsShown - * @param int $totalNum - * @return string - */ - protected function shortDialog( $term, $resultsShown, $totalNum ) { - $searchWidget = new MediaWiki\Widget\SearchInputWidget( [ - 'id' => 'searchText', - 'name' => 'search', - 'autofocus' => trim( $term ) === '', - 'value' => $term, - 'dataLocation' => 'content', - 'infusable' => true, - ] ); - - $layout = new OOUI\ActionFieldLayout( $searchWidget, new OOUI\ButtonInputWidget( [ - 'type' => 'submit', - 'label' => $this->msg( 'searchbutton' )->text(), - 'flags' => [ 'progressive', 'primary' ], - ] ), [ - 'align' => 'top', - ] ); - - $out = - Html::hidden( 'title', $this->getPageTitle()->getPrefixedText() ) . - Html::hidden( 'profile', $this->profile ) . - Html::hidden( 'fulltext', 'Search' ) . - $layout; - - // Results-info - if ( $totalNum > 0 && $this->offset < $totalNum ) { - $top = $this->msg( 'search-showingresults' ) - ->numParams( $this->offset + 1, $this->offset + $resultsShown, $totalNum ) - ->numParams( $resultsShown ) - ->parse(); - $out .= Xml::tags( 'div', [ 'class' => 'results-info' ], $top ); - } - - return $out; - } - - /** - * Make a search link with some target namespaces - * - * @param string $term - * @param array $namespaces Ignored - * @param string $label Link's text - * @param string $tooltip Link's tooltip - * @param array $params Query string parameters - * @return string HTML fragment - */ - protected function makeSearchLink( $term, $namespaces, $label, $tooltip, $params = [] ) { - $opt = $params; - foreach ( $namespaces as $n ) { - $opt['ns' . $n] = 1; - } - - $stParams = array_merge( - [ - 'search' => $term, - 'fulltext' => $this->msg( 'search' )->text() - ], - $opt - ); - - return Xml::element( - 'a', - [ - 'href' => $this->getPageTitle()->getLocalURL( $stParams ), - 'title' => $tooltip - ], - $label - ); - } - - /** - * Check if query starts with image: prefix - * - * @param string $term The string to check - * @return bool - */ - protected function startsWithImage( $term ) { - global $wgContLang; - - $parts = explode( ':', $term ); - if ( count( $parts ) > 1 ) { - return $wgContLang->getNsIndex( $parts[0] ) == NS_FILE; - } - - return false; - } - - /** * @since 1.18 * * @return SearchEngine diff --git a/includes/specials/SpecialShortpages.php b/includes/specials/SpecialShortpages.php index a78b082432d7..3282a7a1cd41 100644 --- a/includes/specials/SpecialShortpages.php +++ b/includes/specials/SpecialShortpages.php @@ -21,6 +21,9 @@ * @ingroup SpecialPage */ +use Wikimedia\Rdbms\ResultWrapper; +use Wikimedia\Rdbms\IDatabase; + /** * SpecialShortpages extends QueryPage. It is used to return the shortest * pages in the database. diff --git a/includes/specials/SpecialStatistics.php b/includes/specials/SpecialStatistics.php index 3342c32bbfef..19850e6a2dd2 100644 --- a/includes/specials/SpecialStatistics.php +++ b/includes/specials/SpecialStatistics.php @@ -95,8 +95,11 @@ class SpecialStatistics extends SpecialPage { if ( !$msg->isDisabled() ) { $descriptionHtml = $this->msg( 'parentheses' )->rawParams( $msg->parse() ) ->escaped(); - $text .= "<br />" . Html::rawElement( 'small', [ 'class' => 'mw-statistic-desc' ], - " $descriptionHtml" ); + $text .= "<br />" . Html::rawElement( + 'small', + [ 'class' => 'mw-statistic-desc' ], + " $descriptionHtml" + ); } } @@ -119,8 +122,10 @@ class SpecialStatistics extends SpecialPage { Xml::tags( 'th', [ 'colspan' => '2' ], $this->msg( 'statistics-header-pages' ) ->parse() ) . Xml::closeElement( 'tr' ) . - $this->formatRow( $linkRenderer->makeKnownLink( $specialAllPagesTitle, - $this->msg( 'statistics-articles' )->text(), [], [ 'hideredirects' => 1 ] ), + $this->formatRow( $linkRenderer->makeKnownLink( + $specialAllPagesTitle, + $this->msg( 'statistics-articles' )->text(), + [], [ 'hideredirects' => 1 ] ), $this->getLanguage()->formatNum( $this->good ), [ 'class' => 'mw-statistics-articles' ], 'statistics-articles-desc' ) . @@ -152,9 +157,9 @@ class SpecialStatistics extends SpecialPage { [ 'class' => 'mw-statistics-edits' ] ) . $this->formatRow( $this->msg( 'statistics-edits-average' )->parse(), - $this->getLanguage() - ->formatNum( sprintf( '%.2f', $this->total ? $this->edits / $this->total : 0 ) ), - [ 'class' => 'mw-statistics-edits-average' ] + $this->getLanguage()->formatNum( + sprintf( '%.2f', $this->total ? $this->edits / $this->total : 0 ) + ), [ 'class' => 'mw-statistics-edits-average' ] ); } @@ -175,7 +180,8 @@ class SpecialStatistics extends SpecialPage { $this->getLanguage()->formatNum( $this->activeUsers ), [ 'class' => 'mw-statistics-users-active' ], 'statistics-users-active-desc', - $this->getLanguage()->formatNum( $this->getConfig()->get( 'ActiveUserDays' ) ) + $this->getLanguage()->formatNum( + $this->getConfig()->get( 'ActiveUserDays' ) ) ); } @@ -184,7 +190,8 @@ class SpecialStatistics extends SpecialPage { $text = ''; foreach ( $this->getConfig()->get( 'GroupPermissions' ) as $group => $permissions ) { # Skip generic * and implicit groups - if ( in_array( $group, $this->getConfig()->get( 'ImplicitGroups' ) ) || $group == '*' ) { + if ( in_array( $group, $this->getConfig()->get( 'ImplicitGroups' ) ) + || $group == '*' ) { continue; } $groupname = htmlspecialchars( $group ); @@ -196,7 +203,8 @@ class SpecialStatistics extends SpecialPage { } $msg = $this->msg( 'grouppage-' . $groupname )->inContentLanguage(); if ( $msg->isBlank() ) { - $grouppageLocalized = MWNamespace::getCanonicalName( NS_PROJECT ) . ':' . $groupname; + $grouppageLocalized = MWNamespace::getCanonicalName( NS_PROJECT ) . + ':' . $groupname; } else { $grouppageLocalized = $msg->text(); } diff --git a/includes/specials/SpecialTrackingCategories.php b/includes/specials/SpecialTrackingCategories.php index 8ff052785eb0..e503d92b41c0 100644 --- a/includes/specials/SpecialTrackingCategories.php +++ b/includes/specials/SpecialTrackingCategories.php @@ -36,26 +36,6 @@ class SpecialTrackingCategories extends SpecialPage { parent::__construct( 'TrackingCategories' ); } - /** - * Tracking categories that exist in core - * - * @var array - */ - private static $coreTrackingCategories = [ - 'index-category', - 'noindex-category', - 'duplicate-args-category', - 'expensive-parserfunction-category', - 'post-expand-template-argument-category', - 'post-expand-template-inclusion-category', - 'hidden-category-category', - 'broken-file-category', - 'node-count-exceeded-category', - 'expansion-depth-exceeded-category', - 'restricted-displaytitle-ignored', - 'deprecated-self-close-category', - ]; - function execute( $par ) { $this->setHeaders(); $this->outputHeader(); @@ -76,10 +56,11 @@ class SpecialTrackingCategories extends SpecialPage { </tr></thead>" ); - $trackingCategories = $this->prepareTrackingCategoriesData(); + $trackingCategories = new TrackingCategories( $this->getConfig() ); + $categoryList = $trackingCategories->getTrackingCategories(); $batch = new LinkBatch(); - foreach ( $trackingCategories as $catMsg => $data ) { + foreach ( $categoryList as $catMsg => $data ) { $batch->addObj( $data['msg'] ); foreach ( $data['cats'] as $catTitle ) { $batch->addObj( $catTitle ); @@ -87,11 +68,11 @@ class SpecialTrackingCategories extends SpecialPage { } $batch->execute(); - Hooks::run( 'SpecialTrackingCategories::preprocess', [ $this, $trackingCategories ] ); + Hooks::run( 'SpecialTrackingCategories::preprocess', [ $this, $categoryList ] ); $linkRenderer = $this->getLinkRenderer(); - foreach ( $trackingCategories as $catMsg => $data ) { + foreach ( $categoryList as $catMsg => $data ) { $allMsgs = []; $catDesc = $catMsg . '-desc'; @@ -143,80 +124,6 @@ class SpecialTrackingCategories extends SpecialPage { $this->getOutput()->addHTML( Html::closeElement( 'table' ) ); } - /** - * Read the global and extract title objects from the corresponding messages - * @return array Array( 'msg' => Title, 'cats' => Title[] ) - */ - private function prepareTrackingCategoriesData() { - $categories = array_merge( - self::$coreTrackingCategories, - ExtensionRegistry::getInstance()->getAttribute( 'TrackingCategories' ), - $this->getConfig()->get( 'TrackingCategories' ) // deprecated - ); - - // Only show magic link tracking categories if they are enabled - $enableMagicLinks = $this->getConfig()->get( 'EnableMagicLinks' ); - if ( $enableMagicLinks['ISBN'] ) { - $categories[] = 'magiclink-tracking-isbn'; - } - if ( $enableMagicLinks['RFC'] ) { - $categories[] = 'magiclink-tracking-rfc'; - } - if ( $enableMagicLinks['PMID'] ) { - $categories[] = 'magiclink-tracking-pmid'; - } - - $trackingCategories = []; - foreach ( $categories as $catMsg ) { - /* - * Check if the tracking category varies by namespace - * Otherwise only pages in the current namespace will be displayed - * If it does vary, show pages considering all namespaces - */ - $msgObj = $this->msg( $catMsg )->inContentLanguage(); - $allCats = []; - $catMsgTitle = Title::makeTitleSafe( NS_MEDIAWIKI, $catMsg ); - if ( !$catMsgTitle ) { - continue; - } - - // Match things like {{NAMESPACE}} and {{NAMESPACENUMBER}}. - // False positives are ok, this is just an efficiency shortcut - if ( strpos( $msgObj->plain(), '{{' ) !== false ) { - $ns = MWNamespace::getValidNamespaces(); - foreach ( $ns as $namesp ) { - $tempTitle = Title::makeTitleSafe( $namesp, $catMsg ); - if ( !$tempTitle ) { - continue; - } - $catName = $msgObj->title( $tempTitle )->text(); - # Allow tracking categories to be disabled by setting them to "-" - if ( $catName !== '-' ) { - $catTitle = Title::makeTitleSafe( NS_CATEGORY, $catName ); - if ( $catTitle ) { - $allCats[] = $catTitle; - } - } - } - } else { - $catName = $msgObj->text(); - # Allow tracking categories to be disabled by setting them to "-" - if ( $catName !== '-' ) { - $catTitle = Title::makeTitleSafe( NS_CATEGORY, $catName ); - if ( $catTitle ) { - $allCats[] = $catTitle; - } - } - } - $trackingCategories[$catMsg] = [ - 'cats' => $allCats, - 'msg' => $catMsgTitle, - ]; - } - - return $trackingCategories; - } - protected function getGroupName() { return 'pages'; } diff --git a/includes/specials/SpecialUnblock.php b/includes/specials/SpecialUnblock.php index 0d42e3fc7022..01125fcff084 100644 --- a/includes/specials/SpecialUnblock.php +++ b/includes/specials/SpecialUnblock.php @@ -186,7 +186,7 @@ class SpecialUnblock extends SpecialPage { return [ [ 'ipb_cant_unblock', $target ] ]; } - # bug 15810: blocked admins should have limited access here. This + # T17810: blocked admins should have limited access here. This # won't allow sysops to remove autoblocks on themselves, but they # should have ipblock-exempt anyway $status = SpecialBlock::checkUnblockSelf( $target, $performer ); diff --git a/includes/specials/SpecialUncategorizedcategories.php b/includes/specials/SpecialUncategorizedcategories.php index 90dfdc57bd45..77b69264577a 100644 --- a/includes/specials/SpecialUncategorizedcategories.php +++ b/includes/specials/SpecialUncategorizedcategories.php @@ -40,7 +40,7 @@ class UncategorizedCategoriesPage extends UncategorizedPagesPage { } /** - * Returns an array of categorie titles (usually without the namespace), which + * Returns an array of category titles (usually without the namespace), which * shouldn't be listed on this page, even if they're uncategorized. * * @return array diff --git a/includes/specials/SpecialUndelete.php b/includes/specials/SpecialUndelete.php index 4c6a5938388f..eb4f0cc07725 100644 --- a/includes/specials/SpecialUndelete.php +++ b/includes/specials/SpecialUndelete.php @@ -20,730 +20,9 @@ * @file * @ingroup SpecialPage */ -use MediaWiki\MediaWikiServices; - -/** - * Used to show archived pages and eventually restore them. - * - * @ingroup SpecialPage - */ -class PageArchive { - /** @var Title */ - protected $title; - - /** @var Status */ - protected $fileStatus; - - /** @var Status */ - protected $revisionStatus; - - /** @var Config */ - protected $config; - - function __construct( $title, Config $config = null ) { - if ( is_null( $title ) ) { - throw new MWException( __METHOD__ . ' given a null title.' ); - } - $this->title = $title; - if ( $config === null ) { - wfDebug( __METHOD__ . ' did not have a Config object passed to it' ); - $config = MediaWikiServices::getInstance()->getMainConfig(); - } - $this->config = $config; - } - - public function doesWrites() { - return true; - } - - /** - * List all deleted pages recorded in the archive table. Returns result - * wrapper with (ar_namespace, ar_title, count) fields, ordered by page - * namespace/title. - * - * @return ResultWrapper - */ - public static function listAllPages() { - $dbr = wfGetDB( DB_REPLICA ); - - return self::listPages( $dbr, '' ); - } - - /** - * List deleted pages recorded in the archive table matching the - * given title prefix. - * Returns result wrapper with (ar_namespace, ar_title, count) fields. - * - * @param string $prefix Title prefix - * @return ResultWrapper - */ - public static function listPagesByPrefix( $prefix ) { - $dbr = wfGetDB( DB_REPLICA ); - - $title = Title::newFromText( $prefix ); - if ( $title ) { - $ns = $title->getNamespace(); - $prefix = $title->getDBkey(); - } else { - // Prolly won't work too good - // @todo handle bare namespace names cleanly? - $ns = 0; - } - - $conds = [ - 'ar_namespace' => $ns, - 'ar_title' . $dbr->buildLike( $prefix, $dbr->anyString() ), - ]; - - return self::listPages( $dbr, $conds ); - } - - /** - * @param IDatabase $dbr - * @param string|array $condition - * @return bool|ResultWrapper - */ - protected static function listPages( $dbr, $condition ) { - return $dbr->select( - [ 'archive' ], - [ - 'ar_namespace', - 'ar_title', - 'count' => 'COUNT(*)' - ], - $condition, - __METHOD__, - [ - 'GROUP BY' => [ 'ar_namespace', 'ar_title' ], - 'ORDER BY' => [ 'ar_namespace', 'ar_title' ], - 'LIMIT' => 100, - ] - ); - } - - /** - * List the revisions of the given page. Returns result wrapper with - * (ar_minor_edit, ar_timestamp, ar_user, ar_user_text, ar_comment) fields. - * - * @return ResultWrapper - */ - function listRevisions() { - $dbr = wfGetDB( DB_REPLICA ); - - $tables = [ 'archive' ]; - - $fields = [ - 'ar_minor_edit', 'ar_timestamp', 'ar_user', 'ar_user_text', - 'ar_comment', 'ar_len', 'ar_deleted', 'ar_rev_id', 'ar_sha1', - ]; - - if ( $this->config->get( 'ContentHandlerUseDB' ) ) { - $fields[] = 'ar_content_format'; - $fields[] = 'ar_content_model'; - } - - $conds = [ 'ar_namespace' => $this->title->getNamespace(), - 'ar_title' => $this->title->getDBkey() ]; - - $options = [ 'ORDER BY' => 'ar_timestamp DESC' ]; - - $join_conds = []; - - ChangeTags::modifyDisplayQuery( - $tables, - $fields, - $conds, - $join_conds, - $options, - '' - ); - - return $dbr->select( $tables, - $fields, - $conds, - __METHOD__, - $options, - $join_conds - ); - } - - /** - * List the deleted file revisions for this page, if it's a file page. - * Returns a result wrapper with various filearchive fields, or null - * if not a file page. - * - * @return ResultWrapper - * @todo Does this belong in Image for fuller encapsulation? - */ - function listFiles() { - if ( $this->title->getNamespace() != NS_FILE ) { - return null; - } - - $dbr = wfGetDB( DB_REPLICA ); - return $dbr->select( - 'filearchive', - ArchivedFile::selectFields(), - [ 'fa_name' => $this->title->getDBkey() ], - __METHOD__, - [ 'ORDER BY' => 'fa_timestamp DESC' ] - ); - } - - /** - * Return a Revision object containing data for the deleted revision. - * Note that the result *may* or *may not* have a null page ID. - * - * @param string $timestamp - * @return Revision|null - */ - function getRevision( $timestamp ) { - $dbr = wfGetDB( DB_REPLICA ); - - $fields = [ - 'ar_rev_id', - 'ar_text', - 'ar_comment', - 'ar_user', - 'ar_user_text', - 'ar_timestamp', - 'ar_minor_edit', - 'ar_flags', - 'ar_text_id', - 'ar_deleted', - 'ar_len', - 'ar_sha1', - ]; - - if ( $this->config->get( 'ContentHandlerUseDB' ) ) { - $fields[] = 'ar_content_format'; - $fields[] = 'ar_content_model'; - } - - $row = $dbr->selectRow( 'archive', - $fields, - [ 'ar_namespace' => $this->title->getNamespace(), - 'ar_title' => $this->title->getDBkey(), - 'ar_timestamp' => $dbr->timestamp( $timestamp ) ], - __METHOD__ ); - - if ( $row ) { - return Revision::newFromArchiveRow( $row, [ 'title' => $this->title ] ); - } - - return null; - } - - /** - * Return the most-previous revision, either live or deleted, against - * the deleted revision given by timestamp. - * - * May produce unexpected results in case of history merges or other - * unusual time issues. - * - * @param string $timestamp - * @return Revision|null Null when there is no previous revision - */ - function getPreviousRevision( $timestamp ) { - $dbr = wfGetDB( DB_REPLICA ); - - // Check the previous deleted revision... - $row = $dbr->selectRow( 'archive', - 'ar_timestamp', - [ 'ar_namespace' => $this->title->getNamespace(), - 'ar_title' => $this->title->getDBkey(), - 'ar_timestamp < ' . - $dbr->addQuotes( $dbr->timestamp( $timestamp ) ) ], - __METHOD__, - [ - 'ORDER BY' => 'ar_timestamp DESC', - 'LIMIT' => 1 ] ); - $prevDeleted = $row ? wfTimestamp( TS_MW, $row->ar_timestamp ) : false; - - $row = $dbr->selectRow( [ 'page', 'revision' ], - [ 'rev_id', 'rev_timestamp' ], - [ - 'page_namespace' => $this->title->getNamespace(), - 'page_title' => $this->title->getDBkey(), - 'page_id = rev_page', - 'rev_timestamp < ' . - $dbr->addQuotes( $dbr->timestamp( $timestamp ) ) ], - __METHOD__, - [ - 'ORDER BY' => 'rev_timestamp DESC', - 'LIMIT' => 1 ] ); - $prevLive = $row ? wfTimestamp( TS_MW, $row->rev_timestamp ) : false; - $prevLiveId = $row ? intval( $row->rev_id ) : null; - - if ( $prevLive && $prevLive > $prevDeleted ) { - // Most prior revision was live - return Revision::newFromId( $prevLiveId ); - } elseif ( $prevDeleted ) { - // Most prior revision was deleted - return $this->getRevision( $prevDeleted ); - } - - // No prior revision on this page. - return null; - } - - /** - * Get the text from an archive row containing ar_text, ar_flags and ar_text_id - * - * @param object $row Database row - * @return string - */ - function getTextFromRow( $row ) { - if ( is_null( $row->ar_text_id ) ) { - // An old row from MediaWiki 1.4 or previous. - // Text is embedded in this row in classic compression format. - return Revision::getRevisionText( $row, 'ar_' ); - } - - // New-style: keyed to the text storage backend. - $dbr = wfGetDB( DB_REPLICA ); - $text = $dbr->selectRow( 'text', - [ 'old_text', 'old_flags' ], - [ 'old_id' => $row->ar_text_id ], - __METHOD__ ); - - return Revision::getRevisionText( $text ); - } - - /** - * Fetch (and decompress if necessary) the stored text of the most - * recently edited deleted revision of the page. - * - * If there are no archived revisions for the page, returns NULL. - * - * @return string|null - */ - function getLastRevisionText() { - $dbr = wfGetDB( DB_REPLICA ); - $row = $dbr->selectRow( 'archive', - [ 'ar_text', 'ar_flags', 'ar_text_id' ], - [ 'ar_namespace' => $this->title->getNamespace(), - 'ar_title' => $this->title->getDBkey() ], - __METHOD__, - [ 'ORDER BY' => 'ar_timestamp DESC' ] ); - - if ( $row ) { - return $this->getTextFromRow( $row ); - } - - return null; - } - /** - * Quick check if any archived revisions are present for the page. - * - * @return bool - */ - function isDeleted() { - $dbr = wfGetDB( DB_REPLICA ); - $n = $dbr->selectField( 'archive', 'COUNT(ar_title)', - [ 'ar_namespace' => $this->title->getNamespace(), - 'ar_title' => $this->title->getDBkey() ], - __METHOD__ - ); - - return ( $n > 0 ); - } - - /** - * Restore the given (or all) text and file revisions for the page. - * Once restored, the items will be removed from the archive tables. - * The deletion log will be updated with an undeletion notice. - * - * This also sets Status objects, $this->fileStatus and $this->revisionStatus - * (depending what operations are attempted). - * - * @param array $timestamps Pass an empty array to restore all revisions, - * otherwise list the ones to undelete. - * @param string $comment - * @param array $fileVersions - * @param bool $unsuppress - * @param User $user User performing the action, or null to use $wgUser - * @param string|string[] $tags Change tags to add to log entry - * ($user should be able to add the specified tags before this is called) - * @return array(number of file revisions restored, number of image revisions - * restored, log message) on success, false on failure. - */ - function undelete( $timestamps, $comment = '', $fileVersions = [], - $unsuppress = false, User $user = null, $tags = null - ) { - // If both the set of text revisions and file revisions are empty, - // restore everything. Otherwise, just restore the requested items. - $restoreAll = empty( $timestamps ) && empty( $fileVersions ); - - $restoreText = $restoreAll || !empty( $timestamps ); - $restoreFiles = $restoreAll || !empty( $fileVersions ); - - if ( $restoreFiles && $this->title->getNamespace() == NS_FILE ) { - $img = wfLocalFile( $this->title ); - $img->load( File::READ_LATEST ); - $this->fileStatus = $img->restore( $fileVersions, $unsuppress ); - if ( !$this->fileStatus->isOK() ) { - return false; - } - $filesRestored = $this->fileStatus->successCount; - } else { - $filesRestored = 0; - } - - if ( $restoreText ) { - $this->revisionStatus = $this->undeleteRevisions( $timestamps, $unsuppress, $comment ); - if ( !$this->revisionStatus->isOK() ) { - return false; - } - - $textRestored = $this->revisionStatus->getValue(); - } else { - $textRestored = 0; - } - - // Touch the log! - - if ( $textRestored && $filesRestored ) { - $reason = wfMessage( 'undeletedrevisions-files' ) - ->numParams( $textRestored, $filesRestored )->inContentLanguage()->text(); - } elseif ( $textRestored ) { - $reason = wfMessage( 'undeletedrevisions' )->numParams( $textRestored ) - ->inContentLanguage()->text(); - } elseif ( $filesRestored ) { - $reason = wfMessage( 'undeletedfiles' )->numParams( $filesRestored ) - ->inContentLanguage()->text(); - } else { - wfDebug( "Undelete: nothing undeleted...\n" ); - - return false; - } - - if ( trim( $comment ) != '' ) { - $reason .= wfMessage( 'colon-separator' )->inContentLanguage()->text() . $comment; - } - - if ( $user === null ) { - global $wgUser; - $user = $wgUser; - } - - $logEntry = new ManualLogEntry( 'delete', 'restore' ); - $logEntry->setPerformer( $user ); - $logEntry->setTarget( $this->title ); - $logEntry->setComment( $reason ); - $logEntry->setTags( $tags ); - - Hooks::run( 'ArticleUndeleteLogEntry', [ $this, &$logEntry, $user ] ); - - $logid = $logEntry->insert(); - $logEntry->publish( $logid ); - - return [ $textRestored, $filesRestored, $reason ]; - } - - /** - * This is the meaty bit -- It restores archived revisions of the given page - * to the revision table. - * - * @param array $timestamps Pass an empty array to restore all revisions, - * otherwise list the ones to undelete. - * @param bool $unsuppress Remove all ar_deleted/fa_deleted restrictions of seletected revs - * @param string $comment - * @throws ReadOnlyError - * @return Status Status object containing the number of revisions restored on success - */ - private function undeleteRevisions( $timestamps, $unsuppress = false, $comment = '' ) { - if ( wfReadOnly() ) { - throw new ReadOnlyError(); - } - - $dbw = wfGetDB( DB_MASTER ); - $dbw->startAtomic( __METHOD__ ); - - $restoreAll = empty( $timestamps ); - - # Does this page already exist? We'll have to update it... - $article = WikiPage::factory( $this->title ); - # Load latest data for the current page (bug 31179) - $article->loadPageData( 'fromdbmaster' ); - $oldcountable = $article->isCountable(); - - $page = $dbw->selectRow( 'page', - [ 'page_id', 'page_latest' ], - [ 'page_namespace' => $this->title->getNamespace(), - 'page_title' => $this->title->getDBkey() ], - __METHOD__, - [ 'FOR UPDATE' ] // lock page - ); - - if ( $page ) { - $makepage = false; - # Page already exists. Import the history, and if necessary - # we'll update the latest revision field in the record. - - # Get the time span of this page - $previousTimestamp = $dbw->selectField( 'revision', 'rev_timestamp', - [ 'rev_id' => $page->page_latest ], - __METHOD__ ); - - if ( $previousTimestamp === false ) { - wfDebug( __METHOD__ . ": existing page refers to a page_latest that does not exist\n" ); - - $status = Status::newGood( 0 ); - $status->warning( 'undeleterevision-missing' ); - $dbw->endAtomic( __METHOD__ ); - - return $status; - } - } else { - # Have to create a new article... - $makepage = true; - $previousTimestamp = 0; - } - - $oldWhere = [ - 'ar_namespace' => $this->title->getNamespace(), - 'ar_title' => $this->title->getDBkey(), - ]; - if ( !$restoreAll ) { - $oldWhere['ar_timestamp'] = array_map( [ &$dbw, 'timestamp' ], $timestamps ); - } - - $fields = [ - 'ar_id', - 'ar_rev_id', - 'rev_id', - 'ar_text', - 'ar_comment', - 'ar_user', - 'ar_user_text', - 'ar_timestamp', - 'ar_minor_edit', - 'ar_flags', - 'ar_text_id', - 'ar_deleted', - 'ar_page_id', - 'ar_len', - 'ar_sha1' - ]; - - if ( $this->config->get( 'ContentHandlerUseDB' ) ) { - $fields[] = 'ar_content_format'; - $fields[] = 'ar_content_model'; - } - - /** - * Select each archived revision... - */ - $result = $dbw->select( - [ 'archive', 'revision' ], - $fields, - $oldWhere, - __METHOD__, - /* options */ - [ 'ORDER BY' => 'ar_timestamp' ], - [ 'revision' => [ 'LEFT JOIN', 'ar_rev_id=rev_id' ] ] - ); - - $rev_count = $result->numRows(); - if ( !$rev_count ) { - wfDebug( __METHOD__ . ": no revisions to restore\n" ); - - $status = Status::newGood( 0 ); - $status->warning( "undelete-no-results" ); - $dbw->endAtomic( __METHOD__ ); - - return $status; - } - - // We use ar_id because there can be duplicate ar_rev_id even for the same - // page. In this case, we may be able to restore the first one. - $restoreFailedArIds = []; - - // Map rev_id to the ar_id that is allowed to use it. When checking later, - // if it doesn't match, the current ar_id can not be restored. - - // Value can be an ar_id or -1 (-1 means no ar_id can use it, since the - // rev_id is taken before we even start the restore). - $allowedRevIdToArIdMap = []; - - $latestRestorableRow = null; - - foreach ( $result as $row ) { - if ( $row->ar_rev_id ) { - // rev_id is taken even before we start restoring. - if ( $row->ar_rev_id === $row->rev_id ) { - $restoreFailedArIds[] = $row->ar_id; - $allowedRevIdToArIdMap[$row->ar_rev_id] = -1; - } else { - // rev_id is not taken yet in the DB, but it might be taken - // by a prior revision in the same restore operation. If - // not, we need to reserve it. - if ( isset( $allowedRevIdToArIdMap[$row->ar_rev_id] ) ) { - $restoreFailedArIds[] = $row->ar_id; - } else { - $allowedRevIdToArIdMap[$row->ar_rev_id] = $row->ar_id; - $latestRestorableRow = $row; - } - } - } else { - // If ar_rev_id is null, there can't be a collision, and a - // rev_id will be chosen automatically. - $latestRestorableRow = $row; - } - } - - $result->seek( 0 ); // move back - - $oldPageId = 0; - if ( $latestRestorableRow !== null ) { - $oldPageId = (int)$latestRestorableRow->ar_page_id; // pass this to ArticleUndelete hook - - // grab the content to check consistency with global state before restoring the page. - $revision = Revision::newFromArchiveRow( $latestRestorableRow, - [ - 'title' => $article->getTitle(), // used to derive default content model - ] - ); - $user = User::newFromName( $revision->getUserText( Revision::RAW ), false ); - $content = $revision->getContent( Revision::RAW ); - - // NOTE: article ID may not be known yet. prepareSave() should not modify the database. - $status = $content->prepareSave( $article, 0, -1, $user ); - if ( !$status->isOK() ) { - $dbw->endAtomic( __METHOD__ ); - - return $status; - } - } - - $newid = false; // newly created page ID - $restored = 0; // number of revisions restored - /** @var Revision $revision */ - $revision = null; - - // If there are no restorable revisions, we can skip most of the steps. - if ( $latestRestorableRow === null ) { - $failedRevisionCount = $rev_count; - } else { - if ( $makepage ) { - // Check the state of the newest to-be version... - if ( !$unsuppress - && ( $latestRestorableRow->ar_deleted & Revision::DELETED_TEXT ) - ) { - $dbw->endAtomic( __METHOD__ ); - - return Status::newFatal( "undeleterevdel" ); - } - // Safe to insert now... - $newid = $article->insertOn( $dbw, $latestRestorableRow->ar_page_id ); - if ( $newid === false ) { - // The old ID is reserved; let's pick another - $newid = $article->insertOn( $dbw ); - } - $pageId = $newid; - } else { - // Check if a deleted revision will become the current revision... - if ( $latestRestorableRow->ar_timestamp > $previousTimestamp ) { - // Check the state of the newest to-be version... - if ( !$unsuppress - && ( $latestRestorableRow->ar_deleted & Revision::DELETED_TEXT ) - ) { - $dbw->endAtomic( __METHOD__ ); - - return Status::newFatal( "undeleterevdel" ); - } - } - - $newid = false; - $pageId = $article->getId(); - } - - foreach ( $result as $row ) { - // Check for key dupes due to needed archive integrity. - if ( $row->ar_rev_id && $allowedRevIdToArIdMap[$row->ar_rev_id] !== $row->ar_id ) { - continue; - } - // Insert one revision at a time...maintaining deletion status - // unless we are specifically removing all restrictions... - $revision = Revision::newFromArchiveRow( $row, - [ - 'page' => $pageId, - 'title' => $this->title, - 'deleted' => $unsuppress ? 0 : $row->ar_deleted - ] ); - - $revision->insertOn( $dbw ); - $restored++; - - Hooks::run( 'ArticleRevisionUndeleted', - [ &$this->title, $revision, $row->ar_page_id ] ); - } - - // Now that it's safely stored, take it out of the archive - // Don't delete rows that we failed to restore - $toDeleteConds = $oldWhere; - $failedRevisionCount = count( $restoreFailedArIds ); - if ( $failedRevisionCount > 0 ) { - $toDeleteConds[] = 'ar_id NOT IN ( ' . $dbw->makeList( $restoreFailedArIds ) . ' )'; - } - - $dbw->delete( 'archive', - $toDeleteConds, - __METHOD__ ); - } - - $status = Status::newGood( $restored ); - - if ( $failedRevisionCount > 0 ) { - $status->warning( - wfMessage( 'undeleterevision-duplicate-revid', $failedRevisionCount ) ); - } - - // Was anything restored at all? - if ( $restored ) { - $created = (bool)$newid; - // Attach the latest revision to the page... - $wasnew = $article->updateIfNewerOn( $dbw, $revision ); - if ( $created || $wasnew ) { - // Update site stats, link tables, etc - $article->doEditUpdates( - $revision, - User::newFromName( $revision->getUserText( Revision::RAW ), false ), - [ - 'created' => $created, - 'oldcountable' => $oldcountable, - 'restored' => true - ] - ); - } - - Hooks::run( 'ArticleUndelete', [ &$this->title, $created, $comment, $oldPageId ] ); - if ( $this->title->getNamespace() == NS_FILE ) { - DeferredUpdates::addUpdate( new HTMLCacheUpdate( $this->title, 'imagelinks' ) ); - } - } - - $dbw->endAtomic( __METHOD__ ); - - return $status; - } - - /** - * @return Status - */ - function getFileStatus() { - return $this->fileStatus; - } - - /** - * @return Status - */ - function getRevisionStatus() { - return $this->revisionStatus; - } -} +use MediaWiki\MediaWikiServices; +use Wikimedia\Rdbms\ResultWrapper; /** * Special page allowing users with the appropriate permissions to view @@ -767,6 +46,10 @@ class SpecialUndelete extends SpecialPage { /** @var Title */ private $mTargetObj; + /** + * @var string Search prefix + */ + private $mSearchPrefix; function __construct() { parent::__construct( 'Undelete', 'deletedhistory' ); @@ -957,6 +240,7 @@ class SpecialUndelete extends SpecialPage { Xml::openElement( 'form', [ 'method' => 'get', 'action' => wfScript() ] ) . Xml::fieldset( $this->msg( 'undelete-search-box' )->text() ) . Html::hidden( 'title', $this->getPageTitle()->getPrefixedDBkey() ) . + Html::hidden( 'fuzzy', $this->getRequest()->getVal( 'fuzzy' ) ) . Html::rawElement( 'label', [ 'for' => 'prefix' ], @@ -967,15 +251,25 @@ class SpecialUndelete extends SpecialPage { 20, $this->mSearchPrefix, [ 'id' => 'prefix', 'autofocus' => '' ] - ) . ' ' . - Xml::submitButton( $this->msg( 'undelete-search-submit' )->text() ) . + ) . + ' ' . + Xml::submitButton( + $this->msg( 'undelete-search-submit' )->text(), + [ 'id' => 'searchUndelete' ] + ) . Xml::closeElement( 'fieldset' ) . Xml::closeElement( 'form' ) ); # List undeletable articles if ( $this->mSearchPrefix ) { - $result = PageArchive::listPagesByPrefix( $this->mSearchPrefix ); + // For now, we enable search engine match only when specifically asked to + // by using fuzzy=1 parameter. + if ( $this->getRequest()->getVal( "fuzzy", false ) ) { + $result = PageArchive::listPagesBySearch( $this->mSearchPrefix ); + } else { + $result = PageArchive::listPagesByPrefix( $this->mSearchPrefix ); + } $this->showList( $result ); } } @@ -999,7 +293,7 @@ class SpecialUndelete extends SpecialPage { $linkRenderer = $this->getLinkRenderer(); $undelete = $this->getPageTitle(); - $out->addHTML( "<ul>\n" ); + $out->addHTML( "<ul id='undeleteResultsList'>\n" ); foreach ( $result as $row ) { $title = Title::makeTitleSafe( $row->ar_namespace, $row->ar_title ); if ( $title !== null ) { @@ -1022,7 +316,7 @@ class SpecialUndelete extends SpecialPage { ); } $revs = $this->msg( 'undeleterevisions' )->numParams( $row->count )->parse(); - $out->addHTML( "<li>{$item} ({$revs})</li>\n" ); + $out->addHTML( "<li class='undeleteResult'>{$item} ({$revs})</li>\n" ); } $result->free(); $out->addHTML( "</ul>\n" ); diff --git a/includes/specials/SpecialUnusedcategories.php b/includes/specials/SpecialUnusedcategories.php index ec39ccf0c91c..1469742a4b8b 100644 --- a/includes/specials/SpecialUnusedcategories.php +++ b/includes/specials/SpecialUnusedcategories.php @@ -55,7 +55,7 @@ class UnusedCategoriesPage extends QueryPage { } /** - * A should come before Z (bug 30907) + * A should come before Z (T32907) * @return bool */ function sortDescending() { diff --git a/includes/specials/SpecialUnusedimages.php b/includes/specials/SpecialUnusedimages.php index 2cc1a7b0e860..9fcbf15f7811 100644 --- a/includes/specials/SpecialUnusedimages.php +++ b/includes/specials/SpecialUnusedimages.php @@ -50,8 +50,6 @@ class UnusedimagesPage extends ImageQueryPage { 'namespace' => NS_FILE, 'title' => 'img_name', 'value' => 'img_timestamp', - 'img_user', 'img_user_text', - 'img_description' ], 'conds' => [ 'il_to IS NULL' ], 'join_conds' => [ 'imagelinks' => [ 'LEFT JOIN', 'il_to = img_name' ] ] diff --git a/includes/specials/SpecialUnwatchedpages.php b/includes/specials/SpecialUnwatchedpages.php index 96878a38aa74..fea7e2160da1 100644 --- a/includes/specials/SpecialUnwatchedpages.php +++ b/includes/specials/SpecialUnwatchedpages.php @@ -24,6 +24,9 @@ * @author Ævar Arnfjörð Bjarmason <avarab@gmail.com> */ +use Wikimedia\Rdbms\ResultWrapper; +use Wikimedia\Rdbms\IDatabase; + /** * A special page that displays a list of pages that are not on anyones watchlist. * diff --git a/includes/specials/SpecialUpload.php b/includes/specials/SpecialUpload.php index 8b8e514ac0cc..f4a4818b3261 100644 --- a/includes/specials/SpecialUpload.php +++ b/includes/specials/SpecialUpload.php @@ -281,10 +281,12 @@ class SpecialUpload extends SpecialPage { $desiredTitleObj = Title::makeTitleSafe( NS_FILE, $this->mDesiredDestName ); $delNotice = ''; // empty by default if ( $desiredTitleObj instanceof Title && !$desiredTitleObj->exists() ) { + $dbr = wfGetDB( DB_REPLICA ); + LogEventsList::showLogExtract( $delNotice, [ 'delete', 'move' ], $desiredTitleObj, '', [ 'lim' => 10, - 'conds' => [ "log_action != 'revision'" ], + 'conds' => [ 'log_action != ' . $dbr->addQuotes( 'revision' ) ], 'showIfEmpty' => false, 'msgKey' => [ 'upload-recreate-warning' ] ] ); @@ -1090,12 +1092,14 @@ class UploadForm extends HTMLForm { global $wgContLang; $mto = $file->transform( [ 'width' => 120 ] ); - $this->addHeaderText( - '<div class="thumb t' . $wgContLang->alignEnd() . '">' . - Html::element( 'img', [ - 'src' => $mto->getUrl(), - 'class' => 'thumbimage', - ] ) . '</div>', 'description' ); + if ( $mto ) { + $this->addHeaderText( + '<div class="thumb t' . $wgContLang->alignEnd() . '">' . + Html::element( 'img', [ + 'src' => $mto->getUrl(), + 'class' => 'thumbimage', + ] ) . '</div>', 'description' ); + } } } diff --git a/includes/specials/SpecialUploadStash.php b/includes/specials/SpecialUploadStash.php index 8478e947f9c2..b0bb595e8505 100644 --- a/includes/specials/SpecialUploadStash.php +++ b/includes/specials/SpecialUploadStash.php @@ -327,7 +327,7 @@ class SpecialUploadStash extends UnlistedSpecialPage { header( "Content-Type: $contentType", true ); header( 'Content-Transfer-Encoding: binary', true ); header( 'Expires: Sun, 17-Jan-2038 19:14:07 GMT', true ); - // Bug 53032 - It shouldn't be a problem here, but let's be safe and not cache + // T55032 - It shouldn't be a problem here, but let's be safe and not cache header( 'Cache-Control: private' ); header( "Content-Length: $size", true ); } diff --git a/includes/specials/SpecialUserLogout.php b/includes/specials/SpecialUserLogout.php index c067f4496ac5..a9b732efb120 100644 --- a/includes/specials/SpecialUserLogout.php +++ b/includes/specials/SpecialUserLogout.php @@ -38,7 +38,7 @@ class SpecialUserLogout extends UnlistedSpecialPage { function execute( $par ) { /** * Some satellite ISPs use broken precaching schemes that log people out straight after - * they're logged in (bug 17790). Luckily, there's a way to detect such requests. + * they're logged in (T19790). Luckily, there's a way to detect such requests. */ if ( isset( $_SERVER['REQUEST_URI'] ) && strpos( $_SERVER['REQUEST_URI'], '&' ) !== false ) { wfDebug( "Special:UserLogout request {$_SERVER['REQUEST_URI']} looks suspicious, denying.\n" ); diff --git a/includes/specials/SpecialUserrights.php b/includes/specials/SpecialUserrights.php index 38bac297d7cb..127b530e0f72 100644 --- a/includes/specials/SpecialUserrights.php +++ b/includes/specials/SpecialUserrights.php @@ -49,19 +49,25 @@ class UserrightsPage extends SpecialPage { } /** - * @param User $user - * @param bool $checkIfSelf + * Check whether the current user (from context) can change the target user's rights. + * + * @param User $targetUser User whose rights are being changed + * @param bool $checkIfSelf If false, assume that the current user can add/remove groups defined + * in $wgGroupsAddToSelf / $wgGroupsRemoveFromSelf, without checking if it's the same as target + * user * @return bool */ - public function userCanChangeRights( $user, $checkIfSelf = true ) { + public function userCanChangeRights( $targetUser, $checkIfSelf = true ) { + $isself = $this->getUser()->equals( $targetUser ); + $available = $this->changeableGroups(); - if ( $user->getId() == 0 ) { + if ( $targetUser->getId() == 0 ) { return false; } return !empty( $available['add'] ) || !empty( $available['remove'] ) - || ( ( $this->isself || !$checkIfSelf ) && + || ( ( $isself || !$checkIfSelf ) && ( !empty( $available['add-self'] ) || !empty( $available['remove-self'] ) ) ); } @@ -79,6 +85,8 @@ class UserrightsPage extends SpecialPage { $session = $request->getSession(); $out = $this->getOutput(); + $out->addModules( [ 'mediawiki.special.userrights' ] ); + if ( $par !== null ) { $this->mTarget = $par; } else { @@ -111,7 +119,6 @@ class UserrightsPage extends SpecialPage { // Remove session data for the success message $session->remove( 'specialUserrightsSaveSuccess' ); - $out->addModules( [ 'mediawiki.special.userrights' ] ); $out->addModuleStyles( 'mediawiki.notification.convertmessagebox.styles' ); $out->addHTML( Html::rawElement( @@ -163,8 +170,8 @@ class UserrightsPage extends SpecialPage { } $targetUser = $this->mFetchedUser; - if ( $targetUser instanceof User ) { // UserRightsProxy doesn't have this method (bug 61252) - $targetUser->clearInstanceCache(); // bug 38989 + if ( $targetUser instanceof User ) { // UserRightsProxy doesn't have this method (T63252) + $targetUser->clearInstanceCache(); // T40989 } if ( $request->getVal( 'conflictcheck-originalgroups' ) @@ -172,18 +179,22 @@ class UserrightsPage extends SpecialPage { ) { $out->addWikiMsg( 'userrights-conflict' ); } else { - $this->saveUserGroups( + $status = $this->saveUserGroups( $this->mTarget, $request->getVal( 'user-reason' ), $targetUser ); - // Set session data for the success message - $session->set( 'specialUserrightsSaveSuccess', 1 ); - - $out->redirect( $this->getSuccessURL() ); + if ( $status->isOK() ) { + // Set session data for the success message + $session->set( 'specialUserrightsSaveSuccess', 1 ); - return; + $out->redirect( $this->getSuccessURL() ); + return; + } else { + // Print an error message and redisplay the form + $out->addWikiText( '<div class="error">' . $status->getWikiText() . '</div>' ); + } } } @@ -198,18 +209,55 @@ class UserrightsPage extends SpecialPage { } /** + * Returns true if this user rights form can set and change user group expiries. + * Subclasses may wish to override this to return false. + * + * @return bool + */ + public function canProcessExpiries() { + return !$this->getConfig()->get( 'DisableUserGroupExpiry' ); + } + + /** + * Converts a user group membership expiry string into a timestamp. Words like + * 'existing' or 'other' should have been filtered out before calling this + * function. + * + * @param string $expiry + * @return string|null|false A string containing a valid timestamp, or null + * if the expiry is infinite, or false if the timestamp is not valid + */ + public static function expiryToTimestamp( $expiry ) { + if ( wfIsInfinity( $expiry ) ) { + return null; + } + + $unix = strtotime( $expiry ); + + if ( !$unix || $unix === -1 ) { + return false; + } + + // @todo FIXME: Non-qualified absolute times are not in users specified timezone + // and there isn't notice about it in the ui (see ProtectionForm::getExpiry) + return wfTimestamp( TS_MW, $unix ); + } + + /** * Save user groups changes in the database. * Data comes from the editUserGroupsForm() form function * * @param string $username Username to apply changes to. * @param string $reason Reason for group change * @param User|UserRightsProxy $user Target user object. - * @return null + * @return Status */ - function saveUserGroups( $username, $reason, $user ) { + protected function saveUserGroups( $username, $reason, $user ) { $allgroups = $this->getAllGroups(); $addgroup = []; + $groupExpiries = []; // associative array of (group name => expiry) $removegroup = []; + $existingUGMs = $user->getGroupMemberships(); // This could possibly create a highly unlikely race condition if permissions are changed between // when the form is loaded and when the form is saved. Ignoring it for the moment. @@ -218,45 +266,103 @@ class UserrightsPage extends SpecialPage { // Later on, this gets filtered for what can actually be removed if ( $this->getRequest()->getCheck( "wpGroup-$group" ) ) { $addgroup[] = $group; + + if ( $this->canProcessExpiries() ) { + // read the expiry information from the request + $expiryDropdown = $this->getRequest()->getVal( "wpExpiry-$group" ); + if ( $expiryDropdown === 'existing' ) { + continue; + } + + if ( $expiryDropdown === 'other' ) { + $expiryValue = $this->getRequest()->getVal( "wpExpiry-$group-other" ); + } else { + $expiryValue = $expiryDropdown; + } + + // validate the expiry + $groupExpiries[$group] = self::expiryToTimestamp( $expiryValue ); + + if ( $groupExpiries[$group] === false ) { + return Status::newFatal( 'userrights-invalid-expiry', $group ); + } + + // not allowed to have things expiring in the past + if ( $groupExpiries[$group] && $groupExpiries[$group] < wfTimestampNow() ) { + return Status::newFatal( 'userrights-expiry-in-past', $group ); + } + + // if the user can only add this group (not remove it), the expiry time + // cannot be brought forward (T156784) + if ( !$this->canRemove( $group ) && + isset( $existingUGMs[$group] ) && + ( $existingUGMs[$group]->getExpiry() ?: 'infinity' ) > + ( $groupExpiries[$group] ?: 'infinity' ) + ) { + return Status::newFatal( 'userrights-cannot-shorten-expiry', $group ); + } + } } else { $removegroup[] = $group; } } - $this->doSaveUserGroups( $user, $addgroup, $removegroup, $reason ); + $this->doSaveUserGroups( $user, $addgroup, $removegroup, $reason, [], $groupExpiries ); + + return Status::newGood(); } /** - * Save user groups changes in the database. + * Save user groups changes in the database. This function does not throw errors; + * instead, it ignores groups that the performer does not have permission to set. * * @param User|UserRightsProxy $user * @param array $add Array of groups to add * @param array $remove Array of groups to remove * @param string $reason Reason for group change * @param array $tags Array of change tags to add to the log entry + * @param array $groupExpiries Associative array of (group name => expiry), + * containing only those groups that are to have new expiry values set * @return array Tuple of added, then removed groups */ - function doSaveUserGroups( $user, $add, $remove, $reason = '', $tags = [] ) { + function doSaveUserGroups( $user, $add, $remove, $reason = '', $tags = [], + $groupExpiries = [] ) { + // Validate input set... $isself = $user->getName() == $this->getUser()->getName(); $groups = $user->getGroups(); + $ugms = $user->getGroupMemberships(); $changeable = $this->changeableGroups(); $addable = array_merge( $changeable['add'], $isself ? $changeable['add-self'] : [] ); $removable = array_merge( $changeable['remove'], $isself ? $changeable['remove-self'] : [] ); $remove = array_unique( array_intersect( (array)$remove, $removable, $groups ) ); - $add = array_unique( array_diff( - array_intersect( (array)$add, $addable ), - $groups ) - ); + $add = array_intersect( (array)$add, $addable ); + + // add only groups that are not already present or that need their expiry updated, + // UNLESS the user can only add this group (not remove it) and the expiry time + // is being brought forward (T156784) + $add = array_filter( $add, + function( $group ) use ( $groups, $groupExpiries, $removable, $ugms ) { + if ( isset( $groupExpiries[$group] ) && + !in_array( $group, $removable ) && + isset( $ugms[$group] ) && + ( $ugms[$group]->getExpiry() ?: 'infinity' ) > + ( $groupExpiries[$group] ?: 'infinity' ) + ) { + return false; + } + return !in_array( $group, $groups ) || array_key_exists( $group, $groupExpiries ); + } ); Hooks::run( 'ChangeUserGroups', [ $this->getUser(), $user, &$add, &$remove ] ); - $oldGroups = $user->getGroups(); + $oldGroups = $groups; + $oldUGMs = $user->getGroupMemberships(); $newGroups = $oldGroups; - // Remove then add groups + // Remove groups, then add new ones/update expiries of existing ones if ( $remove ) { foreach ( $remove as $index => $group ) { if ( !$user->removeGroup( $group ) ) { @@ -267,44 +373,81 @@ class UserrightsPage extends SpecialPage { } if ( $add ) { foreach ( $add as $index => $group ) { - if ( !$user->addGroup( $group ) ) { + $expiry = isset( $groupExpiries[$group] ) ? $groupExpiries[$group] : null; + if ( !$user->addGroup( $group, $expiry ) ) { unset( $add[$index] ); } } $newGroups = array_merge( $newGroups, $add ); } $newGroups = array_unique( $newGroups ); + $newUGMs = $user->getGroupMemberships(); // Ensure that caches are cleared $user->invalidateCache(); // update groups in external authentication database - Hooks::run( 'UserGroupsChanged', [ $user, $add, $remove, $this->getUser(), $reason ] ); + Hooks::run( 'UserGroupsChanged', [ $user, $add, $remove, $this->getUser(), + $reason, $oldUGMs, $newUGMs ] ); MediaWiki\Auth\AuthManager::callLegacyAuthPlugin( 'updateExternalDBGroups', [ $user, $add, $remove ] ); wfDebug( 'oldGroups: ' . print_r( $oldGroups, true ) . "\n" ); wfDebug( 'newGroups: ' . print_r( $newGroups, true ) . "\n" ); + wfDebug( 'oldUGMs: ' . print_r( $oldUGMs, true ) . "\n" ); + wfDebug( 'newUGMs: ' . print_r( $newUGMs, true ) . "\n" ); // Deprecated in favor of UserGroupsChanged hook Hooks::run( 'UserRights', [ &$user, $add, $remove ], '1.26' ); - if ( $newGroups != $oldGroups ) { - $this->addLogEntry( $user, $oldGroups, $newGroups, $reason, $tags ); + // Only add a log entry if something actually changed + if ( $newGroups != $oldGroups || $newUGMs != $oldUGMs ) { + $this->addLogEntry( $user, $oldGroups, $newGroups, $reason, $tags, $oldUGMs, $newUGMs ); } return [ $add, $remove ]; } /** + * Serialise a UserGroupMembership object for storage in the log_params section + * of the logging table. Only keeps essential data, removing redundant fields. + * + * @param UserGroupMembership|null $ugm May be null if things get borked + * @return array + */ + protected static function serialiseUgmForLog( $ugm ) { + if ( !$ugm instanceof UserGroupMembership ) { + return null; + } + return [ 'expiry' => $ugm->getExpiry() ]; + } + + /** * Add a rights log entry for an action. - * @param User $user + * @param User|UserRightsProxy $user * @param array $oldGroups * @param array $newGroups * @param array $reason - * @param array $tags + * @param array $tags Change tags for the log entry + * @param array $oldUGMs Associative array of (group name => UserGroupMembership) + * @param array $newUGMs Associative array of (group name => UserGroupMembership) */ - function addLogEntry( $user, $oldGroups, $newGroups, $reason, $tags ) { + protected function addLogEntry( $user, $oldGroups, $newGroups, $reason, $tags, + $oldUGMs, $newUGMs ) { + + // make sure $oldUGMs and $newUGMs are in the same order, and serialise + // each UGM object to a simplified array + $oldUGMs = array_map( function( $group ) use ( $oldUGMs ) { + return isset( $oldUGMs[$group] ) ? + self::serialiseUgmForLog( $oldUGMs[$group] ) : + null; + }, $oldGroups ); + $newUGMs = array_map( function( $group ) use ( $newUGMs ) { + return isset( $newUGMs[$group] ) ? + self::serialiseUgmForLog( $newUGMs[$group] ) : + null; + }, $newGroups ); + $logEntry = new ManualLogEntry( 'rights', 'rights' ); $logEntry->setPerformer( $this->getUser() ); $logEntry->setTarget( $user->getUserPage() ); @@ -312,6 +455,8 @@ class UserrightsPage extends SpecialPage { $logEntry->setParameters( [ '4::oldgroups' => $oldGroups, '5::newgroups' => $newGroups, + 'oldmetadata' => $oldUGMs, + 'newmetadata' => $newUGMs, ] ); $logid = $logEntry->insert(); if ( count( $tags ) ) { @@ -335,8 +480,8 @@ class UserrightsPage extends SpecialPage { } $groups = $user->getGroups(); - - $this->showEditUserGroupsForm( $user, $groups ); + $groupMemberships = $user->getGroupMemberships(); + $this->showEditUserGroupsForm( $user, $groups, $groupMemberships ); // This isn't really ideal logging behavior, but let's not hide the // interwiki logs if we're using them as is. @@ -466,61 +611,50 @@ class UserrightsPage extends SpecialPage { } /** - * Go through used and available groups and return the ones that this - * form will be able to manipulate based on the current user's system - * permissions. - * - * @param array $groups List of groups the given user is in - * @return array Tuple of addable, then removable groups - */ - protected function splitGroups( $groups ) { - list( $addable, $removable, $addself, $removeself ) = array_values( $this->changeableGroups() ); - - $removable = array_intersect( - array_merge( $this->isself ? $removeself : [], $removable ), - $groups - ); // Can't remove groups the user doesn't have - $addable = array_diff( - array_merge( $this->isself ? $addself : [], $addable ), - $groups - ); // Can't add groups the user does have - - return [ $addable, $removable ]; - } - - /** * Show the form to edit group memberships. * * @param User|UserRightsProxy $user User or UserRightsProxy you're editing - * @param array $groups Array of groups the user is in + * @param array $groups Array of groups the user is in. Not used by this implementation + * anymore, but kept for backward compatibility with subclasses + * @param array $groupMemberships Associative array of (group name => UserGroupMembership + * object) containing the groups the user is in */ - protected function showEditUserGroupsForm( $user, $groups ) { - $list = []; - $membersList = []; - foreach ( $groups as $group ) { - $list[] = self::buildGroupLink( $group ); - $membersList[] = self::buildGroupMemberLink( $group ); + protected function showEditUserGroupsForm( $user, $groups, $groupMemberships ) { + $list = $membersList = $tempList = $tempMembersList = []; + foreach ( $groupMemberships as $ugm ) { + $linkG = UserGroupMembership::getLink( $ugm, $this->getContext(), 'html' ); + $linkM = UserGroupMembership::getLink( $ugm, $this->getContext(), 'html', + $user->getName() ); + if ( $ugm->getExpiry() ) { + $tempList[] = $linkG; + $tempMembersList[] = $linkM; + } else { + $list[] = $linkG; + $membersList[] = $linkM; + + } } $autoList = []; $autoMembersList = []; if ( $user instanceof User ) { foreach ( Autopromote::getAutopromoteGroups( $user ) as $group ) { - $autoList[] = self::buildGroupLink( $group ); - $autoMembersList[] = self::buildGroupMemberLink( $group ); + $autoList[] = UserGroupMembership::getLink( $group, $this->getContext(), 'html' ); + $autoMembersList[] = UserGroupMembership::getLink( $group, $this->getContext(), + 'html', $user->getName() ); } } $language = $this->getLanguage(); $displayedList = $this->msg( 'userrights-groupsmember-type' ) ->rawParams( - $language->listToText( $list ), - $language->listToText( $membersList ) + $language->commaList( array_merge( $tempList, $list ) ), + $language->commaList( array_merge( $tempMembersList, $membersList ) ) )->escaped(); $displayedAutolist = $this->msg( 'userrights-groupsmember-type' ) ->rawParams( - $language->listToText( $autoList ), - $language->listToText( $autoMembersList ) + $language->commaList( $autoList ), + $language->commaList( $autoMembersList ) )->escaped(); $grouplist = ''; @@ -549,7 +683,8 @@ class UserrightsPage extends SpecialPage { Linker::TOOL_LINKS_EMAIL /* Add "send e-mail" link */ ); - list( $groupCheckboxes, $canChangeAny ) = $this->groupCheckboxes( $groups, $user ); + list( $groupCheckboxes, $canChangeAny ) = + $this->groupCheckboxes( $groupMemberships, $user ); $this->getOutput()->addHTML( Xml::openElement( 'form', @@ -616,26 +751,6 @@ class UserrightsPage extends SpecialPage { } /** - * Format a link to a group description page - * - * @param string $group - * @return string - */ - private static function buildGroupLink( $group ) { - return User::makeGroupLinkHTML( $group, User::getGroupName( $group ) ); - } - - /** - * Format a link to a group member description page - * - * @param string $group - * @return string - */ - private static function buildGroupMemberLink( $group ) { - return User::makeGroupLinkHTML( $group, User::getGroupMember( $group ) ); - } - - /** * Returns an array of all groups that may be edited * @return array Array of groups that may be edited. */ @@ -646,8 +761,8 @@ class UserrightsPage extends SpecialPage { /** * Adds a table with checkboxes where you can select what groups to add/remove * - * @todo Just pass the username string? - * @param array $usergroups Groups the user belongs to + * @param array $usergroups Associative array of (group name as string => + * UserGroupMembership object) for groups the user belongs to * @param User $user * @return Array with 2 elements: the XHTML table element with checkxboes, and * whether any groups are changeable @@ -656,28 +771,42 @@ class UserrightsPage extends SpecialPage { $allgroups = $this->getAllGroups(); $ret = ''; + // Get the list of preset expiry times from the system message + $expiryOptionsMsg = $this->msg( 'userrights-expiry-options' )->inContentLanguage(); + $expiryOptions = $expiryOptionsMsg->isDisabled() ? + [] : + explode( ',', $expiryOptionsMsg->text() ); + // Put all column info into an associative array so that extensions can // more easily manage it. $columns = [ 'unchangeable' => [], 'changeable' => [] ]; foreach ( $allgroups as $group ) { - $set = in_array( $group, $usergroups ); + $set = isset( $usergroups[$group] ); + // Users who can add the group, but not remove it, can only lengthen + // expiries, not shorten them. So they should only see the expiry + // dropdown if the group currently has a finite expiry + $canOnlyLengthenExpiry = ( $set && $this->canAdd( $group ) && + !$this->canRemove( $group ) && $usergroups[$group]->getExpiry() ); // Should the checkbox be disabled? - $disabled = !( + $disabledCheckbox = !( ( $set && $this->canRemove( $group ) ) || ( !$set && $this->canAdd( $group ) ) ); + // Should the expiry elements be disabled? + $disabledExpiry = $disabledCheckbox && !$canOnlyLengthenExpiry; // Do we need to point out that this action is irreversible? - $irreversible = !$disabled && ( + $irreversible = !$disabledCheckbox && ( ( $set && !$this->canAdd( $group ) ) || ( !$set && !$this->canRemove( $group ) ) ); $checkbox = [ 'set' => $set, - 'disabled' => $disabled, + 'disabled' => $disabledCheckbox, + 'disabled-expiry' => $disabledExpiry, 'irreversible' => $irreversible ]; - if ( $disabled ) { + if ( $disabledCheckbox && $disabledExpiry ) { $columns['unchangeable'][$group] = $checkbox; } else { $columns['changeable'][$group] = $checkbox; @@ -708,18 +837,110 @@ class UserrightsPage extends SpecialPage { foreach ( $column as $group => $checkbox ) { $attr = $checkbox['disabled'] ? [ 'disabled' => 'disabled' ] : []; - $member = User::getGroupMember( $group, $user->getName() ); + $member = UserGroupMembership::getGroupMemberName( $group, $user->getName() ); if ( $checkbox['irreversible'] ) { $text = $this->msg( 'userrights-irreversible-marker', $member )->text(); + } elseif ( $checkbox['disabled'] && !$checkbox['disabled-expiry'] ) { + $text = $this->msg( 'userrights-no-shorten-expiry-marker', $member )->text(); } else { $text = $member; } $checkboxHtml = Xml::checkLabel( $text, "wpGroup-" . $group, "wpGroup-" . $group, $checkbox['set'], $attr ); - $ret .= "\t\t" . ( $checkbox['disabled'] - ? Xml::tags( 'span', [ 'class' => 'mw-userrights-disabled' ], $checkboxHtml ) - : $checkboxHtml - ) . "<br />\n"; + $ret .= "\t\t" . ( ( $checkbox['disabled'] && $checkbox['disabled-expiry'] ) + ? Xml::tags( 'div', [ 'class' => 'mw-userrights-disabled' ], $checkboxHtml ) + : Xml::tags( 'div', [], $checkboxHtml ) + ) . "\n"; + + if ( $this->canProcessExpiries() ) { + $uiUser = $this->getUser(); + $uiLanguage = $this->getLanguage(); + + $currentExpiry = isset( $usergroups[$group] ) ? + $usergroups[$group]->getExpiry() : + null; + + // If the user can't modify the expiry, print the current expiry below + // it in plain text. Otherwise provide UI to set/change the expiry + if ( $checkbox['set'] && + ( $checkbox['irreversible'] || $checkbox['disabled-expiry'] ) + ) { + if ( $currentExpiry ) { + $expiryFormatted = $uiLanguage->userTimeAndDate( $currentExpiry, $uiUser ); + $expiryFormattedD = $uiLanguage->userDate( $currentExpiry, $uiUser ); + $expiryFormattedT = $uiLanguage->userTime( $currentExpiry, $uiUser ); + $expiryHtml = $this->msg( 'userrights-expiry-current' )->params( + $expiryFormatted, $expiryFormattedD, $expiryFormattedT )->text(); + } else { + $expiryHtml = $this->msg( 'userrights-expiry-none' )->text(); + } + $expiryHtml .= "<br />\n"; + } else { + $expiryHtml = Xml::element( 'span', null, + $this->msg( 'userrights-expiry' )->text() ); + $expiryHtml .= Xml::openElement( 'span' ); + + // add a form element to set the expiry date + $expiryFormOptions = new XmlSelect( + "wpExpiry-$group", + "mw-input-wpExpiry-$group", // forward compatibility with HTMLForm + $currentExpiry ? 'existing' : 'infinite' + ); + if ( $checkbox['disabled-expiry'] ) { + $expiryFormOptions->setAttribute( 'disabled', 'disabled' ); + } + + if ( $currentExpiry ) { + $timestamp = $uiLanguage->userTimeAndDate( $currentExpiry, $uiUser ); + $d = $uiLanguage->userDate( $currentExpiry, $uiUser ); + $t = $uiLanguage->userTime( $currentExpiry, $uiUser ); + $existingExpiryMessage = $this->msg( 'userrights-expiry-existing', + $timestamp, $d, $t ); + $expiryFormOptions->addOption( $existingExpiryMessage->text(), 'existing' ); + } + + $expiryFormOptions->addOption( + $this->msg( 'userrights-expiry-none' )->text(), + 'infinite' + ); + $expiryFormOptions->addOption( + $this->msg( 'userrights-expiry-othertime' )->text(), + 'other' + ); + foreach ( $expiryOptions as $option ) { + if ( strpos( $option, ":" ) === false ) { + $displayText = $value = $option; + } else { + list( $displayText, $value ) = explode( ":", $option ); + } + $expiryFormOptions->addOption( $displayText, htmlspecialchars( $value ) ); + } + + // Add expiry dropdown + $expiryHtml .= $expiryFormOptions->getHTML() . '<br />'; + + // Add custom expiry field + $attribs = [ 'id' => "mw-input-wpExpiry-$group-other" ]; + if ( $checkbox['disabled-expiry'] ) { + $attribs['disabled'] = 'disabled'; + } + $expiryHtml .= Xml::input( "wpExpiry-$group-other", 30, '', $attribs ); + + // If the user group is set but the checkbox is disabled, mimic a + // checked checkbox in the form submission + if ( $checkbox['set'] && $checkbox['disabled'] ) { + $expiryHtml .= Html::hidden( "wpGroup-$group", 1 ); + } + + $expiryHtml .= Xml::closeElement( 'span' ); + } + + $divAttribs = [ + 'id' => "mw-userrights-nested-wpGroup-$group", + 'class' => 'mw-userrights-nested', + ]; + $ret .= "\t\t\t" . Xml::tags( 'div', $divAttribs, $expiryHtml ) . "\n"; + } } $ret .= "\t</td>\n"; } @@ -802,4 +1023,3 @@ class UserrightsPage extends SpecialPage { return 'users'; } } - diff --git a/includes/specials/SpecialWantedfiles.php b/includes/specials/SpecialWantedfiles.php index 74d5e5d3469e..6d481f8fdca8 100644 --- a/includes/specials/SpecialWantedfiles.php +++ b/includes/specials/SpecialWantedfiles.php @@ -83,7 +83,7 @@ class WantedFilesPage extends WantedQueryPage { * KLUGE: The results may contain false positives for files * that exist e.g. in a shared repo. Setting this at least * keeps them from showing up as redlinks in the output, even - * if it doesn't fix the real problem (bug 6220). + * if it doesn't fix the real problem (T8220). * * @note could also have existing links here from broken file * redirects. diff --git a/includes/specials/SpecialWantedpages.php b/includes/specials/SpecialWantedpages.php index c37ecbd17adf..8cea6ccb7768 100644 --- a/includes/specials/SpecialWantedpages.php +++ b/includes/specials/SpecialWantedpages.php @@ -85,7 +85,9 @@ class WantedPagesPage extends WantedQueryPage { ] ]; // Replacement for the WantedPages::getSQL hook - Hooks::run( 'WantedPages::getQueryInfo', [ &$this, &$query ] ); + // Avoid PHP 7.1 warning from passing $this by reference + $wantedPages = $this; + Hooks::run( 'WantedPages::getQueryInfo', [ &$wantedPages, &$query ] ); return $query; } diff --git a/includes/specials/SpecialWatchlist.php b/includes/specials/SpecialWatchlist.php index 85ac2de68022..c1c9ab0f27f5 100644 --- a/includes/specials/SpecialWatchlist.php +++ b/includes/specials/SpecialWatchlist.php @@ -22,6 +22,8 @@ */ use MediaWiki\MediaWikiServices; +use Wikimedia\Rdbms\ResultWrapper; +use Wikimedia\Rdbms\IDatabase; /** * A special page that lists last changes made to the wiki, @@ -79,6 +81,7 @@ class SpecialWatchlist extends ChangesListSpecialPage { if ( ( $config->get( 'EnotifWatchlist' ) || $config->get( 'ShowUpdatedMarker' ) ) && $request->getVal( 'reset' ) && $request->wasPosted() + && $user->matchEditToken( $request->getVal( 'token' ) ) ) { $user->clearAllNotifications(); $output->redirect( $this->getPageTitle()->getFullURL( $opts->getChangedValues() ) ); @@ -104,6 +107,58 @@ class SpecialWatchlist extends ChangesListSpecialPage { } /** + * @inheritdoc + */ + protected function transformFilterDefinition( array $filterDefinition ) { + if ( isset( $filterDefinition['showHideSuffix'] ) ) { + $filterDefinition['showHide'] = 'wl' . $filterDefinition['showHideSuffix']; + } + + return $filterDefinition; + } + + /** + * @inheritdoc + */ + protected function registerFilters() { + parent::registerFilters(); + + $user = $this->getUser(); + + $significance = $this->getFilterGroup( 'significance' ); + $hideMinor = $significance->getFilter( 'hideminor' ); + $hideMinor->setDefault( $user->getBoolOption( 'watchlisthideminor' ) ); + + $automated = $this->getFilterGroup( 'automated' ); + $hideBots = $automated->getFilter( 'hidebots' ); + $hideBots->setDefault( $user->getBoolOption( 'watchlisthidebots' ) ); + + $registration = $this->getFilterGroup( 'registration' ); + $hideAnons = $registration->getFilter( 'hideanons' ); + $hideAnons->setDefault( $user->getBoolOption( 'watchlisthideanons' ) ); + $hideLiu = $registration->getFilter( 'hideliu' ); + $hideLiu->setDefault( $user->getBoolOption( 'watchlisthideliu' ) ); + + $reviewStatus = $this->getFilterGroup( 'reviewStatus' ); + if ( $reviewStatus !== null ) { + // Conditional on feature being available and rights + $hidePatrolled = $reviewStatus->getFilter( 'hidepatrolled' ); + $hidePatrolled->setDefault( $user->getBoolOption( 'watchlisthidepatrolled' ) ); + } + + $authorship = $this->getFilterGroup( 'authorship' ); + $hideMyself = $authorship->getFilter( 'hidemyself' ); + $hideMyself->setDefault( $user->getBoolOption( 'watchlisthideown' ) ); + + $changeType = $this->getFilterGroup( 'changeType' ); + $hideCategorization = $changeType->getFilter( 'hidecategorization' ); + if ( $hideCategorization !== null ) { + // Conditional on feature being available + $hideCategorization->setDefault( $user->getBoolOption( 'watchlisthidecategorization' ) ); + } + } + + /** * Get a FormOptions object containing the default options * * @return FormOptions @@ -114,18 +169,6 @@ class SpecialWatchlist extends ChangesListSpecialPage { $opts->add( 'days', $user->getOption( 'watchlistdays' ), FormOptions::FLOAT ); $opts->add( 'extended', $user->getBoolOption( 'extendwatchlist' ) ); - if ( $this->getRequest()->getVal( 'action' ) == 'submit' ) { - // The user has submitted the form, so we dont need the default values - return $opts; - } - - $opts->add( 'hideminor', $user->getBoolOption( 'watchlisthideminor' ) ); - $opts->add( 'hidebots', $user->getBoolOption( 'watchlisthidebots' ) ); - $opts->add( 'hideanons', $user->getBoolOption( 'watchlisthideanons' ) ); - $opts->add( 'hideliu', $user->getBoolOption( 'watchlisthideliu' ) ); - $opts->add( 'hidepatrolled', $user->getBoolOption( 'watchlisthidepatrolled' ) ); - $opts->add( 'hidemyself', $user->getBoolOption( 'watchlisthideown' ) ); - $opts->add( 'hidecategorization', $user->getBoolOption( 'watchlisthidecategorization' ) ); return $opts; } @@ -171,6 +214,26 @@ class SpecialWatchlist extends ChangesListSpecialPage { } } + if ( $this->getRequest()->getVal( 'action' ) == 'submit' ) { + $allBooleansFalse = []; + + // If the user submitted the form, start with a baseline of "all + // booleans are false", then change the ones they checked. This + // means we ignore the defaults. + + // This is how we handle the fact that HTML forms don't submit + // unchecked boxes. + foreach ( $this->filterGroups as $filterGroup ) { + if ( $filterGroup instanceof ChangesListBooleanFilterGroup ) { + foreach ( $filterGroup->getFilters() as $filter ) { + $allBooleansFalse[$filter->getName()] = false; + } + } + } + + $params = $params + $allBooleansFalse; + } + // Not the prettiest way to achieve this… FormOptions internally depends on data sanitization // methods defined on WebRequest and removing this dependency would cause some code duplication. $request = new DerivativeRequest( $this->getRequest(), $params ); @@ -180,32 +243,28 @@ class SpecialWatchlist extends ChangesListSpecialPage { } /** - * Return an array of conditions depending of options set in $opts - * - * @param FormOptions $opts - * @return array + * @inheritdoc */ - public function buildMainQueryConds( FormOptions $opts ) { + protected function buildQuery( &$tables, &$fields, &$conds, &$query_options, + &$join_conds, FormOptions $opts ) { + $dbr = $this->getDB(); - $conds = parent::buildMainQueryConds( $opts ); + parent::buildQuery( $tables, $fields, $conds, $query_options, $join_conds, + $opts ); // Calculate cutoff if ( $opts['days'] > 0 ) { $conds[] = 'rc_timestamp > ' . $dbr->addQuotes( $dbr->timestamp( time() - intval( $opts['days'] * 86400 ) ) ); } - - return $conds; } /** - * Process the query - * - * @param array $conds - * @param FormOptions $opts - * @return bool|ResultWrapper Result or false (for Recentchangeslinked only) + * @inheritdoc */ - public function doMainQuery( $conds, $opts ) { + protected function doMainQuery( $tables, $fields, $conds, $query_options, + $join_conds, FormOptions $opts ) { + $dbr = $this->getDB(); $user = $this->getUser(); @@ -230,19 +289,23 @@ class SpecialWatchlist extends ChangesListSpecialPage { $usePage = true; } - $tables = [ 'recentchanges', 'watchlist' ]; - $fields = RecentChange::selectFields(); - $query_options = [ 'ORDER BY' => 'rc_timestamp DESC' ]; - $join_conds = [ - 'watchlist' => [ - 'INNER JOIN', - [ - 'wl_user' => $user->getId(), - 'wl_namespace=rc_namespace', - 'wl_title=rc_title' + $tables = array_merge( [ 'recentchanges', 'watchlist' ], $tables ); + $fields = array_merge( RecentChange::selectFields(), $fields ); + + $query_options = array_merge( [ 'ORDER BY' => 'rc_timestamp DESC' ], $query_options ); + $join_conds = array_merge( + [ + 'watchlist' => [ + 'INNER JOIN', + [ + 'wl_user' => $user->getId(), + 'wl_namespace=rc_namespace', + 'wl_title=rc_title' + ], ], ], - ]; + $join_conds + ); if ( $this->getConfig()->get( 'ShowUpdatedMarker' ) ) { $fields[] = 'wl_notificationtimestamp'; @@ -360,7 +423,7 @@ class SpecialWatchlist extends ChangesListSpecialPage { $dbr->dataSeek( $rows, 0 ); - $list = ChangesList::newFromContext( $this->getContext() ); + $list = ChangesList::newFromContext( $this->getContext(), $this->filterGroups ); $list->setWatchlistDivs(); $list->initChangesListRows( $rows ); $dbr->dataSeek( $rows, 0 ); @@ -447,31 +510,23 @@ class SpecialWatchlist extends ChangesListSpecialPage { $cutofflinks = $this->msg( 'wlshowtime' ) . ' ' . $this->cutoffselector( $opts ); # Spit out some control panel links - $filters = [ - 'hideminor' => 'wlshowhideminor', - 'hidebots' => 'wlshowhidebots', - 'hideanons' => 'wlshowhideanons', - 'hideliu' => 'wlshowhideliu', - 'hidemyself' => 'wlshowhidemine', - 'hidepatrolled' => 'wlshowhidepatr' - ]; - - if ( $this->getConfig()->get( 'RCWatchCategoryMembership' ) ) { - $filters['hidecategorization'] = 'wlshowhidecategorization'; - } - - foreach ( $this->getRenderableCustomFilters( $this->getCustomFilters() ) as $key => $params ) { - $filters[$key] = $params['msg']; - } - - // Disable some if needed - if ( !$user->useRCPatrol() ) { - unset( $filters['hidepatrolled'] ); - } - $links = []; - foreach ( $filters as $name => $msg ) { - $links[] = $this->showHideCheck( $nondefaults, $msg, $name, $opts[$name] ); + $context = $this->getContext(); + $namesOfDisplayedFilters = []; + foreach ( $this->getFilterGroups() as $groupName => $group ) { + if ( !$group->isPerGroupRequestParameter() ) { + foreach ( $group->getFilters() as $filterName => $filter ) { + if ( $filter->displaysOnUnstructuredUi( $this ) ) { + $namesOfDisplayedFilters[] = $filterName; + $links[] = $this->showHideCheck( + $nondefaults, + $filter->getShowHide(), + $filterName, + $opts[$filterName] + ); + } + } + } } $hiddenFields = $nondefaults; @@ -480,8 +535,8 @@ class SpecialWatchlist extends ChangesListSpecialPage { unset( $hiddenFields['invert'] ); unset( $hiddenFields['associated'] ); unset( $hiddenFields['days'] ); - foreach ( $filters as $key => $value ) { - unset( $hiddenFields[$key] ); + foreach ( $namesOfDisplayedFilters as $filterName ) { + unset( $hiddenFields[$filterName] ); } # Create output @@ -606,6 +661,7 @@ class SpecialWatchlist extends ChangesListSpecialPage { 'id' => 'mw-watchlist-resetbutton' ] ) . "\n" . Xml::submitButton( $this->msg( 'enotif_reset' )->text(), [ 'name' => 'mw-watchlist-reset-submit' ] ) . "\n" . + Html::hidden( 'token', $user->getEditToken() ) . "\n" . Html::hidden( 'reset', 'all' ) . "\n"; foreach ( $nondefaults as $key => $value ) { $form .= Html::hidden( $key, $value ) . "\n"; diff --git a/includes/specials/SpecialWhatlinkshere.php b/includes/specials/SpecialWhatlinkshere.php index 439b6ab3cba9..6f91c46f8616 100644 --- a/includes/specials/SpecialWhatlinkshere.php +++ b/includes/specials/SpecialWhatlinkshere.php @@ -21,6 +21,8 @@ * @todo Use some variant of Pager or something; the pagination here is lousy. */ +use Wikimedia\Rdbms\IDatabase; + /** * Implements Special:Whatlinkshere * diff --git a/includes/specials/helpers/LoginHelper.php b/includes/specials/helpers/LoginHelper.php index f853f4173bad..cfcbf652c079 100644 --- a/includes/specials/helpers/LoginHelper.php +++ b/includes/specials/helpers/LoginHelper.php @@ -89,7 +89,7 @@ class LoginHelper extends ContextSource { } if ( $type === 'successredirect' ) { - $redirectUrl = $returnToTitle->getFullURL( $returnToQuery, false, $proto ); + $redirectUrl = $returnToTitle->getFullUrlForRedirect( $returnToQuery, $proto ); $this->getOutput()->redirect( $redirectUrl ); } else { $this->getOutput()->addReturnTo( $returnToTitle, $returnToQuery, null, $options ); diff --git a/includes/specials/pagers/ActiveUsersPager.php b/includes/specials/pagers/ActiveUsersPager.php index 645a1150413e..0d6f493d596d 100644 --- a/includes/specials/pagers/ActiveUsersPager.php +++ b/includes/specials/pagers/ActiveUsersPager.php @@ -101,12 +101,21 @@ class ActiveUsersPager extends UsersPager { $tables[] = 'user_groups'; $conds[] = 'ug_user = user_id'; $conds['ug_group'] = $this->groups; + if ( !$this->getConfig()->get( 'DisableUserGroupExpiry' ) ) { + $conds[] = 'ug_expiry IS NULL OR ug_expiry >= ' . $dbr->addQuotes( $dbr->timestamp() ); + } } if ( $this->excludegroups !== [] ) { foreach ( $this->excludegroups as $group ) { $conds[] = 'NOT EXISTS (' . $dbr->selectSQLText( - 'user_groups', '1', [ 'ug_user = user_id', 'ug_group' => $group ] - ) . ')'; + 'user_groups', '1', [ + 'ug_user = user_id', + 'ug_group' => $group, + $this->getConfig()->get( 'DisableUserGroupExpiry' ) ? + '1' : + 'ug_expiry IS NULL OR ug_expiry >= ' . $dbr->addQuotes( $dbr->timestamp() ) + ] + ) . ')'; } } if ( !$this->getUser()->isAllowed( 'hideuser' ) ) { @@ -165,9 +174,9 @@ class ActiveUsersPager extends UsersPager { $list = []; $user = User::newFromId( $row->user_id ); - $groups_list = self::getGroups( intval( $row->user_id ), $this->userGroupCache ); - foreach ( $groups_list as $group ) { - $list[] = self::buildGroupLink( $group, $userName ); + $ugms = self::getGroupMemberships( intval( $row->user_id ), $this->userGroupCache ); + foreach ( $ugms as $ugm ) { + $list[] = $this->buildGroupLink( $ugm, $userName ); } $groups = $lang->commaList( $list ); diff --git a/includes/specials/pagers/AllMessagesTablePager.php b/includes/specials/pagers/AllMessagesTablePager.php index efc51ef3ff98..ca1b7dca9d8e 100644 --- a/includes/specials/pagers/AllMessagesTablePager.php +++ b/includes/specials/pagers/AllMessagesTablePager.php @@ -19,6 +19,8 @@ * @ingroup Pager */ +use Wikimedia\Rdbms\FakeResultWrapper; + /** * Use TablePager for prettified output. We have to pretend that we're * getting data from a table when in fact not all of it comes from the database. diff --git a/includes/specials/pagers/BlockListPager.php b/includes/specials/pagers/BlockListPager.php index a4124db5f4b2..9a447ef8f374 100644 --- a/includes/specials/pagers/BlockListPager.php +++ b/includes/specials/pagers/BlockListPager.php @@ -23,6 +23,7 @@ * @ingroup Pager */ use MediaWiki\MediaWikiServices; +use Wikimedia\Rdbms\ResultWrapper; class BlockListPager extends TablePager { diff --git a/includes/specials/pagers/CategoryPager.php b/includes/specials/pagers/CategoryPager.php index 345577d6eab1..7db90c178853 100644 --- a/includes/specials/pagers/CategoryPager.php +++ b/includes/specials/pagers/CategoryPager.php @@ -92,21 +92,24 @@ class CategoryPager extends AlphabeticPager { } public function getStartForm( $from ) { - return Xml::tags( - 'form', - [ 'method' => 'get', 'action' => wfScript() ], - Html::hidden( 'title', $this->getTitle()->getPrefixedText() ) . - Xml::fieldset( - $this->msg( 'categories' )->text(), - Xml::inputLabel( - $this->msg( 'categoriesfrom' )->text(), - 'from', 'from', 20, $from, [ 'class' => 'mw-ui-input-inline' ] ) . - ' ' . - Html::submitButton( - $this->msg( 'categories-submit' )->text(), - [], [ 'mw-ui-progressive' ] - ) - ) - ); + $formDescriptor = [ + 'from' => [ + 'type' => 'title', + 'namespace' => NS_CATEGORY, + 'relative' => true, + 'label-message' => 'categoriesfrom', + 'name' => 'from', + 'id' => 'from', + 'size' => 20, + 'default' => $from, + ], + ]; + + $htmlForm = HTMLForm::factory( 'ooui', $formDescriptor, $this->getContext() ) + ->setSubmitTextMsg( 'categories-submit' ) + ->setWrapperLegendMsg( 'categories' ) + ->setMethod( 'get' ); + return $htmlForm->prepareForm()->getHTML( false ); } + } diff --git a/includes/specials/pagers/ContribsPager.php b/includes/specials/pagers/ContribsPager.php index 39c55c8a0c35..11336251ec64 100644 --- a/includes/specials/pagers/ContribsPager.php +++ b/includes/specials/pagers/ContribsPager.php @@ -24,6 +24,9 @@ * @ingroup Pager */ use MediaWiki\MediaWikiServices; +use Wikimedia\Rdbms\ResultWrapper; +use Wikimedia\Rdbms\FakeResultWrapper; +use Wikimedia\Rdbms\IDatabase; class ContribsPager extends ReverseChronologicalPager { @@ -160,7 +163,7 @@ class ContribsPager extends ReverseChronologicalPager { $user = $this->getUser(); $conds = array_merge( $userCond, $this->getNamespaceCond() ); - // Paranoia: avoid brute force searches (bug 17342) + // Paranoia: avoid brute force searches (T19342) if ( !$user->isAllowed( 'deletedhistory' ) ) { $conds[] = $this->mDb->bitAnd( 'rev_deleted', Revision::DELETED_USER ) . ' = 0'; } elseif ( !$user->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) { @@ -200,7 +203,9 @@ class ContribsPager extends ReverseChronologicalPager { $this->tagFilter ); - Hooks::run( 'ContribsPager::getQueryInfo', [ &$this, &$queryInfo ] ); + // Avoid PHP 7.1 warning from passing $this by reference + $pager = $this; + Hooks::run( 'ContribsPager::getQueryInfo', [ &$pager, &$queryInfo ] ); return $queryInfo; } @@ -222,7 +227,11 @@ class ContribsPager extends ReverseChronologicalPager { $join_conds['user_groups'] = [ 'LEFT JOIN', [ 'ug_user = rev_user', - 'ug_group' => $groupsWithBotPermission + 'ug_group' => $groupsWithBotPermission, + $this->getConfig()->get( 'DisableUserGroupExpiry' ) ? + '1' : + 'ug_expiry IS NULL OR ug_expiry >= ' . + $this->mDb->addQuotes( $this->mDb->timestamp() ) ] ]; } @@ -396,7 +405,7 @@ class ContribsPager extends ReverseChronologicalPager { $difftext = $linkRenderer->makeKnownLink( $page, new HtmlArmor( $this->messages['diff'] ), - [], + [ 'class' => 'mw-changeslist-diff' ], [ 'diff' => 'prev', 'oldid' => $row->rev_id @@ -408,13 +417,13 @@ class ContribsPager extends ReverseChronologicalPager { $histlink = $linkRenderer->makeKnownLink( $page, new HtmlArmor( $this->messages['hist'] ), - [], + [ 'class' => 'mw-changeslist-history' ], [ 'action' => 'history' ] ); if ( $row->rev_parent_id === null ) { // For some reason rev_parent_id isn't populated for this row. - // Its rumoured this is true on wikipedia for some revisions (bug 34922). + // Its rumoured this is true on wikipedia for some revisions (T36922). // Next best thing is to have the total number of bytes. $chardiff = ' <span class="mw-changeslist-separator">. .</span> '; $chardiff .= Linker::formatRevisionSize( $row->rev_len ); diff --git a/includes/specials/pagers/DeletedContribsPager.php b/includes/specials/pagers/DeletedContribsPager.php index 9ffcce9ef8b8..78e1092dc5ef 100644 --- a/includes/specials/pagers/DeletedContribsPager.php +++ b/includes/specials/pagers/DeletedContribsPager.php @@ -23,6 +23,8 @@ * @ingroup Pager */ use MediaWiki\MediaWikiServices; +use Wikimedia\Rdbms\ResultWrapper; +use Wikimedia\Rdbms\FakeResultWrapper; class DeletedContribsPager extends IndexPager { @@ -59,7 +61,7 @@ class DeletedContribsPager extends IndexPager { list( $index, $userCond ) = $this->getUserCond(); $conds = array_merge( $userCond, $this->getNamespaceCond() ); $user = $this->getUser(); - // Paranoia: avoid brute force searches (bug 17792) + // Paranoia: avoid brute force searches (T19792) if ( !$user->isAllowed( 'deletedhistory' ) ) { $conds[] = $this->mDb->bitAnd( 'ar_deleted', Revision::DELETED_USER ) . ' = 0'; } elseif ( !$user->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) { @@ -127,7 +129,7 @@ class DeletedContribsPager extends IndexPager { $condition = []; $condition['ar_user_text'] = $this->target; - $index = 'usertext_timestamp'; + $index = 'ar_usertext_timestamp'; return [ $index, $condition ]; } diff --git a/includes/specials/pagers/ImageListPager.php b/includes/specials/pagers/ImageListPager.php index 59dea025a615..47b059b7656a 100644 --- a/includes/specials/pagers/ImageListPager.php +++ b/includes/specials/pagers/ImageListPager.php @@ -23,6 +23,8 @@ * @ingroup Pager */ use MediaWiki\MediaWikiServices; +use Wikimedia\Rdbms\ResultWrapper; +use Wikimedia\Rdbms\FakeResultWrapper; class ImageListPager extends TablePager { @@ -191,7 +193,8 @@ class ImageListPager extends TablePager { } $sortable = [ 'img_timestamp', 'img_name', 'img_size' ]; /* For reference, the indicies we can use for sorting are: - * On the image table: img_usertext_timestamp, img_size, img_timestamp + * On the image table: img_user_timestamp, img_usertext_timestamp, + * img_size, img_timestamp * On oldimage: oi_usertext_timestamp, oi_name_timestamp * * In particular that means we cannot sort by timestamp when not filtering @@ -449,7 +452,7 @@ class ImageListPager extends TablePager { $imgfile = $this->msg( 'imgfile' )->text(); } - // Weird files can maybe exist? Bug 22227 + // Weird files can maybe exist? T24227 $filePage = Title::makeTitleSafe( NS_FILE, $value ); if ( $filePage ) { $link = $linkRenderer->makeKnownLink( diff --git a/includes/specials/pagers/MergeHistoryPager.php b/includes/specials/pagers/MergeHistoryPager.php index 56229b3b76ad..bbf97e13cb64 100644 --- a/includes/specials/pagers/MergeHistoryPager.php +++ b/includes/specials/pagers/MergeHistoryPager.php @@ -54,15 +54,17 @@ class MergeHistoryPager extends ReverseChronologicalPager { $batch = new LinkBatch(); # Give some pointers to make (last) links $this->mForm->prevId = []; + $rev_id = null; foreach ( $this->mResult as $row ) { $batch->addObj( Title::makeTitleSafe( NS_USER, $row->user_name ) ); $batch->addObj( Title::makeTitleSafe( NS_USER_TALK, $row->user_name ) ); - $rev_id = isset( $rev_id ) ? $rev_id : $row->rev_id; - if ( $rev_id > $row->rev_id ) { - $this->mForm->prevId[$rev_id] = $row->rev_id; - } elseif ( $rev_id < $row->rev_id ) { - $this->mForm->prevId[$row->rev_id] = $rev_id; + if ( isset( $rev_id ) ) { + if ( $rev_id > $row->rev_id ) { + $this->mForm->prevId[$rev_id] = $row->rev_id; + } elseif ( $rev_id < $row->rev_id ) { + $this->mForm->prevId[$row->rev_id] = $rev_id; + } } $rev_id = $row->rev_id; diff --git a/includes/specials/pagers/NewFilesPager.php b/includes/specials/pagers/NewFilesPager.php index e22b939fd676..b78193002aca 100644 --- a/includes/specials/pagers/NewFilesPager.php +++ b/includes/specials/pagers/NewFilesPager.php @@ -55,17 +55,31 @@ class NewFilesPager extends ReverseChronologicalPager { $fields = [ 'img_name', 'img_user', 'img_timestamp' ]; $options = []; + $user = $opts->getValue( 'user' ); + if ( $user !== '' ) { + $userId = User::idFromName( $user ); + if ( $userId ) { + $conds['img_user'] = $userId; + } else { + $conds['img_user_text'] = $user; + } + } + if ( !$opts->getValue( 'showbots' ) ) { $groupsWithBotPermission = User::getGroupsWithPermission( 'bot' ); if ( count( $groupsWithBotPermission ) ) { + $dbr = wfGetDB( DB_REPLICA ); $tables[] = 'user_groups'; $conds[] = 'ug_group IS NULL'; $jconds['user_groups'] = [ 'LEFT JOIN', [ 'ug_group' => $groupsWithBotPermission, - 'ug_user = img_user' + 'ug_user = img_user', + $this->getConfig()->get( 'DisableUserGroupExpiry' ) ? + '1' : + 'ug_expiry IS NULL OR ug_expiry >= ' . $dbr->addQuotes( $dbr->timestamp() ) ] ]; } diff --git a/includes/specials/pagers/NewPagesPager.php b/includes/specials/pagers/NewPagesPager.php index e298f103a23d..dafd244ee524 100644 --- a/includes/specials/pagers/NewPagesPager.php +++ b/includes/specials/pagers/NewPagesPager.php @@ -100,8 +100,10 @@ class NewPagesPager extends ReverseChronologicalPager { ]; $join_conds = [ 'page' => [ 'INNER JOIN', 'page_id=rc_cur_id' ] ]; + // Avoid PHP 7.1 warning from passing $this by reference + $pager = $this; Hooks::run( 'SpecialNewpagesConditions', - [ &$this, $this->opts, &$conds, &$tables, &$fields, &$join_conds ] ); + [ &$pager, $this->opts, &$conds, &$tables, &$fields, &$join_conds ] ); $options = []; diff --git a/includes/specials/pagers/ProtectedPagesPager.php b/includes/specials/pagers/ProtectedPagesPager.php new file mode 100644 index 000000000000..45dced882902 --- /dev/null +++ b/includes/specials/pagers/ProtectedPagesPager.php @@ -0,0 +1,335 @@ +<?php +/** + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @ingroup Pager + */ + +use \MediaWiki\Linker\LinkRenderer; + +/** + * @todo document + */ +class ProtectedPagesPager extends TablePager { + public $mForm, $mConds; + private $type, $level, $namespace, $sizetype, $size, $indefonly, $cascadeonly, $noredirect; + + /** + * @var LinkRenderer + */ + private $linkRenderer; + + /** + * @param SpecialProtectedpages $form + * @param array $conds + * @param $type + * @param $level + * @param $namespace + * @param string $sizetype + * @param int $size + * @param bool $indefonly + * @param bool $cascadeonly + * @param bool $noredirect + * @param LinkRenderer $linkRenderer + */ + function __construct( $form, $conds = [], $type, $level, $namespace, + $sizetype = '', $size = 0, $indefonly = false, $cascadeonly = false, $noredirect = false, + LinkRenderer $linkRenderer + ) { + $this->mForm = $form; + $this->mConds = $conds; + $this->type = ( $type ) ? $type : 'edit'; + $this->level = $level; + $this->namespace = $namespace; + $this->sizetype = $sizetype; + $this->size = intval( $size ); + $this->indefonly = (bool)$indefonly; + $this->cascadeonly = (bool)$cascadeonly; + $this->noredirect = (bool)$noredirect; + $this->linkRenderer = $linkRenderer; + parent::__construct( $form->getContext() ); + } + + function preprocessResults( $result ) { + # Do a link batch query + $lb = new LinkBatch; + $userids = []; + + foreach ( $result as $row ) { + $lb->add( $row->page_namespace, $row->page_title ); + // field is nullable, maybe null on old protections + if ( $row->log_user !== null ) { + $userids[] = $row->log_user; + } + } + + // fill LinkBatch with user page and user talk + if ( count( $userids ) ) { + $userCache = UserCache::singleton(); + $userCache->doQuery( $userids, [], __METHOD__ ); + foreach ( $userids as $userid ) { + $name = $userCache->getProp( $userid, 'name' ); + if ( $name !== false ) { + $lb->add( NS_USER, $name ); + $lb->add( NS_USER_TALK, $name ); + } + } + } + + $lb->execute(); + } + + function getFieldNames() { + static $headers = null; + + if ( $headers == [] ) { + $headers = [ + 'log_timestamp' => 'protectedpages-timestamp', + 'pr_page' => 'protectedpages-page', + 'pr_expiry' => 'protectedpages-expiry', + 'log_user' => 'protectedpages-performer', + 'pr_params' => 'protectedpages-params', + 'log_comment' => 'protectedpages-reason', + ]; + foreach ( $headers as $key => $val ) { + $headers[$key] = $this->msg( $val )->text(); + } + } + + return $headers; + } + + /** + * @param string $field + * @param string $value + * @return string HTML + * @throws MWException + */ + function formatValue( $field, $value ) { + /** @var $row object */ + $row = $this->mCurrentRow; + + switch ( $field ) { + case 'log_timestamp': + // when timestamp is null, this is a old protection row + if ( $value === null ) { + $formatted = Html::rawElement( + 'span', + [ 'class' => 'mw-protectedpages-unknown' ], + $this->msg( 'protectedpages-unknown-timestamp' )->escaped() + ); + } else { + $formatted = htmlspecialchars( $this->getLanguage()->userTimeAndDate( + $value, $this->getUser() ) ); + } + break; + + case 'pr_page': + $title = Title::makeTitleSafe( $row->page_namespace, $row->page_title ); + if ( !$title ) { + $formatted = Html::element( + 'span', + [ 'class' => 'mw-invalidtitle' ], + Linker::getInvalidTitleDescription( + $this->getContext(), + $row->page_namespace, + $row->page_title + ) + ); + } else { + $formatted = $this->linkRenderer->makeLink( $title ); + } + if ( !is_null( $row->page_len ) ) { + $formatted .= $this->getLanguage()->getDirMark() . + ' ' . Html::rawElement( + 'span', + [ 'class' => 'mw-protectedpages-length' ], + Linker::formatRevisionSize( $row->page_len ) + ); + } + break; + + case 'pr_expiry': + $formatted = htmlspecialchars( $this->getLanguage()->formatExpiry( + $value, /* User preference timezone */true ) ); + $title = Title::makeTitleSafe( $row->page_namespace, $row->page_title ); + if ( $this->getUser()->isAllowed( 'protect' ) && $title ) { + $changeProtection = $this->linkRenderer->makeKnownLink( + $title, + $this->msg( 'protect_change' )->text(), + [], + [ 'action' => 'unprotect' ] + ); + $formatted .= ' ' . Html::rawElement( + 'span', + [ 'class' => 'mw-protectedpages-actions' ], + $this->msg( 'parentheses' )->rawParams( $changeProtection )->escaped() + ); + } + break; + + case 'log_user': + // when timestamp is null, this is a old protection row + if ( $row->log_timestamp === null ) { + $formatted = Html::rawElement( + 'span', + [ 'class' => 'mw-protectedpages-unknown' ], + $this->msg( 'protectedpages-unknown-performer' )->escaped() + ); + } else { + $username = UserCache::singleton()->getProp( $value, 'name' ); + if ( LogEventsList::userCanBitfield( + $row->log_deleted, + LogPage::DELETED_USER, + $this->getUser() + ) ) { + if ( $username === false ) { + $formatted = htmlspecialchars( $value ); + } else { + $formatted = Linker::userLink( $value, $username ) + . Linker::userToolLinks( $value, $username ); + } + } else { + $formatted = $this->msg( 'rev-deleted-user' )->escaped(); + } + if ( LogEventsList::isDeleted( $row, LogPage::DELETED_USER ) ) { + $formatted = '<span class="history-deleted">' . $formatted . '</span>'; + } + } + break; + + case 'pr_params': + $params = []; + // Messages: restriction-level-sysop, restriction-level-autoconfirmed + $params[] = $this->msg( 'restriction-level-' . $row->pr_level )->escaped(); + if ( $row->pr_cascade ) { + $params[] = $this->msg( 'protect-summary-cascade' )->escaped(); + } + $formatted = $this->getLanguage()->commaList( $params ); + break; + + case 'log_comment': + // when timestamp is null, this is an old protection row + if ( $row->log_timestamp === null ) { + $formatted = Html::rawElement( + 'span', + [ 'class' => 'mw-protectedpages-unknown' ], + $this->msg( 'protectedpages-unknown-reason' )->escaped() + ); + } else { + if ( LogEventsList::userCanBitfield( + $row->log_deleted, + LogPage::DELETED_COMMENT, + $this->getUser() + ) ) { + $formatted = Linker::formatComment( $value !== null ? $value : '' ); + } else { + $formatted = $this->msg( 'rev-deleted-comment' )->escaped(); + } + if ( LogEventsList::isDeleted( $row, LogPage::DELETED_COMMENT ) ) { + $formatted = '<span class="history-deleted">' . $formatted . '</span>'; + } + } + break; + + default: + throw new MWException( "Unknown field '$field'" ); + } + + return $formatted; + } + + function getQueryInfo() { + $conds = $this->mConds; + $conds[] = 'pr_expiry > ' . $this->mDb->addQuotes( $this->mDb->timestamp() ) . + ' OR pr_expiry IS NULL'; + $conds[] = 'page_id=pr_page'; + $conds[] = 'pr_type=' . $this->mDb->addQuotes( $this->type ); + + if ( $this->sizetype == 'min' ) { + $conds[] = 'page_len>=' . $this->size; + } elseif ( $this->sizetype == 'max' ) { + $conds[] = 'page_len<=' . $this->size; + } + + if ( $this->indefonly ) { + $infinity = $this->mDb->addQuotes( $this->mDb->getInfinity() ); + $conds[] = "pr_expiry = $infinity OR pr_expiry IS NULL"; + } + if ( $this->cascadeonly ) { + $conds[] = 'pr_cascade = 1'; + } + if ( $this->noredirect ) { + $conds[] = 'page_is_redirect = 0'; + } + + if ( $this->level ) { + $conds[] = 'pr_level=' . $this->mDb->addQuotes( $this->level ); + } + if ( !is_null( $this->namespace ) ) { + $conds[] = 'page_namespace=' . $this->mDb->addQuotes( $this->namespace ); + } + + return [ + 'tables' => [ 'page', 'page_restrictions', 'log_search', 'logging' ], + 'fields' => [ + 'pr_id', + 'page_namespace', + 'page_title', + 'page_len', + 'pr_type', + 'pr_level', + 'pr_expiry', + 'pr_cascade', + 'log_timestamp', + 'log_user', + 'log_comment', + 'log_deleted', + ], + 'conds' => $conds, + 'join_conds' => [ + 'log_search' => [ + 'LEFT JOIN', [ + 'ls_field' => 'pr_id', 'ls_value = ' . $this->mDb->buildStringCast( 'pr_id' ) + ] + ], + 'logging' => [ + 'LEFT JOIN', [ + 'ls_log_id = log_id' + ] + ] + ] + ]; + } + + protected function getTableClass() { + return parent::getTableClass() . ' mw-protectedpages'; + } + + function getIndexField() { + return 'pr_id'; + } + + function getDefaultSort() { + return 'pr_id'; + } + + function isFieldSortable( $field ) { + // no index for sorting exists + return false; + } +} diff --git a/includes/specials/pagers/UsersPager.php b/includes/specials/pagers/UsersPager.php index 901be3899369..d599599031c5 100644 --- a/includes/specials/pagers/UsersPager.php +++ b/includes/specials/pagers/UsersPager.php @@ -112,6 +112,9 @@ class UsersPager extends AlphabeticPager { if ( $this->requestedGroup != '' ) { $conds['ug_group'] = $this->requestedGroup; + if ( !$this->getConfig()->get( 'DisableUserGroupExpiry' ) ) { + $conds[] = 'ug_expiry IS NULL OR ug_expiry >= ' . $dbr->addQuotes( $dbr->timestamp() ); + } } if ( $this->requestedUser != '' ) { @@ -161,7 +164,7 @@ class UsersPager extends AlphabeticPager { * @return string */ function formatRow( $row ) { - if ( $row->user_id == 0 ) { # Bug 16487 + if ( $row->user_id == 0 ) { # T18487 return ''; } @@ -177,12 +180,12 @@ class UsersPager extends AlphabeticPager { $lang = $this->getLanguage(); $groups = ''; - $groups_list = self::getGroups( intval( $row->user_id ), $this->userGroupCache ); + $ugms = self::getGroupMemberships( intval( $row->user_id ), $this->userGroupCache ); - if ( !$this->including && count( $groups_list ) > 0 ) { + if ( !$this->including && count( $ugms ) > 0 ) { $list = []; - foreach ( $groups_list as $group ) { - $list[] = self::buildGroupLink( $group, $userName ); + foreach ( $ugms as $ugm ) { + $list[] = $this->buildGroupLink( $ugm, $userName ); } $groups = $lang->commaList( $list ); } @@ -231,15 +234,18 @@ class UsersPager extends AlphabeticPager { $dbr = wfGetDB( DB_REPLICA ); $groupRes = $dbr->select( 'user_groups', - [ 'ug_user', 'ug_group' ], + UserGroupMembership::selectFields(), [ 'ug_user' => $userIds ], __METHOD__ ); $cache = []; $groups = []; foreach ( $groupRes as $row ) { - $cache[intval( $row->ug_user )][] = $row->ug_group; - $groups[$row->ug_group] = true; + $ugm = UserGroupMembership::newFromRow( $row ); + if ( !$ugm->isExpired() ) { + $cache[$row->ug_user][$row->ug_group] = $ugm; + $groups[$row->ug_group] = true; + } } // Give extensions a chance to add things like global user group data @@ -250,7 +256,7 @@ class UsersPager extends AlphabeticPager { // Add page of groups to link batch foreach ( $groups as $group => $unused ) { - $groupPage = User::getGroupPage( $group ); + $groupPage = UserGroupMembership::getGroupPage( $group ); if ( $groupPage ) { $batch->addObj( $groupPage ); } @@ -340,7 +346,7 @@ class UsersPager extends AlphabeticPager { function getAllGroups() { $result = []; foreach ( User::getAllGroups() as $group ) { - $result[$group] = User::getGroupName( $group ); + $result[$group] = UserGroupMembership::getGroupName( $group ); } asort( $result ); @@ -365,36 +371,30 @@ class UsersPager extends AlphabeticPager { } /** - * Get a list of groups the specified user belongs to + * Get an associative array containing groups the specified user belongs to, + * and the relevant UserGroupMembership objects * * @param int $uid User id * @param array|null $cache - * @return array + * @return array (group name => UserGroupMembership object) */ - protected static function getGroups( $uid, $cache = null ) { + protected static function getGroupMemberships( $uid, $cache = null ) { if ( $cache === null ) { $user = User::newFromId( $uid ); - $effectiveGroups = $user->getEffectiveGroups(); + return $user->getGroupMemberships(); } else { - $effectiveGroups = isset( $cache[$uid] ) ? $cache[$uid] : []; + return isset( $cache[$uid] ) ? $cache[$uid] : []; } - $groups = array_diff( $effectiveGroups, User::getImplicitGroups() ); - - return $groups; } /** * Format a link to a group description page * - * @param string $group Group name + * @param string|UserGroupMembership $group Group name or UserGroupMembership object * @param string $username Username * @return string */ - protected static function buildGroupLink( $group, $username ) { - return User::makeGroupLinkHTML( - $group, - User::getGroupMember( $group, $username ) - ); + protected function buildGroupLink( $group, $username ) { + return UserGroupMembership::getLink( $group, $this->getContext(), 'html', $username ); } - } diff --git a/includes/tidy/Balancer.php b/includes/tidy/Balancer.php index 1346e1cdd074..3467b49cae83 100644 --- a/includes/tidy/Balancer.php +++ b/includes/tidy/Balancer.php @@ -75,7 +75,7 @@ class BalanceSets { self::HTML_NAMESPACE => [ 'html' => true, 'head' => true, 'body' => true, 'frameset' => true, 'frame' => true, - 'plaintext' => true, 'isindex' => true, + 'plaintext' => true, 'xmp' => true, 'iframe' => true, 'noembed' => true, 'noscript' => true, 'script' => true, 'title' => true @@ -119,9 +119,9 @@ class BalanceSets { 'h2' => true, 'h3' => true, 'h4' => true, 'h5' => true, 'h6' => true, 'head' => true, 'header' => true, 'hgroup' => true, 'hr' => true, 'html' => true, 'iframe' => true, 'img' => true, - 'input' => true, 'isindex' => true, 'li' => true, 'link' => true, + 'input' => true, 'li' => true, 'link' => true, 'listing' => true, 'main' => true, 'marquee' => true, - 'menu' => true, 'menuitem' => true, 'meta' => true, 'nav' => true, + 'menu' => true, 'meta' => true, 'nav' => true, 'noembed' => true, 'noframes' => true, 'noscript' => true, 'object' => true, 'ol' => true, 'p' => true, 'param' => true, 'plaintext' => true, 'pre' => true, 'script' => true, @@ -156,7 +156,8 @@ class BalanceSets { public static $impliedEndTagsSet = [ self::HTML_NAMESPACE => [ - 'dd' => true, 'dt' => true, 'li' => true, 'optgroup' => true, + 'dd' => true, 'dt' => true, 'li' => true, + 'menuitem' => true, 'optgroup' => true, 'option' => true, 'p' => true, 'rb' => true, 'rp' => true, 'rt' => true, 'rtc' => true ] @@ -498,6 +499,16 @@ class BalanceElement { $this->attribs = [ 'class' => "mw-empty-elt" ]; } $blank = false; + } elseif ( + $this->isA( BalanceSets::$extraLinefeedSet ) && + count( $this->children ) > 0 && + substr( $this->children[0], 0, 1 ) == "\n" + ) { + // Double the linefeed after pre/listing/textarea + // according to the (old) HTML5 fragment serialization + // algorithm (see https://github.com/whatwg/html/issues/944) + // to ensure this will round-trip. + array_unshift( $this->children, "\n" ); } $flat = $blank ? '' : "{$this}"; } else { @@ -529,15 +540,6 @@ class BalanceElement { $out .= "{$elt}"; } $out .= "</{$this->localName}>"; - if ( - $this->isA( BalanceSets::$extraLinefeedSet ) && - $out[$len] === "\n" - ) { - // Double the linefeed after pre/listing/textarea - // according to the HTML5 fragment serialization algorithm. - $out = substr( $out, 0, $len + 1 ) . - substr( $out, $len ); - } } else { $out = "<{$this->localName}{$encAttribs} />"; Assert::invariant( @@ -1410,6 +1412,7 @@ class BalanceActiveFormattingElements { private $noahTableStack = [ [] ]; public function __destruct() { + $next = null; for ( $node = $this->head; $node; $node = $next ) { $next = $node->nextAFE; $node->prevAFE = $node->nextAFE = $node->nextNoah = null; @@ -1769,7 +1772,7 @@ class BalanceActiveFormattingElements { * and escaped. * - All null characters are assumed to have been removed. * - The following elements are disallowed: <html>, <head>, <body>, <frameset>, - * <frame>, <plaintext>, <isindex>, <xmp>, <iframe>, + * <frame>, <plaintext>, <xmp>, <iframe>, * <noembed>, <noscript>, <script>, <title>. As a result, * further simplifications can be made: * - `frameset-ok` is not tracked. @@ -1821,7 +1824,7 @@ class Balancer { * Regex borrowed from Tim Starling's "remex-html" project. */ const VALID_COMMENT_REGEX = "~ !-- - ( # 1. Comment match detector + ( # 1. Comment match detector > | -> | # Invalid short close ( # 2. Comment contents (?: @@ -1836,15 +1839,15 @@ class Balancer { ( # 3. Comment close --> | # Normal close --!> | # Comment end bang - ( # 4. Indicate matches requiring EOF - --! | # EOF in comment end bang state - -- | # EOF in comment end state - - | # EOF in comment end dash state - # EOF in comment state + ( # 4. Indicate matches requiring EOF + --! | # EOF in comment end bang state + -- | # EOF in comment end state + - | # EOF in comment end dash state + (?#nothing) # EOF in comment state ) ) ) - ([^<]*) \z # 5. Non-tag text after the comment + ([^<]*) \z # 5. Non-tag text after the comment ~xs"; /** @@ -1864,7 +1867,9 @@ class Balancer { * provide historical compatibility with the old "tidy" * program: <p>-wrapping is done to the children of * <body> and <blockquote> elements, and empty elements - * are removed. + * are removed. The <pre>/<listing>/<textarea> serialization + * is also tweaked to allow lossless round trips. + * (See: https://github.com/whatwg/html/issues/944) * 'allowComments': boolean, defaults to true. * When true, allows HTML comments in the input. * The Sanitizer generally strips all comments, so if you @@ -1996,6 +2001,7 @@ class Balancer { // Some hoops we have to jump through $adjusted = $this->stack->adjustedCurrentNode( $this->fragmentContext ); + // The spec calls this the "tree construction dispatcher". $isForeign = true; if ( $this->stack->length() === 0 || @@ -2036,6 +2042,9 @@ class Balancer { if ( $token === 'text' ) { $this->stack->insertText( $value ); return true; + } elseif ( $token === 'comment' ) { + $this->stack->insertComment( $value ); + return true; } elseif ( $token === 'tag' ) { switch ( $value ) { case 'font': @@ -2467,7 +2476,6 @@ class Balancer { case 'header': case 'hgroup': case 'main': - case 'menu': case 'nav': case 'ol': case 'p': @@ -2480,6 +2488,16 @@ class Balancer { $this->stack->insertHTMLElement( $value, $attribs ); return true; + case 'menu': + if ( $this->stack->inButtonScope( "p" ) ) { + $this->inBodyMode( 'endtag', 'p' ); + } + if ( $this->stack->currentNode->isHtmlNamed( 'menuitem' ) ) { + $this->stack->pop(); + } + $this->stack->insertHTMLElement( $value, $attribs ); + return true; + case 'h1': case 'h2': case 'h3': @@ -2655,7 +2673,6 @@ class Balancer { // (hence we don't need to examine the tag's "type" attribute) return true; - case 'menuitem': case 'param': case 'source': case 'track': @@ -2667,6 +2684,9 @@ class Balancer { if ( $this->stack->inButtonScope( 'p' ) ) { $this->inBodyMode( 'endtag', 'p' ); } + if ( $this->stack->currentNode->isHtmlNamed( 'menuitem' ) ) { + $this->stack->pop(); + } $this->stack->insertHTMLElement( $value, $attribs ); $this->stack->pop(); return true; @@ -2675,8 +2695,6 @@ class Balancer { // warts! return $this->inBodyMode( $token, 'img', $attribs, $selfClose ); - // OMITTED: <isindex> - case 'textarea': $this->stack->insertHTMLElement( $value, $attribs ); $this->ignoreLinefeed = true; @@ -2714,6 +2732,14 @@ class Balancer { $this->stack->insertHTMLElement( $value, $attribs ); return true; + case 'menuitem': + if ( $this->stack->currentNode->isHtmlNamed( 'menuitem' ) ) { + $this->stack->pop(); + } + $this->afe->reconstruct( $this->stack ); + $this->stack->insertHTMLElement( $value, $attribs ); + return true; + case 'rb': case 'rtc': if ( $this->stack->inScope( 'ruby' ) ) { diff --git a/includes/tidy/RemexCompatFormatter.php b/includes/tidy/RemexCompatFormatter.php new file mode 100644 index 000000000000..3dc727bc89b5 --- /dev/null +++ b/includes/tidy/RemexCompatFormatter.php @@ -0,0 +1,71 @@ +<?php + +namespace MediaWiki\Tidy; + +use RemexHtml\HTMLData; +use RemexHtml\Serializer\HtmlFormatter; +use RemexHtml\Serializer\SerializerNode; +use RemexHtml\Tokenizer\PlainAttributes; + +/** + * @internal + */ +class RemexCompatFormatter extends HtmlFormatter { + private static $markedEmptyElements = [ + 'li' => true, + 'p' => true, + 'tr' => true, + ]; + + public function __construct( $options = [] ) { + parent::__construct( $options ); + $this->attributeEscapes["\xc2\xa0"] = ' '; + unset( $this->attributeEscapes["&"] ); + $this->textEscapes["\xc2\xa0"] = ' '; + unset( $this->textEscapes["&"] ); + } + + public function startDocument( $fragmentNamespace, $fragmentName ) { + return ''; + } + + public function element( SerializerNode $parent, SerializerNode $node, $contents ) { + $data = $node->snData; + if ( $data && $data->isPWrapper ) { + if ( $data->nonblankNodeCount ) { + return "<p>$contents</p>"; + } else { + return $contents; + } + } + + $name = $node->name; + $attrs = $node->attrs; + if ( isset( self::$markedEmptyElements[$name] ) && $attrs->count() === 0 ) { + if ( strspn( $contents, "\t\n\f\r " ) === strlen( $contents ) ) { + return "<{$name} class=\"mw-empty-elt\">$contents</{$name}>"; + } + } + + $s = "<$name"; + foreach ( $attrs->getValues() as $attrName => $attrValue ) { + $encValue = strtr( $attrValue, $this->attributeEscapes ); + $s .= " $attrName=\"$encValue\""; + } + if ( $node->namespace === HTMLData::NS_HTML && isset( $this->voidElements[$name] ) ) { + $s .= ' />'; + return $s; + } + + $s .= '>'; + if ( $node->namespace === HTMLData::NS_HTML + && isset( $contents[0] ) && $contents[0] === "\n" + && isset( $this->prefixLfElements[$name] ) + ) { + $s .= "\n$contents</$name>"; + } else { + $s .= "$contents</$name>"; + } + return $s; + } +} diff --git a/includes/tidy/RemexCompatMunger.php b/includes/tidy/RemexCompatMunger.php new file mode 100644 index 000000000000..dbcf568c054e --- /dev/null +++ b/includes/tidy/RemexCompatMunger.php @@ -0,0 +1,474 @@ +<?php + +namespace MediaWiki\Tidy; + +use RemexHtml\HTMLData; +use RemexHtml\Serializer\Serializer; +use RemexHtml\Serializer\SerializerNode; +use RemexHtml\Tokenizer\Attributes; +use RemexHtml\Tokenizer\PlainAttributes; +use RemexHtml\TreeBuilder\TreeBuilder; +use RemexHtml\TreeBuilder\TreeHandler; +use RemexHtml\TreeBuilder\Element; + +/** + * @internal + */ +class RemexCompatMunger implements TreeHandler { + private static $onlyInlineElements = [ + "a" => true, + "abbr" => true, + "acronym" => true, + "applet" => true, + "b" => true, + "basefont" => true, + "bdo" => true, + "big" => true, + "br" => true, + "button" => true, + "cite" => true, + "code" => true, + "dfn" => true, + "em" => true, + "font" => true, + "i" => true, + "iframe" => true, + "img" => true, + "input" => true, + "kbd" => true, + "label" => true, + "legend" => true, + "map" => true, + "object" => true, + "param" => true, + "q" => true, + "rb" => true, + "rbc" => true, + "rp" => true, + "rt" => true, + "rtc" => true, + "ruby" => true, + "s" => true, + "samp" => true, + "select" => true, + "small" => true, + "span" => true, + "strike" => true, + "strong" => true, + "sub" => true, + "sup" => true, + "textarea" => true, + "tt" => true, + "u" => true, + "var" => true, + ]; + + private static $formattingElements = [ + 'a' => true, + 'b' => true, + 'big' => true, + 'code' => true, + 'em' => true, + 'font' => true, + 'i' => true, + 'nobr' => true, + 's' => true, + 'small' => true, + 'strike' => true, + 'strong' => true, + 'tt' => true, + 'u' => true, + ]; + + /** + * Constructor + * + * @param Serializer $serializer + */ + public function __construct( Serializer $serializer ) { + $this->serializer = $serializer; + } + + public function startDocument( $fragmentNamespace, $fragmentName ) { + $this->serializer->startDocument( $fragmentNamespace, $fragmentName ); + $root = $this->serializer->getRootNode(); + $root->snData = new RemexMungerData; + $root->snData->needsPWrapping = true; + } + + public function endDocument( $pos ) { + $this->serializer->endDocument( $pos ); + } + + private function getParentForInsert( $preposition, $refElement ) { + if ( $preposition === TreeBuilder::ROOT ) { + return [ $this->serializer->getRootNode(), null ]; + } elseif ( $preposition === TreeBuilder::BEFORE ) { + $refNode = $refElement->userData; + return [ $this->serializer->getParentNode( $refNode ), $refNode ]; + } else { + $refNode = $refElement->userData; + $refData = $refNode->snData; + if ( $refData->currentCloneElement ) { + // Follow a chain of clone links if necessary + $origRefData = $refData; + while ( $refData->currentCloneElement ) { + $refElement = $refData->currentCloneElement; + $refNode = $refElement->userData; + $refData = $refNode->snData; + } + // Cache the end of the chain in the requested element + $origRefData->currentCloneElement = $refElement; + } elseif ( $refData->childPElement ) { + $refElement = $refData->childPElement; + $refNode = $refElement->userData; + } + return [ $refNode, $refNode ]; + } + } + + /** + * Insert a p-wrapper + * + * @param SerializerNode $parent + * @param integer $sourceStart + * @return SerializerNode + */ + private function insertPWrapper( SerializerNode $parent, $sourceStart ) { + $pWrap = new Element( HTMLData::NS_HTML, 'mw:p-wrap', new PlainAttributes ); + $this->serializer->insertElement( TreeBuilder::UNDER, $parent, $pWrap, false, + $sourceStart, 0 ); + $data = new RemexMungerData; + $data->isPWrapper = true; + $data->wrapBaseNode = $parent; + $pWrap->userData->snData = $data; + $parent->snData->childPElement = $pWrap; + return $pWrap->userData; + } + + public function characters( $preposition, $refElement, $text, $start, $length, + $sourceStart, $sourceLength + ) { + $isBlank = strspn( $text, "\t\n\f\r ", $start, $length ) === $length; + + list( $parent, $refNode ) = $this->getParentForInsert( $preposition, $refElement ); + $parentData = $parent->snData; + + if ( $preposition === TreeBuilder::UNDER ) { + if ( $parentData->needsPWrapping && !$isBlank ) { + // Add a p-wrapper for bare text under body/blockquote + $refNode = $this->insertPWrapper( $refNode, $sourceStart ); + $parent = $refNode; + $parentData = $parent->snData; + } elseif ( $parentData->isSplittable && !$parentData->ancestorPNode ) { + // The parent is splittable and in block mode, so split the tag stack + $refNode = $this->splitTagStack( $refNode, true, $sourceStart ); + $parent = $refNode; + $parentData = $parent->snData; + } + } + + if ( !$isBlank ) { + // Non-whitespace characters detected + $parentData->nonblankNodeCount++; + } + $this->serializer->characters( $preposition, $refNode, $text, $start, + $length, $sourceStart, $sourceLength ); + } + + /** + * Insert or reparent an element. Create p-wrappers or split the tag stack + * as necessary. + * + * Consider the following insertion locations. The parent may be: + * + * - A: A body or blockquote (!!needsPWrapping) + * - B: A p-wrapper (!!isPWrapper) + * - C: A descendant of a p-wrapper (!!ancestorPNode) + * - CS: With splittable formatting elements in the stack region up to + * the p-wrapper + * - CU: With one or more unsplittable elements in the stack region up + * to the p-wrapper + * - D: Not a descendant of a p-wrapper (!ancestorNode) + * - DS: With splittable formatting elements in the stack region up to + * the body or blockquote + * - DU: With one or more unsplittable elements in the stack region up + * to the body or blockquote + * + * And consider that we may insert two types of element: + * - b: block + * - i: inline + * + * We handle the insertion as follows: + * + * - A/i: Create a p-wrapper, insert under it + * - A/b: Insert as normal + * - B/i: Insert as normal + * - B/b: Close the p-wrapper, insert under the body/blockquote (wrap + * base) instead) + * - C/i: Insert as normal + * - CS/b: Split the tag stack, insert the block under cloned formatting + * elements which have the wrap base (the parent of the p-wrap) as + * their ultimate parent. + * - CU/b: Disable the p-wrap, by reparenting the currently open child + * of the p-wrap under the p-wrap's parent. Then insert the block as + * normal. + * - D/b: Insert as normal + * - DS/i: Split the tag stack, creating a new p-wrapper as the ultimate + * parent of the formatting elements thus cloned. The parent of the + * p-wrapper is the body or blockquote. + * - DU/i: Insert as normal + * + * FIXME: fostering ($preposition == BEFORE) is mostly done by inserting as + * normal, the full algorithm is not followed. + * + * @param integer $preposition + * @param Element|SerializerNode|null $refElement + * @param Element $element + * @param bool $void + * @param integer $sourceStart + * @param integer $sourceLength + */ + public function insertElement( $preposition, $refElement, Element $element, $void, + $sourceStart, $sourceLength + ) { + list( $parent, $newRef ) = $this->getParentForInsert( $preposition, $refElement ); + $parentData = $parent->snData; + $parentNs = $parent->namespace; + $parentName = $parent->name; + $elementName = $element->htmlName; + + $inline = isset( self::$onlyInlineElements[$elementName] ); + $under = $preposition === TreeBuilder::UNDER; + + if ( $under && $parentData->isPWrapper && !$inline ) { + // [B/b] The element is non-inline and the parent is a p-wrapper, + // close the parent and insert into its parent instead + $newParent = $this->serializer->getParentNode( $parent ); + $parent = $newParent; + $parentData = $parent->snData; + $pElement = $parentData->childPElement; + $parentData->childPElement = null; + $newRef = $refElement->userData; + $this->endTag( $pElement, $sourceStart, 0 ); + } elseif ( $under && $parentData->isSplittable + && (bool)$parentData->ancestorPNode !== $inline + ) { + // [CS/b, DS/i] The parent is splittable and the current element is + // inline in block context, or if the current element is a block + // under a p-wrapper, split the tag stack. + $newRef = $this->splitTagStack( $newRef, $inline, $sourceStart ); + $parent = $newRef; + $parentData = $parent->snData; + } elseif ( $under && $parentData->needsPWrapping && $inline ) { + // [A/i] If the element is inline and we are in body/blockquote, + // we need to create a p-wrapper + $newRef = $this->insertPWrapper( $newRef, $sourceStart ); + $parent = $newRef; + $parentData = $parent->snData; + } elseif ( $parentData->ancestorPNode && !$inline ) { + // [CU/b] If the element is non-inline and (despite attempting to + // split above) there is still an ancestor p-wrap, disable that + // p-wrap + $this->disablePWrapper( $parent, $sourceStart ); + } + // else [A/b, B/i, C/i, D/b, DU/i] insert as normal + + // An element with element children is a non-blank element + $parentData->nonblankNodeCount++; + + // Insert the element downstream and so initialise its userData + $this->serializer->insertElement( $preposition, $newRef, + $element, $void, $sourceStart, $sourceLength ); + + // Initialise snData + if ( !$element->userData->snData ) { + $elementData = $element->userData->snData = new RemexMungerData; + } else { + $elementData = $element->userData->snData; + } + if ( ( $parentData->isPWrapper || $parentData->isSplittable ) + && isset( self::$formattingElements[$elementName] ) + ) { + $elementData->isSplittable = true; + } + if ( $parentData->isPWrapper ) { + $elementData->ancestorPNode = $parent; + } elseif ( $parentData->ancestorPNode ) { + $elementData->ancestorPNode = $parentData->ancestorPNode; + } + if ( $parentData->wrapBaseNode ) { + $elementData->wrapBaseNode = $parentData->wrapBaseNode; + } elseif ( $parentData->needsPWrapping ) { + $elementData->wrapBaseNode = $parent; + } + if ( $elementName === 'body' + || $elementName === 'blockquote' + || $elementName === 'html' + ) { + $elementData->needsPWrapping = true; + } + } + + /** + * Clone nodes in a stack range and return the new parent + * + * @param SerializerNode $parentNode + * @param bool $inline + * @param integer $pos The source position + * @return SerializerNode + */ + private function splitTagStack( SerializerNode $parentNode, $inline, $pos ) { + $parentData = $parentNode->snData; + $wrapBase = $parentData->wrapBaseNode; + $pWrap = $parentData->ancestorPNode; + if ( !$pWrap ) { + $cloneEnd = $wrapBase; + } else { + $cloneEnd = $parentData->ancestorPNode; + } + + $serializer = $this->serializer; + $node = $parentNode; + $root = $serializer->getRootNode(); + $nodes = []; + $removableNodes = []; + $haveContent = false; + while ( $node !== $cloneEnd ) { + $nextParent = $serializer->getParentNode( $node ); + if ( $nextParent === $root ) { + throw new \Exception( 'Did not find end of clone range' ); + } + $nodes[] = $node; + if ( $node->snData->nonblankNodeCount === 0 ) { + $removableNodes[] = $node; + $nextParent->snData->nonblankNodeCount--; + } + $node = $nextParent; + } + + if ( $inline ) { + $pWrap = $this->insertPWrapper( $wrapBase, $pos ); + $node = $pWrap; + } else { + if ( $pWrap ) { + // End the p-wrap which was open, cancel the diversion + $wrapBase->snData->childPElement = null; + } + $pWrap = null; + $node = $wrapBase; + } + + for ( $i = count( $nodes ) - 1; $i >= 0; $i-- ) { + $oldNode = $nodes[$i]; + $oldData = $oldNode->snData; + $nodeParent = $node; + $element = new Element( $oldNode->namespace, $oldNode->name, $oldNode->attrs ); + $this->serializer->insertElement( TreeBuilder::UNDER, $nodeParent, + $element, false, $pos, 0 ); + $oldData->currentCloneElement = $element; + + $newNode = $element->userData; + $newData = $newNode->snData = new RemexMungerData; + if ( $pWrap ) { + $newData->ancestorPNode = $pWrap; + } + $newData->isSplittable = true; + $newData->wrapBaseNode = $wrapBase; + $newData->isPWrapper = $oldData->isPWrapper; + + $nodeParent->snData->nonblankNodeCount++; + + $node = $newNode; + } + foreach ( $removableNodes as $rNode ) { + $fakeElement = new Element( $rNode->namespace, $rNode->name, $rNode->attrs ); + $fakeElement->userData = $rNode; + $this->serializer->removeNode( $fakeElement, $pos ); + } + return $node; + } + + /** + * Find the ancestor of $node which is a child of a p-wrapper, and + * reparent that node so that it is placed after the end of the p-wrapper + */ + private function disablePWrapper( SerializerNode $node, $sourceStart ) { + $nodeData = $node->snData; + $pWrapNode = $nodeData->ancestorPNode; + $newParent = $this->serializer->getParentNode( $pWrapNode ); + if ( $pWrapNode !== $this->serializer->getLastChild( $newParent ) ) { + // Fostering or something? Abort! + return; + } + + $nextParent = $node; + do { + $victim = $nextParent; + $victim->snData->ancestorPNode = null; + $nextParent = $this->serializer->getParentNode( $victim ); + } while ( $nextParent !== $pWrapNode ); + + // Make a fake Element to use in a reparenting operation + $victimElement = new Element( $victim->namespace, $victim->name, $victim->attrs ); + $victimElement->userData = $victim; + + // Reparent + $this->serializer->insertElement( TreeBuilder::UNDER, $newParent, $victimElement, + false, $sourceStart, 0 ); + + // Decrement nonblank node count + $pWrapNode->snData->nonblankNodeCount--; + + // Cancel the diversion so that no more elements are inserted under this p-wrap + $newParent->snData->childPElement = null; + } + + public function endTag( Element $element, $sourceStart, $sourceLength ) { + $data = $element->userData->snData; + if ( $data->childPElement ) { + $this->endTag( $data->childPElement, $sourceStart, 0 ); + } + $this->serializer->endTag( $element, $sourceStart, $sourceLength ); + $element->userData->snData = null; + $element->userData = null; + } + + public function doctype( $name, $public, $system, $quirks, $sourceStart, $sourceLength ) { + $this->serializer->doctype( $name, $public, $system, $quirks, + $sourceStart, $sourceLength ); + } + + public function comment( $preposition, $refElement, $text, $sourceStart, $sourceLength ) { + list( $parent, $refNode ) = $this->getParentForInsert( $preposition, $refElement ); + $this->serializer->comment( $preposition, $refNode, $text, + $sourceStart, $sourceLength ); + } + + public function error( $text, $pos ) { + $this->serializer->error( $text, $pos ); + } + + public function mergeAttributes( Element $element, Attributes $attrs, $sourceStart ) { + $this->serializer->mergeAttributes( $element, $attrs, $sourceStart ); + } + + public function removeNode( Element $element, $sourceStart ) { + $this->serializer->removeNode( $element, $sourceStart ); + } + + public function reparentChildren( Element $element, Element $newParent, $sourceStart ) { + $self = $element->userData; + $children = $self->children; + $self->children = []; + $this->insertElement( TreeBuilder::UNDER, $element, $newParent, false, $sourceStart, 0 ); + $newParentNode = $newParent->userData; + $newParentId = $newParentNode->id; + foreach ( $children as $child ) { + if ( is_object( $child ) ) { + $child->parentId = $newParentId; + } + } + $newParentNode->children = $children; + } +} diff --git a/includes/tidy/RemexDriver.php b/includes/tidy/RemexDriver.php new file mode 100644 index 000000000000..e02af88fd9f2 --- /dev/null +++ b/includes/tidy/RemexDriver.php @@ -0,0 +1,57 @@ +<?php + +namespace MediaWiki\Tidy; + +use RemexHtml\Serializer\Serializer; +use RemexHtml\Tokenizer\Tokenizer; +use RemexHtml\TreeBuilder\Dispatcher; +use RemexHtml\TreeBuilder\TreeBuilder; +use RemexHtml\TreeBuilder\TreeMutationTracer; + +class RemexDriver extends TidyDriverBase { + private $trace; + private $pwrap; + + public function __construct( array $config ) { + $config += [ + 'treeMutationTrace' => false, + 'pwrap' => true + ]; + $this->trace = $config['treeMutationTrace']; + $this->pwrap = $config['pwrap']; + parent::__construct( $config ); + } + + public function tidy( $text ) { + $formatter = new RemexCompatFormatter; + $serializer = new Serializer( $formatter ); + if ( $this->pwrap ) { + $munger = new RemexCompatMunger( $serializer ); + } else { + $munger = $serializer; + } + if ( $this->trace ) { + $tracer = new TreeMutationTracer( $munger, function ( $msg ) { + wfDebug( "RemexHtml: $msg" ); + } ); + } else { + $tracer = $munger; + } + $treeBuilder = new TreeBuilder( $tracer, [ + 'ignoreErrors' => true, + 'ignoreNulls' => true, + ] ); + $dispatcher = new Dispatcher( $treeBuilder ); + $tokenizer = new Tokenizer( $dispatcher, $text, [ + 'ignoreErrors' => true, + 'ignoreCharRefs' => true, + 'ignoreNulls' => true, + 'skipPreprocess' => true, + ] ); + $tokenizer->execute( [ + 'fragmentNamespace' => \RemexHtml\HTMLData::NS_HTML, + 'fragmentName' => 'body' + ] ); + return $serializer->getResult(); + } +} diff --git a/includes/tidy/RemexMungerData.php b/includes/tidy/RemexMungerData.php new file mode 100644 index 000000000000..d614a3818350 --- /dev/null +++ b/includes/tidy/RemexMungerData.php @@ -0,0 +1,78 @@ +<?php + +namespace MediaWiki\Tidy; + +/** + * @internal + */ +class RemexMungerData { + /** + * The Element for the mw:p-wrap which is a child of the current node. If + * this is set, inline insertions into this node will be diverted so that + * they insert into the p-wrap. + * + * @var \RemexHtml\TreeBuilder\Element|null + */ + public $childPElement; + + /** + * This tracks the mw:p-wrap node in the Serializer stack which is an + * ancestor of this node. If there is no mw:p-wrap ancestor, it is null. + * + * @var \RemexHtml\Serializer\SerializerNode|null + */ + public $ancestorPNode; + + /** + * The wrap base node is the body or blockquote node which is the parent + * of active p-wrappers. This is set if there is an ancestor p-wrapper, + * or if a p-wrapper was closed due to a block element being encountered + * inside it. + * + * @var \RemexHtml\Serializer\SerializerNode|null + */ + public $wrapBaseNode; + + /** + * Stack splitting (essentially our idea of AFE reconstruction) can clone + * formatting elements which are split over multiple paragraphs. + * TreeBuilder is not aware of the cloning, and continues to insert into + * the original element. This is set to the newer clone if this node was + * cloned, i.e. if there is an active diversion of the insertion location. + * + * @var \RemexHtml\TreeBuilder\Element|null + */ + public $currentCloneElement; + + /** + * Is the node a p-wrapper, with name mw:p-wrap? + * + * @var bool + */ + public $isPWrapper = false; + + /** + * Is the node splittable, i.e. a formatting element or a node with a + * formatting element ancestor which is under an active or deactivated + * p-wrapper. + * + * @var bool + */ + public $isSplittable = false; + + /** + * This is true if the node is a body or blockquote, which activates + * p-wrapping of child nodes. + */ + public $needsPWrapping = false; + + /** + * The number of child nodes, not counting whitespace-only text nodes or + * comments. + */ + public $nonblankNodeCount = 0; + + public function __set( $name, $value ) { + throw new \Exception( "Cannot set property \"$name\"" ); + } +} diff --git a/includes/tidy/TidyDriverBase.php b/includes/tidy/TidyDriverBase.php index 96ee8c394f6c..d3f9d48591f6 100644 --- a/includes/tidy/TidyDriverBase.php +++ b/includes/tidy/TidyDriverBase.php @@ -27,7 +27,7 @@ abstract class TidyDriverBase { * @return bool Whether the HTML is valid */ public function validate( $text, &$errorStr ) { - throw new \MWException( get_class( $this ) . " does not support validate()" ); + throw new \MWException( static::class . ' does not support validate()' ); } /** diff --git a/includes/title/NamespaceAwareForeignTitleFactory.php b/includes/title/NamespaceAwareForeignTitleFactory.php index 2d67a2877c41..4d24cb850ca1 100644 --- a/includes/title/NamespaceAwareForeignTitleFactory.php +++ b/includes/title/NamespaceAwareForeignTitleFactory.php @@ -115,15 +115,23 @@ class NamespaceAwareForeignTitleFactory implements ForeignTitleFactory { protected function parseTitleWithNs( $title, $ns ) { $pieces = explode( ':', $title, 2 ); + // Is $title of the form Namespace:Title (true), or just Title (false)? + $titleIncludesNamespace = ( $ns != '0' && count( $pieces ) === 2 ); + if ( isset( $this->foreignNamespaces[$ns] ) ) { $namespaceName = $this->foreignNamespaces[$ns]; } else { - $namespaceName = $ns == '0' ? '' : $pieces[0]; + // If the foreign wiki is misconfigured, XML dumps can contain a page with + // a non-zero namespace ID, but whose title doesn't contain a colon + // (T114115). In those cases, output a made-up namespace name to avoid + // collisions. The ImportTitleFactory might replace this with something + // more appropriate. + $namespaceName = $titleIncludesNamespace ? $pieces[0] : "Ns$ns"; } // We assume that the portion of the page title before the colon is the - // namespace name, except in the case of namespace 0 - if ( $ns != '0' ) { + // namespace name, except in the case of namespace 0. + if ( $titleIncludesNamespace ) { $pageName = $pieces[1]; } else { $pageName = $title; diff --git a/includes/upload/UploadBase.php b/includes/upload/UploadBase.php index 96f8638557d5..2c0afdf00f8f 100644 --- a/includes/upload/UploadBase.php +++ b/includes/upload/UploadBase.php @@ -20,6 +20,7 @@ * @file * @ingroup Upload */ +use MediaWiki\MediaWikiServices; /** * @defgroup Upload Upload related @@ -297,7 +298,7 @@ abstract class UploadBase { * @param string $srcPath The source path * @return string|bool The real path if it was a virtual URL Returns false on failure */ - function getRealPath( $srcPath ) { + public function getRealPath( $srcPath ) { $repo = RepoGroup::singleton()->getLocalRepo(); if ( $repo->isVirtualUrl( $srcPath ) ) { /** @todo Just make uploads work with storage paths UploadFromStash @@ -560,7 +561,7 @@ abstract class UploadBase { * * @param array $entry */ - function zipEntryCallback( $entry ) { + public function zipEntryCallback( $entry ) { $names = [ $entry['name'] ]; // If there is a null character, cut off the name at it, because JDK's @@ -895,7 +896,7 @@ abstract class UploadBase { return $this->mTitle; } - // Windows may be broken with special characters, see bug 1780 + // Windows may be broken with special characters, see T3780 if ( !preg_match( '/^[\x0-\x7f]*$/', $nt->getText() ) && !RepoGroup::singleton()->getLocalRepo()->backendSupportsUnicodePaths() ) { @@ -1209,7 +1210,7 @@ abstract class UploadBase { } // Some browsers will interpret obscure xml encodings as UTF-8, while - // PHP/expat will interpret the given encoding in the xml declaration (bug 47304) + // PHP/expat will interpret the given encoding in the xml declaration (T49304) if ( $extension == 'svg' || strpos( $mime, 'image/svg' ) === 0 ) { if ( self::checkXMLEncodingMissmatch( $file ) ) { return true; @@ -1358,11 +1359,14 @@ abstract class UploadBase { $filename, [ $this, 'checkSvgScriptCallback' ], true, - [ 'processing_instruction_handler' => 'UploadBase::checkSvgPICallback' ] + [ + 'processing_instruction_handler' => 'UploadBase::checkSvgPICallback', + 'external_dtd_handler' => 'UploadBase::checkSvgExternalDTD', + ] ); if ( $check->wellFormed !== true ) { - // Invalid xml (bug 58553) - // But only when non-partial (bug 65724) + // Invalid xml (T60553) + // But only when non-partial (T67724) return $partial ? false : [ 'uploadinvalidxml' ]; } elseif ( $check->filterMatch ) { if ( $this->mSVGNSError ) { @@ -1382,7 +1386,7 @@ abstract class UploadBase { * @return bool (true if the filter identified something bad) */ public static function checkSvgPICallback( $target, $data ) { - // Don't allow external stylesheets (bug 57550) + // Don't allow external stylesheets (T59550) if ( preg_match( '/xml-stylesheet/i', $target ) ) { return [ 'upload-scripted-pi-callback' ]; } @@ -1391,6 +1395,34 @@ abstract class UploadBase { } /** + * Verify that DTD urls referenced are only the standard dtds + * + * Browsers seem to ignore external dtds. However just to be on the + * safe side, only allow dtds from the svg standard. + * + * @param string $type PUBLIC or SYSTEM + * @param string $publicId The well-known public identifier for the dtd + * @param string $systemId The url for the external dtd + */ + public static function checkSvgExternalDTD( $type, $publicId, $systemId ) { + // This doesn't include the XHTML+MathML+SVG doctype since we don't + // allow XHTML anyways. + $allowedDTDs = [ + 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd', + 'http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd', + 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11-basic.dtd', + 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11-tiny.dtd' + ]; + if ( $type !== 'PUBLIC' + || !in_array( $systemId, $allowedDTDs ) + || strpos( $publicId, "-//W3C//" ) !== 0 + ) { + return [ 'upload-scripted-dtd' ]; + } + return false; + } + + /** * @todo Replace this with a whitelist filter! * @param string $element * @param array $attribs @@ -1401,7 +1433,7 @@ abstract class UploadBase { list( $namespace, $strippedElement ) = $this->splitXmlNamespace( $element ); // We specifically don't include: - // http://www.w3.org/1999/xhtml (bug 60771) + // http://www.w3.org/1999/xhtml (T62771) static $validNamespaces = [ '', 'adobe:ns:meta/', @@ -1440,6 +1472,7 @@ abstract class UploadBase { 'http://www.w3.org/1999/02/22-rdf-syntax-ns#', 'http://www.w3.org/2000/svg', 'http://www.w3.org/tr/rec-rdf-syntax/', + 'http://www.w3.org/2000/01/rdf-schema#', ]; // Inkscape mangles namespace definitions created by Adobe Illustrator. @@ -2082,7 +2115,7 @@ abstract class UploadBase { public static function getSessionStatus( User $user, $statusKey ) { $key = wfMemcKey( 'uploadstatus', $user->getId() ?: md5( $user->getName() ), $statusKey ); - return ObjectCache::getMainStashInstance()->get( $key ); + return MediaWikiServices::getInstance()->getMainObjectStash()->get( $key ); } /** @@ -2098,7 +2131,7 @@ abstract class UploadBase { public static function setSessionStatus( User $user, $statusKey, $value ) { $key = wfMemcKey( 'uploadstatus', $user->getId() ?: md5( $user->getName() ), $statusKey ); - $cache = ObjectCache::getMainStashInstance(); + $cache = MediaWikiServices::getInstance()->getMainObjectStash(); if ( $value === false ) { $cache->delete( $key ); } else { diff --git a/includes/upload/UploadFromUrl.php b/includes/upload/UploadFromUrl.php index 865f6300be7a..2b13dd8b526b 100644 --- a/includes/upload/UploadFromUrl.php +++ b/includes/upload/UploadFromUrl.php @@ -225,7 +225,7 @@ class UploadFromUrl extends UploadBase { // Well... that's not good! wfDebugLog( 'fileupload', - 'Short write ' . $this->nbytes . '/' . strlen( $buffer ) . + 'Short write ' . $nbytes . '/' . strlen( $buffer ) . ' bytes, aborting with ' . $this->mFileSize . ' uploaded so far' ); fclose( $this->mTmpHandle ); diff --git a/includes/user/PasswordReset.php b/includes/user/PasswordReset.php index c1aef22ba19f..4ee256c495bf 100644 --- a/includes/user/PasswordReset.php +++ b/includes/user/PasswordReset.php @@ -176,7 +176,7 @@ class PasswordReset implements LoggerAwareInterface { $firstUser = $users[0]; if ( !$firstUser instanceof User || !$firstUser->getId() ) { - // Don't parse username as wikitext (bug 65501) + // Don't parse username as wikitext (T67501) return StatusValue::newFatal( wfMessage( 'nosuchuser', wfEscapeWikiText( $username ) ) ); } @@ -192,7 +192,7 @@ class PasswordReset implements LoggerAwareInterface { wfEscapeWikiText( $firstUser->getName() ) ) ); } - // We need to have a valid IP address for the hook, but per bug 18347, we should + // We need to have a valid IP address for the hook, but per T20347, we should // send the user's name if they're logged in. $ip = $performingUser->getRequest()->getIP(); if ( !$ip ) { diff --git a/includes/user/User.php b/includes/user/User.php index fed64c2a682e..e9f6dce4b29a 100644 --- a/includes/user/User.php +++ b/includes/user/User.php @@ -66,7 +66,7 @@ class User implements IDBAccessObject { /** * @const int Serialized record version. */ - const VERSION = 10; + const VERSION = 11; /** * Exclude user options that are set to their default value. @@ -104,7 +104,7 @@ class User implements IDBAccessObject { 'mRegistration', 'mEditCount', // user_groups table - 'mGroups', + 'mGroupMemberships', // user_properties table 'mOptionOverrides', ]; @@ -146,7 +146,6 @@ class User implements IDBAccessObject { 'editmyuserjs', 'editmywatchlist', 'editsemiprotected', - 'editusercssjs', # deprecated 'editusercss', 'edituserjs', 'hideuser', @@ -225,8 +224,13 @@ class User implements IDBAccessObject { protected $mRegistration; /** @var int */ protected $mEditCount; - /** @var array */ - public $mGroups; + /** + * @var array No longer used since 1.29; use User::getGroups() instead + * @deprecated since 1.29 + */ + private $mGroups; + /** @var array Associative array of (group name => UserGroupMembership object) */ + protected $mGroupMemberships; /** @var array */ protected $mOptionOverrides; // @} @@ -283,9 +287,7 @@ class User implements IDBAccessObject { /** @var array */ public $mOptions; - /** - * @var WebRequest - */ + /** @var WebRequest */ private $mRequest; /** @var Block */ @@ -469,6 +471,17 @@ class User implements IDBAccessObject { } /** + * @param WANObjectCache $cache + * @return string[] + * @since 1.28 + */ + public function getMutableCacheKeys( WANObjectCache $cache ) { + $id = $this->getId(); + + return $id ? [ $this->getCacheKey( $cache ) ] : []; + } + + /** * Load user data from shared cache, given mId has already been set. * * @return bool True @@ -935,7 +948,7 @@ class User implements IDBAccessObject { // Ensure that the username isn't longer than 235 bytes, so that // (at least for the builtin skins) user javascript and css files - // will work. (bug 23080) + // will work. (T25080) if ( strlen( $name ) > 235 ) { wfDebugLog( 'username', __METHOD__ . ": '$name' invalid due to length" ); @@ -1060,7 +1073,7 @@ class User implements IDBAccessObject { } // Clean up name according to title rules, - // but only when validation is requested (bug 12654) + // but only when validation is requested (T14654) $t = ( $validate !== false ) ? Title::newFromText( $name, NS_USER ) : Title::makeTitle( NS_USER, $name ); // Check for invalid titles @@ -1138,7 +1151,7 @@ class User implements IDBAccessObject { $this->mEmailToken = ''; $this->mEmailTokenExpires = null; $this->mRegistration = wfTimestamp( TS_MW ); - $this->mGroups = []; + $this->mGroupMemberships = []; Hooks::run( 'UserLoadDefaults', [ $this, $name ] ); } @@ -1250,7 +1263,7 @@ class User implements IDBAccessObject { if ( $s !== false ) { // Initialise user table data $this->loadFromRow( $s ); - $this->mGroups = null; // deferred + $this->mGroupMemberships = null; // deferred $this->getEditCount(); // revalidation for nulls return true; } else { @@ -1267,13 +1280,16 @@ class User implements IDBAccessObject { * @param stdClass $row Row from the user table to load. * @param array $data Further user data to load into the object * - * user_groups Array with groups out of the user_groups table - * user_properties Array with properties out of the user_properties table + * user_groups Array of arrays or stdClass result rows out of the user_groups + * table. Previously you were supposed to pass an array of strings + * here, but we also need expiry info nowadays, so an array of + * strings is ignored. + * user_properties Array with properties out of the user_properties table */ protected function loadFromRow( $row, $data = null ) { $all = true; - $this->mGroups = null; // deferred + $this->mGroupMemberships = null; // deferred if ( isset( $row->user_name ) ) { $this->mName = $row->user_name; @@ -1342,7 +1358,18 @@ class User implements IDBAccessObject { if ( is_array( $data ) ) { if ( isset( $data['user_groups'] ) && is_array( $data['user_groups'] ) ) { - $this->mGroups = $data['user_groups']; + if ( !count( $data['user_groups'] ) ) { + $this->mGroupMemberships = []; + } else { + $firstGroup = reset( $data['user_groups'] ); + if ( is_array( $firstGroup ) || is_object( $firstGroup ) ) { + $this->mGroupMemberships = []; + foreach ( $data['user_groups'] as $row ) { + $ugm = UserGroupMembership::newFromRow( (object)$row ); + $this->mGroupMemberships[$ugm->getGroup()] = $ugm; + } + } + } } if ( isset( $data['user_properties'] ) && is_array( $data['user_properties'] ) ) { $this->loadOptions( $data['user_properties'] ); @@ -1366,18 +1393,12 @@ class User implements IDBAccessObject { * Load the groups from the database if they aren't already loaded. */ private function loadGroups() { - if ( is_null( $this->mGroups ) ) { + if ( is_null( $this->mGroupMemberships ) ) { $db = ( $this->queryFlagsUsed & self::READ_LATEST ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_REPLICA ); - $res = $db->select( 'user_groups', - [ 'ug_group' ], - [ 'ug_user' => $this->mId ], - __METHOD__ ); - $this->mGroups = []; - foreach ( $res as $row ) { - $this->mGroups[] = $row->ug_group; - } + $this->mGroupMemberships = UserGroupMembership::getMembershipsForUser( + $this->mId, $db ); } } @@ -1509,7 +1530,7 @@ class User implements IDBAccessObject { $this->mRights = null; $this->mEffectiveGroups = null; $this->mImplicitGroups = null; - $this->mGroups = null; + $this->mGroupMemberships = null; $this->mOptions = null; $this->mOptionsLoaded = false; $this->mEditCount = null; @@ -1615,29 +1636,9 @@ class User implements IDBAccessObject { // User/IP blocking $block = Block::newFromTarget( $this, $ip, !$bFromSlave ); - // If no block has been found, check for a cookie indicating that the user is blocked. - $blockCookieVal = (int)$this->getRequest()->getCookie( 'BlockID' ); - if ( !$block instanceof Block && $blockCookieVal > 0 ) { - // Load the Block from the ID in the cookie. - $tmpBlock = Block::newFromID( $blockCookieVal ); - if ( $tmpBlock instanceof Block ) { - // Check the validity of the block. - $blockIsValid = $tmpBlock->getType() == Block::TYPE_USER - && !$tmpBlock->isExpired() - && $tmpBlock->isAutoblocking(); - $config = RequestContext::getMain()->getConfig(); - $useBlockCookie = ( $config->get( 'CookieSetOnAutoblock' ) === true ); - if ( $blockIsValid && $useBlockCookie ) { - // Use the block. - $block = $tmpBlock; - $this->blockTrigger = 'cookie-block'; - } else { - // If the block is not valid, clear the block cookie (but don't delete it, - // because it needs to be cleared from LocalStorage as well and an empty string - // value is checked for in the mediawiki.user.blockcookie module). - $tmpBlock->setCookie( $this->getRequest()->response(), true ); - } - } + // Cookie blocking + if ( !$block instanceof Block ) { + $block = $this->getBlockFromCookieValue( $this->getRequest()->getCookie( 'BlockID' ) ); } // Proxy blocking @@ -1662,7 +1663,7 @@ class User implements IDBAccessObject { } } - // (bug 23343) Apply IP blocks to the contents of XFF headers, if enabled + // (T25343) Apply IP blocks to the contents of XFF headers, if enabled if ( !$block instanceof Block && $wgApplyIpBlocksToXff && $ip !== null @@ -1717,6 +1718,44 @@ class User implements IDBAccessObject { } /** + * Try to load a Block from an ID given in a cookie value. + * @param string|null $blockCookieVal The cookie value to check. + * @return Block|bool The Block object, or false if none could be loaded. + */ + protected function getBlockFromCookieValue( $blockCookieVal ) { + // Make sure there's something to check. The cookie value must start with a number. + if ( strlen( $blockCookieVal ) < 1 || !is_numeric( substr( $blockCookieVal, 0, 1 ) ) ) { + return false; + } + // Load the Block from the ID in the cookie. + $blockCookieId = Block::getIdFromCookieValue( $blockCookieVal ); + if ( $blockCookieId !== null ) { + // An ID was found in the cookie. + $tmpBlock = Block::newFromID( $blockCookieId ); + if ( $tmpBlock instanceof Block ) { + // Check the validity of the block. + $blockIsValid = $tmpBlock->getType() == Block::TYPE_USER + && !$tmpBlock->isExpired() + && $tmpBlock->isAutoblocking(); + $config = RequestContext::getMain()->getConfig(); + $useBlockCookie = ( $config->get( 'CookieSetOnAutoblock' ) === true ); + if ( $blockIsValid && $useBlockCookie ) { + // Use the block. + $this->blockTrigger = 'cookie-block'; + return $tmpBlock; + } else { + // If the block is not valid, remove the cookie. + Block::clearCookie( $this->getRequest()->response() ); + } + } else { + // If the block doesn't exist, remove the cookie. + Block::clearCookie( $this->getRequest()->response() ); + } + } + return false; + } + + /** * Whether the given IP is in a DNS blacklist. * * @param string $ip IP to check @@ -1748,7 +1787,7 @@ class User implements IDBAccessObject { $found = false; // @todo FIXME: IPv6 ??? (https://bugs.php.net/bug.php?id=33170) if ( IP::isIPv4( $ip ) ) { - // Reverse IP, bug 21255 + // Reverse IP, T23255 $ipReversed = implode( '.', array_reverse( explode( '.', $ip ) ) ); foreach ( (array)$bases as $base ) { @@ -1823,7 +1862,7 @@ class User implements IDBAccessObject { */ public function isPingLimitable() { global $wgRateLimitsExcludedIPs; - if ( in_array( $this->getRequest()->getIP(), $wgRateLimitsExcludedIPs ) ) { + if ( IP::isInRanges( $this->getRequest()->getIP(), $wgRateLimitsExcludedIPs ) ) { // No other good way currently to disable rate limits // for specific IPs. :P // But this is a crappy hack and should die. @@ -3176,7 +3215,7 @@ class User implements IDBAccessObject { /** * Get the permissions this user has. - * @return array Array of String permission names + * @return string[] permission names */ public function getRights() { if ( is_null( $this->mRights ) ) { @@ -3222,7 +3261,20 @@ class User implements IDBAccessObject { public function getGroups() { $this->load(); $this->loadGroups(); - return $this->mGroups; + return array_keys( $this->mGroupMemberships ); + } + + /** + * Get the list of explicit group memberships this user has, stored as + * UserGroupMembership objects. Implicit groups are not included. + * + * @return array Associative array of (group name as string => UserGroupMembership object) + * @since 1.29 + */ + public function getGroupMemberships() { + $this->load(); + $this->loadGroups(); + return $this->mGroupMemberships; } /** @@ -3333,34 +3385,35 @@ class User implements IDBAccessObject { } /** - * Add the user to the given group. - * This takes immediate effect. + * Add the user to the given group. This takes immediate effect. + * If the user is already in the group, the expiry time will be updated to the new + * expiry time. (If $expiry is omitted or null, the membership will be altered to + * never expire.) + * * @param string $group Name of the group to add + * @param string $expiry Optional expiry timestamp in any format acceptable to + * wfTimestamp(), or null if the group assignment should not expire * @return bool */ - public function addGroup( $group ) { + public function addGroup( $group, $expiry = null ) { $this->load(); + $this->loadGroups(); - if ( !Hooks::run( 'UserAddGroup', [ $this, &$group ] ) ) { + if ( $expiry ) { + $expiry = wfTimestamp( TS_MW, $expiry ); + } + + if ( !Hooks::run( 'UserAddGroup', [ $this, &$group, &$expiry ] ) ) { return false; } - $dbw = wfGetDB( DB_MASTER ); - if ( $this->getId() ) { - $dbw->insert( 'user_groups', - [ - 'ug_user' => $this->getId(), - 'ug_group' => $group, - ], - __METHOD__, - [ 'IGNORE' ] ); + // create the new UserGroupMembership and put it in the DB + $ugm = new UserGroupMembership( $this->mId, $group, $expiry ); + if ( !$ugm->insert( true ) ) { + return false; } - $this->loadGroups(); - $this->mGroups[] = $group; - // In case loadGroups was not called before, we now have the right twice. - // Get rid of the duplicate. - $this->mGroups = array_unique( $this->mGroups ); + $this->mGroupMemberships[$group] = $ugm; // Refresh the groups caches, and clear the rights cache so it will be // refreshed on the next call to $this->getRights(). @@ -3380,29 +3433,19 @@ class User implements IDBAccessObject { */ public function removeGroup( $group ) { $this->load(); + if ( !Hooks::run( 'UserRemoveGroup', [ $this, &$group ] ) ) { return false; } - $dbw = wfGetDB( DB_MASTER ); - $dbw->delete( 'user_groups', - [ - 'ug_user' => $this->getId(), - 'ug_group' => $group, - ], __METHOD__ - ); - // Remember that the user was in this group - $dbw->insert( 'user_former_groups', - [ - 'ufg_user' => $this->getId(), - 'ufg_group' => $group, - ], - __METHOD__, - [ 'IGNORE' ] - ); + $ugm = UserGroupMembership::getMembership( $this->mId, $group ); + // delete the membership entry + if ( !$ugm || !$ugm->delete() ) { + return false; + } $this->loadGroups(); - $this->mGroups = array_diff( $this->mGroups, [ $group ] ); + unset( $this->mGroupMemberships[$group] ); // Refresh the groups caches, and clear the rights cache so it will be // refreshed on the next call to $this->getRights(). @@ -3728,6 +3771,42 @@ class User implements IDBAccessObject { } /** + * Compute experienced level based on edit count and registration date. + * + * @return string 'newcomer', 'learner', or 'experienced' + */ + public function getExperienceLevel() { + global $wgLearnerEdits, + $wgExperiencedUserEdits, + $wgLearnerMemberSince, + $wgExperiencedUserMemberSince; + + if ( $this->isAnon() ) { + return false; + } + + $editCount = $this->getEditCount(); + $registration = $this->getRegistration(); + $now = time(); + $learnerRegistration = wfTimestamp( TS_MW, $now - $wgLearnerMemberSince * 86400 ); + $experiencedRegistration = wfTimestamp( TS_MW, $now - $wgExperiencedUserMemberSince * 86400 ); + + if ( + $editCount < $wgLearnerEdits || + $registration > $learnerRegistration + ) { + return 'newcomer'; + } elseif ( + $editCount > $wgExperiencedUserEdits && + $registration <= $experiencedRegistration + ) { + return 'experienced'; + } else { + return 'learner'; + } + } + + /** * Set a cookie on the user's client. Wrapper for * WebResponse::setCookie * @deprecated since 1.27 @@ -4046,7 +4125,7 @@ class User implements IDBAccessObject { * } * // do something with $user... * - * However, this was vulnerable to a race condition (bug 16020). By + * However, this was vulnerable to a race condition (T18020). By * initialising the user object if the user exists, we aim to support this * calling sequence as far as possible. * @@ -4159,7 +4238,7 @@ class User implements IDBAccessObject { return $this->mBlock; } - # bug 13611: if the IP address the user is trying to create an account from is + # T15611: if the IP address the user is trying to create an account from is # blocked with createaccount disabled, prevent new account creation there even # when the user is logged in if ( $this->mBlockedFromCreateAccount === false && !$this->isAllowed( 'ipblock-exempt' ) ) { @@ -4452,7 +4531,7 @@ class User implements IDBAccessObject { * @note Since these URLs get dropped directly into emails, using the * short English names avoids insanely long URL-encoded links, which * also sometimes can get corrupted in some browsers/mailers - * (bug 6957 with Gmail and Internet Explorer). + * (T8957 with Gmail and Internet Explorer). * * @param string $page Special page * @param string $token Token @@ -4736,25 +4815,27 @@ class User implements IDBAccessObject { /** * Get the localized descriptive name for a group, if it exists + * @deprecated since 1.29 Use UserGroupMembership::getGroupName instead * * @param string $group Internal group name * @return string Localized descriptive group name */ public static function getGroupName( $group ) { - $msg = wfMessage( "group-$group" ); - return $msg->isBlank() ? $group : $msg->text(); + wfDeprecated( __METHOD__, '1.29' ); + return UserGroupMembership::getGroupName( $group ); } /** * Get the localized descriptive name for a member of a group, if it exists + * @deprecated since 1.29 Use UserGroupMembership::getGroupMemberName instead * * @param string $group Internal group name * @param string $username Username for gender (since 1.19) * @return string Localized name for group member */ public static function getGroupMember( $group, $username = '#' ) { - $msg = wfMessage( "group-$group-member", $username ); - return $msg->isBlank() ? $group : $msg->text(); + wfDeprecated( __METHOD__, '1.29' ); + return UserGroupMembership::getGroupMemberName( $group, $username ); } /** @@ -4804,34 +4885,33 @@ class User implements IDBAccessObject { /** * Get the title of a page describing a particular group + * @deprecated since 1.29 Use UserGroupMembership::getGroupPage instead * * @param string $group Internal group name * @return Title|bool Title of the page if it exists, false otherwise */ public static function getGroupPage( $group ) { - $msg = wfMessage( 'grouppage-' . $group )->inContentLanguage(); - if ( $msg->exists() ) { - $title = Title::newFromText( $msg->text() ); - if ( is_object( $title ) ) { - return $title; - } - } - return false; + wfDeprecated( __METHOD__, '1.29' ); + return UserGroupMembership::getGroupPage( $group ); } /** * Create a link to the group in HTML, if available; * else return the group name. + * @deprecated since 1.29 Use UserGroupMembership::getLink instead, or + * make the link yourself if you need custom text * * @param string $group Internal name of the group * @param string $text The text of the link * @return string HTML link to the group */ public static function makeGroupLinkHTML( $group, $text = '' ) { + wfDeprecated( __METHOD__, '1.29' ); + if ( $text == '' ) { - $text = self::getGroupName( $group ); + $text = UserGroupMembership::getGroupName( $group ); } - $title = self::getGroupPage( $group ); + $title = UserGroupMembership::getGroupPage( $group ); if ( $title ) { return Linker::link( $title, htmlspecialchars( $text ) ); } else { @@ -4842,16 +4922,20 @@ class User implements IDBAccessObject { /** * Create a link to the group in Wikitext, if available; * else return the group name. + * @deprecated since 1.29 Use UserGroupMembership::getLink instead, or + * make the link yourself if you need custom text * * @param string $group Internal name of the group * @param string $text The text of the link * @return string Wikilink to the group */ public static function makeGroupLinkWiki( $group, $text = '' ) { + wfDeprecated( __METHOD__, '1.29' ); + if ( $text == '' ) { - $text = self::getGroupName( $group ); + $text = UserGroupMembership::getGroupName( $group ); } - $title = self::getGroupPage( $group ); + $title = UserGroupMembership::getGroupPage( $group ); if ( $title ) { $page = $title->getFullText(); return "[[$page|$text]]"; @@ -5092,54 +5176,6 @@ class User implements IDBAccessObject { } /** - * Make a new-style password hash - * - * @param string $password Plain-text password - * @param bool|string $salt Optional salt, may be random or the user ID. - * If unspecified or false, will generate one automatically - * @return string Password hash - * @deprecated since 1.24, use Password class - */ - public static function crypt( $password, $salt = false ) { - wfDeprecated( __METHOD__, '1.24' ); - $passwordFactory = new PasswordFactory(); - $passwordFactory->init( RequestContext::getMain()->getConfig() ); - $hash = $passwordFactory->newFromPlaintext( $password ); - return $hash->toString(); - } - - /** - * Compare a password hash with a plain-text password. Requires the user - * ID if there's a chance that the hash is an old-style hash. - * - * @param string $hash Password hash - * @param string $password Plain-text password to compare - * @param string|bool $userId User ID for old-style password salt - * - * @return bool - * @deprecated since 1.24, use Password class - */ - public static function comparePasswords( $hash, $password, $userId = false ) { - wfDeprecated( __METHOD__, '1.24' ); - - // Check for *really* old password hashes that don't even have a type - // The old hash format was just an md5 hex hash, with no type information - if ( preg_match( '/^[0-9a-f]{32}$/', $hash ) ) { - global $wgPasswordSalt; - if ( $wgPasswordSalt ) { - $password = ":B:{$userId}:{$hash}"; - } else { - $password = ":A:{$hash}"; - } - } - - $passwordFactory = new PasswordFactory(); - $passwordFactory->init( RequestContext::getMain()->getConfig() ); - $hash = $passwordFactory->newFromCiphertext( $hash ); - return $hash->equals( $password ); - } - - /** * Add a newuser log entry for this user. * Before 1.19 the return value was always true. * @@ -5229,6 +5265,13 @@ class User implements IDBAccessObject { $this->mOptionOverrides = []; $data = []; foreach ( $res as $row ) { + // Convert '0' to 0. PHP's boolean conversion considers them both + // false, but e.g. JavaScript considers the former as true. + // @todo: T54542 Somehow determine the desired type (string/int/bool) + // and convert all values here. + if ( $row->up_value === '0' ) { + $row->up_value = 0; + } $data[$row->up_property] = $row->up_value; } } @@ -5355,7 +5398,7 @@ class User implements IDBAccessObject { # Note that the pattern requirement will always be satisfied if the # input is empty, so we need required in all cases. - # @todo FIXME: Bug 23769: This needs to not claim the password is required + # @todo FIXME: T25769: This needs to not claim the password is required # if e-mail confirmation is being used. Since HTML5 input validation # is b0rked anyway in some browsers, just return nothing. When it's # re-enabled, fix this code to not output required for e-mail @@ -5411,10 +5454,10 @@ class User implements IDBAccessObject { static function newFatalPermissionDeniedStatus( $permission ) { global $wgLang; - $groups = array_map( - [ 'User', 'makeGroupLinkWiki' ], - User::getGroupsWithPermission( $permission ) - ); + $groups = []; + foreach ( User::getGroupsWithPermission( $permission ) as $group ) { + $groups[] = UserGroupMembership::getLink( $group, RequestContext::getMain(), 'wiki' ); + } if ( $groups ) { return Status::newFatal( 'badaccess-groups', $wgLang->commaList( $groups ), count( $groups ) ); diff --git a/includes/user/UserArray.php b/includes/user/UserArray.php index dddc850bd034..ab6683b297ec 100644 --- a/includes/user/UserArray.php +++ b/includes/user/UserArray.php @@ -20,6 +20,8 @@ * @file */ +use Wikimedia\Rdbms\ResultWrapper; + abstract class UserArray implements Iterator { /** * @param ResultWrapper $res @@ -38,7 +40,7 @@ abstract class UserArray implements Iterator { /** * @param array $ids - * @return UserArrayFromResult + * @return UserArrayFromResult|ArrayIterator */ static function newFromIDs( $ids ) { $ids = array_map( 'intval', (array)$ids ); // paranoia @@ -59,7 +61,7 @@ abstract class UserArray implements Iterator { /** * @since 1.25 * @param array $names - * @return UserArrayFromResult + * @return UserArrayFromResult|ArrayIterator */ static function newFromNames( $names ) { $names = array_map( 'strval', (array)$names ); // paranoia diff --git a/includes/user/UserArrayFromResult.php b/includes/user/UserArrayFromResult.php index fb533d08b4c2..527df7fa442f 100644 --- a/includes/user/UserArrayFromResult.php +++ b/includes/user/UserArrayFromResult.php @@ -20,6 +20,8 @@ * @file */ +use Wikimedia\Rdbms\ResultWrapper; + class UserArrayFromResult extends UserArray implements Countable { /** @var ResultWrapper */ public $res; @@ -27,7 +29,7 @@ class UserArrayFromResult extends UserArray implements Countable { /** @var int */ public $key; - /** @var bool|stdClass */ + /** @var bool|User */ public $current; /** diff --git a/includes/user/UserGroupMembership.php b/includes/user/UserGroupMembership.php new file mode 100644 index 000000000000..81a4083bb49d --- /dev/null +++ b/includes/user/UserGroupMembership.php @@ -0,0 +1,477 @@ +<?php +/** + * Represents the membership of a user to a user group. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + */ + +use Wikimedia\Rdbms\IDatabase; + +/** + * Represents a "user group membership" -- a specific instance of a user belonging + * to a group. For example, the fact that user Mary belongs to the sysop group is a + * user group membership. + * + * The class encapsulates rows in the user_groups table. The logic is low-level and + * doesn't run any hooks. Often, you will want to call User::addGroup() or + * User::removeGroup() instead. + * + * @since 1.29 + */ +class UserGroupMembership { + /** @var int The ID of the user who belongs to the group */ + private $userId; + + /** @var string */ + private $group; + + /** @var string|null Timestamp of expiry in TS_MW format, or null if no expiry */ + private $expiry; + + /** + * @param int $userId The ID of the user who belongs to the group + * @param string $group The internal group name + * @param string|null $expiry Timestamp of expiry in TS_MW format, or null if no expiry + */ + public function __construct( $userId = 0, $group = null, $expiry = null ) { + global $wgDisableUserGroupExpiry; + if ( $wgDisableUserGroupExpiry ) { + $expiry = null; + } + + $this->userId = (int)$userId; + $this->group = $group; // TODO throw on invalid group? + $this->expiry = $expiry ?: null; + } + + /** + * @return int + */ + public function getUserId() { + return $this->userId; + } + + /** + * @return string + */ + public function getGroup() { + return $this->group; + } + + /** + * @return string|null Timestamp of expiry in TS_MW format, or null if no expiry + */ + public function getExpiry() { + global $wgDisableUserGroupExpiry; + if ( $wgDisableUserGroupExpiry ) { + return null; + } + + return $this->expiry; + } + + protected function initFromRow( $row ) { + global $wgDisableUserGroupExpiry; + + $this->userId = (int)$row->ug_user; + $this->group = $row->ug_group; + if ( $wgDisableUserGroupExpiry ) { + $this->expiry = null; + } else { + $this->expiry = $row->ug_expiry === null ? + null : + wfTimestamp( TS_MW, $row->ug_expiry ); + } + } + + /** + * Creates a new UserGroupMembership object from a database row. + * + * @param stdClass $row The row from the user_groups table + * @return UserGroupMembership + */ + public static function newFromRow( $row ) { + $ugm = new self; + $ugm->initFromRow( $row ); + return $ugm; + } + + /** + * Returns the list of user_groups fields that should be selected to create + * a new user group membership. + * @return array + */ + public static function selectFields() { + global $wgDisableUserGroupExpiry; + if ( $wgDisableUserGroupExpiry ) { + return [ + 'ug_user', + 'ug_group', + ]; + } else { + return [ + 'ug_user', + 'ug_group', + 'ug_expiry', + ]; + } + } + + /** + * Delete the row from the user_groups table. + * + * @throws MWException + * @param IDatabase|null $dbw Optional master database connection to use + * @return bool Whether or not anything was deleted + */ + public function delete( IDatabase $dbw = null ) { + global $wgDisableUserGroupExpiry; + if ( wfReadOnly() ) { + return false; + } + + if ( $dbw === null ) { + $dbw = wfGetDB( DB_MASTER ); + } + + if ( $wgDisableUserGroupExpiry ) { + $dbw->delete( 'user_groups', $this->getDatabaseArray( $dbw ), __METHOD__ ); + } else { + $dbw->delete( + 'user_groups', + [ 'ug_user' => $this->userId, 'ug_group' => $this->group ], + __METHOD__ ); + } + if ( !$dbw->affectedRows() ) { + return false; + } + + // Remember that the user was in this group + $dbw->insert( + 'user_former_groups', + [ 'ufg_user' => $this->userId, 'ufg_group' => $this->group ], + __METHOD__, + [ 'IGNORE' ] ); + + return true; + } + + /** + * Insert a user right membership into the database. When $allowUpdate is false, + * the function fails if there is a conflicting membership entry (same user and + * group) already in the table. + * + * @throws MWException + * @param bool $allowUpdate Whether to perform "upsert" instead of INSERT + * @param IDatabase|null $dbw If you have one available + * @return bool Whether or not anything was inserted + */ + public function insert( $allowUpdate = false, IDatabase $dbw = null ) { + global $wgDisableUserGroupExpiry; + if ( $dbw === null ) { + $dbw = wfGetDB( DB_MASTER ); + } + + // Purge old, expired memberships from the DB + self::purgeExpired( $dbw ); + + // Check that the values make sense + if ( $this->group === null ) { + throw new UnexpectedValueException( + 'Don\'t try inserting an uninitialized UserGroupMembership object' ); + } elseif ( $this->userId <= 0 ) { + throw new UnexpectedValueException( + 'UserGroupMembership::insert() needs a positive user ID. ' . + 'Did you forget to add your User object to the database before calling addGroup()?' ); + } + + $row = $this->getDatabaseArray( $dbw ); + $dbw->insert( 'user_groups', $row, __METHOD__, [ 'IGNORE' ] ); + $affected = $dbw->affectedRows(); + + // Don't collide with expired user group memberships + // Do this after trying to insert, in order to avoid locking + if ( !$wgDisableUserGroupExpiry && !$affected ) { + $conds = [ + 'ug_user' => $row['ug_user'], + 'ug_group' => $row['ug_group'], + ]; + // if we're unconditionally updating, check that the expiry is not already the + // same as what we are trying to update it to; otherwise, only update if + // the expiry date is in the past + if ( $allowUpdate ) { + if ( $this->expiry ) { + $conds[] = 'ug_expiry IS NULL OR ug_expiry != ' . + $dbw->addQuotes( $dbw->timestamp( $this->expiry ) ); + } else { + $conds[] = 'ug_expiry IS NOT NULL'; + } + } else { + $conds[] = 'ug_expiry < ' . $dbw->addQuotes( $dbw->timestamp() ); + } + + $row = $dbw->selectRow( 'user_groups', $this::selectFields(), $conds, __METHOD__ ); + if ( $row ) { + $dbw->update( + 'user_groups', + [ 'ug_expiry' => $this->expiry ? $dbw->timestamp( $this->expiry ) : null ], + [ 'ug_user' => $row->ug_user, 'ug_group' => $row->ug_group ], + __METHOD__ ); + $affected = $dbw->affectedRows(); + } + } + + return $affected > 0; + } + + /** + * Get an array suitable for passing to $dbw->insert() or $dbw->update() + * @param IDatabase $db + * @return array + */ + protected function getDatabaseArray( IDatabase $db ) { + global $wgDisableUserGroupExpiry; + + $a = [ + 'ug_user' => $this->userId, + 'ug_group' => $this->group, + ]; + if ( !$wgDisableUserGroupExpiry ) { + $a['ug_expiry'] = $this->expiry ? $db->timestamp( $this->expiry ) : null; + } + return $a; + } + + /** + * Has the membership expired? + * @return bool + */ + public function isExpired() { + global $wgDisableUserGroupExpiry; + if ( $wgDisableUserGroupExpiry || !$this->expiry ) { + return false; + } else { + return wfTimestampNow() > $this->expiry; + } + } + + /** + * Purge expired memberships from the user_groups table + * + * @param IDatabase|null $dbw + */ + public static function purgeExpired( IDatabase $dbw = null ) { + global $wgDisableUserGroupExpiry; + if ( $wgDisableUserGroupExpiry || wfReadOnly() ) { + return; + } + + if ( $dbw === null ) { + $dbw = wfGetDB( DB_MASTER ); + } + + DeferredUpdates::addUpdate( new AtomicSectionUpdate( + $dbw, + __METHOD__, + function ( IDatabase $dbw, $fname ) { + $expiryCond = [ 'ug_expiry < ' . $dbw->addQuotes( $dbw->timestamp() ) ]; + $res = $dbw->select( 'user_groups', self::selectFields(), $expiryCond, $fname ); + + // save an array of users/groups to insert to user_former_groups + $usersAndGroups = []; + foreach ( $res as $row ) { + $usersAndGroups[] = [ 'ufg_user' => $row->ug_user, 'ufg_group' => $row->ug_group ]; + } + + // delete 'em all + $dbw->delete( 'user_groups', $expiryCond, $fname ); + + // and push the groups to user_former_groups + $dbw->insert( 'user_former_groups', $usersAndGroups, __METHOD__, [ 'IGNORE' ] ); + } + ) ); + } + + /** + * Returns UserGroupMembership objects for all the groups a user currently + * belongs to. + * + * @param int $userId ID of the user to search for + * @param IDatabase|null $db Optional database connection + * @return array Associative array of (group name => UserGroupMembership object) + */ + public static function getMembershipsForUser( $userId, IDatabase $db = null ) { + if ( !$db ) { + $db = wfGetDB( DB_REPLICA ); + } + + $res = $db->select( 'user_groups', + self::selectFields(), + [ 'ug_user' => $userId ], + __METHOD__ ); + + $ugms = []; + foreach ( $res as $row ) { + $ugm = self::newFromRow( $row ); + if ( !$ugm->isExpired() ) { + $ugms[$ugm->group] = $ugm; + } + } + + return $ugms; + } + + /** + * Returns a UserGroupMembership object that pertains to the given user and group, + * or false if the user does not belong to that group (or the assignment has + * expired). + * + * @param int $userId ID of the user to search for + * @param string $group User group name + * @param IDatabase|null $db Optional database connection + * @return UserGroupMembership|false + */ + public static function getMembership( $userId, $group, IDatabase $db = null ) { + if ( !$db ) { + $db = wfGetDB( DB_REPLICA ); + } + + $row = $db->selectRow( 'user_groups', + self::selectFields(), + [ 'ug_user' => $userId, 'ug_group' => $group ], + __METHOD__ ); + if ( !$row ) { + return false; + } + + $ugm = self::newFromRow( $row ); + if ( !$ugm->isExpired() ) { + return $ugm; + } else { + return false; + } + } + + /** + * Gets a link for a user group, possibly including the expiry date if relevant. + * + * @param string|UserGroupMembership $ugm Either a group name as a string, or + * a UserGroupMembership object + * @param IContextSource $context + * @param string $format Either 'wiki' or 'html' + * @param string|null $userName If you want to use the group member message + * ("administrator"), pass the name of the user who belongs to the group; it + * is used for GENDER of the group member message. If you instead want the + * group name message ("Administrators"), omit this parameter. + * @return string + */ + public static function getLink( $ugm, IContextSource $context, $format, + $userName = null ) { + + if ( $format !== 'wiki' && $format !== 'html' ) { + throw new MWException( 'UserGroupMembership::getLink() $format parameter should be ' . + "'wiki' or 'html'" ); + } + + if ( $ugm instanceof UserGroupMembership ) { + $expiry = $ugm->getExpiry(); + $group = $ugm->getGroup(); + } else { + $expiry = null; + $group = $ugm; + } + + if ( $userName !== null ) { + $groupName = self::getGroupMemberName( $group, $userName ); + } else { + $groupName = self::getGroupName( $group ); + } + + // link to the group description page, if it exists + $linkTitle = self::getGroupPage( $group ); + if ( $linkTitle ) { + if ( $format === 'wiki' ) { + $linkPage = $linkTitle->getFullText(); + $groupLink = "[[$linkPage|$groupName]]"; + } else { + $groupLink = Linker::link( $linkTitle, htmlspecialchars( $groupName ) ); + } + } else { + $groupLink = htmlspecialchars( $groupName ); + } + + if ( $expiry ) { + // format the expiry to a nice string + $uiLanguage = $context->getLanguage(); + $uiUser = $context->getUser(); + $expiryDT = $uiLanguage->userTimeAndDate( $expiry, $uiUser ); + $expiryD = $uiLanguage->userDate( $expiry, $uiUser ); + $expiryT = $uiLanguage->userTime( $expiry, $uiUser ); + if ( $format === 'html' ) { + $groupLink = Message::rawParam( $groupLink ); + } + return $context->msg( 'group-membership-link-with-expiry' ) + ->params( $groupLink, $expiryDT, $expiryD, $expiryT )->text(); + } else { + return $groupLink; + } + } + + /** + * Gets the localized friendly name for a group, if it exists. For example, + * "Administrators" or "Bureaucrats" + * + * @param string $group Internal group name + * @return string Localized friendly group name + */ + public static function getGroupName( $group ) { + $msg = wfMessage( "group-$group" ); + return $msg->isBlank() ? $group : $msg->text(); + } + + /** + * Gets the localized name for a member of a group, if it exists. For example, + * "administrator" or "bureaucrat" + * + * @param string $group Internal group name + * @param string $username Username for gender + * @return string Localized name for group member + */ + public static function getGroupMemberName( $group, $username ) { + $msg = wfMessage( "group-$group-member", $username ); + return $msg->isBlank() ? $group : $msg->text(); + } + + /** + * Gets the title of a page describing a particular user group. When the name + * of the group appears in the UI, it can link to this page. + * + * @param string $group Internal group name + * @return Title|bool Title of the page if it exists, false otherwise + */ + public static function getGroupPage( $group ) { + $msg = wfMessage( "grouppage-$group" )->inContentLanguage(); + if ( $msg->exists() ) { + $title = Title::newFromText( $msg->text() ); + if ( is_object( $title ) ) { + return $title; + } + } + return false; + } +} diff --git a/includes/user/UserRightsProxy.php b/includes/user/UserRightsProxy.php index 69bc503b111a..4df73f7328e1 100644 --- a/includes/user/UserRightsProxy.php +++ b/includes/user/UserRightsProxy.php @@ -20,6 +20,8 @@ * @file */ +use Wikimedia\Rdbms\IDatabase; + /** * Cut-down copy of User interface for local-interwiki-database * user rights manipulation. @@ -198,50 +200,47 @@ class UserRightsProxy { * @return array */ function getGroups() { - $res = $this->db->select( 'user_groups', - [ 'ug_group' ], - [ 'ug_user' => $this->id ], - __METHOD__ ); - $groups = []; - foreach ( $res as $row ) { - $groups[] = $row->ug_group; - } - return $groups; + return array_keys( self::getGroupMemberships() ); } /** - * Replaces User::addUserGroup() - * @param string $group + * Replaces User::getGroupMemberships() + * + * @return array + * @since 1.29 + */ + function getGroupMemberships() { + return UserGroupMembership::getMembershipsForUser( $this->id, $this->db ); + } + + /** + * Replaces User::addGroup() * + * @param string $group + * @param string|null $expiry * @return bool */ - function addGroup( $group ) { - $this->db->insert( 'user_groups', - [ - 'ug_user' => $this->id, - 'ug_group' => $group, - ], - __METHOD__, - [ 'IGNORE' ] ); + function addGroup( $group, $expiry = null ) { + if ( $expiry ) { + $expiry = wfTimestamp( TS_MW, $expiry ); + } - return true; + $ugm = new UserGroupMembership( $this->id, $group, $expiry ); + return $ugm->insert( true, $this->db ); } /** - * Replaces User::removeUserGroup() - * @param string $group + * Replaces User::removeGroup() * + * @param string $group * @return bool */ function removeGroup( $group ) { - $this->db->delete( 'user_groups', - [ - 'ug_user' => $this->id, - 'ug_group' => $group, - ], - __METHOD__ ); - - return true; + $ugm = UserGroupMembership::getMembership( $this->id, $group, $this->db ); + if ( !$ugm ) { + return false; + } + return $ugm->delete( $this->db ); } /** diff --git a/includes/utils/AutoloadGenerator.php b/includes/utils/AutoloadGenerator.php index 319b5d4854c6..1dac0b152aa4 100644 --- a/includes/utils/AutoloadGenerator.php +++ b/includes/utils/AutoloadGenerator.php @@ -152,7 +152,7 @@ class AutoloadGenerator { ksort( $json[$key] ); // Return the whole JSON file - return FormatJson::encode( $json, true ) . "\n"; + return FormatJson::encode( $json, "\t", FormatJson::ALL_OK ) . "\n"; } /** @@ -291,10 +291,6 @@ EOD; foreach ( glob( $this->basepath . '/*.php' ) as $file ) { $this->readFile( $file ); } - - // Legacy aliases - $this->forceClassPath( 'DatabaseBase', - $this->basepath . '/includes/libs/rdbms/database/Database.php' ); } } @@ -324,6 +320,11 @@ class ClassCollector { protected $tokens; /** + * @var array Class alias with target/name fields + */ + protected $alias; + + /** * @var string $code PHP code (including <?php) to detect class names from * @return array List of FQCN detected within the tokens */ @@ -331,6 +332,7 @@ class ClassCollector { $this->namespace = ''; $this->classes = []; $this->startToken = null; + $this->alias = null; $this->tokens = []; foreach ( token_get_all( $code ) as $token ) { @@ -353,6 +355,8 @@ class ClassCollector { if ( is_string( $token ) ) { return; } + // Note: When changing class name discovery logic, + // AutoLoaderTest.php may also need to be updated. switch ( $token[0] ) { case T_NAMESPACE: case T_CLASS: @@ -360,6 +364,12 @@ class ClassCollector { case T_TRAIT: case T_DOUBLE_COLON: $this->startToken = $token; + break; + case T_STRING: + if ( $token[1] === 'class_alias' ) { + $this->startToken = $token; + $this->alias = []; + } } } @@ -383,6 +393,58 @@ class ClassCollector { } break; + case T_STRING: + if ( $this->alias !== null ) { + // Flow 1 - Two string literals: + // - T_STRING class_alias + // - '(' + // - T_CONSTANT_ENCAPSED_STRING 'TargetClass' + // - ',' + // - T_WHITESPACE + // - T_CONSTANT_ENCAPSED_STRING 'AliasName' + // - ')' + // Flow 2 - Use of ::class syntax for first parameter + // - T_STRING class_alias + // - '(' + // - T_STRING TargetClass + // - T_DOUBLE_COLON :: + // - T_CLASS class + // - ',' + // - T_WHITESPACE + // - T_CONSTANT_ENCAPSED_STRING 'AliasName' + // - ')' + if ( $token === '(' ) { + // Start of a function call to class_alias() + $this->alias = [ 'target' => false, 'name' => false ]; + } elseif ( $token === ',' ) { + // Record that we're past the first parameter + if ( $this->alias['target'] === false ) { + $this->alias['target'] = true; + } + } elseif ( is_array( $token ) && $token[0] === T_CONSTANT_ENCAPSED_STRING ) { + if ( $this->alias['target'] === true ) { + // We already saw a first argument, this must be the second. + // Strip quotes from the string literal. + $this->alias['name'] = substr( $token[1], 1, -1 ); + } + } elseif ( $token === ')' ) { + // End of function call + $this->classes[] = $this->alias['name']; + $this->alias = null; + $this->startToken = null; + } elseif ( !is_array( $token ) || ( + $token[0] !== T_STRING && + $token[0] !== T_DOUBLE_COLON && + $token[0] !== T_CLASS && + $token[0] !== T_WHITESPACE + ) ) { + // Ignore this call to class_alias() - compat/Timestamp.php + $this->alias = null; + $this->startToken = null; + } + } + break; + case T_CLASS: case T_INTERFACE: case T_TRAIT: diff --git a/includes/utils/BatchRowIterator.php b/includes/utils/BatchRowIterator.php index ef2c14a9b793..e107fb15bac4 100644 --- a/includes/utils/BatchRowIterator.php +++ b/includes/utils/BatchRowIterator.php @@ -1,4 +1,7 @@ <?php + +use Wikimedia\Rdbms\IDatabase; + /** * Allows iterating a large number of rows in batches transparently. * By default when iterated over returns the full query result as an @@ -230,7 +233,7 @@ class BatchRowIterator implements RecursiveIterator { * `=` conditions while the final key uses a `>` condition * * Example output: - * [ '( foo = 42 AND bar > 7 ) OR ( foo > 42 )' ] + * [ '( foo = 42 AND bar > 7 ) OR ( foo > 42 )' ] * * @return array The SQL conditions necessary to select the next set * of rows in the batched query diff --git a/includes/utils/BatchRowWriter.php b/includes/utils/BatchRowWriter.php index a6e47c89764a..70afb91c1bca 100644 --- a/includes/utils/BatchRowWriter.php +++ b/includes/utils/BatchRowWriter.php @@ -20,6 +20,7 @@ * @file * @ingroup Maintenance */ +use Wikimedia\Rdbms\IDatabase; use \MediaWiki\MediaWikiServices; class BatchRowWriter { diff --git a/includes/utils/MWCryptHKDF.php b/includes/utils/MWCryptHKDF.php index 3bddd7794a6c..1c8d4861afef 100644 --- a/includes/utils/MWCryptHKDF.php +++ b/includes/utils/MWCryptHKDF.php @@ -47,11 +47,11 @@ class MWCryptHKDF { * From http://eprint.iacr.org/2010/264.pdf: * * The scheme HKDF is specifed as: - * HKDF(XTS, SKM, CTXinfo, L) = K(1) || K(2) || ... || K(t) + * HKDF(XTS, SKM, CTXinfo, L) = K(1) || K(2) || ... || K(t) * where the values K(i) are defined as follows: - * PRK = HMAC(XTS, SKM) - * K(1) = HMAC(PRK, CTXinfo || 0); - * K(i+1) = HMAC(PRK, K(i) || CTXinfo || i), 1 <= i < t; + * PRK = HMAC(XTS, SKM) + * K(1) = HMAC(PRK, CTXinfo || 0); + * K(i+1) = HMAC(PRK, K(i) || CTXinfo || i), 1 <= i < t; * where t = [L/k] and the value K(t) is truncated to its first d = L mod k bits; * the counter i is non-wrapping and of a given fixed size, e.g., a single byte. * Note that the length of the HMAC output is the same as its key length and therefore diff --git a/includes/widget/DateInputWidget.php b/includes/widget/DateInputWidget.php index f011f0b8af4f..507dab6fac9e 100644 --- a/includes/widget/DateInputWidget.php +++ b/includes/widget/DateInputWidget.php @@ -19,6 +19,7 @@ class DateInputWidget extends \OOUI\TextInputWidget { protected $inputFormat = null; protected $displayFormat = null; + protected $longDisplayFormat = null; protected $placeholderLabel = null; protected $placeholderDateFormat = null; protected $precision = null; @@ -36,6 +37,9 @@ class DateInputWidget extends \OOUI\TextInputWidget { * while the widget is inactive. Should be as unambiguous as possible (for example, prefer * to spell out the month, rather than rely on the order), even if that makes it longer. * Applicable only if the widget is infused. (default: language-specific) + * @param string $config['longDisplayFormat'] If a custom displayFormat is not specified, use + * unabbreviated day of the week and month names in the default language-specific + * displayFormat. (default: false) * @param string $config['placeholderLabel'] Placeholder text shown when the widget is not * selected. Applicable only if the widget is infused. (default: taken from message * `mw-widgets-dateinput-no-date`) @@ -58,6 +62,7 @@ class DateInputWidget extends \OOUI\TextInputWidget { $config = array_merge( [ // Default config values 'precision' => 'day', + 'longDisplayFormat' => false, ], $config ); // Properties @@ -79,6 +84,9 @@ class DateInputWidget extends \OOUI\TextInputWidget { if ( isset( $config['displayFormat'] ) ) { $this->displayFormat = $config['displayFormat']; } + if ( isset( $config['longDisplayFormat'] ) ) { + $this->longDisplayFormat = $config['longDisplayFormat']; + } if ( isset( $config['placeholderLabel'] ) ) { $this->placeholderLabel = $config['placeholderLabel']; } @@ -134,6 +142,9 @@ class DateInputWidget extends \OOUI\TextInputWidget { if ( $this->displayFormat !== null ) { $config['displayFormat'] = $this->displayFormat; } + if ( $this->longDisplayFormat !== null ) { + $config['longDisplayFormat'] = $this->longDisplayFormat; + } if ( $this->placeholderLabel !== null ) { $config['placeholderLabel'] = $this->placeholderLabel; } diff --git a/includes/widget/SearchInputWidget.php b/includes/widget/SearchInputWidget.php index 0d7162957ecb..49510da9c906 100644 --- a/includes/widget/SearchInputWidget.php +++ b/includes/widget/SearchInputWidget.php @@ -31,7 +31,6 @@ class SearchInputWidget extends TitleInputWidget { public function __construct( array $config = [] ) { $config = array_merge( [ 'maxLength' => null, - 'type' => 'search', 'icon' => 'search', ], $config ); @@ -56,6 +55,10 @@ class SearchInputWidget extends TitleInputWidget { $this->addClasses( [ 'mw-widget-searchInputWidget' ] ); } + protected function getInputElement( $config ) { + return ( new \OOUI\Tag( 'input' ) )->setAttributes( [ 'type' => 'search' ] ); + } + protected function getJavaScriptClassName() { return 'mw.widgets.SearchInputWidget'; } diff --git a/includes/widget/UsersMultiselectWidget.php b/includes/widget/UsersMultiselectWidget.php new file mode 100644 index 000000000000..d24ab7bf6639 --- /dev/null +++ b/includes/widget/UsersMultiselectWidget.php @@ -0,0 +1,68 @@ +<?php +/** + * MediaWiki Widgets – UsersMultiselectWidget class. + * + * @copyright 2017 MediaWiki Widgets Team and others; see AUTHORS.txt + * @license The MIT License (MIT); see LICENSE.txt + */ +namespace MediaWiki\Widget; + +use \OOUI\TextInputWidget; + +/** + * Widget to select multiple users. + */ +class UsersMultiselectWidget extends \OOUI\Widget { + + protected $usersArray = []; + protected $inputName = null; + protected $inputPlaceholder = null; + + /** + * @param array $config Configuration options + * @param array $config['users'] Array of usernames to use as preset data + * @param array $config['placeholder'] Placeholder message for input + * @param array $config['name'] Name attribute (used in forms) + */ + public function __construct( array $config = [] ) { + parent::__construct( $config ); + + // Properties + if ( isset( $config['default'] ) ) { + $this->usersArray = $config['default']; + } + if ( isset( $config['name'] ) ) { + $this->inputName = $config['name']; + } + if ( isset( $config['placeholder'] ) ) { + $this->inputPlaceholder = $config['placeholder']; + } + + $textarea = new TextInputWidget( [ + 'name' => $this->inputName, + 'multiline' => true, + 'value' => implode( "\n", $this->usersArray ), + 'rows' => 25, + ] ); + $this->prependContent( $textarea ); + } + + protected function getJavaScriptClassName() { + return 'mw.widgets.UsersMultiselectWidget'; + } + + public function getConfig( &$config ) { + if ( $this->usersArray !== null ) { + $config['data'] = $this->usersArray; + } + if ( $this->inputName !== null ) { + $config['name'] = $this->inputName; + } + if ( $this->inputPlaceholder !== null ) { + $config['placeholder'] = $this->inputPlaceholder; + } + + return parent::getConfig( $config ); + } + +} diff --git a/includes/widget/search/BasicSearchResultSetWidget.php b/includes/widget/search/BasicSearchResultSetWidget.php new file mode 100644 index 000000000000..07094afca693 --- /dev/null +++ b/includes/widget/search/BasicSearchResultSetWidget.php @@ -0,0 +1,135 @@ +<?php + +namespace MediaWiki\Widget\Search; + +use Message; +use SearchResultSet; +use SpecialSearch; +use Status; + +/** + * Renders the search result area. Handles Title and Full-Text search results, + * along with inline and sidebar secondary (interwiki) results. + */ +class BasicSearchResultSetWidget { + /** @var SpecialSearch */ + protected $specialPage; + /** @var SearchResultWidget */ + protected $resultWidget; + /** @var InterwikiSearchResultSetWidget */ + protected $sidebarWidget; + + public function __construct( + SpecialSearch $specialPage, + SearchResultWidget $resultWidget, + SearchResultSetWidget $sidebarWidget + ) { + $this->specialPage = $specialPage; + $this->resultWidget = $resultWidget; + $this->sidebarWidget = $sidebarWidget; + } + + /** + * @param string $term The search term to highlight + * @param int $offset The offset of the first result in the result set + * @param SearchResultSet|null $titleResultSet Results of searching only page titles + * @param SearchResultSet|null $textResultSet Results of general full text search. + * @return string HTML + */ + public function render( + $term, + $offset, + SearchResultSet $titleResultSet = null, + SearchResultSet $textResultSet = null + ) { + global $wgContLang; + + $hasTitle = $titleResultSet ? $titleResultSet->numRows() > 0 : false; + $hasText = $textResultSet ? $textResultSet->numRows() > 0 : false; + $hasSecondary = $textResultSet + ? $textResultSet->hasInterwikiResults( SearchResultSet::SECONDARY_RESULTS ) + : false; + $hasSecondaryInline = $textResultSet + ? $textResultSet->hasInterwikiResults( SearchResultSet::INLINE_RESULTS ) + : false; + + if ( !$hasTitle && !$hasText && !$hasSecondary && !$hasSecondaryInline ) { + return ''; + } + + $out = ''; + if ( $hasTitle ) { + $out .= $this->header( $this->specialPage->msg( 'titlematches' ) ) + . $this->renderResultSet( $titleResultSet, $offset ); + } + + if ( $hasText ) { + if ( $hasTitle ) { + $out .= "<div class='mw-search-visualclear'></div>" . + $this->header( $this->specialPage->msg( 'textmatches' ) ); + } + $out .= $this->renderResultSet( $textResultSet, $offset ); + } + + if ( $hasSecondaryInline ) { + $iwResults = $textResultSet->getInterwikiResults( SearchResultSet::INLINE_RESULTS ); + foreach ( $iwResults as $interwiki => $results ) { + if ( $results instanceof Status || $results->numRows() === 0 ) { + // ignore bad interwikis for now + continue; + } + $out .= + "<p class='mw-search-interwiki-header mw-search-visualclear'>" . + $this->specialPage->msg( "search-interwiki-results-{$interwiki}" )->parse() . + "</p>"; + $out .= $this->renderResultSet( $results, $offset ); + } + } + + if ( $hasSecondary ) { + $out .= $this->sidebarWidget->render( + $term, + $textResultSet->getInterwikiResults( SearchResultSet::SECONDARY_RESULTS ) + ); + } + + // Convert the whole thing to desired language variant + // TODO: Move this up to Special:Search? + return $wgContLang->convert( $out ); + } + + /** + * Generate a headline for a section of the search results. In prior + * implementations this was rendering wikitext of '==$1==', but seems + * a waste to call the full parser to generate this tiny bit of html + * + * @param Message $msg i18n message to use as header + * @return string HTML + */ + protected function header( Message $msg ) { + return + "<h2>" . + "<span class='mw-headline'>" . $msg->escaped() . "</span>" . + "</h2>"; + } + + /** + * @param SearchResultSet $resultSet The search results to render + * @param int $offset Offset of the first result in $resultSet + * @return string HTML + */ + protected function renderResultSet( SearchResultSet $resultSet, $offset ) { + global $wgContLang; + + $terms = $wgContLang->convertForSearchResult( $resultSet->termMatches() ); + + $hits = []; + $result = $resultSet->next(); + while ( $result ) { + $hits[] .= $this->resultWidget->render( $result, $terms, $offset++ ); + $result = $resultSet->next(); + } + + return "<ul class='mw-search-results'>" . implode( '', $hits ) . "</ul>"; + } +} diff --git a/includes/widget/search/DidYouMeanWidget.php b/includes/widget/search/DidYouMeanWidget.php new file mode 100644 index 000000000000..3aee87bf3b36 --- /dev/null +++ b/includes/widget/search/DidYouMeanWidget.php @@ -0,0 +1,102 @@ +<?php + +namespace MediaWiki\Widget\Search; + +use Linker; +use SearchResultSet; +use SpecialSearch; + +/** + * Renders a suggested search for the user, or tells the user + * a suggested search was run instead of the one provided. + */ +class DidYouMeanWidget { + /** @var SpecialSearch */ + protected $specialSearch; + + public function __construct( SpecialSearch $specialSearch ) { + $this->specialSearch = $specialSearch; + } + + /** + * @param string $term The user provided search term + * @param SearchResultSet $resultSet + * @return string HTML + */ + public function render( $term, SearchResultSet $resultSet ) { + if ( $resultSet->hasRewrittenQuery() ) { + $html = $this->rewrittenHtml( $term, $resultSet ); + } elseif ( $resultSet->hasSuggestion() ) { + $html = $this->suggestionHtml( $resultSet ); + } else { + return ''; + } + + return "<div class='searchdidyoumean'>$html</div>"; + } + + /** + * Generates HTML shown to user when their query has been internally + * rewritten, and the results of the rewritten query are being returned. + * + * @param string $term The users search input + * @param SearchResultSet $resultSet The response to the search request + * @return string HTML Links the user to their original $term query, and the + * one suggested by $resultSet + */ + protected function rewrittenHtml( $term, SearchResultSet $resultSet ) { + $params = [ + 'search' => $resultSet->getQueryAfterRewrite(), + // Don't magic this link into a 'go' link, it should always + // show search results. + 'fultext' => 1, + ]; + $stParams = array_merge( $params, $this->specialSearch->powerSearchOptions() ); + + $rewritten = Linker::linkKnown( + $this->specialSearch->getPageTitle(), + $resultSet->getQueryAfterRewriteSnippet() ?: null, + [ 'id' => 'mw-search-DYM-rewritten' ], + $stParams + ); + + $stParams['search'] = $term; + $stParams['runsuggestion'] = 0; + $original = Linker::linkKnown( + $this->specialSearch->getPageTitle(), + htmlspecialchars( $term, ENT_QUOTES, 'UTF-8' ), + [ 'id' => 'mwsearch-DYM-original' ], + $stParams + ); + + return $this->specialSearch->msg( 'search-rewritten' ) + ->rawParams( $rewritten, $original ) + ->escaped(); + } + + /** + * Generates HTML shown to the user when we have a suggestion about + * a query that might give more/better results than their current + * query. + * + * @param SearchResultSet $resultSet + * @return string HTML + */ + protected function suggestionHtml( SearchResultSet $resultSet ) { + $params = [ + 'search' => $resultSet->getSuggestionQuery(), + 'fulltext' => 1, + ]; + $stParams = array_merge( $params, $this->specialSearch->powerSearchOptions() ); + + $suggest = Linker::linkKnown( + $this->specialSearch->getPageTitle(), + $resultSet->getSuggestionSnippet() ?: null, + [ 'id' => 'mw-search-DYM-suggestion' ], + $stParams + ); + + return $this->specialSearch->msg( 'search-suggest' ) + ->rawParams( $suggest )->parse(); + } +} diff --git a/includes/widget/search/FullSearchResultWidget.php b/includes/widget/search/FullSearchResultWidget.php index a93e1fcde383..0d0fa1241100 100644 --- a/includes/widget/search/FullSearchResultWidget.php +++ b/includes/widget/search/FullSearchResultWidget.php @@ -131,16 +131,16 @@ class FullSearchResultWidget implements SearchResultWidget { // clone to prevent hook from changing the title stored inside $result $title = clone $result->getTitle(); - $queryString = []; + $query = []; Hooks::run( 'ShowSearchHitTitle', - [ $title, &$snippet, $result, $terms, $this->specialPage, &$queryString ] ); + [ &$title, &$snippet, $result, $terms, $this->specialPage, &$query ] ); $link = $this->linkRenderer->makeLink( $title, $snippet, [ 'data-serp-pos' => $position ], - $queryString + $query ); return $link; diff --git a/includes/widget/search/InterwikiSearchResultSetWidget.php b/includes/widget/search/InterwikiSearchResultSetWidget.php new file mode 100644 index 000000000000..1911c7909f7f --- /dev/null +++ b/includes/widget/search/InterwikiSearchResultSetWidget.php @@ -0,0 +1,184 @@ +<?php + +namespace MediaWiki\Widget\Search; + +use MediaWiki\Interwiki\InterwikiLookup; +use MediaWiki\Linker\LinkRenderer; +use SearchResultSet; +use SpecialSearch; +use Title; +use Html; + +/** + * Renders one or more SearchResultSets into a sidebar grouped by + * interwiki prefix. Includes a per-wiki header indicating where + * the results are from. + */ +class InterwikiSearchResultSetWidget implements SearchResultSetWidget { + /** @var SpecialSearch */ + protected $specialSearch; + /** @var SearchResultWidget */ + protected $resultWidget; + /** @var string[]|null */ + protected $customCaptions; + /** @var LinkRenderer */ + protected $linkRenderer; + /** @var InterwikiLookup */ + protected $iwLookup; + /** @var $output */ + protected $output; + /** @var $iwPrefixDisplayTypes */ + protected $iwPrefixDisplayTypes; + + public function __construct( + SpecialSearch $specialSearch, + SearchResultWidget $resultWidget, + LinkRenderer $linkRenderer, + InterwikiLookup $iwLookup + ) { + $this->specialSearch = $specialSearch; + $this->resultWidget = $resultWidget; + $this->linkRenderer = $linkRenderer; + $this->iwLookup = $iwLookup; + $this->output = $specialSearch->getOutput(); + $this->iwPrefixDisplayTypes = $specialSearch->getConfig()->get( + 'InterwikiPrefixDisplayTypes' + ); + } + /** + * @param string $term User provided search term + * @param SearchResultSet|SearchResultSet[] $resultSets List of interwiki + * results to render. + * @return string HTML + */ + public function render( $term, $resultSets ) { + if ( !is_array( $resultSets ) ) { + $resultSets = [ $resultSets ]; + } + + $this->loadCustomCaptions(); + + $this->output->addModules( 'mediawiki.special.search.commonsInterwikiWidget' ); + $this->output->addModuleStyles( 'mediawiki.special.search.interwikiwidget.styles' ); + + $iwResults = []; + foreach ( $resultSets as $resultSet ) { + $result = $resultSet->next(); + while ( $result ) { + if ( !$result->isBrokenTitle() ) { + $iwResults[$result->getTitle()->getInterwiki()][] = $result; + } + $result = $resultSet->next(); + } + } + + $iwResultSetPos = 1; + $iwResultListOutput = ''; + + foreach ( $iwResults as $iwPrefix => $results ) { + // TODO: Assumes interwiki results are never paginated + $position = 0; + $iwResultItemOutput = ''; + + $iwDisplayType = isset( $this->iwPrefixDisplayTypes[$iwPrefix] ) + ? $this->iwPrefixDisplayTypes[$iwPrefix] + : ""; + + foreach ( $results as $result ) { + $iwResultItemOutput .= $this->resultWidget->render( $result, $term, $position++ ); + } + + $headerHtml = $this->headerHtml( $term, $iwPrefix ); + $footerHtml = $this->footerHtml( $term, $iwPrefix ); + $iwResultListOutput .= Html::rawElement( 'li', + [ + 'class' => 'iw-resultset iw-resultset--' . $iwDisplayType, + 'data-iw-resultset-pos' => $iwResultSetPos + ], + $headerHtml . + $iwResultItemOutput . + $footerHtml + ); + + $iwResultSetPos++; + } + + return Html::rawElement( + 'div', + [ 'id' => 'mw-interwiki-results' ], + Html::rawElement( + 'p', + [ 'class' => 'iw-headline' ], + $this->specialSearch->msg( 'search-interwiki-caption' )->parse() + ) . + Html::rawElement( + 'ul', [ 'class' => 'iw-results', ], $iwResultListOutput + ) + ); + } + + /** + * Generates an appropriate HTML header for the given interwiki prefix + * + * @param string $term User provided search term + * @param string $iwPrefix Interwiki prefix of wiki to show header for + * @return string HTML + */ + protected function headerHtml( $term, $iwPrefix ) { + + $iwDisplayType = isset( $this->iwPrefixDisplayTypes[$iwPrefix] ) + ? $this->iwPrefixDisplayTypes[$iwPrefix] + : ""; + + if ( isset( $this->customCaptions[$iwPrefix] ) ) { + /* customCaptions composed by loadCustomCaptions() with pre-escaped content. */ + $caption = $this->customCaptions[$iwPrefix]; + } else { + $interwiki = $this->iwLookup->fetch( $iwPrefix ); + $parsed = wfParseUrl( wfExpandUrl( $interwiki ? $interwiki->getURL() : '/' ) ); + $caption = $this->specialSearch->msg( 'search-interwiki-default', $parsed['host'] )->escaped(); + } + + return Html::rawElement( 'div', [ 'class' => 'iw-result__header' ], + Html::rawElement( 'span', [ 'class' => 'iw-result__icon iw-result__icon--' . $iwDisplayType ] ) + . $caption + ); + } + + /** + * Generates an HTML footer for the given interwiki prefix + * + * @param string $term User provided search term + * @param string $iwPrefix Interwiki prefix of wiki to show footer for + * @return string HTML + */ + protected function footerHtml( $term, $iwPrefix ) { + + $href = Title::makeTitle( NS_SPECIAL, 'Search', null, $iwPrefix )->getLocalURL( + [ 'search' => $term, 'fulltext' => 1 ] + ); + + $searchLink = Html::rawElement( + 'a', + [ 'href' => $href ], + $this->specialSearch->msg( 'search-interwiki-more-results' )->escaped() + ); + + return Html::rawElement( 'div', [ 'class' => 'iw-result__footer' ], $searchLink ); + } + + protected function loadCustomCaptions() { + if ( $this->customCaptions !== null ) { + return; + } + + $this->customCaptions = []; + $customLines = explode( "\n", $this->specialSearch->msg( 'search-interwiki-custom' )->escaped() ); + foreach ( $customLines as $line ) { + $parts = explode( ':', $line, 2 ); + if ( count( $parts ) === 2 ) { + $this->customCaptions[$parts[0]] = $parts[1]; + } + } + } +} diff --git a/includes/widget/search/InterwikiSearchResultWidget.php b/includes/widget/search/InterwikiSearchResultWidget.php new file mode 100644 index 000000000000..6b51db5aeda5 --- /dev/null +++ b/includes/widget/search/InterwikiSearchResultWidget.php @@ -0,0 +1,86 @@ +<?php + +namespace MediaWiki\Widget\Search; + +use HtmlArmor; +use MediaWiki\Linker\LinkRenderer; +use SearchResult; +use SpecialSearch; +use Title; +use Html; + +/** + * Renders an enhanced interwiki result + */ +class InterwikiSearchResultWidget implements SearchResultWidget { + /** @var SpecialSearch */ + protected $specialSearch; + /** @var LinkRenderer */ + protected $linkRenderer; + /** @var $iwPrefixDisplayTypes */ + protected $iwPrefixDisplayTypes; + + public function __construct( SpecialSearch $specialSearch, LinkRenderer $linkRenderer ) { + $this->specialSearch = $specialSearch; + $this->linkRenderer = $linkRenderer; + $this->iwPrefixDisplayTypes = $specialSearch->getConfig()->get( 'InterwikiPrefixDisplayTypes' ); + } + + /** + * @param SearchResult $result The result to render + * @param string $terms Terms to be highlighted (@see SearchResult::getTextSnippet) + * @param int $position The result position, including offset + * @return string HTML + */ + public function render( SearchResult $result, $terms, $position ) { + + $title = $result->getTitle(); + $iwPrefix = $result->getTitle()->getInterwiki(); + $titleSnippet = $result->getTitleSnippet(); + $snippet = $result->getTextSnippet( $terms ); + $displayType = isset( $this->iwPrefixDisplayTypes[$iwPrefix] ) + ? $this->iwPrefixDisplayTypes[$iwPrefix] + : ""; + + if ( $titleSnippet ) { + $titleSnippet = new HtmlArmor( $titleSnippet ); + } else { + $titleSnippet = null; + } + + $link = $this->linkRenderer->makeLink( $title, $titleSnippet ); + + $redirectTitle = $result->getRedirectTitle(); + $redirect = ''; + if ( $redirectTitle !== null ) { + + $redirectText = $result->getRedirectSnippet(); + + if ( $redirectText ) { + $redirectText = new HtmlArmor( $redirectText ); + } else { + $redirectText = null; + } + + $redirect = Html::rawElement( 'span', [ 'class' => 'iw-result__redirect' ], + $this->specialSearch->msg( 'search-redirect' )->rawParams( + $this->linkRenderer->makeLink( $redirectTitle, $redirectText ) + )->escaped() + ); + } + + switch ( $displayType ) { + case 'definition': + return "<div class='iw-result__content'>" . + "<span class='iw-result__title'>{$link} {$redirect}: </span>" . + $snippet . + "</div>"; + case 'quotation': + return "<div class='iw-result__content'>{$snippet}</div>" . + "<div class='iw-result__title'>{$link} {$redirect}</div>"; + default: + return "<div class='iw-result__title'>{$link} {$redirect}</div>" . + "<div class='iw-result__content'>{$snippet}</div>"; + } + } +} diff --git a/includes/widget/search/SearchFormWidget.php b/includes/widget/search/SearchFormWidget.php new file mode 100644 index 000000000000..a7407a062fcd --- /dev/null +++ b/includes/widget/search/SearchFormWidget.php @@ -0,0 +1,312 @@ +<?php + +namespace MediaWiki\Widget\Search; + +use Hooks; +use Html; +use MediaWiki\Widget\SearchInputWidget; +use MWNamespace; +use SearchEngineConfig; +use SpecialSearch; +use Xml; + +class SearchFormWidget { + /** @var SpecialSearch */ + protected $specialSearch; + /** @var SearchEngineConfig */ + protected $searchConfig; + /** @var array */ + protected $profiles; + + /** + * @param SpecialSearch $specialSearch + * @param SearchEngineConfig $searchConfig + * @param array $profiles + */ + public function __construct( + SpecialSearch $specialSearch, + SearchEngineConfig $searchConfig, + array $profiles + ) { + $this->specialSearch = $specialSearch; + $this->searchConfig = $searchConfig; + $this->profiles = $profiles; + } + + /** + * @param string $profile The current search profile + * @param string $term The current search term + * @param int $numResults The number of results shown + * @param int $totalResults The total estimated results found + * @param int $offset Current offset in search results + * @param bool $isPowerSearch Is the 'advanced' section open? + * @return string HTML + */ + public function render( + $profile, + $term, + $numResults, + $totalResults, + $offset, + $isPowerSearch + ) { + return Xml::openElement( + 'form', + [ + 'id' => $isPowerSearch ? 'powersearch' : 'search', + 'method' => 'get', + 'action' => wfScript(), + ] + ) . + '<div id="mw-search-top-table">' . + $this->shortDialogHtml( $profile, $term, $numResults, $totalResults, $offset ) . + '</div>' . + "<div class='mw-search-visualclear'></div>" . + "<div class='mw-search-profile-tabs'>" . + $this->profileTabsHtml( $profile, $term ) . + "<div style='clear:both'></div>" . + "</div>" . + $this->optionsHtml( $term, $isPowerSearch, $profile ) . + '</form>'; + } + + /** + * @param string $profile The current search profile + * @param string $term The current search term + * @param int $numResults The number of results shown + * @param int $totalResults The total estimated results found + * @param int $offset Current offset in search results + * @return string HTML + */ + protected function shortDialogHtml( $profile, $term, $numResults, $totalResults, $offset ) { + $html = ''; + + $searchWidget = new SearchInputWidget( [ + 'id' => 'searchText', + 'name' => 'search', + 'autofocus' => trim( $term ) === '', + 'value' => $term, + 'dataLocation' => 'content', + 'infusable' => true, + ] ); + + $layout = new \OOUI\ActionFieldLayout( $searchWidget, new \OOUI\ButtonInputWidget( [ + 'type' => 'submit', + 'label' => $this->specialSearch->msg( 'searchbutton' )->text(), + 'flags' => [ 'progressive', 'primary' ], + ] ), [ + 'align' => 'top', + ] ); + + $html .= $layout; + + if ( $totalResults > 0 && $offset < $totalResults ) { + $html .= Xml::tags( + 'div', + [ 'class' => 'results-info' ], + $this->specialSearch->msg( 'search-showingresults' ) + ->numParams( $offset + 1, $offset + $numResults, $totalResults ) + ->numParams( $numResults ) + ->parse() + ); + } + + $html .= + Html::hidden( 'title', $this->specialSearch->getPageTitle()->getPrefixedText() ) . + Html::hidden( 'profile', $profile ) . + Html::hidden( 'fulltext', '1' ); + + return $html; + } + + /** + * Generates HTML for the list of available search profiles. + * + * @param string $profile The currently selected profile + * @param string $term The user provided search terms + * @return string HTML + */ + protected function profileTabsHtml( $profile, $term ) { + $bareterm = $this->startsWithImage( $term ) + ? substr( $term, strpos( $term, ':' ) + 1 ) + : $term; + $lang = $this->specialSearch->getLanguage(); + $items = []; + foreach ( $this->profiles as $id => $profileConfig ) { + $profileConfig['parameters']['profile'] = $id; + $tooltipParam = isset( $profileConfig['namespace-messages'] ) + ? $lang->commaList( $profileConfig['namespace-messages'] ) + : null; + $items[] = Xml::tags( + 'li', + [ 'class' => $profile === $id ? 'current' : 'normal' ], + $this->makeSearchLink( + $bareterm, + $this->specialSearch->msg( $profileConfig['message'] )->text(), + $this->specialSearch->msg( $profileConfig['tooltip'], $tooltipParam )->text(), + $profileConfig['parameters'] + ) + ); + } + + return + "<div class='search-types'>" . + "<ul>" . implode( '', $items ) . "</ul>" . + "</div>"; + } + + /** + * Check if query starts with image: prefix + * + * @param string $term The string to check + * @return bool + */ + protected function startsWithImage( $term ) { + global $wgContLang; + + $parts = explode( ':', $term ); + return count( $parts ) > 1 + ? $wgContLang->getNsIndex( $parts[0] ) === NS_FILE + : false; + } + + /** + * Make a search link with some target namespaces + * + * @param string $term The term to search for + * @param string $label Link's text + * @param string $tooltip Link's tooltip + * @param array $params Query string parameters + * @return string HTML fragment + */ + protected function makeSearchLink( $term, $label, $tooltip, array $params = [] ) { + $params += [ + 'search' => $term, + 'fulltext' => 1, + ]; + + return Xml::element( + 'a', + [ + 'href' => $this->specialSearch->getPageTitle()->getLocalURL( $params ), + 'title' => $tooltip, + ], + $label + ); + } + + /** + * Generates HTML for advanced options available with the currently + * selected search profile. + * + * @param string $term User provided search term + * @param bool $isPowerSearch Is the advanced search profile enabled? + * @param string $profile The current search profile + * @return string HTML + */ + protected function optionsHtml( $term, $isPowerSearch, $profile ) { + $html = ''; + + if ( $isPowerSearch ) { + $html .= $this->powerSearchBox( $term, [] ); + } else { + $form = ''; + Hooks::run( 'SpecialSearchProfileForm', [ + $this->specialSearch, &$form, $profile, $term, [] + ] ); + $html .= $form; + } + + return $html; + } + + /** + * @param string $term The current search term + * @param array $opts Additional key/value pairs that will be submitted + * with the generated form. + * @return string HTML + */ + protected function powerSearchBox( $term, array $opts ) { + global $wgContLang; + + $rows = []; + $activeNamespaces = $this->specialSearch->getNamespaces(); + foreach ( $this->searchConfig->searchableNamespaces() as $namespace => $name ) { + $subject = MWNamespace::getSubject( $namespace ); + if ( !isset( $rows[$subject] ) ) { + $rows[$subject] = ""; + } + + $name = $wgContLang->getConverter()->convertNamespace( $namespace ); + if ( $name === '' ) { + $name = $this->specialSearch->msg( 'blanknamespace' )->text(); + } + + $rows[$subject] .= + '<td>' . + Xml::checkLabel( + $name, + "ns{$namespace}", + "mw-search-ns{$namespace}", + in_array( $namespace, $activeNamespaces ) + ) . + '</td>'; + } + + // Lays out namespaces in multiple floating two-column tables so they'll + // be arranged nicely while still accomodating diferent screen widths + $tableRows = []; + foreach ( $rows as $row ) { + $tableRows[] = "<tr>{$row}</tr>"; + } + $namespaceTables = []; + foreach ( array_chunk( $tableRows, 4 ) as $chunk ) { + $namespaceTables[] = implode( '', $chunk ); + } + + $showSections = [ + 'namespaceTables' => "<table>" . implode( '</table><table>', $namespaceTables ) . '</table>', + ]; + Hooks::run( 'SpecialSearchPowerBox', [ &$showSections, $term, $opts ] ); + + $hidden = ''; + foreach ( $opts as $key => $value ) { + $hidden .= Html::hidden( $key, $value ); + } + + $divider = "<div class='divider'></div>"; + + // Stuff to feed SpecialSearch::saveNamespaces() + $user = $this->specialSearch->getUser(); + $remember = ''; + if ( $user->isLoggedIn() ) { + $remember = $divider . Xml::checkLabel( + $this->specialSearch->msg( 'powersearch-remember' )->text(), + 'nsRemember', + 'mw-search-powersearch-remember', + false, + // The token goes here rather than in a hidden field so it + // is only sent when necessary (not every form submission) + [ 'value' => $user->getEditToken( + 'searchnamespace', + $this->specialSearch->getRequest() + ) ] + ); + } + + return + "<fieldset id='mw-searchoptions'>" . + "<legend>" . $this->specialSearch->msg( 'powersearch-legend' )->escaped() . '</legend>' . + "<h4>" . $this->specialSearch->msg( 'powersearch-ns' )->parse() . '</h4>' . + // populated by js if available + "<div id='mw-search-togglebox'></div>" . + $divider . + implode( + $divider, + $showSections + ) . + $hidden . + $remember . + "</fieldset>"; + } +} diff --git a/includes/widget/search/SearchResultSetWidget.php b/includes/widget/search/SearchResultSetWidget.php new file mode 100644 index 000000000000..6df6e65c2aca --- /dev/null +++ b/includes/widget/search/SearchResultSetWidget.php @@ -0,0 +1,18 @@ +<?php + +namespace MediaWiki\Widget\Search; + +use SearchResultSet; + +/** + * Renders a set of search results to HTML + */ +interface SearchResultSetWidget { + /** + * @param string $term User provided search term + * @param SearchResultSet|SearchResultSet[] $resultSets List of interwiki + * results to render. + * @return string HTML + */ + public function render( $term, $resultSets ); +} diff --git a/includes/widget/search/SearchResultWidget.php b/includes/widget/search/SearchResultWidget.php index b53cd5d2d219..3fbdbef2a920 100644 --- a/includes/widget/search/SearchResultWidget.php +++ b/includes/widget/search/SearchResultWidget.php @@ -11,7 +11,7 @@ interface SearchResultWidget { /** * @param SearchResult $result The result to render * @param string $terms Terms to be highlighted (@see SearchResult::getTextSnippet) - * @param int $position The result position, including offset + * @param int $position The zero indexed result position, including offset * @return string HTML */ public function render( SearchResult $result, $terms, $position ); diff --git a/includes/widget/search/SimpleSearchResultSetWidget.php b/includes/widget/search/SimpleSearchResultSetWidget.php new file mode 100644 index 000000000000..04e1e2100f2f --- /dev/null +++ b/includes/widget/search/SimpleSearchResultSetWidget.php @@ -0,0 +1,132 @@ +<?php + +namespace MediaWiki\Widget\Search; + +use MediaWiki\Interwiki\InterwikiLookup; +use MediaWiki\Linker\LinkRenderer; +use SearchResultSet; +use SpecialSearch; +use Title; +use Html; + +/** + * Renders one or more SearchResultSets into a sidebar grouped by + * interwiki prefix. Includes a per-wiki header indicating where + * the results are from. + */ +class SimpleSearchResultSetWidget implements SearchResultSetWidget{ + /** @var SpecialSearch */ + protected $specialSearch; + /** @var SearchResultWidget */ + protected $resultWidget; + /** @var string[]|null */ + protected $customCaptions; + /** @var LinkRenderer */ + protected $linkRenderer; + /** @var InterwikiLookup */ + protected $iwLookup; + + public function __construct( + SpecialSearch $specialSearch, + SearchResultWidget $resultWidget, + LinkRenderer $linkRenderer, + InterwikiLookup $iwLookup + ) { + $this->specialSearch = $specialSearch; + $this->resultWidget = $resultWidget; + $this->linkRenderer = $linkRenderer; + $this->iwLookup = $iwLookup; + } + + /** + * @param string $term User provided search term + * @param SearchResultSet|SearchResultSet[] $resultSets List of interwiki + * results to render. + * @return string HTML + */ + public function render( $term, $resultSets ) { + if ( !is_array( $resultSets ) ) { + $resultSets = [ $resultSets ]; + } + + $this->loadCustomCaptions(); + + $iwResults = []; + foreach ( $resultSets as $resultSet ) { + $result = $resultSet->next(); + while ( $result ) { + if ( !$result->isBrokenTitle() ) { + $iwResults[$result->getTitle()->getInterwiki()][] = $result; + } + $result = $resultSet->next(); + } + } + + $out = ''; + foreach ( $iwResults as $iwPrefix => $results ) { + $out .= $this->headerHtml( $iwPrefix, $term ); + $out .= "<ul class='mw-search-iwresults'>"; + // TODO: Assumes interwiki results are never paginated + $position = 0; + foreach ( $results as $result ) { + $out .= $this->resultWidget->render( $result, $term, $position++ ); + } + $out .= "</ul>"; + } + + return + "<div id='mw-search-interwiki'>" . + "<div id='mw-search-interwiki-caption'>" . + $this->specialSearch->msg( 'search-interwiki-caption' )->parse() . + '</div>' . + $out . + "</div>"; + } + + /** + * Generates an appropriate HTML header for the given interwiki prefix + * + * @param string $iwPrefix Interwiki prefix of wiki to show header for + * @param string $term User provided search term + * @return string HTML + */ + protected function headerHtml( $iwPrefix, $term ) { + if ( isset( $this->customCaptions[$iwPrefix] ) ) { + $caption = $this->customCaptions[$iwPrefix]; + } else { + $interwiki = $this->iwLookup->fetch( $iwPrefix ); + $parsed = wfParseUrl( wfExpandUrl( $interwiki ? $interwiki->getURL() : '/' ) ); + $caption = $this->specialSearch->msg( 'search-interwiki-default', $parsed['host'] )->escaped(); + } + + $href = Title::makeTitle( NS_SPECIAL, 'Search', null, $iwPrefix )->getLocalURL( + [ 'search' => $term, 'fulltext' => 1 ] + ); + $searchLink = Html::rawElement( + 'a', + [ 'href' => $href ], + $this->specialSearch->msg( 'search-interwiki-more' )->escaped() + ); + + return + "<div class='mw-search-interwiki-project'>" . + "<span class='mw-search-interwiki-more'>{$searchLink}</span>" . + $caption . + "</div>"; + } + + protected function loadCustomCaptions() { + if ( $this->customCaptions !== null ) { + return; + } + + $this->customCaptions = []; + $customLines = explode( "\n", $this->specialSearch->msg( 'search-interwiki-custom' )->escaped() ); + foreach ( $customLines as $line ) { + $parts = explode( ':', $line, 2 ); + if ( count( $parts ) === 2 ) { + $this->customCaptions[$parts[0]] = $parts[1]; + } + } + } +} |