Merge "EditPage: Don't throw exceptions for invalid content models"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Tue, 13 Sep 2016 00:46:42 +0000 (00:46 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Tue, 13 Sep 2016 00:46:42 +0000 (00:46 +0000)
66 files changed:
RELEASE-NOTES-1.28
includes/EditPage.php
includes/MagicWord.php
includes/MediaWiki.php
includes/OutputPage.php
includes/PathRouter.php
includes/WebRequest.php
includes/api/ApiLogin.php
includes/api/i18n/lij.json
includes/api/i18n/pl.json
includes/db/ChronologyProtector.php
includes/db/Database.php
includes/db/DatabaseMssql.php
includes/db/DatabaseMysqlBase.php
includes/db/DatabaseOracle.php
includes/db/DatabasePostgres.php
includes/db/loadbalancer/LBFactory.php
includes/debug/logger/LegacySpi.php
includes/debug/logger/NullSpi.php
includes/exception/UserNotLoggedIn.php
includes/jobqueue/JobQueueGroup.php
includes/jobqueue/utils/PurgeJobUtils.php
includes/libs/WaitConditionLoop.php
includes/libs/objectcache/IExpiringStore.php
includes/libs/objectcache/MemcachedBagOStuff.php
includes/libs/objectcache/RESTBagOStuff.php
includes/objectcache/RedisBagOStuff.php
includes/objectcache/SqlBagOStuff.php
includes/page/WikiPage.php
includes/resourceloader/ResourceLoader.php
includes/resourceloader/ResourceLoaderFileModule.php
includes/resourceloader/ResourceLoaderImageModule.php
includes/resourceloader/ResourceLoaderModule.php
includes/skins/BaseTemplate.php
includes/skins/Skin.php
includes/specials/SpecialBotPasswords.php
includes/user/BotPassword.php
languages/i18n/ast.json
languages/i18n/be-tarask.json
languages/i18n/be.json
languages/i18n/ckb.json
languages/i18n/diq.json
languages/i18n/dty.json
languages/i18n/en.json
languages/i18n/eo.json
languages/i18n/es.json
languages/i18n/fr.json
languages/i18n/ko.json
languages/i18n/lb.json
languages/i18n/lt.json
languages/i18n/nan.json
languages/i18n/nn.json
languages/i18n/pl.json
languages/i18n/pt.json
languages/i18n/qqq.json
languages/i18n/sv.json
languages/i18n/ur.json
languages/i18n/zh-hans.json
languages/messages/MessagesUr.php
maintenance/doMaintenance.php
resources/Resources.php
resources/lib/phpjs-sha1/LICENSE.txt [deleted file]
resources/src/mediawiki.special/mediawiki.special.movePage.js
tests/parser/parserTests.txt
tests/phpunit/includes/user/BotPasswordTest.php
tests/phpunit/structure/ContentHandlerSanityTest.php [new file with mode: 0644]

index 979da5c..1f4b8dc 100644 (file)
@@ -54,6 +54,11 @@ production.
 * mw.Api has a new option, useUS, to use U+001F (Unit Separator) when
   appropriate for sending multi-valued parameters. This defaults to true when
   the mw.Api instance seems to be for the local wiki.
+* After a client performs an action which alters a database that has replica databases,
+  MediaWiki will wait for the replica databases to synchronize with the master database
+  while it renders the HTML output. However, if the output is a redirect, it will instead
+  alter the redirect URL to include a ?cpPosTime parameter that triggers the database
+  synchronization when the URL is followed by the client.
 
 === External library changes in 1.28 ===
 
index ed3e212..140cd72 100644 (file)
@@ -3050,14 +3050,14 @@ class EditPage {
         * subclasses may reorganize the form.
         * Note that you do not need to worry about the label's for=, it will be
         * inferred by the id given to the input. You can remove them both by
-        * passing array( 'id' => false ) to $userInputAttrs.
+        * passing [ 'id' => false ] to $userInputAttrs.
         *
         * @param string $summary The value of the summary input
         * @param string $labelText The html to place inside the label
         * @param array $inputAttrs Array of attrs to use on the input
         * @param array $spanLabelAttrs Array of attrs to use on the span inside the label
         *
-        * @return array An array in the format array( $label, $input )
+        * @return array An array in the format [ $label, $input ]
         */
        function getSummaryInput( $summary = "", $labelText = null,
                $inputAttrs = null, $spanLabelAttrs = null
index 13f706d..391e05a 100644 (file)
  *
  * @par Example:
  * @code
- * $magicWords = array();
+ * $magicWords = [];
  *
- * $magicWords['en'] = array(
- *     'magicwordkey' => array( 0, 'case_insensitive_magic_word' ),
- *     'magicwordkey2' => array( 1, 'CASE_sensitive_magic_word2' ),
- * );
+ * $magicWords['en'] = [
+ *     'magicwordkey' => [ 0, 'case_insensitive_magic_word' ],
+ *     'magicwordkey2' => [ 1, 'CASE_sensitive_magic_word2' ],
+ * ];
  * @endcode
  *
  * For magic words which are also Parser variables, add a MagicWordwgVariableIDs
index bca7a21..7f20de1 100644 (file)
@@ -535,10 +535,11 @@ class MediaWiki {
 
        /**
         * @see MediaWiki::preOutputCommit()
+        * @param callable $postCommitWork [default: null]
         * @since 1.26
         */
-       public function doPreOutputCommit() {
-               self::preOutputCommit( $this->context );
+       public function doPreOutputCommit( callable $postCommitWork = null ) {
+               self::preOutputCommit( $this->context, $postCommitWork );
        }
 
        /**
@@ -546,33 +547,61 @@ class MediaWiki {
         * the user can receive a response (in case commit fails)
         *
         * @param IContextSource $context
+        * @param callable $postCommitWork [default: null]
         * @since 1.27
         */
-       public static function preOutputCommit( IContextSource $context ) {
+       public static function preOutputCommit(
+               IContextSource $context, callable $postCommitWork = null
+       ) {
                // Either all DBs should commit or none
                ignore_user_abort( true );
 
                $config = $context->getConfig();
-
+               $request = $context->getRequest();
+               $output = $context->getOutput();
                $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
+
                // Commit all changes
                $lbFactory->commitMasterChanges(
                        __METHOD__,
                        // Abort if any transaction was too big
                        [ 'maxWriteDuration' => $config->get( 'MaxUserDBWriteDuration' ) ]
                );
+               wfDebug( __METHOD__ . ': primary transaction round committed' );
 
+               // Run updates that need to block the user or affect output (this is the last chance)
                DeferredUpdates::doUpdates( 'enqueue', DeferredUpdates::PRESEND );
                wfDebug( __METHOD__ . ': pre-send deferred updates completed' );
 
-               // Record ChronologyProtector positions
-               $lbFactory->shutdown();
-               wfDebug( __METHOD__ . ': all transactions committed' );
+               // Decide when clients block on ChronologyProtector DB position writes
+               if (
+                       $request->wasPosted() &&
+                       $output->getRedirect() &&
+                       $lbFactory->hasOrMadeRecentMasterChanges( INF ) &&
+                       self::isWikiClusterURL( $output->getRedirect(), $context )
+               ) {
+                       // OutputPage::output() will be fast; $postCommitWork will not be useful for
+                       // masking the latency of syncing DB positions accross all datacenters synchronously.
+                       // Instead, make use of the RTT time of the client follow redirects.
+                       $flags = $lbFactory::SHUTDOWN_CHRONPROT_ASYNC;
+                       // Client's next request should see 1+ positions with this DBMasterPos::asOf() time
+                       $safeUrl = $lbFactory->appendPreShutdownTimeAsQuery(
+                               $output->getRedirect(),
+                               microtime( true )
+                       );
+                       $output->redirect( $safeUrl );
+               } else {
+                       // OutputPage::output() is fairly slow; run it in $postCommitWork to mask
+                       // the latency of syncing DB positions accross all datacenters synchronously
+                       $flags = $lbFactory::SHUTDOWN_CHRONPROT_SYNC;
+               }
+               // Record ChronologyProtector positions for DBs affected in this request at this point
+               $lbFactory->shutdown( $flags, $postCommitWork );
+               wfDebug( __METHOD__ . ': LBFactory shutdown completed' );
 
                // Set a cookie to tell all CDN edge nodes to "stick" the user to the DC that handles this
                // POST request (e.g. the "master" data center). Also have the user briefly bypass CDN so
                // ChronologyProtector works for cacheable URLs.
-               $request = $context->getRequest();
                if ( $request->wasPosted() && $lbFactory->hasOrMadeRecentMasterChanges() ) {
                        $expires = time() + $config->get( 'DataCenterUpdateStickTTL' );
                        $options = [ 'prefix' => '' ];
@@ -584,7 +613,7 @@ class MediaWiki {
                // also intimately related to the value of $wgCdnReboundPurgeDelay.
                if ( $lbFactory->laggedReplicaUsed() ) {
                        $maxAge = $config->get( 'CdnMaxageLagged' );
-                       $context->getOutput()->lowerCdnMaxage( $maxAge );
+                       $output->lowerCdnMaxage( $maxAge );
                        $request->response()->header( "X-Database-Lagged: true" );
                        wfDebugLog( 'replication', "Lagged DB used; CDN cache TTL limited to $maxAge seconds" );
                }
@@ -592,11 +621,42 @@ class MediaWiki {
                // Avoid long-term cache pollution due to message cache rebuild timeouts (T133069)
                if ( MessageCache::singleton()->isDisabled() ) {
                        $maxAge = $config->get( 'CdnMaxageSubstitute' );
-                       $context->getOutput()->lowerCdnMaxage( $maxAge );
+                       $output->lowerCdnMaxage( $maxAge );
                        $request->response()->header( "X-Response-Substitute: true" );
                }
        }
 
+       /**
+        * @param string $url
+        * @param IContextSource $context
+        * @return bool Whether $url is to something on this wiki farm
+        */
+       private function isWikiClusterURL( $url, IContextSource $context ) {
+               static $relevantKeys = [ 'host' => true, 'port' => true ];
+
+               $infoCandidate = wfParseUrl( $url );
+               if ( $infoCandidate === false ) {
+                       return false;
+               }
+
+               $infoCandidate = array_intersect_key( $infoCandidate, $relevantKeys );
+               $clusterHosts = array_merge(
+                       // Local wiki host (the most common case)
+                       [ $context->getConfig()->get( 'CanonicalServer' ) ],
+                       // Any local/remote wiki virtual hosts for this wiki farm
+                       $context->getConfig()->get( 'LocalVirtualHosts' )
+               );
+
+               foreach ( $clusterHosts as $clusterHost ) {
+                       $infoHost = array_intersect_key( wfParseUrl( $clusterHost ), $relevantKeys );
+                       if ( $infoCandidate === $infoHost ) {
+                               return true;
+                       }
+               }
+
+               return false;
+       }
+
        /**
         * This function does work that can be done *after* the
         * user gets the HTTP response so they don't block on it
@@ -614,10 +674,9 @@ class MediaWiki {
                // Show visible profiling data if enabled (which cannot be post-send)
                Profiler::instance()->logDataPageOutputOnly();
 
-               $that = $this;
-               $callback = function () use ( $that, $mode ) {
+               $callback = function () use ( $mode ) {
                        try {
-                               $that->restInPeace( $mode );
+                               $this->restInPeace( $mode );
                        } catch ( Exception $e ) {
                                MWExceptionHandler::handleException( $e );
                        }
@@ -643,6 +702,7 @@ class MediaWiki {
        private function main() {
                global $wgTitle;
 
+               $output = $this->context->getOutput();
                $request = $this->context->getRequest();
 
                // Send Ajax requests to the Ajax dispatcher.
@@ -656,6 +716,7 @@ class MediaWiki {
 
                        $dispatcher = new AjaxDispatcher( $this->config );
                        $dispatcher->performAction( $this->context->getUser() );
+
                        return;
                }
 
@@ -717,11 +778,11 @@ class MediaWiki {
                                // Setup dummy Title, otherwise OutputPage::redirect will fail
                                $title = Title::newFromText( 'REDIR', NS_MAIN );
                                $this->context->setTitle( $title );
-                               $output = $this->context->getOutput();
                                // Since we only do this redir to change proto, always send a vary header
                                $output->addVaryHeader( 'X-Forwarded-Proto' );
                                $output->redirect( $redirUrl );
                                $output->output();
+
                                return;
                        }
                }
@@ -733,14 +794,15 @@ class MediaWiki {
                                if ( $cache->isCacheGood( /* Assume up to date */ ) ) {
                                        // Check incoming headers to see if client has this cached
                                        $timestamp = $cache->cacheTimestamp();
-                                       if ( !$this->context->getOutput()->checkLastModified( $timestamp ) ) {
+                                       if ( !$output->checkLastModified( $timestamp ) ) {
                                                $cache->loadFromFileCache( $this->context );
                                        }
                                        // Do any stats increment/watchlist stuff
                                        // Assume we're viewing the latest revision (this should always be the case with file cache)
                                        $this->context->getWikiPage()->doViewUpdates( $this->context->getUser() );
                                        // Tell OutputPage that output is taken care of
-                                       $this->context->getOutput()->disable();
+                                       $output->disable();
+
                                        return;
                                }
                        }
@@ -749,13 +811,24 @@ class MediaWiki {
                // Actually do the work of the request and build up any output
                $this->performRequest();
 
+               // GUI-ify and stash the page output in MediaWiki::doPreOutputCommit() while
+               // ChronologyProtector synchronizes DB positions or slaves accross all datacenters.
+               $buffer = null;
+               $outputWork = function () use ( $output, &$buffer ) {
+                       if ( $buffer === null ) {
+                               $buffer = $output->output( true );
+                       }
+
+                       return $buffer;
+               };
+
                // Now commit any transactions, so that unreported errors after
                // output() don't roll back the whole DB transaction and so that
                // we avoid having both success and error text in the response
-               $this->doPreOutputCommit();
+               $this->doPreOutputCommit( $outputWork );
 
-               // Output everything!
-               $this->context->getOutput()->output();
+               // Now send the actual output
+               print $outputWork();
        }
 
        /**
index 9b2d8da..d9230b0 100644 (file)
@@ -2214,10 +2214,16 @@ class OutputPage extends ContextSource {
        /**
         * Finally, all the text has been munged and accumulated into
         * the object, let's actually output it:
+        *
+        * @param bool $return Set to true to get the result as a string rather than sending it
+        * @return string|null
+        * @throws Exception
+        * @throws FatalError
+        * @throws MWException
         */
-       public function output() {
+       public function output( $return = false ) {
                if ( $this->mDoNothing ) {
-                       return;
+                       return $return ? '' : null;
                }
 
                $response = $this->getRequest()->response();
@@ -2253,7 +2259,7 @@ class OutputPage extends ContextSource {
                                }
                        }
 
-                       return;
+                       return $return ? '' : null;
                } elseif ( $this->mStatusCode ) {
                        $response->statusHeader( $this->mStatusCode );
                }
@@ -2322,8 +2328,12 @@ class OutputPage extends ContextSource {
 
                $this->sendCacheControl();
 
-               ob_end_flush();
-
+               if ( $return ) {
+                       return ob_get_clean();
+               } else {
+                       ob_end_flush();
+                       return null;
+               }
        }
 
        /**
index 005c341..049b32f 100644 (file)
  *
  * $router->add( "/wiki/$1" );
  *   - Matches /wiki/Foo style urls and extracts the title
- * $router->add( array( 'edit' => "/edit/$key" ), array( 'action' => '$key' ) );
+ * $router->add( [ 'edit' => "/edit/$key" ], [ 'action' => '$key' ] );
  *   - Matches /edit/Foo style urls and sets action=edit
  * $router->add( '/$2/$1',
- *   array( 'variant' => '$2' ),
- *   array( '$2' => array( 'zh-hant', 'zh-hans' )
+ *   [ 'variant' => '$2' ],
+ *   [ '$2' => [ 'zh-hant', 'zh-hans' ] ]
  * );
  *   - Matches /zh-hant/Foo or /zh-hans/Foo
- * $router->addStrict( "/foo/Bar", array( 'title' => 'Baz' ) );
+ * $router->addStrict( "/foo/Bar", [ 'title' => 'Baz' ] );
  *   - Matches /foo/Bar explicitly and uses "Baz" as the title
- * $router->add( '/help/$1', array( 'title' => 'Help:$1' ) );
+ * $router->add( '/help/$1', [ 'title' => 'Help:$1' ] );
  *   - Matches /help/Foo with "Help:Foo" as the title
- * $router->add( '/$1', array( 'foo' => array( 'value' => 'bar$2' ) );
+ * $router->add( '/$1', [ 'foo' => [ 'value' => 'bar$2' ] ] );
  *   - Matches /Foo and sets 'foo' to 'bar$2' without $2 being replaced
- * $router->add( '/$1', array( 'data:foo' => 'bar' ), array( 'callback' => 'functionname' ) );
+ * $router->add( '/$1', [ 'data:foo' => 'bar' ], [ 'callback' => 'functionname' ] );
  *   - Matches /Foo, adds the key 'foo' with the value 'bar' to the data array
  *     and calls functionname( &$matches, $data );
  *
@@ -56,7 +56,7 @@
  *   - The default behavior is equivalent to `array( 'title' => '$1' )`,
  *     if you don't want the title parameter you can explicitly use `array( 'title' => false )`
  *   - You can specify a value that won't have replacements in it
- *     using `'foo' => array( 'value' => 'bar' );`
+ *     using `'foo' => [ 'value' => 'bar' ];`
  *
  * Options:
  *   - The option keys $1, $2, etc... can be specified to restrict the possible values
index 8f78164..a5ae461 100644 (file)
@@ -520,7 +520,7 @@ class WebRequest {
         * @return int
         */
        public function getInt( $name, $default = 0 ) {
-               return intval( $this->getVal( $name, $default ) );
+               return intval( $this->getRawVal( $name, $default ) );
        }
 
        /**
@@ -532,7 +532,7 @@ class WebRequest {
         * @return int|null
         */
        public function getIntOrNull( $name ) {
-               $val = $this->getVal( $name );
+               $val = $this->getRawVal( $name );
                return is_numeric( $val )
                        ? intval( $val )
                        : null;
@@ -549,7 +549,7 @@ class WebRequest {
         * @return float
         */
        public function getFloat( $name, $default = 0.0 ) {
-               return floatval( $this->getVal( $name, $default ) );
+               return floatval( $this->getRawVal( $name, $default ) );
        }
 
        /**
@@ -562,7 +562,7 @@ class WebRequest {
         * @return bool
         */
        public function getBool( $name, $default = false ) {
-               return (bool)$this->getVal( $name, $default );
+               return (bool)$this->getRawVal( $name, $default );
        }
 
        /**
@@ -575,7 +575,8 @@ class WebRequest {
         * @return bool
         */
        public function getFuzzyBool( $name, $default = false ) {
-               return $this->getBool( $name, $default ) && strcasecmp( $this->getVal( $name ), 'false' ) !== 0;
+               return $this->getBool( $name, $default )
+                       && strcasecmp( $this->getRawVal( $name ), 'false' ) !== 0;
        }
 
        /**
@@ -589,7 +590,7 @@ class WebRequest {
        public function getCheck( $name ) {
                # Checkboxes and buttons are only present when clicked
                # Presence connotes truth, absence false
-               return $this->getVal( $name, null ) !== null;
+               return $this->getRawVal( $name, null ) !== null;
        }
 
        /**
index 9bc0b3a..55edd99 100644 (file)
@@ -110,17 +110,18 @@ class ApiLogin extends ApiBase {
                }
 
                // Try bot passwords
-               if ( $authRes === false && $this->getConfig()->get( 'EnableBotPasswords' ) &&
-                       strpos( $params['name'], BotPassword::getSeparator() ) !== false
+               if (
+                       $authRes === false && $this->getConfig()->get( 'EnableBotPasswords' ) &&
+                       ( $botLoginData = BotPassword::canonicalizeLoginData( $params['name'], $params['password'] ) )
                ) {
                        $status = BotPassword::login(
-                               $params['name'], $params['password'], $this->getRequest()
+                               $botLoginData[0], $botLoginData[1], $this->getRequest()
                        );
                        if ( $status->isOK() ) {
                                $session = $status->getValue();
                                $authRes = 'Success';
                                $loginType = 'BotPassword';
-                       } else {
+                       } elseif ( !$botLoginData[2] ) {
                                $authRes = 'Failed';
                                $message = $status->getMessage();
                                LoggerFactory::getInstance( 'authentication' )->info(
index 8b07da9..5ea8613 100644 (file)
        "apihelp-expandtemplates-paramvalue-prop-volatile": "Se l'output o segge volatile e o no 'agge da ese riadoeuviou atr'onde a l'interno da paggina.",
        "apihelp-expandtemplates-paramvalue-prop-ttl": "O tempo mascimo doppo o quæ e memoizaçioin tempoannie (cache) do risultou dovieivan ese invalidæ.",
        "apihelp-feedcontributions-param-feedformat": "O formato do feed.",
+       "apihelp-feedrecentchanges-param-feedformat": "O formato do feed.",
+       "apihelp-feedrecentchanges-param-namespace": "Namespace a-o quæ limitâ i risultæ.",
+       "apihelp-feedrecentchanges-param-associated": "Inciodi namespace associou (discuscion ò prinçipâ)",
+       "apihelp-feedrecentchanges-param-days": "Intervallo de giorni pe-i quæ limitâ i risultæ.",
+       "apihelp-feedrecentchanges-param-limit": "Nummero mascimo di risultæ da restituî.",
+       "apihelp-feedrecentchanges-param-from": "Mostra i cangiamenti da alloa.",
+       "apihelp-feedrecentchanges-param-hideminor": "Ascondi e modiffiche menoî.",
+       "apihelp-feedrecentchanges-param-hidebots": "Ascondi e modiffiche fæte da di bot.",
+       "apihelp-feedrecentchanges-param-hideanons": "Ascondi e modiffiche fæte da di utenti anonnimi.",
+       "apihelp-feedrecentchanges-param-hideliu": "Ascondi e modiffiche fæte da-i utenti registræ.",
+       "apihelp-feedrecentchanges-param-hidepatrolled": "Ascondi e modiffiche veificæ.",
+       "apihelp-feedrecentchanges-param-hidemyself": "O l'asconde e modiffiche fæte da l'utente attoale.",
+       "apihelp-feedrecentchanges-param-hidecategorization": "Ascondi e variaçioin d'apartegninça a-e categorie.",
+       "apihelp-feedrecentchanges-param-tagfilter": "Filtra pe etichetta.",
+       "apihelp-feedrecentchanges-param-target": "Mostra solo e modifiche a-e paggine collegæ da questa paggina.",
+       "apihelp-feedrecentchanges-param-showlinkedto": "Fanni védde sôlo i cangiaménti a-e pàggine colegæ a-a quella speçificâ",
+       "apihelp-feedrecentchanges-param-categories": "Mostra solo e variaçioin in sce-e paggine de tutte queste categorie.",
+       "apihelp-feedrecentchanges-param-categories_any": "Mostra invece solo e variaçioin in sce-e paggine inte 'na qualonque categoria.",
        "apihelp-feedrecentchanges-example-simple": "Mostra i urtime modiffiche.",
        "apihelp-feedrecentchanges-example-30days": "Mostra e modifiche di urtimi 30 giorni.",
        "apihelp-feedwatchlist-param-feedformat": "O formato do feed.",
        "apihelp-options-example-reset": "Reimposta tutte e preferençe.",
        "apihelp-paraminfo-description": "Otegni de informaçioin in scî modduli API.",
        "apihelp-paraminfo-param-helpformat": "Formato de stringhe d'agiutto.",
-       "apihelp-parse-param-summary": "Ogetto da analizâ."
+       "apihelp-parse-param-summary": "Ogetto da analizâ.",
+       "apihelp-query+allcategories-param-prop": "Quæ propietæ otegnî:",
+       "apihelp-query+allcategories-paramvalue-prop-size": "Azonzi o nummero de paggine inta categoria.",
+       "apihelp-query+allcategories-paramvalue-prop-hidden": "Etichetta e categorie che son ascose con <code>_&#95;HIDDENCAT_&#95;</code>.",
+       "apihelp-query+allcategories-example-size": "Elenca e categorie con de informaçioin in sciô numero de paggine in ciascun-a.",
+       "apihelp-query+alldeletedrevisions-description": "Elenca tutte e verscioin scassæ da 'n utente ò inte 'n namespace.",
+       "apihelp-query+alldeletedrevisions-paraminfo-useronly": "O poeu ese doeuviou solo con <var>$3user</var>.",
+       "apihelp-query+alldeletedrevisions-paraminfo-nonuseronly": "O no poeu ese doeuviou con <var>$3user</var>.",
+       "apihelp-query+alldeletedrevisions-param-start": "O timestamp da-o quæ començâ l'elenco.",
+       "apihelp-query+alldeletedrevisions-param-end": "O timestamp a-o quæ interrompî l'elenco.",
+       "apihelp-query+alldeletedrevisions-param-from": "Comença l'elenco a questo tittolo.",
+       "apihelp-query+alldeletedrevisions-param-to": "Interrompi l'elenco a questo titolo.",
+       "apihelp-query+alldeletedrevisions-param-prefix": "Riçerca pe tutti i titoli de pagine che començan con questo valô.",
+       "apihelp-query+alldeletedrevisions-param-user": "Elenca solo e verscioin de questo utente.",
+       "apihelp-query+alldeletedrevisions-param-excludeuser": "No elencâ e verscioin de questo utente.",
+       "apihelp-query+alldeletedrevisions-param-namespace": "Elenca solo e paggine inte questo namespace.",
+       "apihelp-query+alldeletedrevisions-example-user": "Elenca i urtimi 50 contributi scassæ de l'utente <kbd>Example</kbd>.",
+       "apihelp-query+alldeletedrevisions-example-ns-main": "Elenca e primme 50 verscioin scassæ into namespace prinçipâ.",
+       "apihelp-query+allfileusages-param-from": "O titolo do file da-o quæ començâ l'elenco.",
+       "apihelp-query+allfileusages-param-to": "O tittolo do file a-o quæ interrompî l'elenco.",
+       "apihelp-query+allfileusages-param-prefix": "Riçerca pe tutti i titoli di file che començan con questo valô.",
+       "apihelp-query+allfileusages-paramvalue-prop-title": "O l'azonze o tittolo do file.",
+       "apihelp-query+allfileusages-param-limit": "Quanti elementi totali restitoî.",
+       "apihelp-query+allfileusages-param-dir": "A direçion inta quæ elencâ.",
+       "apihelp-query+allfileusages-example-generator": "Otegni e paggine contegninte i file.",
+       "apihelp-query+allimages-param-sort": "Propietæ d'amerçamento.",
+       "apihelp-query+allimages-param-dir": "A direçion inta quæ elencâ.",
+       "apihelp-query+allimages-param-from": "O titolo de l'inmagine da-a quæ començâ l'elenco. O poeu ese doeuviou solo con $1sort=name."
 }
index 6023be0..0dfd5f5 100644 (file)
        "apihelp-managetags-param-reason": "Opcjonalny powód utworzenia, usunięcia, włączenia lub wyłączenia znacznika.",
        "apihelp-managetags-param-ignorewarnings": "Czy zignorować ostrzeżenia, które pojawiają się w trakcie operacji.",
        "apihelp-mergehistory-description": "Łączenie historii edycji.",
+       "apihelp-mergehistory-param-reason": "Powód łączenia historii.",
        "apihelp-move-description": "Przenieś stronę.",
        "apihelp-move-param-reason": "Powód zmiany nazwy.",
        "apihelp-move-param-movetalk": "Zmień nazwę strony dyskusji, jeśli istnieje.",
index 39c9957..1cdb49f 100644 (file)
@@ -33,6 +33,10 @@ class ChronologyProtector {
        protected $key;
        /** @var string Hash of client parameters */
        protected $clientId;
+       /** @var float|null Minimum UNIX timestamp of 1+ expected startup positions */
+       protected $waitForPosTime;
+       /** @var int Max seconds to wait on positions to appear */
+       protected $waitForPosTimeout = self::POS_WAIT_TIMEOUT;
        /** @var bool Whether to no-op all method calls */
        protected $enabled = true;
        /** @var bool Whether to check and wait on positions */
@@ -47,15 +51,22 @@ class ChronologyProtector {
        /** @var float[] Map of (DB master name => 1) */
        protected $shutdownTouchDBs = [];
 
+       /** @var integer Seconds to store positions */
+       const POSITION_TTL = 60;
+       /** @var integer Max time to wait for positions to appear */
+       const POS_WAIT_TIMEOUT = 5;
+
        /**
         * @param BagOStuff $store
         * @param array $client Map of (ip: <IP>, agent: <user-agent>)
+        * @param float $posTime UNIX timestamp
         * @since 1.27
         */
-       public function __construct( BagOStuff $store, array $client ) {
+       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->waitForPosTime = $posTime;
        }
 
        /**
@@ -130,20 +141,23 @@ class ChronologyProtector {
         * Notify the ChronologyProtector that the LBFactory is done calling shutdownLB() for now.
         * May commit chronology data to persistent storage.
         *
+        * @param callable|null $workCallback Work to do instead of waiting on syncing positions
+        * @param string $mode One of (sync, async); whether to wait on remote datacenters
         * @return DBMasterPos[] Empty on success; returns the (db name => position) map on failure
         */
-       public function shutdown() {
+       public function shutdown( callable $workCallback = null, $mode = 'sync' ) {
                if ( !$this->enabled ) {
                        return [];
                }
 
+               $store = $this->store;
                // Some callers might want to know if a user recently touched a DB.
                // These writes do not need to block on all datacenters receiving them.
                foreach ( $this->shutdownTouchDBs as $dbName => $unused ) {
-                       $this->store->set(
+                       $store->set(
                                $this->getTouchedKey( $this->store, $dbName ),
                                microtime( true ),
-                               BagOStuff::TTL_DAY
+                               $store::TTL_DAY
                        );
                }
 
@@ -159,29 +173,42 @@ class ChronologyProtector {
                // CP-protected writes should overwhemingly go to the master datacenter, so get DC-local
                // lock to merge the values. Use a DC-local get() and a synchronous all-DC set(). This
                // makes it possible for the BagOStuff class to write in parallel to all DCs with one RTT.
-               if ( $this->store->lock( $this->key, 3 ) ) {
-                       $ok = $this->store->set(
+               if ( $store->lock( $this->key, 3 ) ) {
+                       if ( $workCallback ) {
+                               // Let the store run the work before blocking on a replication sync barrier. By the
+                               // time it's done with the work, the barrier should be fast if replication caught up.
+                               $store->addBusyCallback( $workCallback );
+                       }
+                       $ok = $store->set(
                                $this->key,
-                               self::mergePositions( $this->store->get( $this->key ), $this->shutdownPositions ),
-                               BagOStuff::TTL_MINUTE,
-                               BagOStuff::WRITE_SYNC
+                               self::mergePositions( $store->get( $this->key ), $this->shutdownPositions ),
+                               self::POSITION_TTL,
+                               ( $mode === 'sync' ) ? $store::WRITE_SYNC : 0
                        );
-                       $this->store->unlock( $this->key );
+                       $store->unlock( $this->key );
                } else {
                        $ok = false;
                }
 
                if ( !$ok ) {
+                       $bouncedPositions = $this->shutdownPositions;
                        // Raced out too many times or stash is down
                        wfDebugLog( 'replication',
                                __METHOD__ . ": failed to save master pos for " .
                                implode( ', ', array_keys( $this->shutdownPositions ) ) . "\n"
                        );
-
-                       return $this->shutdownPositions;
+               } elseif ( $mode === 'sync' &&
+                       $store->getQoS( $store::ATTR_SYNCWRITES ) < $store::QOS_SYNCWRITES_BE
+               ) {
+                       // Positions may not be in all datacenters, force LBFactory to play it safe
+                       wfDebugLog( 'replication',
+                               __METHOD__ . ": store does not report ability to sync replicas. " );
+                       $bouncedPositions = $this->shutdownPositions;
+               } else {
+                       $bouncedPositions = [];
                }
 
-               return [];
+               return $bouncedPositions;
        }
 
        /**
@@ -212,7 +239,33 @@ class ChronologyProtector {
 
                $this->initialized = true;
                if ( $this->wait ) {
-                       $data = $this->store->get( $this->key );
+                       // If there is an expectation to see master positions with a certain min
+                       // timestamp, then block until they appear, or until a timeout is reached.
+                       if ( $this->waitForPosTime ) {
+                               $data = null;
+                               $loop = new WaitConditionLoop(
+                                       function () use ( &$data ) {
+                                               $data = $this->store->get( $this->key );
+
+                                               return ( self::minPosTime( $data ) >= $this->waitForPosTime )
+                                                       ? WaitConditionLoop::CONDITION_REACHED
+                                                       : WaitConditionLoop::CONDITION_CONTINUE;
+                                       },
+                                       $this->waitForPosTimeout
+                               );
+                               $result = $loop->invoke();
+                               $waitedMs = $loop->getLastWaitTime() * 1e3;
+
+                               if ( $result == $loop::CONDITION_REACHED ) {
+                                       $msg = "expected and found pos time {$this->waitForPosTime} ({$waitedMs}ms)";
+                               } else {
+                                       $msg = "expected but missed pos time {$this->waitForPosTime} ({$waitedMs}ms)";
+                               }
+                               wfDebugLog( 'replication', $msg );
+                       } else {
+                               $data = $this->store->get( $this->key );
+                       }
+
                        $this->startupPositions = $data ? $data['positions'] : [];
                        wfDebugLog( 'replication', __METHOD__ . ": key is {$this->key} (read)\n" );
                } else {
@@ -221,6 +274,24 @@ class ChronologyProtector {
                }
        }
 
+       /**
+        * @param array|bool $data
+        * @return float|null
+        */
+       private static function minPosTime( $data ) {
+               if ( !isset( $data['positions'] ) ) {
+                       return null;
+               }
+
+               $min = null;
+               foreach ( $data['positions'] as $pos ) {
+                       /** @var DBMasterPos $pos */
+                       $min = $min ? min( $pos->asOfTime(), $min ) : $pos->asOfTime();
+               }
+
+               return $min;
+       }
+
        /**
         * @param array|bool $curValue
         * @param DBMasterPos[] $shutdownPositions
index eca80d8..0a1774d 100644 (file)
@@ -1344,8 +1344,13 @@ abstract class DatabaseBase implements IDatabase {
                } else {
                        $useIndex = '';
                }
+               if ( isset( $options['IGNORE INDEX'] ) && is_string( $options['IGNORE INDEX'] ) ) {
+                       $ignoreIndex = $this->ignoreIndexClause( $options['IGNORE INDEX'] );
+               } else {
+                       $ignoreIndex = '';
+               }
 
-               return [ $startOpts, $useIndex, $preLimitTail, $postLimitTail ];
+               return [ $startOpts, $useIndex, $preLimitTail, $postLimitTail, $ignoreIndex ];
        }
 
        /**
@@ -1413,31 +1418,34 @@ abstract class DatabaseBase implements IDatabase {
                $useIndexes = ( isset( $options['USE INDEX'] ) && is_array( $options['USE INDEX'] ) )
                        ? $options['USE INDEX']
                        : [];
+               $ignoreIndexes = ( isset( $options['IGNORE INDEX'] ) && is_array( $options['IGNORE INDEX'] ) )
+                       ? $options['IGNORE INDEX']
+                       : [];
 
                if ( is_array( $table ) ) {
                        $from = ' FROM ' .
-                               $this->tableNamesWithUseIndexOrJOIN( $table, $useIndexes, $join_conds );
+                               $this->tableNamesWithIndexClauseOrJOIN( $table, $useIndexes, $ignoreIndexes, $join_conds );
                } elseif ( $table != '' ) {
                        if ( $table[0] == ' ' ) {
                                $from = ' FROM ' . $table;
                        } else {
                                $from = ' FROM ' .
-                                       $this->tableNamesWithUseIndexOrJOIN( [ $table ], $useIndexes, [] );
+                                       $this->tableNamesWithIndexClauseOrJOIN( [ $table ], $useIndexes, $ignoreIndexes, [] );
                        }
                } else {
                        $from = '';
                }
 
-               list( $startOpts, $useIndex, $preLimitTail, $postLimitTail ) =
+               list( $startOpts, $useIndex, $preLimitTail, $postLimitTail, $ignoreIndex ) =
                        $this->makeSelectOptions( $options );
 
                if ( !empty( $conds ) ) {
                        if ( is_array( $conds ) ) {
                                $conds = $this->makeList( $conds, LIST_AND );
                        }
-                       $sql = "SELECT $startOpts $vars $from $useIndex WHERE $conds $preLimitTail";
+                       $sql = "SELECT $startOpts $vars $from $useIndex $ignoreIndex WHERE $conds $preLimitTail";
                } else {
-                       $sql = "SELECT $startOpts $vars $from $useIndex $preLimitTail";
+                       $sql = "SELECT $startOpts $vars $from $useIndex $ignoreIndex $preLimitTail";
                }
 
                if ( isset( $options['LIMIT'] ) ) {
@@ -2048,19 +2056,21 @@ abstract class DatabaseBase implements IDatabase {
 
        /**
         * Get the aliased table name clause for a FROM clause
-        * which might have a JOIN and/or USE INDEX clause
+        * which might have a JOIN and/or USE INDEX or IGNORE INDEX clause
         *
         * @param array $tables ( [alias] => table )
         * @param array $use_index Same as for select()
+        * @param array $ignore_index Same as for select()
         * @param array $join_conds Same as for select()
         * @return string
         */
-       protected function tableNamesWithUseIndexOrJOIN(
-               $tables, $use_index = [], $join_conds = []
+       protected function tableNamesWithIndexClauseOrJOIN(
+               $tables, $use_index = [], $ignore_index = [], $join_conds = []
        ) {
                $ret = [];
                $retJOIN = [];
                $use_index = (array)$use_index;
+               $ignore_index = (array)$ignore_index;
                $join_conds = (array)$join_conds;
 
                foreach ( $tables as $alias => $table ) {
@@ -2079,6 +2089,12 @@ abstract class DatabaseBase implements IDatabase {
                                                $tableClause .= ' ' . $use;
                                        }
                                }
+                               if ( isset( $ignore_index[$alias] ) ) { // has IGNORE INDEX?
+                                       $ignore = $this->ignoreIndexClause( implode( ',', (array)$ignore_index[$alias] ) );
+                                       if ( $ignore != '' ) {
+                                               $tableClause .= ' ' . $ignore;
+                                       }
+                               }
                                $on = $this->makeList( (array)$conds, LIST_AND );
                                if ( $on != '' ) {
                                        $tableClause .= ' ON (' . $on . ')';
@@ -2092,6 +2108,14 @@ abstract class DatabaseBase implements IDatabase {
                                        implode( ',', (array)$use_index[$alias] )
                                );
 
+                               $ret[] = $tableClause;
+                       } elseif ( isset( $ignore_index[$alias] ) ) {
+                               // Is there an INDEX clause for this table?
+                               $tableClause = $this->tableNameWithAlias( $table, $alias );
+                               $tableClause .= ' ' . $this->ignoreIndexClause(
+                                       implode( ',', (array)$ignore_index[$alias] )
+                               );
+
                                $ret[] = $tableClause;
                        } else {
                                $tableClause = $this->tableNameWithAlias( $table, $alias );
@@ -2224,6 +2248,20 @@ abstract class DatabaseBase implements IDatabase {
                return '';
        }
 
+       /**
+        * IGNORE INDEX clause. Unlikely to be useful for anything but MySQL. This
+        * is only needed because a) MySQL must be as efficient as possible due to
+        * its use on Wikipedia, and b) MySQL 4.0 is kind of dumb sometimes about
+        * which index to pick. Anyway, other databases might have different
+        * indexes on a given table. So don't bother overriding this unless you're
+        * MySQL.
+        * @param string $index
+        * @return string
+        */
+       public function ignoreIndexClause( $index ) {
+               return '';
+       }
+
        public function replace( $table, $uniqueIndexes, $rows, $fname = __METHOD__ ) {
                $quotedTable = $this->tableName( $table );
 
@@ -2488,7 +2526,8 @@ abstract class DatabaseBase implements IDatabase {
                        $selectOptions = [ $selectOptions ];
                }
 
-               list( $startOpts, $useIndex, $tailOpts ) = $this->makeSelectOptions( $selectOptions );
+               list( $startOpts, $useIndex, $tailOpts, $ignoreIndex ) = $this->makeSelectOptions(
+                       $selectOptions );
 
                if ( is_array( $srcTable ) ) {
                        $srcTable = implode( ',', array_map( [ &$this, 'tableName' ], $srcTable ) );
@@ -2498,7 +2537,7 @@ abstract class DatabaseBase implements IDatabase {
 
                $sql = "INSERT $insertOptions INTO $destTable (" . implode( ',', array_keys( $varMap ) ) . ')' .
                        " SELECT $startOpts " . implode( ',', $varMap ) .
-                       " FROM $srcTable $useIndex ";
+                       " FROM $srcTable $useIndex $ignoreIndex ";
 
                if ( $conds != '*' ) {
                        if ( is_array( $conds ) ) {
index 058c33e..f8770d2 100644 (file)
@@ -1216,7 +1216,7 @@ class DatabaseMssql extends Database {
                }
 
                // we want this to be compatible with the output of parent::makeSelectOptions()
-               return [ $startOpts, '', $tailOpts, '' ];
+               return [ $startOpts, '', $tailOpts, '', '' ];
        }
 
        /**
index e813f80..1b60ea1 100644 (file)
@@ -880,6 +880,14 @@ abstract class DatabaseMysqlBase extends Database {
                return "FORCE INDEX (" . $this->indexName( $index ) . ")";
        }
 
+       /**
+        * @param string $index
+        * @return string
+        */
+       function ignoreIndexClause( $index ) {
+               return "IGNORE INDEX (" . $this->indexName( $index ) . ")";
+       }
+
        /**
         * @return string
         */
index 171191b..ebeb3a6 100644 (file)
@@ -739,7 +739,8 @@ class DatabaseOracle extends Database {
                if ( !is_array( $selectOptions ) ) {
                        $selectOptions = [ $selectOptions ];
                }
-               list( $startOpts, $useIndex, $tailOpts ) = $this->makeSelectOptions( $selectOptions );
+               list( $startOpts, $useIndex, $tailOpts, $ignoreIndex ) =
+                       $this->makeSelectOptions( $selectOptions );
                if ( is_array( $srcTable ) ) {
                        $srcTable = implode( ',', array_map( [ &$this, 'tableName' ], $srcTable ) );
                } else {
@@ -761,7 +762,7 @@ class DatabaseOracle extends Database {
 
                $sql = "INSERT INTO $destTable (" . implode( ',', array_keys( $varMap ) ) . ')' .
                        " SELECT $startOpts " . implode( ',', $varMap ) .
-                       " FROM $srcTable $useIndex ";
+                       " FROM $srcTable $useIndex $ignoreIndex ";
                if ( $conds != '*' ) {
                        $sql .= ' WHERE ' . $this->makeList( $conds, LIST_AND );
                }
@@ -1375,7 +1376,13 @@ class DatabaseOracle extends Database {
                        $useIndex = '';
                }
 
-               return [ $startOpts, $useIndex, $preLimitTail, $postLimitTail ];
+               if ( isset( $options['IGNORE INDEX'] ) && !is_array( $options['IGNORE INDEX'] ) ) {
+                       $ignoreIndex = $this->ignoreIndexClause( $options['IGNORE INDEX'] );
+               } else {
+                       $ignoreIndex = '';
+               }
+
+               return [ $startOpts, $useIndex, $preLimitTail, $postLimitTail, $ignoreIndex ];
        }
 
        public function delete( $table, $conds, $fname = __METHOD__ ) {
index 0a178de..22445c0 100644 (file)
@@ -927,7 +927,8 @@ __INDEXATTR__;
                if ( !is_array( $selectOptions ) ) {
                        $selectOptions = [ $selectOptions ];
                }
-               list( $startOpts, $useIndex, $tailOpts ) = $this->makeSelectOptions( $selectOptions );
+               list( $startOpts, $useIndex, $tailOpts, $ignoreIndex ) =
+                       $this->makeSelectOptions( $selectOptions );
                if ( is_array( $srcTable ) ) {
                        $srcTable = implode( ',', array_map( [ &$this, 'tableName' ], $srcTable ) );
                } else {
@@ -936,7 +937,7 @@ __INDEXATTR__;
 
                $sql = "INSERT INTO $destTable (" . implode( ',', array_keys( $varMap ) ) . ')' .
                        " SELECT $startOpts " . implode( ',', $varMap ) .
-                       " FROM $srcTable $useIndex";
+                       " FROM $srcTable $useIndex $ignoreIndex ";
 
                if ( $conds != '*' ) {
                        $sql .= ' WHERE ' . $this->makeList( $conds, LIST_AND );
@@ -1482,7 +1483,7 @@ SQL;
         */
        function makeSelectOptions( $options ) {
                $preLimitTail = $postLimitTail = '';
-               $startOpts = $useIndex = '';
+               $startOpts = $useIndex = $ignoreIndex = '';
 
                $noKeyOptions = [];
                foreach ( $options as $key => $option ) {
@@ -1512,7 +1513,7 @@ SQL;
                        $startOpts .= 'DISTINCT';
                }
 
-               return [ $startOpts, $useIndex, $preLimitTail, $postLimitTail ];
+               return [ $startOpts, $useIndex, $preLimitTail, $postLimitTail, $ignoreIndex ];
        }
 
        function getDBname() {
index cd8dff3..3120889 100644 (file)
@@ -51,7 +51,9 @@ abstract class LBFactory implements DestructibleService {
        /** @var callable[] */
        protected $replicationWaitCallbacks = [];
 
-       const SHUTDOWN_NO_CHRONPROT = 1; // don't save ChronologyProtector positions (for async code)
+       const SHUTDOWN_NO_CHRONPROT = 0; // don't save DB positions at all
+       const SHUTDOWN_CHRONPROT_ASYNC = 1; // save DB positions, but don't wait on remote DCs
+       const SHUTDOWN_CHRONPROT_SYNC = 2; // save DB positions, waiting on all DCs
 
        /**
         * Construct a factory based on a configuration array (typically from $wgLBFactoryConf)
@@ -87,7 +89,7 @@ abstract class LBFactory implements DestructibleService {
         * @see LoadBalancer::disable()
         */
        public function destroy() {
-               $this->shutdown();
+               $this->shutdown( self::SHUTDOWN_NO_CHRONPROT );
                $this->forEachLBCallMethod( 'disable' );
        }
 
@@ -199,12 +201,18 @@ abstract class LBFactory implements DestructibleService {
 
        /**
         * Prepare all tracked load balancers for shutdown
-        * @param integer $flags Supports SHUTDOWN_* flags
-        */
-       public function shutdown( $flags = 0 ) {
-               if ( !( $flags & self::SHUTDOWN_NO_CHRONPROT ) ) {
-                       $this->shutdownChronologyProtector( $this->chronProt );
+        * @param integer $mode One of the class SHUTDOWN_* constants
+        * @param callable|null $workCallback Work to mask ChronologyProtector writes
+        */
+       public function shutdown(
+               $mode = self::SHUTDOWN_CHRONPROT_SYNC, callable $workCallback = null
+       ) {
+               if ( $mode === self::SHUTDOWN_CHRONPROT_SYNC ) {
+                       $this->shutdownChronologyProtector( $this->chronProt, $workCallback, 'sync' );
+               } elseif ( $mode === self::SHUTDOWN_CHRONPROT_ASYNC ) {
+                       $this->shutdownChronologyProtector( $this->chronProt, null, 'async' );
                }
+
                $this->commitMasterChanges( __METHOD__ ); // sanity
        }
 
@@ -387,13 +395,14 @@ abstract class LBFactory implements DestructibleService {
 
        /**
         * Determine if any master connection has pending/written changes from this request
+        * @param float $age How many seconds ago is "recent" [defaults to LB lag wait timeout]
         * @return bool
         * @since 1.27
         */
-       public function hasOrMadeRecentMasterChanges() {
+       public function hasOrMadeRecentMasterChanges( $age = null ) {
                $ret = false;
-               $this->forEachLB( function ( LoadBalancer $lb ) use ( &$ret ) {
-                       $ret = $ret || $lb->hasOrMadeRecentMasterChanges();
+               $this->forEachLB( function ( LoadBalancer $lb ) use ( $age, &$ret ) {
+                       $ret = $ret || $lb->hasOrMadeRecentMasterChanges( $age );
                } );
                return $ret;
        }
@@ -584,8 +593,9 @@ abstract class LBFactory implements DestructibleService {
                        ObjectCache::getMainStashInstance(),
                        [
                                'ip' => $request->getIP(),
-                               'agent' => $request->getHeader( 'User-Agent' )
-                       ]
+                               'agent' => $request->getHeader( 'User-Agent' ),
+                       ],
+                       $request->getFloat( 'cpPosTime', null )
                );
                if ( PHP_SAPI === 'cli' ) {
                        $chronProt->setEnabled( false );
@@ -599,15 +609,26 @@ abstract class LBFactory implements DestructibleService {
        }
 
        /**
+        * Get and record all of the staged DB positions into persistent memory storage
+        *
         * @param ChronologyProtector $cp
+        * @param callable|null $workCallback Work to do instead of waiting on syncing positions
+        * @param string $mode One of (sync, async); whether to wait on remote datacenters
         */
-       protected function shutdownChronologyProtector( ChronologyProtector $cp ) {
-               // Get all the master positions needed
+       protected function shutdownChronologyProtector(
+               ChronologyProtector $cp, $workCallback, $mode
+       ) {
+               // Record all the master positions needed
                $this->forEachLB( function ( LoadBalancer $lb ) use ( $cp ) {
                        $cp->shutdownLB( $lb );
                } );
-               // Write them to the stash
-               $unsavedPositions = $cp->shutdown();
+               // Write them to the persistent stash. Try to do something useful by running $work
+               // while ChronologyProtector waits for the stash write to replicate to all DCs.
+               $unsavedPositions = $cp->shutdown( $workCallback, $mode );
+               if ( $unsavedPositions && $workCallback ) {
+                       // Invoke callback in case it did not cache the result yet
+                       $workCallback(); // work now to block for less time in waitForAll()
+               }
                // If the positions failed to write to the stash, at least wait on local datacenter
                // replica DBs to catch up before responding. Even if there are several DCs, this increases
                // the chance that the user will see their own changes immediately afterwards. As long
@@ -629,6 +650,29 @@ abstract class LBFactory implements DestructibleService {
                }
        }
 
+       /**
+        * Append ?cpPosTime parameter to a URL for ChronologyProtector purposes if needed
+        *
+        * Note that unlike cookies, this works accross domains
+        *
+        * @param string $url
+        * @param float $time UNIX timestamp just before shutdown() was called
+        * @return string
+        * @since 1.28
+        */
+       public function appendPreShutdownTimeAsQuery( $url, $time ) {
+               $usedCluster = 0;
+               $this->forEachLB( function ( LoadBalancer $lb ) use ( &$usedCluster ) {
+                       $usedCluster |= ( $lb->getServerCount() > 1 );
+               } );
+
+               if ( !$usedCluster ) {
+                       return $url; // no master/replica clusters touched
+               }
+
+               return wfAppendQuery( $url, [ 'cpPosTime' => $time ] );
+       }
+
        /**
         * Close all open database connections on all open load balancers.
         * @since 1.28
@@ -636,5 +680,4 @@ abstract class LBFactory implements DestructibleService {
        public function closeAll() {
                $this->forEachLBCallMethod( 'closeAll', [] );
        }
-
 }
index a6b53ec..4cf8313 100644 (file)
@@ -25,9 +25,9 @@ namespace MediaWiki\Logger;
  *
  * Usage:
  * @code
- * $wgMWLoggerDefaultSpi = array(
+ * $wgMWLoggerDefaultSpi = [
  *   'class' => '\\MediaWiki\\Logger\\LegacySpi',
- * );
+ * ];
  * @endcode
  *
  * @see \MediaWiki\Logger\LoggerFactory
index f92ff7d..82308d0 100644 (file)
@@ -28,9 +28,9 @@ use Psr\Log\NullLogger;
  *
  * Usage:
  *
- *     $wgMWLoggerDefaultSpi = array(
+ *     $wgMWLoggerDefaultSpi = [
  *         'class' => '\\MediaWiki\\Logger\\NullSpi',
- *     );
+ *     ];
  *
  * @see \MediaWiki\Logger\LoggerFactory
  * @since 1.25
index b7c3489..43c5b09 100644 (file)
@@ -62,7 +62,7 @@ class UserNotLoggedIn extends ErrorPageError {
         * @param string $titleMsg A message key to set the page title.
         *        Optional, default: 'exception-nologin'
         * @param array $params Parameters to wfMessage().
-        *        Optional, default: array()
+        *        Optional, default: []
         */
        public function __construct(
                $reasonMsg = 'exception-nologin-text',
index 8d57562..de5f410 100644 (file)
@@ -142,6 +142,20 @@ class JobQueueGroup {
                                $this->cache->clear( 'queues-ready' );
                        }
                }
+
+               $cache = ObjectCache::getLocalClusterInstance();
+               $cache->set(
+                       $cache->makeGlobalKey( 'jobqueue', $this->wiki, 'hasjobs', self::TYPE_ANY ),
+                       'true',
+                       15
+               );
+               if ( array_intersect( array_keys( $jobsByType ), $this->getDefaultQueueTypes() ) ) {
+                       $cache->set(
+                               $cache->makeGlobalKey( 'jobqueue', $this->wiki, 'hasjobs', self::TYPE_DEFAULT ),
+                               'true',
+                               15
+                       );
+               }
        }
 
        /**
@@ -298,8 +312,8 @@ class JobQueueGroup {
         * @since 1.23
         */
        public function queuesHaveJobs( $type = self::TYPE_ANY ) {
-               $key = wfMemcKey( 'jobqueue', 'queueshavejobs', $type );
                $cache = ObjectCache::getLocalClusterInstance();
+               $key = $cache->makeGlobalKey( 'jobqueue', $this->wiki, 'hasjobs', $type );
 
                $value = $cache->get( $key );
                if ( $value === false ) {
index 329bc23..5eafcb3 100644 (file)
@@ -20,6 +20,8 @@
  *
  * @file
  */
+use MediaWiki\MediaWikiServices;
+
 class PurgeJobUtils {
        /**
         * Invalidate the cache of a list of pages from a single namespace.
@@ -34,7 +36,9 @@ class PurgeJobUtils {
                        return;
                }
 
-               $dbw->onTransactionPreCommitOrIdle( function() use ( $dbw, $namespace, $dbkeys ) {
+               $dbw->onTransactionIdle( function() use ( $dbw, $namespace, $dbkeys ) {
+                       $services = MediaWikiServices::getInstance();
+                       $lbFactory = $services->getDBLoadBalancerFactory();
                        // Determine which pages need to be updated.
                        // This is necessary to prevent the job queue from smashing the DB with
                        // large numbers of concurrent invalidations of the same page.
@@ -50,22 +54,24 @@ class PurgeJobUtils {
                                __METHOD__
                        );
 
-                       if ( $ids === [] ) {
+                       if ( !$ids ) {
                                return;
                        }
 
-                       // Do the update.
-                       // We still need the page_touched condition, in case the row has changed since
-                       // the non-locking select above.
-                       $dbw->update(
-                               'page',
-                               [ 'page_touched' => $now ],
-                               [
-                                       'page_id' => $ids,
-                                       'page_touched < ' . $dbw->addQuotes( $now )
-                               ],
-                               __METHOD__
-                       );
+                       $batchSize = $services->getMainConfig()->get( 'UpdateRowsPerQuery' );
+                       $ticket = $lbFactory->getEmptyTransactionTicket( __METHOD__ );
+                       foreach ( array_chunk( $ids, $batchSize ) as $idBatch ) {
+                               $dbw->update(
+                                       'page',
+                                       [ 'page_touched' => $now ],
+                                       [
+                                               'page_id' => $idBatch,
+                                               'page_touched < ' . $dbw->addQuotes( $now ) // handle races
+                                       ],
+                                       __METHOD__
+                               );
+                               $lbFactory->commitAndWaitForReplication( __METHOD__, $ticket );
+                       }
                } );
        }
 }
index 3339eb3..969e86e 100644 (file)
@@ -34,6 +34,8 @@ class WaitConditionLoop {
        private $timeout;
        /** @var float Seconds */
        private $lastWaitTime;
+       /** @var integer|null */
+       private $rusageMode;
 
        const CONDITION_REACHED = 1;
        const CONDITION_CONTINUE = 0; // evaluates as falsey
@@ -50,6 +52,12 @@ class WaitConditionLoop {
                $this->condition = $condition;
                $this->timeout = $timeout;
                $this->busyCallbacks =& $busyCallbacks;
+
+               if ( defined( 'HHVM_VERSION' ) && PHP_OS === 'Linux' ) {
+                       $this->rusageMode = 2; // RUSAGE_THREAD
+               } elseif ( function_exists( 'getrusage' ) ) {
+                       $this->rusageMode = 0; // RUSAGE_SELF
+               }
        }
 
        /**
@@ -139,18 +147,14 @@ class WaitConditionLoop {
         * @return float Returns 0.0 if not supported (Windows on PHP < 7)
         */
        protected function getCpuTime() {
-               $time = 0.0;
-
-               if ( defined( 'HHVM_VERSION' ) && PHP_OS === 'Linux' ) {
-                       $ru = getrusage( 2 /* RUSAGE_THREAD */ );
-               } else {
-                       $ru = getrusage( 0 /* RUSAGE_SELF */ );
-               }
-               if ( $ru ) {
-                       $time += $ru['ru_utime.tv_sec'] + $ru['ru_utime.tv_usec'] / 1e6;
-                       $time += $ru['ru_stime.tv_sec'] + $ru['ru_stime.tv_usec'] / 1e6;
+               if ( $this->rusageMode === null ) {
+                       return microtime( true ); // assume worst case (all time is CPU)
                }
 
+               $ru = getrusage( $this->rusageMode );
+               $time = $ru['ru_utime.tv_sec'] + $ru['ru_utime.tv_usec'] / 1e6;
+               $time += $ru['ru_stime.tv_sec'] + $ru['ru_stime.tv_usec'] / 1e6;
+
                return $time;
        }
 
index 62c4fa5..0e09f16 100644 (file)
@@ -47,6 +47,12 @@ interface IExpiringStore {
        // Medium attributes constants related to emulation or media type
        const ATTR_EMULATION = 1;
        const QOS_EMULATION_SQL = 1;
+       // Medium attributes constants related to replica consistency
+       const ATTR_SYNCWRITES = 2; // SYNC_WRITES flag support
+       const QOS_SYNCWRITES_NONE = 1; // replication only supports eventual consistency or less
+       const QOS_SYNCWRITES_BE = 2; // best effort synchronous with limited retries
+       const QOS_SYNCWRITES_QC = 3; // write quorum applied directly to state machines where R+W > N
+       const QOS_SYNCWRITES_SS = 4; // strict-serializable, nodes refuse reads if possible stale
        // Generic "unknown" value that is useful for comparisons (e.g. always good enough)
        const QOS_UNKNOWN = INF;
 }
index 5967441..6973392 100644 (file)
@@ -30,6 +30,12 @@ class MemcachedBagOStuff extends BagOStuff {
        /** @var MemcachedClient|Memcached */
        protected $client;
 
+       function __construct( array $params ) {
+               parent::__construct( $params );
+
+               $this->attrMap[self::ATTR_SYNCWRITES] = self::QOS_SYNCWRITES_BE; // unreliable
+       }
+
        /**
         * Fill in some defaults for missing keys in $params.
         *
index 9fc3fe1..ae91be5 100644 (file)
@@ -51,6 +51,8 @@ class RESTBagOStuff extends BagOStuff {
                }
                // Make sure URL ends with /
                $this->url = rtrim( $params['url'], '/' ) . '/';
+               // Default config, R+W > N; no locks on reads though; writes go straight to state-machine
+               $this->attrMap[self::ATTR_SYNCWRITES] = self::QOS_SYNCWRITES_QC;
        }
 
        /**
index f9d201f..64cd686 100644 (file)
@@ -83,6 +83,8 @@ class RedisBagOStuff extends BagOStuff {
                } else {
                        $this->automaticFailover = true;
                }
+
+               $this->attrMap[self::ATTR_SYNCWRITES] = self::QOS_SYNCWRITES_NONE;
        }
 
        protected function doGet( $key, $flags = 0 ) {
index 3baae50..d06213f 100644 (file)
@@ -97,6 +97,7 @@ class SqlBagOStuff extends BagOStuff {
                parent::__construct( $params );
 
                $this->attrMap[self::ATTR_EMULATION] = self::QOS_EMULATION_SQL;
+               $this->attrMap[self::ATTR_SYNCWRITES] = self::QOS_SYNCWRITES_NONE;
 
                if ( isset( $params['servers'] ) ) {
                        $this->serverInfos = [];
@@ -119,6 +120,7 @@ class SqlBagOStuff extends BagOStuff {
                        // Default to using the main wiki's database servers
                        $this->serverInfos = false;
                        $this->numServers = 1;
+                       $this->attrMap[self::ATTR_SYNCWRITES] = self::QOS_SYNCWRITES_BE;
                }
                if ( isset( $params['purgePeriod'] ) ) {
                        $this->purgePeriod = intval( $params['purgePeriod'] );
index d5dfd3d..938f292 100644 (file)
@@ -1119,6 +1119,9 @@ class WikiPage implements Page, IDBAccessObject {
                }
 
                $this->mTitle->invalidateCache();
+
+               // Clear file cache
+               HTMLFileCache::clearFileCache( $this->getTitle() );
                // Send purge after above page_touched update was committed
                DeferredUpdates::addUpdate(
                        new CdnCacheUpdate( $this->mTitle->getCdnUrls() ),
index ad1ed49..97a86c3 100644 (file)
@@ -1273,9 +1273,9 @@ MESSAGE;
         * Values considered empty:
         *
         * - null
-        * - array()
+        * - []
         * - new XmlJsCode( '{}' )
-        * - new stdClass() // (object) array()
+        * - new stdClass() // (object) []
         *
         * @param Array $array
         */
index 574e535..2dcc841 100644 (file)
@@ -177,26 +177,26 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
         *         // Scripts to always include
         *         'scripts' => [file path string or array of file path strings],
         *         // Scripts to include in specific language contexts
-        *         'languageScripts' => array(
+        *         'languageScripts' => [
         *             [language code] => [file path string or array of file path strings],
-        *         ),
+        *         ],
         *         // Scripts to include in specific skin contexts
-        *         'skinScripts' => array(
+        *         'skinScripts' => [
         *             [skin name] => [file path string or array of file path strings],
-        *         ),
+        *         ],
         *         // Scripts to include in debug contexts
         *         'debugScripts' => [file path string or array of file path strings],
         *         // Modules which must be loaded before this module
         *         'dependencies' => [module name string or array of module name strings],
-        *         'templates' => array(
+        *         'templates' => [
         *             [template alias with file.ext] => [file path to a template file],
-        *         ),
+        *         ],
         *         // Styles to always load
         *         'styles' => [file path string or array of file path strings],
         *         // Styles to include in specific skin contexts
-        *         'skinStyles' => array(
+        *         'skinStyles' => [
         *             [skin name] => [file path string or array of file path strings],
-        *         ),
+        *         ],
         *         // Messages to always load
         *         'messages' => [array of message key strings],
         *         // Group which this module should be loaded together with
index 43327c9..6a8957e 100644 (file)
@@ -59,7 +59,7 @@ class ResourceLoaderImageModule extends ResourceLoaderModule {
         * Below is a description for the $options array:
         * @par Construction options:
         * @code
-        *     array(
+        *     [
         *         // Base path to prepend to all local paths in $options. Defaults to $IP
         *         'localBasePath' => [base path],
         *         // Path to JSON file that contains any of the settings below
@@ -72,33 +72,33 @@ class ResourceLoaderImageModule extends ResourceLoaderModule {
         *         'selectorWithoutVariant' => [CSS selector template, variables: {prefix} {name}],
         *         'selectorWithVariant' => [CSS selector template, variables: {prefix} {name} {variant}],
         *         // List of variants that may be used for the image files
-        *         'variants' => array(
-        *             [theme name] => array(
-        *                 [variant name] => array(
+        *         'variants' => [
+        *             [theme name] => [
+        *                 [variant name] => [
         *                     'color' => [color string, e.g. '#ffff00'],
         *                     'global' => [boolean, if true, this variant is available
         *                                  for all images of this type],
-        *                 ),
+        *                 ],
         *                 ...
-        *             ),
+        *             ],
         *             ...
-        *         ),
+        *         ],
         *         // List of image files and their options
-        *         'images' => array(
-        *             [theme name] => array(
-        *                 [icon name] => array(
+        *         'images' => [
+        *             [theme name] => [
+        *                 [icon name] => [
         *                     'file' => [file path string or array whose values are file path strings
         *                                    and whose keys are 'default', 'ltr', 'rtl', a single
         *                                    language code like 'en', or a list of language codes like
         *                                    'en,de,ar'],
         *                     'variants' => [array of variant name strings, variants
         *                                    available for this image],
-        *                 ),
+        *                 ],
         *                 ...
-        *             ),
+        *             ],
         *             ...
-        *         ),
-        *     )
+        *         ],
+        *     ]
         * @endcode
         * @throws InvalidArgumentException
         */
index 44f7e12..2351efd 100644 (file)
@@ -789,10 +789,10 @@ abstract class ResourceLoaderModule implements LoggerAwareInterface {
         *
         * @code
         *     $summary = parent::getDefinitionSummary( $context );
-        *     $summary[] = array(
+        *     $summary[] = [
         *         'foo' => 123,
         *         'bar' => 'quux',
-        *     );
+        *     ];
         *     return $summary;
         * @endcode
         *
index 71ca57b..2d37a0f 100644 (file)
@@ -320,10 +320,10 @@ 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'] = array(
+        *  $item['data'] = [
         *       'foo' => 1,
         *       'bar' => 'baz',
-        *  );
+        *  ];
         * will render as element properties:
         *  data-foo='1' data-bar='baz'
         *
@@ -333,7 +333,7 @@ abstract class BaseTemplate extends QuickTemplate {
         *   a link in. This should be an array of arrays containing a 'tag' and
         *   optionally an 'attributes' key. If you only have one element you don't
         *   need to wrap it in another array. eg: To use <a><span>...</span></a>
-        *   in all links use array( 'text-wrapper' => array( 'tag' => 'span' ) )
+        *   in all links use [ 'text-wrapper' => [ 'tag' => 'span' ] ]
         *   for your options.
         *   - 'link-class' key can be used to specify additional classes to apply
         *   to all links.
index 6b4acfa..b60aa10 100644 (file)
@@ -1169,7 +1169,7 @@ abstract class Skin extends ContextSource {
         *
         * BaseTemplate::getSidebar can be used to simplify the format and id generation in new skins.
         *
-        * The format of the returned array is array( heading => content, ... ), where:
+        * The format of the returned array is [ heading => content, ... ], where:
         * - heading is the heading of a navigation portlet. It is either:
         *   - magic string to be handled by the skins ('SEARCH' / 'LANGUAGES' / 'TOOLBOX' / ...)
         *   - a message name (e.g. 'navigation'), the message should be HTML-escaped by the skin
index 61ab642..9975e41 100644 (file)
@@ -290,9 +290,7 @@ class SpecialBotPasswords extends FormSpecialPage {
                ] );
 
                if ( $this->operation === 'insert' || !empty( $data['resetPassword'] ) ) {
-                       $this->password = PasswordFactory::generateRandomPasswordString(
-                               max( 32, $this->getConfig()->get( 'MinimalPasswordLength' ) )
-                       );
+                       $this->password = BotPassword::generatePassword( $this->getConfig() );
                        $passwordFactory = new PasswordFactory();
                        $passwordFactory->init( RequestContext::getMain()->getConfig() );
                        $password = $passwordFactory->newFromPlaintext( $this->password );
@@ -335,7 +333,9 @@ class SpecialBotPasswords extends FormSpecialPage {
                        $out->addWikiMsg(
                                'botpasswords-newpassword',
                                htmlspecialchars( $username . $sep . $this->par ),
-                               htmlspecialchars( $this->password )
+                               htmlspecialchars( $this->password ),
+                               htmlspecialchars( $username ),
+                               htmlspecialchars( $this->par . $sep . $this->password )
                        );
                        $this->password = null;
                }
index 4ce3cde..0bbe12e 100644 (file)
@@ -388,6 +388,44 @@ class BotPassword implements IDBAccessObject {
                return (bool)$dbw->affectedRows();
        }
 
+       /**
+        * Returns a (raw, unhashed) random password string.
+        * @param Config $config
+        * @return string
+        */
+       public static function generatePassword( $config ) {
+               return PasswordFactory::generateRandomPasswordString(
+                       max( 32, $config->get( 'MinimalPasswordLength' ) ) );
+       }
+
+       /**
+        * There are two ways to login with a bot password: "username@appId", "password" and
+        * "username", "appId@password". Transform it so it is always in the first form.
+        * Returns [bot username, bot password, could be normal password?] where the last one is a flag
+        * meaning this could either be a bot password or a normal password, it cannot be decided for
+        * certain (although in such cases it almost always will be a bot password).
+        * If this cannot be a bot password login just return false.
+        * @param string $username
+        * @param string $password
+        * @return array|false
+        */
+       public static function canonicalizeLoginData( $username, $password ) {
+               $sep = BotPassword::getSeparator();
+               if ( strpos( $username, $sep ) !== false ) {
+                       // the separator is not valid in usernames so this must be a bot login
+                       return [ $username, $password, false ];
+               } elseif ( strlen( $password ) > 32 && strpos( $password, $sep ) !== false ) {
+                       // the strlen check helps minimize the password information obtainable from timing
+                       $segments = explode( $sep, $password );
+                       $password = array_pop( $segments );
+                       $appId = implode( $sep, $segments );
+                       if ( preg_match( '/^[0-9a-w]{32,}$/', $password ) ) {
+                               return [ $username . $sep . $appId, $password, true ];
+                       }
+               }
+               return false;
+       }
+
        /**
         * Try to log the user in
         * @param string $username Combined user name and app ID
index bd2a0a3..9a766c9 100644 (file)
        "createacct-yourpasswordagain-ph": "Escriba nuevamente la contraseña",
        "userlogin-remembermypassword": "Caltener abierta la sesión",
        "userlogin-signwithsecure": "Usar una conexón segura",
+       "cannotlogin-title": "Nun pudo aniciase sesión",
+       "cannotlogin-text": "Nun ye posible aniciar sesión.",
        "cannotloginnow-title": "Nun puede aniciase sesión agora",
        "cannotloginnow-text": "Nun puede aniciase sesión cuando s'usa $1.",
+       "cannotcreateaccount-title": "Nun pueden crease cuentes",
+       "cannotcreateaccount-text": "La creación direuta de cuentes nun ta activada nesta wiki.",
        "yourdomainname": "El to dominiu:",
        "password-change-forbidden": "Nun se pueden camudar les contraseñes nesta wiki.",
        "externaldberror": "O hebo un fallu d'autenticación de la base de datos o nun tienes permisu p'anovar la to cuenta esterna.",
        "pageinfo-article-id": "ID de la páxina",
        "pageinfo-language": "Llingua del conteníu de la páxina",
        "pageinfo-content-model": "Plantía del conteníu de la páxina",
+       "pageinfo-content-model-change": "camudar",
        "pageinfo-robot-policy": "Indexación por robots",
        "pageinfo-robot-index": "Permitío",
        "pageinfo-robot-noindex": "Torgao",
index 69ff42b..89591c2 100644 (file)
        "createacct-yourpasswordagain-ph": "Увядзіце пароль зноў",
        "userlogin-remembermypassword": "Запомніць мяне",
        "userlogin-signwithsecure": "Скарыстацца бясьпечным злучэньнем",
+       "cannotlogin-title": "Немагчыма ўвайсьці",
+       "cannotlogin-text": "Уваход у сыстэму немагчымы.",
        "cannotloginnow-title": "Цяпер немагчыма ўвайсьці",
        "cannotloginnow-text": "Уваход у сыстэму немагчымы пры выкарыстаньні $1.",
        "yourdomainname": "Ваш дамэн:",
index a30b31a..55f2e8a 100644 (file)
        "createacct-yourpasswordagain-ph": "Увядзіце пароль яшчэ раз",
        "userlogin-remembermypassword": "Заставацца ў сістэме",
        "userlogin-signwithsecure": "Выкарыстоўваць абароненае злучэнне",
+       "cannotlogin-title": "Немагчыма ўвайсці",
+       "cannotlogin-text": "Уваход у сістэму немагчымы.",
        "cannotloginnow-title": "Зараз немагчыма ўвайсці",
        "cannotloginnow-text": "Пры выкарыстанні $1 немагчыма прадставіцца сістэме.",
+       "cannotcreateaccount-title": "Немагчыма стварыць уліковыя запісы",
+       "cannotcreateaccount-text": "Непасрэднае стварэнне ўліковых запісаў не ўключана на гэтай вікі.",
        "yourdomainname": "Ваш дамен:",
        "password-change-forbidden": "Вы не можаце змяняць паролі на гэтай Вікі.",
        "externaldberror": "Або памылка вонкавай аўтэнтыкацыі ў базе дадзеных, або вам не дазволена абнаўляць свой вонкавы рахунак.",
        "uploaded-href-attribute-svg": "у SVG файлах атрыбутам href дазволены толькі мэты віду http:// або https://, знойдзена <code>&lt;$1 $2=\"$3\"&gt;</code>.",
        "uploaded-href-unsafe-target-svg": "У ўкладзеным SVG файле знойдзена спасылка на небяспечныя звесткі: URI мэты <code>&lt;$1 $2=\"$3\"&gt;</code>.",
        "uploaded-animate-svg": "У ўкладзеным SVG файле знойдзены тэг \"animate\", здольны змяніць спасылку з дапамогай атрыбута \"from\" <code>&lt;$1 $2=\"$3\"&gt;</code>.",
+       "uploaded-setting-event-handler-svg": "Устаноўка атрыбутаў апрацоўкі падзей заблакавана, у ўкладзеным SVG-файле знойдзены код <code>&lt;$1 $2=\"$3\"&gt;</code>.",
+       "uploaded-setting-href-svg": "Выкарыстанне тэга \"set\" для дадання атрыбута \"href\" у бацькоўскі элемент заблакавана.",
        "uploadscriptednamespace": "Гэты файл SVG утрымлівае недапушчальную прастору імёнаў \"$1\".",
        "uploadinvalidxml": "Немагчыма прааналізаваць XML ва ўкладзеным файле.",
        "uploadvirus": "Файл утрымлівае вірус! Падрабязнасці: $1",
index c6b47a7..059cb7b 100644 (file)
        "whatlinkshere-next": "{{PLURAL:$1|دیکە|$1ی تر}}",
        "whatlinkshere-links": "← بەستەرەکان",
        "whatlinkshere-hideredirs": "ڕەوانەکەرەکان $1",
-       "whatlinkshere-hidetrans": "$1 ھێنانەناوەوەکان",
+       "whatlinkshere-hidetrans": "ھێنانەناوەوەکان $1",
        "whatlinkshere-hidelinks": "$1 بەستەر",
        "whatlinkshere-hideimages": "$1 بەستەرەکانی پەڕگە",
        "whatlinkshere-filters": "پاڵێوکەکان",
index 4de026d..9647503 100644 (file)
        "tooltip-pt-login": "Mayê şıma ronıştış akerdışi rê dawet keme; labelê ronıştış mecburi niyo",
        "tooltip-pt-logout": "Bıveciye",
        "tooltip-pt-createaccount": "Şıma rê tewsiyey ma xorê jew hesab akerê. Fına zi hesab akerdış mecburi niyo.",
-       "tooltip-ca-talk": "Heqa zerrekê pele de werênayış",
+       "tooltip-ca-talk": "Heqa zerreka perrer vaten",
        "tooltip-ca-edit": "Ena pele bıvurne",
        "tooltip-ca-addsection": "Zu bınnusteya newi ak",
        "tooltip-ca-viewsource": "Ena pele kılit biya.\nŞıma şenê çımeyê aye bıvênê",
index 61ae466..7dd59f3 100644 (file)
        "createacct-yourpasswordagain-ph": "आँजि पासवर्ड भरऽ",
        "userlogin-remembermypassword": "मुलाई अघाडी झान्या काम गराइराख्या",
        "userlogin-signwithsecure": "सुक्षित जडान प्रयोग गद्द्या",
-       "cannotlogin-title": "लà¤\97à¤\87न à¤\85रिसà¤\95à¥\8dदà¥\88न",
+       "cannotlogin-title": "à¤\85à¤\88ल à¤­à¤¿à¤¤à¤° à¤\9dान à¤¨à¤¾à¤\87à¤\81 à¤ªà¤¾à¤\88नà¥\8b",
        "cannotlogin-text": "येइमी लगइन सम्भव नाइथिन।",
        "cannotloginnow-title": "अईल भितर झान नाइँ पाईनो",
        "cannotloginnow-text": "भितर जान असंभव छ जब प्रयोग $1|",
        "userrights-groupsmember": "को सदस्य:",
        "userrights-groupsmember-auto": "अंतर्निहित सदस्य:",
        "userrights-reason": "कारण:",
+       "userrights-changeable-col": "तमले परिवर्तन गद्द सक्दया समूहअन",
        "userrights-unchangeable-col": "तमीले परिवर्तन गद्द नसक्ने समूहहरू",
        "userrights-conflict": "प्रयोगकर्ताको अधिकार परिवर्तनमी मतभेद भयो ! कृपया तमरो परिवर्तन पुनरावलोकन तथा पुष्टि गर ।",
        "userrights-removed-self": "तमले सफलतापूर्वक आफनो अधिकारहरूलाई मेटाया । त्यै कारण तम आब यो पानो हेद्द नाइसक्दा ।",
index ad2b6b6..558a452 100644 (file)
        "botpasswords-updated-body": "The bot password for bot name \"$1\" of user \"$2\" was updated.",
        "botpasswords-deleted-title": "Bot password deleted",
        "botpasswords-deleted-body": "The bot password for bot name \"$1\" of user \"$2\" was deleted.",
-       "botpasswords-newpassword": "The new password to log in with <strong>$1</strong> is <strong>$2</strong>. <em>Please record this for future reference.</em>",
+       "botpasswords-newpassword": "The new password to log in with <strong>$1</strong> is <strong>$2</strong>. <em>Please record this for future reference.</em> <br> (For old bots which require the login name to be the same as the eventual username, you can also use <strong>$3</strong> as username and <strong>$4</strong> as password.)",
        "botpasswords-no-provider": "BotPasswordsSessionProvider is not available.",
        "botpasswords-restriction-failed": "Bot password restrictions prevent this login.",
        "botpasswords-invalid-name": "The username specified does not contain the bot password separator (\"$1\").",
index 2da1d16..8f93619 100644 (file)
        "ok": "Bone",
        "retrievedfrom": "Elŝutita el  \"$1\"",
        "youhavenewmessages": "{{PLURAL:$3|Vi havas}} $1 ($2).",
-       "youhavenewmessagesfromusers": "Riceviĝis $1 de {{PLURAL:$3|alia uzanto|$3 uzantoj}} ($2).\n\nVi havas $1 de {{PLURAL:$3|alia uzanto|$3 uzantoj}} ($2).",
+       "youhavenewmessagesfromusers": "Vi havas {{PLURAL:$1|mesaĝon|$1 mesaĝojn}} de {{PLURAL:$3|alia uzanto|$3 uzantoj}} ($2).",
        "youhavenewmessagesmanyusers": "Riceviĝis $1 de multaj uzantoj ($2).",
        "newmessageslinkplural": "{{PLURAL:$1|nova mesaĝo|999=novaj mesaĝoj}}",
        "newmessagesdifflinkplural": "$1 {{PLURAL:$1|ŝanĝo|ŝanĝoj}}",
        "createacct-yourpasswordagain-ph": "Retajpu pasvorton",
        "userlogin-remembermypassword": "Memori mian ensaluton",
        "userlogin-signwithsecure": "Uzu sekurigitan konekton",
+       "cannotlogin-title": "Ne eblas ensaluti",
+       "cannotlogin-text": "Ensaluto estas neebla.",
        "cannotloginnow-title": "Nuntempe ne eblas ensaluti",
        "cannotloginnow-text": "Ne eblas ensaluti dum uzado de $1.",
+       "cannotcreateaccount-title": "Ne eblas krei konton",
+       "cannotcreateaccount-text": "Senpera kreo de uzantokonto ne estas enŝaltita en ĉi tiu vikio.",
        "yourdomainname": "Via domajno",
        "password-change-forbidden": "Ve ne povas ŝanĝi pasvortojn en ĉi tiu vikio.",
        "externaldberror": "Aŭ estis datenbaza eraro rilate al ekstera aŭtentikigado, aŭ vi ne rajtas ĝisdatigi vian eksteran konton.",
        "action-applychangetags": "aldoni etikedojn al viaj propraj ŝanĝoj",
        "action-changetags": "aldoni kaj forigi arbitrajn etikedojn ĉe unuopaj revizioj kaj protokoleroj",
        "action-deletechangetags": "Forigi etikedojn de la datenbazo.",
+       "action-purge": "malplenigi servilan kaŝmemoron",
        "nchanges": "$1 {{PLURAL:$1|ŝanĝo|ŝanĝoj}}",
        "enhancedrc-since-last-visit": "$1 {{PLURAL:$1|ekde lasta vizito}}",
        "enhancedrc-history": "historio",
        "file-thumbnail-no": "La dosiernomo komencas kun <strong>$1</strong>.\nĜi ŝajnas kiel bildo de malgrandigita grandeco ''(thumbnail)''.\nSe vi havas ĉi tiun bildon en plena distingivo, alŝutu ĉi tiun, alikaze bonvolu ŝanĝi la dosieran nomon.",
        "fileexists-forbidden": "Dosiero kun ĉi tiu nomo jam ekzistas kaj ne povas anstataŭigi ĝin.\nSe vi ankoraŭ volas alŝuti vian dosieron, bonvolu reprovi kun nova nomo.\n[[File:$1|thumb|center|$1]]",
        "fileexists-shared-forbidden": "Dosiero kun ĉi tia nomo jam ekzistas en la komuna dosierujo.\nSe vi ankoraŭ volas alŝuti vian dosieron, bonvolu retroigi kaj uzi novan nomon.[[File:$1|thumb|center|$1]]",
+       "fileexists-no-change": "La alŝutaĵo estas preciza kopio de la nuna versio de <strong>[[:$1]]</strong>.",
+       "fileexists-duplicate-version": "La alŝutaĵo estas preciza kopio de {{PLURAL:$2|malnova versio|malnovaj versioj}} de <strong>[[:$1]]</strong>.",
        "file-exists-duplicate": "Ĉi tiu dosiero estas duplikato de la {{PLURAL:$1|jena dosiero|jenaj dosieroj}}:",
        "file-deleted-duplicate": "Duplikata dosiero de ĉi tiu dosiero ([[:$1]]) estis antaŭe forigita. Vi legu la forigan historion de tiu dosiero antaŭ provi realŝuti ĝin.",
        "file-deleted-duplicate-notitle": "Dosiero identa al ĉi tiu dosiero estis forigita antaŭ nelonge kaj la titolo estis subpremita.\nVi demandu iun, kiu havas la eblecon, rigardi la subpremitajn dosierajn datojn, por kontroli la situacion antaŭ rea alŝutado.",
        "upload-http-error": "HTTP-eraro okazis: $1",
        "upload-copy-upload-invalid-domain": "Kopio-alŝutoj ne disponiĝas el ĉi tiu domajno.",
        "upload-foreign-cant-upload": "Tiu vikio ne estas agorita por alŝuti alŝutitan dosieron al la petita fora dosierdeponejo.",
-       "upload-foreign-cant-load-config": "La ŝarĝado de agordo pri dosieran alŝuton malsukcesis por la fora dosiera deponejo.",
+       "upload-foreign-cant-load-config": "Malsukcesis ŝargi la agordon por dosier-alŝutoj al ekstera dosier-deponejo.",
        "upload-dialog-disabled": "Alŝutoj de dosiero per ĉi tiun dialogon estas malfunkciigita sur ĉi tiu vikio.",
        "upload-dialog-title": "Alŝuti dosieron",
        "upload-dialog-button-cancel": "Nuligi",
index dfab8c9..1e5f0df 100644 (file)
        "createacct-yourpasswordagain-ph": "Repite la contraseña",
        "userlogin-remembermypassword": "Mantener mi sesión iniciada",
        "userlogin-signwithsecure": "Usar conexión segura",
+       "cannotlogin-title": "No se puede iniciar sesión",
        "cannotloginnow-title": "No se puede iniciar sesión ahora",
        "cannotloginnow-text": "No se puede iniciar sesión cuando se usa $1.",
+       "cannotcreateaccount-title": "No se pueden crear cuentas",
+       "cannotcreateaccount-text": "La creación directa de cuentas no está activada en este wiki.",
        "yourdomainname": "Tu dominio:",
        "password-change-forbidden": "No puedes cambiar las contraseñas en este wiki.",
        "externaldberror": "Hubo un error de autenticación en la base de datos, o bien no tienes autorización para actualizar tu cuenta externa.",
        "pageinfo-article-id": "Identificador de la página",
        "pageinfo-language": "Idioma de la página",
        "pageinfo-content-model": "Modelo de contenido de la página",
+       "pageinfo-content-model-change": "cambiar",
        "pageinfo-robot-policy": "Indización por robots",
        "pageinfo-robot-index": "Permitido",
        "pageinfo-robot-noindex": "No permitido",
index 7bce27b..4086700 100644 (file)
                        "Matma Rex",
                        "Dcausse",
                        "Lucas",
-                       "Mabroukb"
+                       "Mabroukb",
+                       "Pymouss"
                ]
        },
        "tog-underline": "Soulignement des liens :",
        "prefs-emailconfirm-label": "Confirmation du courriel :",
        "youremail": "Courriel :",
        "username": "{{GENDER:$1|Nom d'utilisateur|Nom d'utilisatrice}} :",
-       "prefs-memberingroups": "{{GENDER:$2|Membre}} {{PLURAL:$1|du groupe|des groupes}} :",
+       "prefs-memberingroups": "{{GENDER:$2|Membre}} {{PLURAL:$1|du groupe|des groupes}}:",
        "prefs-registration": "Date d'inscription :",
        "yourrealname": "Nom réel :",
        "yourlanguage": "Langue :",
index 2a93de8..74d60e9 100644 (file)
        "october-gen": "10월",
        "november-gen": "11월",
        "december-gen": "12월",
-       "jan": "1",
-       "feb": "2",
-       "mar": "3",
-       "apr": "4",
-       "may": "5",
-       "jun": "6",
-       "jul": "7",
-       "aug": "8",
-       "sep": "9",
-       "oct": "10",
-       "nov": "11",
-       "dec": "12",
+       "jan": "1",
+       "feb": "2",
+       "mar": "3",
+       "apr": "4",
+       "may": "5",
+       "jun": "6",
+       "jul": "7",
+       "aug": "8",
+       "sep": "9",
+       "oct": "10",
+       "nov": "11",
+       "dec": "12",
        "january-date": "1월 $1일",
        "february-date": "2월 $1일",
        "march-date": "3월 $1일",
index 5781171..6041095 100644 (file)
        "cannotloginnow-title": "Aloggen ass elo net méiglech",
        "cannotloginnow-text": "Aloggen ass net méiglech wann dir $1 benotzt.",
        "cannotcreateaccount-title": "Benotzerkont kënnen net opgemaach ginn",
+       "cannotcreateaccount-text": "D'direkt Uleeë vu Benotzerkonten ass an dëser Wiki net aktivéiert.",
        "yourdomainname": "Ären Domän:",
        "password-change-forbidden": "Dir däerft op dëser Wiki Passwierder net änneren.",
        "externaldberror": "Entweder ass e Feeler bei der externer Authentifizéierung geschitt, oder Dir däerft Ären externe Benotzerkont net aktualiséieren.",
        "tags-edit-revision-submit": "Ännerungen op {{PLURAL:$1|dës Versioun|$1 Versiounen}} uwennen",
        "tags-edit-success": "D'Ännerunge goufen applizéiert.",
        "tags-edit-failure": "D'Ännerunge konnten net applizéiert ginn: $1",
+       "tags-edit-nooldid-title": "Net-valabel Zilversioun",
        "tags-edit-none-selected": "Sicht mindestens eng Markéierung eraus déi dir dobäisetzen oder ewechhuele wëllt.",
        "comparepages": "Säite vergläichen",
        "compare-page1": "Säit 1",
index 8633f4f..f3cf09b 100644 (file)
        "createacct-yourpasswordagain-ph": "Įveskite slaptažodį dar kartą",
        "userlogin-remembermypassword": "Įsiminti mane",
        "userlogin-signwithsecure": "Naudoti saugią jungtį",
+       "cannotlogin-title": "Negalima prisijungti",
+       "cannotlogin-text": "Prisijungti neįmanoma.",
        "cannotloginnow-title": "Dabar negalima prisijungti",
        "cannotloginnow-text": "Prisijungimas negalimas, kai naudojama $1.",
+       "cannotcreateaccount-title": "Negali kurti paskyrų",
+       "cannotcreateaccount-text": "Tiesioginis paskyros kūrimas nėra įgalintas šioje viki.",
        "yourdomainname": "Jūsų domenas:",
        "password-change-forbidden": "Jus negalite keisti slaptažodžių šioje wiki.",
        "externaldberror": "Yra arba išorinė autorizacijos duomenų bazės klaida arba jums neleidžiama atnaujinti jūsų išorinės paskyros.",
        "file-thumbnail-no": "Failo pavadinimas prasideda  <strong>$1</strong>.\nAtrodo, kad yra sumažinto dydžio paveikslėlis ''(miniatiūra)''.\nJei jūs turite šį paveisklėlį pilna raiška, įkelkite šitą, priešingu atveju prašome pakeisti failo pavadinimą.",
        "fileexists-forbidden": "Failas tokiu pačiu vardu jau egzistuoja ir negali būti perrašytas;\nprašome eiti atgal ir įkelti šį failą kitu vardu. [[File:$1|thumb|center|$1]]",
        "fileexists-shared-forbidden": "Failas tokiu vardu jau egzistuoja bendrojoje failų saugykloje;\nJei visvien norite įkelti savo failą, prašome eiti atgal ir įkelti šį failą kitu vardu. [[File:$1|thumb|center|$1]]",
+       "fileexists-no-change": "Įkėlimas yra <strong>[[:$1]]</strong> dabartinės versijos tikslus dublikatas.",
+       "fileexists-duplicate-version": "Įkėlimas yra <strong>[[:$1]]</strong> {{PLURAL:$2|senesnės versijos|senesnių versijų}} tikslus dublikatas.",
        "file-exists-duplicate": "Šis failas yra {{PLURAL:$1|šio failo|šių failų}} dublikatas:",
        "file-deleted-duplicate": "Failas, identiškas šiam failui ([[:$1]]), seniau buvo ištrintas. Prieš įkeldami jį vėl patikrinkite šio failo ištrynimo istoriją.",
        "file-deleted-duplicate-notitle": "Rinkmena, visiškai atitinkanti šią, anksčiau buvo ištrinta, o jos pavadinimas uždraustas. Jums reiktų paprašyti kieno nors, turinčio galimybę peržiūrėti uždraustą rinkmeną, kad jis išaiškintų padėtį, prieš bandant vėl kelti rinkmeną.",
        "filerevert-submit": "Grąžinti",
        "filerevert-success": "<span class=\"plainlinks\">'''[[Media:$1|$1]]''' buvo sugrąžintas į versiją $4 ($2, $3).</span>",
        "filerevert-badversion": "Nėra jokių ankstesnių vietinių šio failo versijų su pateiktu laiku.",
+       "filerevert-identical": "Dabartinė failo versija jau yra identiška pasirinktajai.",
        "filedelete": "Trinti $1",
        "filedelete-legend": "Trinti rinkmeną",
        "filedelete-intro": "Jūs ketinate ištrinti failą '''[[Media:$1|$1]]''' su visa istorija.",
        "pageinfo-article-id": "Puslapio ID",
        "pageinfo-language": "Puslapio turinio kalba",
        "pageinfo-content-model": "Puslapio turinio modelis",
+       "pageinfo-content-model-change": "keisti",
        "pageinfo-robot-policy": "Robotų indeksavimas",
        "pageinfo-robot-index": "Leidžiama",
        "pageinfo-robot-noindex": "Neleidžiama",
index bf9f083..c72ae68 100644 (file)
        "backend-fail-delete": "Bô-hoat-tō· kā tóng-àn \"$1\" thâi tiāu",
        "license": "Siū-khoân:",
        "license-header": "Siū-khoân",
+       "imgfile": "tóng-àn",
        "listfiles": "Iáⁿ-siōng lia̍t-toaⁿ",
        "listfiles_date": "Ji̍t-kî",
        "listfiles_name": "Miâ",
index 77ca16e..58b6b3d 100644 (file)
        "withoutinterwiki-legend": "Prefiks",
        "withoutinterwiki-submit": "Vis",
        "fewestrevisions": "Sidene med færrast endringar",
-       "nbytes": "$1 {{PLURAL:$1|byte|byte}}",
+       "nbytes": "$1 {{PLURAL:$1|byte}}",
        "ncategories": "$1 {{PLURAL:$1|kategori|kategoriar}}",
        "ninterwikis": "{{PLURAL:$1|éin interwiki|$1 interwikiar}}",
        "nlinks": "{{PLURAL:$1|Éi lenkje|$1 lenkjer}}",
index 6cef6a0..c66b261 100644 (file)
        "cannotloginnow-title": "W tej chwili nie można się teraz zalogować",
        "cannotloginnow-text": "Podczas korzystania z $1 nie można się zalogować.",
        "cannotcreateaccount-title": "Nie można utworzyć kont",
+       "cannotcreateaccount-text": "Bezpośrednie tworzenie konta nie jest włączone na tej wiki.",
        "yourdomainname": "Twoja domena:",
        "password-change-forbidden": "Nie można zmieniać haseł na tej wiki.",
        "externaldberror": "Wystąpił błąd autentyfikacyjnej bazy danych lub nie posiadasz uprawnień koniecznych do aktualizacji zewnętrznego konta.",
        "file-thumbnail-no": "Nazwa pliku zaczyna się od <strong>$1</strong>.\nWydaje się, że jest to pomniejszona grafika ''(miniaturka)''.\nJeśli posiadasz tę grafikę w pełnym rozmiarze – prześlij ją. Jeśli chcesz wysłać tę – zmień nazwę przesyłanego obecnie pliku.",
        "fileexists-forbidden": "Plik o tej nazwie już istnieje i nie może zostać nadpisany.\nJeśli chcesz przesłać plik cofnij się i prześlij go pod inną nazwą. [[File:$1|thumb|center|$1]]",
        "fileexists-shared-forbidden": "Plik o tej nazwie już istnieje we współdzielonym repozytorium plików.\nCofnij się i załaduj plik pod inną nazwą. [[File:$1|thumb|center|$1]]",
+       "fileexists-duplicate-version": "{{PLURAL:$2|Przesłany plik jest dokładną kopią starszej wersji pliku|Przesłane pliki są dokładnymi kopiami starszych wersji plików}} <strong>[[:$1]]</strong>.",
        "file-exists-duplicate": "Ten plik jest kopią {{PLURAL:$1|pliku|następujących plików}}:",
        "file-deleted-duplicate": "Identyczny do tego plik ([[:$1]]) został wcześniej usunięty.\nSprawdź historię usunięć tamtego pliku zanim prześlesz go ponownie.",
        "file-deleted-duplicate-notitle": "Plik jest identyczny z plikiem, który został wcześniej usunięty, a jego nazwa została ukryta. Należy poprosić kogoś z możliwością przeglądania ukrytych danych, aby przeanalizował sytuację przed przystąpieniem do jego ponownego przesłania.",
        "undeletedrevisions": "odtworzono {{PLURAL:$1|1 wersję|$1 wersje|$1 wersji}}",
        "undeletedrevisions-files": "odtworzono $1 {{PLURAL:$1|wersję|wersje|wersji}} i $2 {{PLURAL:$2|plik|pliki|plików}}",
        "undeletedfiles": "odtworzył $1 {{PLURAL:$1|plik|pliki|plików}}",
-       "cannotundelete": "Odtworzenie nie powiodło się:\n$1",
+       "cannotundelete": "Niektóre lub wszystkie odtworzenia nie powiodły się:\n$1",
        "undeletedpage": "'''Odtworzono stronę $1.'''\n\nZobacz [[Special:Log/delete|rejestr usunięć]], jeśli chcesz przejrzeć ostatnie operacje usuwania i odtwarzania stron.",
        "undelete-header": "Zobacz [[Special:Log/delete|rejestr usunięć]], aby sprawdzić ostatnio usunięte strony.",
        "undelete-search-title": "Przeszukiwanie usuniętych stron",
        "pageinfo-article-id": "Identyfikator strony",
        "pageinfo-language": "Język zawartości strony",
        "pageinfo-content-model": "Model zawartości",
+       "pageinfo-content-model-change": "zmień",
        "pageinfo-robot-policy": "Indeksowanie przez roboty",
        "pageinfo-robot-index": "Dozwolone",
        "pageinfo-robot-noindex": "Niedozwolone",
index 21334a4..0f214ff 100644 (file)
@@ -71,7 +71,8 @@
                        "Josep Maria Roca Peña",
                        "Luan",
                        "Gato Preto",
-                       "Jdforrester"
+                       "Jdforrester",
+                       "Mansil"
                ]
        },
        "tog-underline": "Sublinhar ligações:",
        "botpasswords-newpassword": "A nova palavra-passe para iniciar sessão com <strong>$1</strong> é <strong>$2</strong>. Por favor, recorde-se dela para futura referência.</em>",
        "botpasswords-no-provider": "BotPasswordsSessionProvider não está disponível.",
        "botpasswords-restriction-failed": "Restrições de senha de robô evitam esta autenticação.",
-       "botpasswords-invalid-name": "O nome de usuário especificado não contém o separador de senha de robô (\"$1\").",
+       "botpasswords-invalid-name": "O nome de utilizador especificado não contém o separador de palavra-passe de robô (\"$1\").",
        "botpasswords-not-exist": "O usuário \"$1\" não possui uma senha de robô \"$2\".",
        "resetpass_forbidden": "Não é possível alterar palavras-passe",
        "resetpass_forbidden-reason": "As palavras-passe não podem ser alteradas: $1",
        "passwordreset-emailerror-capture2": "O envio do correio {{GENDER:$2|ao usuário|à usuária}} falhou: $1 {{PLURAL:$3|O nome de usuário e senha são mostradas abaixo|A lista de nomes de usuários e senhas é mostrada abaixo}}.",
        "passwordreset-nocaller": "Um interlocutor deve ser fornecido",
        "passwordreset-nosuchcaller": "A pessoa que chama não existe: $1",
-       "passwordreset-ignored": "A redefinição de senha não foi realizada. Talvez o provedor não tenha sido configurado, sim?",
+       "passwordreset-ignored": "A reposição de palavra-passe não foi realizada. Talvez não tenha sido configurado o provedor?",
        "passwordreset-invalideamil": "Correio eletrónico inválido",
        "passwordreset-nodata": "Não foram fornecidos nome de utilizador(a) nem endereço de correio eletrónico",
        "changeemail": "Alterar ou remover o endereço de correio eletrónico",
        "apisandbox-loading-results": "A receber resultados da API...",
        "apisandbox-request-url-label": "URL do pedido:",
        "apisandbox-request-time": "Tempo de processamento: {{PLURAL:$1|$1 ms}}",
-       "apisandbox-results-fixtoken": "Corrija o identificador e envie-o novamente",
-       "apisandbox-results-fixtoken-fail": "Não foi possível recuperar o identificador \"$1\".",
+       "apisandbox-results-fixtoken": "Corrija o identificador e volte a submete-lo",
+       "apisandbox-results-fixtoken-fail": "Não foi possível obter o identificador \"$1\".",
        "apisandbox-alert-page": "Os campos nesta página não são válidos.",
        "apisandbox-alert-field": "O valor deste campo não é válido.",
        "booksources": "Fontes bibliográficas",
        "authpage-cannot-login-continue": "Não é possível continuar a iniciar sessão. A sua sessão pode ter expirado.",
        "authpage-cannot-create": "Não é possível iniciar a criação da conta.",
        "authpage-cannot-create-continue": "Não é possível continuar a criação da conta. A sua sessão pode ter expirado.",
-       "authpage-cannot-link": "Não se pode iniciar a vinculação da conta.",
+       "authpage-cannot-link": "Não é possível iniciar a associação da conta.",
        "authpage-cannot-link-continue": "Não é possível continuar a criação da conta. A sua sessão pode ter expirado.",
        "cannotauth-not-allowed-title": "Permissão negada",
        "cannotauth-not-allowed": "Não possui permissão para utilizar esta página",
index 023af97..bd8a904 100644 (file)
        "botpasswords-updated-body": "Success message when a bot password is updated. Parameters:\n* $1 - Bot name\n* $2 - User name",
        "botpasswords-deleted-title": "Title of the success page when a bot password is deleted.",
        "botpasswords-deleted-body": "Success message when a bot password is deleted. Parameters:\n* $1 - Bot name\n* $2 - User name",
-       "botpasswords-newpassword": "Success message to display the new password when a bot password is created or updated. Parameters:\n* $1 - User name to be used for login.\n* $2 - Password to be used for login.",
+       "botpasswords-newpassword": "Success message to display the new password when a bot password is created or updated. Parameters:\n* $1 - User name to be used for login.\n* $2 - Password to be used for login.\n* $3, $4 - an alternative version of the user name and password, respectively, which is less preferred, but more compatible with old bots.",
        "botpasswords-no-provider": "Error message when login is attempted but the BotPasswordsSessionProvider is not included in <code>$wgSessionProviders</code>.",
        "botpasswords-restriction-failed": "Error message when login is rejected because the configured restrictions were not satisfied.",
        "botpasswords-invalid-name": "Error message when a username lacking the separator character is passed to BotPassword. Parameters:\n* $1 - The separator character.",
index 5331c26..3ea3fc4 100644 (file)
        "createacct-yourpasswordagain-ph": "Ange lösenordet igen",
        "userlogin-remembermypassword": "Håll mig inloggad",
        "userlogin-signwithsecure": "Använd säker anslutning",
+       "cannotlogin-title": "Kan inte logga in",
+       "cannotlogin-text": "Det går inte att logga in.",
        "cannotloginnow-title": "Kan inte logga in nu",
        "cannotloginnow-text": "Det går inte att logga in med $1.",
+       "cannotcreateaccount-title": "Kan inte skapa konton",
+       "cannotcreateaccount-text": "Direkt kontoregistrering är inte aktiverat på denna wiki.",
        "yourdomainname": "Din domän",
        "password-change-forbidden": "Du kan inte ändra lösenord på denna wiki.",
        "externaldberror": "Antingen inträffade autentiseringsproblem med en extern databas, eller så får du inte uppdatera ditt externa konto.",
index ec98058..10d820b 100644 (file)
        "randomincategory-category": "زمرہ:",
        "randomincategory-submit": "جانا",
        "statistics": "اعداد و شمار",
-       "statistics-header-pages": "احصائÛ\92 ØµÙ\81حات",
-       "statistics-header-edits": "احصائÛ\92 ØªØ¯Ù\88Û\8cÙ\86",
+       "statistics-header-pages": "صÙ\81حات Ú©Û\92 Ø§Ø¹Ø¯Ø§Ø¯ Ù\88 Ø´Ù\85ار",
+       "statistics-header-edits": "ترÙ\85Û\8cÙ\85Û\8c Ø§Ø¹Ø¯Ø§Ø¯ Ù\88 Ø´Ù\85ار",
        "statistics-header-users": "ارکان کے اعداد و شمار",
-       "statistics-header-hooks": "احصائÛ\92 Ø¯Û\8cÚ¯ر",
+       "statistics-header-hooks": "دÛ\8cگر Ø§Ø¹Ø¯Ø§Ø¯ Ù\88 Ø´Ù\85ار",
        "statistics-articles": "مندرج صفحات",
        "statistics-pages": "صفحات",
        "statistics-pages-desc": "(ویکی اقتباسات کے کل صفحات، بشمولِ تبادلۂ خیال، رجوع مکررات وغیرہ۔)",
-       "statistics-files": "زبراثÙ\82اÙ\84 Ø´Ø¯Û\81 Ù\85Ù\84Ù\81ات",
-       "statistics-edits": "ویکی اقتباسات کے آغاز سے کل صفحاتی ترمیم",
+       "statistics-files": "اپÙ\84Ù\88Ú\88 Ú©Ø±Ø¯Û\81 Ù\81ائÙ\84Û\8cÚº",
+       "statistics-edits": "{{SITENAME}} کے آغاز سے کل صفحاتی ترامیم",
        "statistics-edits-average": "فی صفحہ اوسط ترامیم",
        "statistics-users": "مندرج [[خاص:فہرست صارفین، صارف فہرست|صارفین]]",
        "statistics-users-active": "متحرک صارفین",
index 2d38231..291a7ea 100644 (file)
        "createacct-another-username-ph": "请输入用户名",
        "yourpassword": "密码:",
        "userlogin-yourpassword": "密码",
-       "userlogin-yourpassword-ph": "请输入的密码",
+       "userlogin-yourpassword-ph": "请输入的密码",
        "createacct-yourpassword-ph": "请输入密码",
        "yourpasswordagain": "请再次输入密码:",
        "createacct-yourpasswordagain": "确认密码",
        "tooltip-ca-nstab-help": "查看帮助页面",
        "tooltip-ca-nstab-category": "查看分类页面",
        "tooltip-minoredit": "标记本编辑为小编辑",
-       "tooltip-save": "保存的更改",
+       "tooltip-save": "保存的更改",
        "tooltip-publish": "发布您的更改",
        "tooltip-preview": "预览您的更改。请在保存前使用此功能。",
        "tooltip-diff": "显示您对该文字所做的更改",
index f9da1ed..d5449bf 100644 (file)
@@ -29,14 +29,14 @@ $fallback8bitEncoding = 'windows-1256';
 $rtl = true;
 
 $namespaceNames = [
-       NS_MEDIA            => 'Ù\88سÛ\8cØ·',
+       NS_MEDIA            => 'Ù\85Û\8cÚ\88Û\8cا',
        NS_SPECIAL          => 'خاص',
        NS_TALK             => 'تبادلۂ_خیال',
        NS_USER             => 'صارف',
        NS_USER_TALK        => 'تبادلۂ_خیال_صارف',
        NS_PROJECT_TALK     => 'تبادلۂ_خیال_$1',
-       NS_FILE             => 'Ù\85Ù\84Ù\81',
-       NS_FILE_TALK        => 'تبادÙ\84Û\82_Ø®Û\8cاÙ\84\85Ù\84Ù\81',
+       NS_FILE             => 'Ù\81ائÙ\84',
+       NS_FILE_TALK        => 'تبادÙ\84Û\82_Ø®Û\8cاÙ\84\81ائÙ\84',
        NS_MEDIAWIKI        => 'میڈیاویکی',
        NS_MEDIAWIKI_TALK   => 'تبادلۂ_خیال_میڈیاویکی',
        NS_TEMPLATE         => 'سانچہ',
@@ -48,9 +48,12 @@ $namespaceNames = [
 ];
 
 $namespaceAliases = [
+       'وسیط'            => NS_MEDIA,
        'زریعہ'            => NS_MEDIA,
        'تصویر'            => NS_FILE,
        'تبادلۂ_خیال_تصویر'   => NS_FILE_TALK,
+       'ملف'            => NS_FILE,
+       'تبادلۂ_خیال_ملف'   => NS_FILE_TALK,
        'میڈیاوکی'          => NS_MEDIAWIKI,
        'تبادلۂ_خیال_میڈیاوکی' => NS_MEDIAWIKI_TALK,
 ];
@@ -62,7 +65,7 @@ $specialPageAliases = [
        'Ancientpages'              => [ 'قدیم_صفحات' ],
        'Badtitle'                  => [ 'خراب_عنوان' ],
        'Blankpage'                 => [ 'خالی_صفحہ' ],
-       'Block'                     => [ 'پابندی', 'آئی_پی_پتہ_پابندی،_پابندی_بر_صارف' ],
+       'Block'                     => [ 'پابندی', 'آئی_پی_پتہ_پابندی', 'پابندی_بر_صارف' ],
        'Booksources'               => [ 'کتابی_وسائل' ],
        'BrokenRedirects'           => [ 'شکستہ_رجوع_مکررات' ],
        'Categories'                => [ 'زمرہ_جات' ],
@@ -77,31 +80,31 @@ $specialPageAliases = [
        'DoubleRedirects'           => [ 'دوہرے_رجوع_مکررات' ],
        'EditWatchlist'             => [ 'ترمیم_زیر_نظر' ],
        'Emailuser'                 => [ 'صارف_ڈاک' ],
-       'Export'                    => [ 'برآمدگی' ],
+       'Export'                    => [ 'برآمد', 'برآمدگی' ],
        'Fewestrevisions'           => [ 'کم_نظر_ثانی_شدہ' ],
-       'FileDuplicateSearch'       => [ 'دہری_ملف_تلاش' ],
-       'Filepath'                  => [ 'راہ_ملف' ],
-       'Import'                    => [ 'درآمدگی' ],
+       'FileDuplicateSearch'       => [ 'تÙ\84اش_دÙ\88Û\81رÛ\8c\81ائÙ\84', 'دÛ\81رÛ\8c\85Ù\84Ù\81_تÙ\84اش' ],
+       'Filepath'                  => [ 'راÛ\81\81ائÙ\84', 'راÛ\81\85Ù\84Ù\81' ],
+       'Import'                    => [ 'درآمد', 'درآمدگی' ],
        'Invalidateemail'           => [ 'ڈاک_تصدیق_منسوخ' ],
        'JavaScriptTest'            => [ 'تجربہ_جاوا_اسکرپٹ' ],
        'BlockList'                 => [ 'فہرست_ممنوع', 'فہرست_دستور_شبکی_ممنوع' ],
        'LinkSearch'                => [ 'تلاش_روابط' ],
        'Listadmins'                => [ 'فہرست_منتظمین' ],
        'Listbots'                  => [ 'فہرست_روبہ_جات' ],
-       'Listfiles'                 => [ 'فہرست_املاف', 'فہرست_تصاویر' ],
+       'Listfiles'                 => [ 'فائلوں_کی_فہرست', 'فہرست_تصاویر' ],
        'Listgrouprights'           => [ 'فہرست_اختیارات_گروہ', 'صارفی_گروہ_اختیارات' ],
        'Listredirects'             => [ 'فہرست_رجوع_مکررات' ],
-       'Listusers'                 => [ 'فہرست_صارفین،_صارف_فہرست' ],
+       'Listusers'                 => [ 'فہرست_صارفین' ],
        'Log'                       => [ 'نوشتہ', 'نوشتہ_جات' ],
        'Lonelypages'               => [ 'یتیم_صفحات' ],
        'Longpages'                 => [ 'طویل_صفحات' ],
        'MergeHistory'              => [ 'ضم_تاریخچہ' ],
        'Movepage'                  => [ 'منتقلی_صفحہ' ],
-       'Mycontributions'           => [ 'میرا_حصہ' ],
+       'Mycontributions'           => [ 'میری_شراکتیں', 'میرا_حصہ' ],
        'Mypage'                    => [ 'میرا_صفحہ' ],
        'Mytalk'                    => [ 'میری_گفتگو' ],
-       'Myuploads'                 => [ 'میرے_زبراثقالات' ],
-       'Newimages'                 => [ 'جدید_املاف', 'جدید_تصاویر' ],
+       'Myuploads'                 => [ 'Ù\85Û\8cرÛ\92§Ù¾Ù\84Ù\88Ú\88', 'Ù\85Û\8cرÛ\92²Ø¨Ø±Ø§Ø«Ù\82اÙ\84ات' ],
+       'Newimages'                 => [ 'جدید_فائلیں', 'جدید_املاف', 'جدید_تصاویر' ],
        'Newpages'                  => [ 'جدید_صفحات' ],
        'PermanentLink'             => [ 'مستقل_ربط' ],
        'Preferences'               => [ 'ترجیحات' ],
@@ -112,33 +115,33 @@ $specialPageAliases = [
        'Randomredirect'            => [ 'تصادفی_رجوع_مکرر' ],
        'Recentchanges'             => [ 'حالیہ_تبدیلیاں' ],
        'Recentchangeslinked'       => [ 'متعلقہ_تبدیلیاں' ],
-       'Revisiondelete'            => [ 'حذف_اعادہ' ],
+       'Revisiondelete'            => [ 'حذف_نظر_ثانی', 'حذف_اعادہ' ],
        'Search'                    => [ 'تلاش' ],
        'Shortpages'                => [ 'مختصر_صفحات' ],
        'Specialpages'              => [ 'خصوصی_صفحات' ],
        'Statistics'                => [ 'شماریات' ],
-       'Tags'                      => [ 'ٹیگز' ],
+       'Tags'                      => [ 'ٹیگ', 'ٹیگز' ],
        'Unblock'                   => [ 'پابندی_ختم' ],
        'Uncategorizedcategories'   => [ 'غیر_زمرہ_بند_زمرہ_جات' ],
-       'Uncategorizedimages'       => [ 'غیر_زمرہ_بند_املاف', 'غیر_زمرہ_بند_تصاویر' ],
+       'Uncategorizedimages'       => [ 'غیر_زمرہ_بند_فائلیں', 'غیر_زمرہ_بند_املاف', 'غیر_زمرہ_بند_تصاویر' ],
        'Uncategorizedpages'        => [ 'غیر_زمرہ_بند_صفحات' ],
        'Uncategorizedtemplates'    => [ 'غیر_زمرہ_بند_سانچے' ],
        'Undelete'                  => [ 'بحال' ],
        'Unusedcategories'          => [ 'غیر_مستعمل_زمرہ_جات' ],
-       'Unusedimages'              => [ 'غیر_مستعمل_املاف', 'غیر_مستعمل_تصاویر' ],
+       'Unusedimages'              => [ 'غیر_مستعمل_فائلیں', 'غیر_مستعمل_املاف', 'غیر_مستعمل_تصاویر' ],
        'Unusedtemplates'           => [ 'غیر_مستعمل_سانچے' ],
        'Unwatchedpages'            => [ 'نادیدہ_صفحات' ],
-       'Upload'                    => [ 'زبراثقال' ],
+       'Upload'                    => [ 'اپÙ\84Ù\88Ú\88', 'زبراثÙ\82اÙ\84' ],
        'Userlogin'                 => [ 'داخل_نوشتگی' ],
        'Userlogout'                => [ 'خارج_نوشتگی' ],
        'Userrights'                => [ 'صارفی_اختیارات' ],
-       'Version'                   => [ 'اخراجہ' ],
+       'Version'                   => [ 'نسخہ', 'اخراجہ' ],
        'Wantedcategories'          => [ 'مطلوبہ_زمرہ_جات' ],
-       'Wantedfiles'               => [ 'مطلوبہ_املاف' ],
+       'Wantedfiles'               => [ 'مطلوبہ_فائلیں', 'مطلوبہ_املاف' ],
        'Wantedpages'               => [ 'مطلوبہ_صفحات', 'شکستہ_روابط' ],
        'Wantedtemplates'           => [ 'مطلوبہ_سانچے' ],
        'Watchlist'                 => [ 'زیر_نظر_فہرست' ],
-       'Whatlinkshere'             => [ 'یہاں_کس_کا_رابطہ' ],
+       'Whatlinkshere'             => [ 'مربوط_صفحات', 'یہاں_کس_کا_رابطہ' ],
        'Withoutinterwiki'          => [ 'بدون_بین_الویکی' ],
 ];
 
index 95bd089..890fe45 100644 (file)
@@ -121,4 +121,4 @@ wfLogProfilingData();
 // Commit and close up!
 $factory = wfGetLBFactory();
 $factory->commitMasterChanges( 'doMaintenance' );
-$factory->shutdown();
+$factory->shutdown( $factory::SHUTDOWN_NO_CHRONPROT );
index 7055f36..63c3490 100644 (file)
@@ -1282,6 +1282,7 @@ return [
                ],
                'dependencies' => [
                        'oojs-ui-core',
+                       'oojs-ui-widgets',
                        'oojs-ui-windows',
                        'oojs-ui.styles.icons-content',
                        'oojs-ui.styles.icons-editing-advanced',
diff --git a/resources/lib/phpjs-sha1/LICENSE.txt b/resources/lib/phpjs-sha1/LICENSE.txt
deleted file mode 100644 (file)
index 04caf53..0000000
+++ /dev/null
@@ -1,20 +0,0 @@
-Copyright (c) 2013 Kevin van Zonneveld (http://kvz.io) 
-and Contributors (http://phpjs.org/authors)
-
-Permission is hereby granted, free of charge, to any person obtaining a copy of
-this software and associated documentation files (the "Software"), to deal in
-the Software without restriction, including without limitation the rights to
-use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
-of the Software, and to permit persons to whom the Software is furnished to do
-so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
index 9af81b8..bce512c 100644 (file)
@@ -1,11 +1,13 @@
 /*!
  * JavaScript for Special:MovePage
  */
-jQuery( function () {
+jQuery( function ( $ ) {
        // Infuse for pretty dropdown
        OO.ui.infuse( 'wpNewTitle' );
        // Limit to 255 bytes, not characters
        OO.ui.infuse( 'wpReason' ).$input.byteLimit();
        // Infuse for nicer "help" popup
-       OO.ui.infuse( 'wpMovetalk-field' );
+       if ( $( '#wpMovetalk-field' ).length ) {
+               OO.ui.infuse( 'wpMovetalk-field' );
+       }
 } );
index c1c421b..e1a54fb 100644 (file)
@@ -3055,6 +3055,28 @@ a
 | c</pre>
 !!end
 
+!! test
+2g. Indented table markup mixed with indented pre content (proposed in bug 6200)
+!! wikitext
+ <table>
+ <tr>
+ <td>
+ Text that should be rendered preformatted
+ </td>
+ </tr>
+ </table>
+!! html
+ <table>
+ <tr>
+ <td>
+<pre>Text that should be rendered preformatted
+</pre>
+ </td>
+ </tr>
+ </table>
+
+!! end
+
 !!test
 3a. Indent-Pre and block tags (single-line html)
 !! wikitext
@@ -6392,26 +6414,55 @@ parsoid=wt2html,html2html
 <span typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"ho\">ha&lt;/div>"}},"i":0}}]}'>ho">ha</span>
 !! end
 
+## We don't support roundtripping of these attributes in Parsoid.
+## Selective serialization takes care of preventing dirty diffs.
+## But, on edits, we dirty-diff the invalid attribute text.
 !! test
-Indented table markup mixed with indented pre content (proposed in bug 6200)
+Invalid text in table attributes should be discarded
+!! options
+parsoid=wt2html
 !! wikitext
- <table>
- <tr>
- <td>
- Text that should be rendered preformatted
- </td>
- </tr>
- </table>
-!! html
- <table>
- <tr>
- <td>
-<pre>Text that should be rendered preformatted
-</pre>
- </td>
- </tr>
- </table>
+{| <span>boo</span> style='border:1px solid black'
+|  <span>boo</span> style='color:blue'  | 1
+|<span>boo</span> style='color:blue'| 2
+|}
+!! html/php
+<table style="border:1px solid black">
+<tr>
+<td style="color:blue"> 1
+</td>
+<td style="color:blue"> 2
+</td></tr></table>
 
+!! html/parsoid
+<table style="border:1px solid black">
+<tr>
+<td style="color:blue"> 1</td>
+<td style="color:blue"> 2</td>
+</tr>
+</table>
+!! end
+
+!! test
+Invalid text in table attributes should be preserved by selective serializer
+!! options
+parsoid={
+  "modes": ["selser"],
+  "changes": [
+    ["td:first-child", "text", "abc"],
+    ["td + td", "text", "xyz"]
+  ]
+}
+!! wikitext
+{| <span>boo</span> style='border:1px solid black'
+|  <span>boo</span> style='color:blue'  | 1
+|<span>boo</span> style='color:blue'| 2
+|}
+!! wikitext/edited
+{| <span>boo</span> style='border:1px solid black'
+|  <span>boo</span> style='color:blue'  |abc
+|<span>boo</span> style='color:blue'|xyz
+|}
 !! end
 
 !! test
@@ -8000,6 +8051,20 @@ title=[[User:test]]
 <p><a rel="mw:WikiLink" href="./User:Test/123" title="User:Test/123" data-parsoid='{"stx":"simple","a":{"href":"./User:Test/123"},"sa":{"href":"/123"}}'>/123</a></p>
 !! end
 
+!! test
+Ensure that transclusion titles are not url-decoded
+!! options
+subpage title=[[Test]]
+parsoid=wt2html
+!! wikitext
+{{Bar%C3%A9}} {{/Bar%C3%A9}}
+!! html/php
+<p>{{Bar%C3%A9}} {{/Bar%C3%A9}}
+</p>
+!! html/parsoid
+<p>{{Bar%C3%A9}} {{/Bar%C3%A9}}</p>
+!! end
+
 !! test
 Purely hash wikilink
 !! options
@@ -19388,9 +19453,11 @@ subpage title=[[Subpage test/L1/L2/L3]]
 parsoid=wt2html
 !! wikitext
 {{../../../../More than parent}}
-!! html
+!! html/php
 <p>{{../../../../More than parent}}
 </p>
+!! html/parsoid
+<p>{{../../../../More than parent}}</p>
 !! end
 
 !! test
@@ -27241,7 +27308,9 @@ Thumbnail output
 unclosed internal link XSS (T137264)
 !! wikitext
 [[#%3Cscript%3Ealert(1)%3C/script%3E|
-!! html
+!! html/php
 <p>[[#&lt;script&gt;alert(1)&lt;/script&gt;|
 </p>
+!! html/parsoid
+<p>[[#%3Cscript%3Ealert(1)%3C/script%3E|</p>
 !! end
index cfd5f78..d637704 100644 (file)
@@ -228,6 +228,33 @@ class BotPasswordTest extends MediaWikiTestCase {
                $this->assertNotNull( BotPassword::newFromCentralId( 43, 'BotPassword' ) );
        }
 
+       /**
+        * @dataProvider provideCanonicalizeLoginData
+        */
+       public function testCanonicalizeLoginData( $username, $password, $expectedResult ) {
+               $result = BotPassword::canonicalizeLoginData( $username, $password );
+               if ( is_array( $expectedResult ) ) {
+                       $this->assertArrayEquals( $expectedResult, $result, true, true );
+               } else {
+                       $this->assertSame( $expectedResult, $result );
+               }
+       }
+
+       public function provideCanonicalizeLoginData() {
+               return [
+                       [ 'user', 'pass', false ],
+                       [ 'user', 'abc@def', false ],
+                       [ 'user@bot', '12345678901234567890123456789012',
+                               [ 'user@bot', '12345678901234567890123456789012', false ] ],
+                       [ 'user', 'bot@12345678901234567890123456789012',
+                               [ 'user@bot', '12345678901234567890123456789012', true ] ],
+                       [ 'user', 'bot@12345678901234567890123456789012345',
+                               [ 'user@bot', '12345678901234567890123456789012345', true ] ],
+                       [ 'user', 'bot@x@12345678901234567890123456789012',
+                               [ 'user@bot@x', '12345678901234567890123456789012', true ] ],
+               ];
+       }
+
        public function testLogin() {
                // Test failure when bot passwords aren't enabled
                $this->setMwGlobals( 'wgEnableBotPasswords', false );
diff --git a/tests/phpunit/structure/ContentHandlerSanityTest.php b/tests/phpunit/structure/ContentHandlerSanityTest.php
new file mode 100644 (file)
index 0000000..98a0fbb
--- /dev/null
@@ -0,0 +1,53 @@
+<?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
+ */
+
+class ContentHandlerSanityTest extends MediaWikiTestCase {
+
+       public static function provideHandlers() {
+               $models = ContentHandler::getContentModels();
+               $handlers = [];
+               foreach ( $models as $model ) {
+                       $handlers[] = [ ContentHandler::getForModelID( $model ) ];
+               }
+
+               return $handlers;
+       }
+
+       /**
+        * @dataProvider provideHandlers
+        * @param ContentHandler $handler
+        */
+       public function testMakeEmptyContent( ContentHandler $handler ) {
+               $content = $handler->makeEmptyContent();
+               $this->assertInstanceOf( Content::class, $content );
+               if ( $handler instanceof TextContentHandler ) {
+                       // TextContentHandler::getContentClass() is protected, so bypass
+                       // that restriction
+                       $testingWrapper = TestingAccessWrapper::newFromObject( $handler );
+                       $this->assertInstanceOf( $testingWrapper->getContentClass(), $content );
+               }
+
+               $handlerClass = get_class( $handler );
+               $contentClass = get_class( $content );
+
+               $this->assertTrue(
+                       $content->isValid(),
+                       "$handlerClass::makeEmptyContent() did not return a valid content ($contentClass::isValid())"
+               );
+       }
+}