Merge "Call ssl_set() in DatabaseMysqli if DBO_SSL is set"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Wed, 24 Aug 2016 19:46:02 +0000 (19:46 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Wed, 24 Aug 2016 19:46:02 +0000 (19:46 +0000)
107 files changed:
RELEASE-NOTES-1.28
autoload.php
composer.json
docs/hooks.txt
includes/Block.php
includes/DefaultSettings.php
includes/EditPage.php
includes/FileDeleteForm.php
includes/Html.php
includes/MediaWiki.php
includes/MediaWikiServices.php
includes/MovePage.php
includes/OutputPage.php
includes/Pingback.php
includes/ServiceWiring.php
includes/Title.php
includes/api/ApiParse.php
includes/api/ApiQueryUsers.php
includes/api/ApiUpload.php
includes/api/i18n/lt.json
includes/collation/Collation.php
includes/collation/NumericUppercaseCollation.php [new file with mode: 0644]
includes/content/JsonContent.php
includes/content/TextContent.php
includes/content/WikitextContent.php
includes/db/CloneDatabase.php
includes/db/Database.php
includes/db/DatabaseMysqlBase.php
includes/db/loadbalancer/LoadBalancer.php
includes/deferred/DataUpdate.php
includes/deferred/EnqueueableDataUpdate.php [new file with mode: 0644]
includes/deferred/LinksUpdate.php
includes/deferred/SqlDataUpdate.php
includes/filebackend/lockmanager/DBLockManager.php
includes/filebackend/lockmanager/MySqlLockManager.php [new file with mode: 0644]
includes/filebackend/lockmanager/PostgreSqlLockManager.php [new file with mode: 0644]
includes/filebackend/lockmanager/RedisLockManager.php
includes/filerepo/file/LocalFile.php
includes/htmlform/HTMLForm.php
includes/htmlform/HTMLFormElement.php
includes/htmlform/HTMLFormField.php
includes/htmlform/fields/HTMLMultiSelectField.php
includes/htmlform/fields/HTMLRadioField.php
includes/htmlform/fields/HTMLSelectNamespace.php
includes/htmlform/fields/HTMLTitleTextField.php
includes/htmlform/fields/HTMLUserTextField.php
includes/installer/i18n/hu.json
includes/installer/i18n/lt.json
includes/installer/i18n/vi.json
includes/jobqueue/JobQueueDB.php
includes/jobqueue/JobRunner.php
includes/jobqueue/utils/PurgeJobUtils.php [new file with mode: 0644]
includes/libs/virtualrest/VirtualRESTServiceClient.php
includes/objectcache/SqlBagOStuff.php
includes/page/Article.php
includes/page/WikiPage.php
includes/parser/Parser.php
includes/specialpage/AuthManagerSpecialPage.php
includes/specialpage/LoginSignupSpecialPage.php
includes/specials/SpecialChangeCredentials.php
includes/specials/SpecialCreateAccount.php
includes/specials/SpecialExport.php
includes/specials/SpecialMyLanguage.php
includes/specials/SpecialRunJobs.php
includes/user/User.php
includes/utils/BatchRowWriter.php
languages/Language.php
languages/i18n/af.json
languages/i18n/ar.json
languages/i18n/be-tarask.json
languages/i18n/be.json
languages/i18n/diq.json
languages/i18n/dty.json
languages/i18n/en.json
languages/i18n/eo.json
languages/i18n/fa.json
languages/i18n/fr.json
languages/i18n/jv.json
languages/i18n/lb.json
languages/i18n/lt.json
languages/i18n/lv.json
languages/i18n/mk.json
languages/i18n/my.json
languages/i18n/nds.json
languages/i18n/nn.json
languages/i18n/oc.json
languages/i18n/pt.json
languages/i18n/te.json
languages/i18n/vi.json
languages/messages/MessagesSk.php
maintenance/namespaceDupes.php
maintenance/validateRegistrationFile.php
resources/src/jquery/jquery.makeCollapsible.js
resources/src/mediawiki.ui/components/icons.less
resources/src/mediawiki.widgets/mw.widgets.CategoryCapsuleItemWidget.js
resources/src/mediawiki/htmlform/autoinfuse.js
resources/src/mediawiki/htmlform/hide-if.js
resources/src/mediawiki/htmlform/multiselect.js
resources/src/mediawiki/mediawiki.js
tests/parser/parserTests.txt
tests/phpunit/includes/MediaWikiServicesTest.php
tests/phpunit/includes/content/TextContentTest.php
tests/phpunit/includes/libs/objectcache/CachedBagOStuffTest.php
tests/phpunit/includes/libs/objectcache/HashBagOStuffTest.php
tests/phpunit/includes/libs/objectcache/MultiWriteBagOStuffTest.php
tests/phpunit/includes/user/UserTest.php
tests/phpunit/structure/ExtensionJsonValidationTest.php

index 5d88fbf..92ac869 100644 (file)
@@ -39,7 +39,8 @@ production.
 * (T141604) Extensions can now provide a better error message when their
   maintenance scripts are run without the extension being installed.
 * (T8948) Numeric sorting in categories is now supported by setting $wgCategoryCollation
-  to uca-default-u-kn or uca-<langcode>-u-kn. If migrating from another
+  to 'uca-default-u-kn' or 'uca-<langcode>-u-kn'. If you can't use UCA collations,
+  a 'numeric' collation is also available. If migrating from another
   collation, you will need to run the updateCollation.php maintenance script.
 
 === External library changes in 1.28 ===
@@ -52,6 +53,16 @@ production.
 ==== Removed and replaced external libraries ====
 
 === Bug fixes in 1.28 ===
+* (T137264) SECURITY: XSS in unclosed internal links
+* (T133147) SECURITY: Escape '<' and ']]>' in inline <style> blocks
+* (T133147) SECURITY: Require login to preview user CSS pages
+* (T132926) SECURITY: Do not allow undeleting a revision deleted file if it is
+  the top file
+* (T129738) SECURITY: Make $wgBlockDisablesLogin also restrict logged in
+  permissions
+* (T129738) SECURITY: Make blocks log users out if $wgBlockDisablesLogin is true
+* (T139670) Move 'UserGetRights' call before application of
+  Session::getAllowedUserRights()
 
 === Action API changes in 1.28 ===
 * Added 'maxarticlesize' property to action=query&meta=siteinfo which contains
@@ -72,6 +83,8 @@ production.
 === Action API internal changes in 1.28 ===
 * Added a new hook, 'ApiMakeParserOptions', to allow extensions to better
   interact with ApiParse and ApiExpandTemplates.
+* (T139565) SECURITY: API: Generate head items in the context of the given title
+* (T115333) SECURITY: Check read permission when loading page content in ApiParse
 
 === Languages updated in 1.28 ===
 
index e7671be..39102fd 100644 (file)
@@ -408,7 +408,7 @@ $wgAutoloadLocalClasses = [
        'EnhancedChangesList' => __DIR__ . '/includes/changes/EnhancedChangesList.php',
        'EnotifNotifyJob' => __DIR__ . '/includes/jobqueue/jobs/EnotifNotifyJob.php',
        'EnqueueJob' => __DIR__ . '/includes/jobqueue/jobs/EnqueueJob.php',
-       'EnqueueableDataUpdate' => __DIR__ . '/includes/deferred/DataUpdate.php',
+       'EnqueueableDataUpdate' => __DIR__ . '/includes/deferred/EnqueueableDataUpdate.php',
        'EraseArchivedFile' => __DIR__ . '/maintenance/eraseArchivedFile.php',
        'ErrorPageError' => __DIR__ . '/includes/exception/ErrorPageError.php',
        'EventRelayer' => __DIR__ . '/includes/libs/eventrelayer/EventRelayer.php',
@@ -953,7 +953,7 @@ $wgAutoloadLocalClasses = [
        'MwSql' => __DIR__ . '/maintenance/sql.php',
        'MySQLField' => __DIR__ . '/includes/db/DatabaseMysqlBase.php',
        'MySQLMasterPos' => __DIR__ . '/includes/db/DatabaseMysqlBase.php',
-       'MySqlLockManager' => __DIR__ . '/includes/filebackend/lockmanager/DBLockManager.php',
+       'MySqlLockManager' => __DIR__ . '/includes/filebackend/lockmanager/MySqlLockManager.php',
        'MysqlInstaller' => __DIR__ . '/includes/installer/MysqlInstaller.php',
        'MysqlUpdater' => __DIR__ . '/includes/installer/MysqlUpdater.php',
        'NaiveForeignTitleFactory' => __DIR__ . '/includes/title/NaiveForeignTitleFactory.php',
@@ -975,6 +975,7 @@ $wgAutoloadLocalClasses = [
        'NullLockManager' => __DIR__ . '/includes/filebackend/lockmanager/LockManager.php',
        'NullRepo' => __DIR__ . '/includes/filerepo/NullRepo.php',
        'NullStatsdDataFactory' => __DIR__ . '/includes/libs/stats/NullStatsdDataFactory.php',
+       'NumericUppercaseCollation' => __DIR__ . '/includes/collation/NumericUppercaseCollation.php',
        'OOUIHTMLForm' => __DIR__ . '/includes/htmlform/OOUIHTMLForm.php',
        'ORAField' => __DIR__ . '/includes/db/DatabaseOracle.php',
        'ORAResult' => __DIR__ . '/includes/db/DatabaseOracle.php',
@@ -1061,7 +1062,7 @@ $wgAutoloadLocalClasses = [
        'PopulateRecentChangesSource' => __DIR__ . '/maintenance/populateRecentChangesSource.php',
        'PopulateRevisionLength' => __DIR__ . '/maintenance/populateRevisionLength.php',
        'PopulateRevisionSha1' => __DIR__ . '/maintenance/populateRevisionSha1.php',
-       'PostgreSqlLockManager' => __DIR__ . '/includes/filebackend/lockmanager/DBLockManager.php',
+       'PostgreSqlLockManager' => __DIR__ . '/includes/filebackend/lockmanager/PostgreSqlLockManager.php',
        'PostgresBlob' => __DIR__ . '/includes/db/DatabasePostgres.php',
        'PostgresField' => __DIR__ . '/includes/db/DatabasePostgres.php',
        'PostgresInstaller' => __DIR__ . '/includes/installer/PostgresInstaller.php',
@@ -1096,6 +1097,7 @@ $wgAutoloadLocalClasses = [
        'PurgeAction' => __DIR__ . '/includes/actions/PurgeAction.php',
        'PurgeChangedFiles' => __DIR__ . '/maintenance/purgeChangedFiles.php',
        'PurgeChangedPages' => __DIR__ . '/maintenance/purgeChangedPages.php',
+       'PurgeJobUtils' => __DIR__ . '/includes/jobqueue/utils/PurgeJobUtils.php',
        'PurgeList' => __DIR__ . '/maintenance/purgeList.php',
        'PurgeOldText' => __DIR__ . '/maintenance/purgeOldText.php',
        'PurgeParserCache' => __DIR__ . '/maintenance/purgeParserCache.php',
index 956739d..2243b7c 100644 (file)
@@ -45,7 +45,7 @@
        },
        "require-dev": {
                "jakub-onderka/php-parallel-lint": "0.9.2",
-               "justinrainbow/json-schema": "~1.6",
+               "justinrainbow/json-schema": "~3.0",
                "mediawiki/mediawiki-codesniffer": "0.7.2",
                "monolog/monolog": "~1.18.2",
                "nikic/php-parser": "2.1.0",
index 5cf8ffe..a0ee8bd 100644 (file)
@@ -297,16 +297,6 @@ After a user account is created.
 $user: the User object that was created. (Parameter added in 1.7)
 $byEmail: true when account was created "by email" (added in 1.12)
 
-'AddNewAccountApiForm': Allow modifying internal login form when creating an
-account via API.
-$apiModule: the ApiCreateAccount module calling
-$loginForm: the LoginForm used
-
-'AddNewAccountApiResult': Modify API output when creating a new account via API.
-$apiModule: the ApiCreateAccount module calling
-$loginForm: the LoginForm used
-&$result: associative array for API result data
-
 'AfterBuildFeedLinks': Executed in OutputPage.php after all feed links (atom, rss,...)
 are created. Can be used to omit specific feeds from being outputted. You must not use
 this hook to add feeds, use OutputPage::addFeedLink() instead.
@@ -2069,13 +2059,6 @@ $e: The exception (in case of a plain old PHP error, a wrapping ErrorException)
 $suppressed: true if the error was suppressed via
   error_reporting()/wfSuppressWarnings()
 
-'LoginAuthenticateAudit': A login attempt for a valid user account either
-succeeded or failed. No return data is accepted; this hook is for auditing only.
-$user: the User object being authenticated against
-$password: the password being submitted and found wanting
-$retval: a LoginForm class constant with authenticateUserData() return
-  value (SUCCESS, WRONG_PASS, etc.).
-
 'LoginFormValidErrorMessages': Called in LoginForm when a function gets valid
 error messages. Allows to add additional error messages (except messages already
 in LoginForm::$validErrorMessages).
index 93df004..79b31bb 100644 (file)
@@ -457,6 +457,7 @@ class Block {
         *      ('id' => block ID, 'autoIds' => array of autoblock IDs)
         */
        public function insert( $dbw = null ) {
+               global $wgBlockDisablesLogin;
                wfDebug( "Block::insert; timestamp {$this->mTimestamp}\n" );
 
                if ( $dbw === null ) {
@@ -499,6 +500,13 @@ class Block {
 
                if ( $affected ) {
                        $auto_ipd_ids = $this->doRetroactiveAutoblock();
+
+                       if ( $wgBlockDisablesLogin && $this->target instanceof User ) {
+                               // Change user login token to force them to be logged out.
+                               $this->target->setToken();
+                               $this->target->saveSettings();
+                       }
+
                        return [ 'id' => $this->mId, 'autoIds' => $auto_ipd_ids ];
                }
 
@@ -961,28 +969,40 @@ class Block {
 
        /**
         * Get/set whether the Block prevents a given action
-        * @param string $action
-        * @param bool|null $x
-        * @return bool
+        *
+        * @param string $action Action to check
+        * @param bool|null $x Value for set, or null to just get value
+        * @return bool|null Null for unrecognized rights.
         */
        public function prevents( $action, $x = null ) {
+               global $wgBlockDisablesLogin;
+               $res = null;
                switch ( $action ) {
                        case 'edit':
                                # For now... <evil laugh>
-                               return true;
-
+                               $res = true;
+                               break;
                        case 'createaccount':
-                               return wfSetVar( $this->mCreateAccount, $x );
-
+                               $res = wfSetVar( $this->mCreateAccount, $x );
+                               break;
                        case 'sendemail':
-                               return wfSetVar( $this->mBlockEmail, $x );
-
+                               $res = wfSetVar( $this->mBlockEmail, $x );
+                               break;
                        case 'editownusertalk':
-                               return wfSetVar( $this->mDisableUsertalk, $x );
-
-                       default:
-                               return null;
+                               $res = wfSetVar( $this->mDisableUsertalk, $x );
+                               break;
+                       case 'read':
+                               $res = false;
+                               break;
                }
+               if ( !$res && $wgBlockDisablesLogin ) {
+                       // If a block would disable login, then it should
+                       // prevent any action that all users cannot do
+                       $anon = new User;
+                       $res = $anon->isAllowed( $action ) ? $res : true;
+               }
+
+               return $res;
        }
 
        /**
index 4e08aa6..f1afc4c 100644 (file)
@@ -685,7 +685,7 @@ $wgUseSharedUploads = false;
 /**
  * Full path on the web server where shared uploads can be found
  */
-$wgSharedUploadPath = "http://commons.wikimedia.org/shared/images";
+$wgSharedUploadPath = null;
 
 /**
  * Fetch commons image description pages and display them on the local wiki?
@@ -695,7 +695,7 @@ $wgFetchCommonsDescriptions = false;
 /**
  * Path on the file system where shared uploads can be found.
  */
-$wgSharedUploadDirectory = "/var/www/wiki3/images";
+$wgSharedUploadDirectory = null;
 
 /**
  * DB name with metadata about shared directory.
@@ -3193,6 +3193,15 @@ $wgHTMLFormAllowTableFormat = true;
  */
 $wgUseMediaWikiUIEverywhere = false;
 
+/**
+ * Whether to label the store-to-database-and-show-to-others button in the editor
+ * as "Save page"/"Save changes" if false (the default) or, if true, instead as
+ * "Publish page"/"Publish changes".
+ *
+ * @since 1.28
+ */
+$wgEditButtonPublishNotSave = false;
+
 /**
  * Permit other namespaces in addition to the w3.org default.
  *
@@ -8033,9 +8042,13 @@ $wgJobRunRate = 1;
  * When $wgJobRunRate > 0, try to run jobs asynchronously, spawning a new process
  * to handle the job execution, instead of blocking the request until the job
  * execution finishes.
+ *
  * @since 1.23
  */
-$wgRunJobsAsync = true;
+$wgRunJobsAsync = (
+       !function_exists( 'register_postsend_function' ) &&
+       !function_exists( 'fastcgi_finish_request' )
+);
 
 /**
  * Number of rows to update per job
@@ -8250,11 +8263,29 @@ $wgPageLanguageUseDB = false;
 
 /**
  * Global configuration variable for Virtual REST Services.
- * Parameters for different services are to be declared inside
- * $wgVirtualRestConfig['modules'], which is to be treated as an associative
- * array. Global parameters will be merged with service-specific ones. The
- * result will then be passed to VirtualRESTService::__construct() in the
- * module.
+ *
+ * Use the 'path' key to define automatically mounted services. The value for this
+ * key is a map of path prefixes to service configuration. The later is an array of:
+ *   - class : the fully qualified class name
+ *   - options : map of arguments to the class constructor
+ * Such services will be available to handle queries under their path from the VRS
+ * singleton, e.g. MediaWikiServices::getInstance()->getVirtualRESTServiceClient();
+ *
+ * Auto-mounting example for Parsoid:
+ *
+ * $wgVirtualRestConfig['paths']['/parsoid/'] = [
+ *     'class' => 'ParsoidVirtualRESTService',
+ *     'options' => [
+ *         'url' => 'http://localhost:8000',
+ *         'prefix' => 'enwiki',
+ *         'domain' => 'en.wikipedia.org'
+ *     ]
+ * ];
+ *
+ * Parameters for different services can also be declared inside the 'modules' value,
+ * which is to be treated as an associative array. The parameters in 'global' will be
+ * merged with service-specific ones. The result will then be passed to
+ * VirtualRESTService::__construct() in the module.
  *
  * Example config for Parsoid:
  *
@@ -8268,6 +8299,7 @@ $wgPageLanguageUseDB = false;
  * @since 1.25
  */
 $wgVirtualRestConfig = [
+       'paths' => [],
        'modules' => [],
        'global' => [
                # Timeout in seconds
@@ -8342,7 +8374,7 @@ $wgEventRelayerConfig = [
  * PHP version, and chosen database backend. The Wikimedia Foundation shares this data with
  * MediaWiki developers to help guide future development efforts.
  *
- * For details about what data is sent, see: https://www.mediawiki.org/wiki/Pingback
+ * For details about what data is sent, see: https://www.mediawiki.org/wiki/Manual:$wgPingback
  *
  * @var bool
  * @since 1.28
index 9b862b9..738eaec 100644 (file)
@@ -401,6 +401,11 @@ class EditPage {
         */
        private $enableApiEditOverride = false;
 
+       /**
+        * @var IContextSource
+        */
+       protected $context;
+
        /**
         * @param Article $article
         */
@@ -408,6 +413,7 @@ class EditPage {
                $this->mArticle = $article;
                $this->page = $article->getPage(); // model object
                $this->mTitle = $article->getTitle();
+               $this->context = $article->getContext();
 
                $this->contentModel = $this->mTitle->getContentModel();
 
@@ -422,6 +428,14 @@ class EditPage {
                return $this->mArticle;
        }
 
+       /**
+        * @since 1.28
+        * @return IContextSource
+        */
+       public function getContext() {
+               return $this->context;
+       }
+
        /**
         * @since 1.19
         * @return Title
@@ -493,7 +507,6 @@ class EditPage {
         * the newly-edited page.
         */
        function edit() {
-               global $wgOut, $wgRequest, $wgUser;
                // Allow extensions to modify/prevent this form or submission
                if ( !Hooks::run( 'AlternateEdit', [ $this ] ) ) {
                        return;
@@ -501,13 +514,15 @@ class EditPage {
 
                wfDebug( __METHOD__ . ": enter\n" );
 
+               $request = $this->context->getRequest();
+               $out = $this->context->getOutput();
                // If they used redlink=1 and the page exists, redirect to the main article
-               if ( $wgRequest->getBool( 'redlink' ) && $this->mTitle->exists() ) {
-                       $wgOut->redirect( $this->mTitle->getFullURL() );
+               if ( $request->getBool( 'redlink' ) && $this->mTitle->exists() ) {
+                       $out->redirect( $this->mTitle->getFullURL() );
                        return;
                }
 
-               $this->importFormData( $wgRequest );
+               $this->importFormData( $request );
                $this->firsttime = false;
 
                if ( wfReadOnly() && $this->save ) {
@@ -536,7 +551,7 @@ class EditPage {
                        wfDebug( __METHOD__ . ": User can't edit\n" );
                        // Auto-block user's IP if the account was "hard" blocked
                        if ( !wfReadOnly() ) {
-                               $user = $wgUser;
+                               $user = $this->context->getUser();
                                DeferredUpdates::addCallableUpdate( function () use ( $user ) {
                                        $user->spreadAnyEditBlock();
                                } );
@@ -610,15 +625,14 @@ class EditPage {
         * @return array
         */
        protected function getEditPermissionErrors( $rigor = 'secure' ) {
-               global $wgUser;
-
-               $permErrors = $this->mTitle->getUserPermissionsErrors( 'edit', $wgUser, $rigor );
+               $user = $this->context->getUser();
+               $permErrors = $this->mTitle->getUserPermissionsErrors( 'edit', $user, $rigor );
                # Can this title be created?
                if ( !$this->mTitle->exists() ) {
                        $permErrors = array_merge(
                                $permErrors,
                                wfArrayDiff2(
-                                       $this->mTitle->getUserPermissionsErrors( 'create', $wgUser, $rigor ),
+                                       $this->mTitle->getUserPermissionsErrors( 'create', $user, $rigor ),
                                        $permErrors
                                )
                        );
@@ -651,13 +665,12 @@ class EditPage {
         * @throws PermissionsError
         */
        protected function displayPermissionsError( array $permErrors ) {
-               global $wgRequest, $wgOut;
-
-               if ( $wgRequest->getBool( 'redlink' ) ) {
+               $out = $this->context->getOutput();
+               if ( $this->context->getRequest()->getBool( 'redlink' ) ) {
                        // The edit page was reached via a red link.
                        // Redirect to the article page and let them click the edit tab if
                        // they really want a permission error.
-                       $wgOut->redirect( $this->mTitle->getFullURL() );
+                       $out->redirect( $this->mTitle->getFullURL() );
                        return;
                }
 
@@ -672,7 +685,7 @@ class EditPage {
 
                $this->displayViewSourcePage(
                        $content,
-                       $wgOut->formatPermissionsErrorMessage( $permErrors, 'edit' )
+                       $out->formatPermissionsErrorMessage( $permErrors, 'edit' )
                );
        }
 
@@ -682,29 +695,28 @@ class EditPage {
         * @param string $errorMessage additional wikitext error message to display
         */
        protected function displayViewSourcePage( Content $content, $errorMessage = '' ) {
-               global $wgOut;
+               $out = $this->context->getOutput();
+               Hooks::run( 'EditPage::showReadOnlyForm:initial', [ $this, &$out ] );
 
-               Hooks::run( 'EditPage::showReadOnlyForm:initial', [ $this, &$wgOut ] );
-
-               $wgOut->setRobotPolicy( 'noindex,nofollow' );
-               $wgOut->setPageTitle( wfMessage(
+               $out->setRobotPolicy( 'noindex,nofollow' );
+               $out->setPageTitle( wfMessage(
                        'viewsource-title',
                        $this->getContextTitle()->getPrefixedText()
                ) );
-               $wgOut->addBacklinkSubtitle( $this->getContextTitle() );
-               $wgOut->addHTML( $this->editFormPageTop );
-               $wgOut->addHTML( $this->editFormTextTop );
+               $out->addBacklinkSubtitle( $this->getContextTitle() );
+               $out->addHTML( $this->editFormPageTop );
+               $out->addHTML( $this->editFormTextTop );
 
                if ( $errorMessage !== '' ) {
-                       $wgOut->addWikiText( $errorMessage );
-                       $wgOut->addHTML( "<hr />\n" );
+                       $out->addWikiText( $errorMessage );
+                       $out->addHTML( "<hr />\n" );
                }
 
                # If the user made changes, preserve them when showing the markup
                # (This happens when a user is blocked during edit, for instance)
                if ( !$this->firsttime ) {
                        $text = $this->textbox1;
-                       $wgOut->addWikiMsg( 'viewyourtext' );
+                       $out->addWikiMsg( 'viewyourtext' );
                } else {
                        try {
                                $text = $this->toEditText( $content );
@@ -713,21 +725,21 @@ class EditPage {
                                # (e.g. for an old revision with a different model)
                                $text = $content->serialize();
                        }
-                       $wgOut->addWikiMsg( 'viewsourcetext' );
+                       $out->addWikiMsg( 'viewsourcetext' );
                }
 
-               $wgOut->addHTML( $this->editFormTextBeforeContent );
+               $out->addHTML( $this->editFormTextBeforeContent );
                $this->showTextbox( $text, 'wpTextbox1', [ 'readonly' ] );
-               $wgOut->addHTML( $this->editFormTextAfterContent );
+               $out->addHTML( $this->editFormTextAfterContent );
 
-               $wgOut->addHTML( Html::rawElement( 'div', [ 'class' => 'templatesUsed' ],
+               $out->addHTML( Html::rawElement( 'div', [ 'class' => 'templatesUsed' ],
                        Linker::formatTemplates( $this->getTemplates() ) ) );
 
-               $wgOut->addModules( 'mediawiki.action.edit.collapsibleFooter' );
+               $out->addModules( 'mediawiki.action.edit.collapsibleFooter' );
 
-               $wgOut->addHTML( $this->editFormTextBottom );
+               $out->addHTML( $this->editFormTextBottom );
                if ( $this->mTitle->exists() ) {
-                       $wgOut->returnToMain( null, $this->mTitle );
+                       $out->returnToMain( null, $this->mTitle );
                }
        }
 
@@ -737,18 +749,19 @@ class EditPage {
         * @return bool
         */
        protected function previewOnOpen() {
-               global $wgRequest, $wgUser, $wgPreviewOnOpenNamespaces;
-               if ( $wgRequest->getVal( 'preview' ) == 'yes' ) {
+               global $wgPreviewOnOpenNamespaces;
+               $request = $this->context->getRequest();
+               if ( $request->getVal( 'preview' ) == 'yes' ) {
                        // Explicit override from request
                        return true;
-               } elseif ( $wgRequest->getVal( 'preview' ) == 'no' ) {
+               } elseif ( $request->getVal( 'preview' ) == 'no' ) {
                        // Explicit override from request
                        return false;
                } elseif ( $this->section == 'new' ) {
                        // Nothing *to* preview for new sections
                        return false;
-               } elseif ( ( $wgRequest->getVal( 'preload' ) !== null || $this->mTitle->exists() )
-                       && $wgUser->getOption( 'previewonfirst' )
+               } elseif ( ( $request->getVal( 'preload' ) !== null || $this->mTitle->exists() )
+                       && $this->context->getUser()->getOption( 'previewonfirst' )
                ) {
                        // Standard preference behavior
                        return true;
@@ -801,7 +814,7 @@ class EditPage {
         * @throws ErrorPageError
         */
        function importFormData( &$request ) {
-               global $wgContLang, $wgUser;
+               global $wgContLang;
 
                # Section edit can come from either the form or a link
                $this->section = $request->getVal( 'wpSection', $request->getVal( 'section' ) );
@@ -913,13 +926,14 @@ class EditPage {
                        $this->watchthis = $request->getCheck( 'wpWatchthis' );
 
                        # Don't force edit summaries when a user is editing their own user or talk page
+                       $user = $this->context->getUser();
                        if ( ( $this->mTitle->mNamespace == NS_USER || $this->mTitle->mNamespace == NS_USER_TALK )
-                               && $this->mTitle->getText() == $wgUser->getName()
+                               && $this->mTitle->getText() == $user->getName()
                        ) {
                                $this->allowBlankSummary = true;
                        } else {
                                $this->allowBlankSummary = $request->getBool( 'wpIgnoreBlankSummary' )
-                                       || !$wgUser->getOption( 'forceeditsummary' );
+                                       || !$user->getOption( 'forceeditsummary' );
                        }
 
                        $this->autoSumm = $request->getText( 'wpAutoSummary' );
@@ -1025,7 +1039,6 @@ class EditPage {
         * @return bool If the requested section is valid
         */
        function initialiseForm() {
-               global $wgUser;
                $this->edittime = $this->page->getTimestamp();
                $this->editRevId = $this->page->getLatest();
 
@@ -1034,20 +1047,21 @@ class EditPage {
                        return false;
                }
                $this->textbox1 = $this->toEditText( $content );
+               $user = $this->context->getUser();
 
                // activate checkboxes if user wants them to be always active
                # Sort out the "watch" checkbox
-               if ( $wgUser->getOption( 'watchdefault' ) ) {
+               if ( $user->getOption( 'watchdefault' ) ) {
                        # Watch all edits
                        $this->watchthis = true;
-               } elseif ( $wgUser->getOption( 'watchcreations' ) && !$this->mTitle->exists() ) {
+               } elseif ( $user->getOption( 'watchcreations' ) && !$this->mTitle->exists() ) {
                        # Watch creations
                        $this->watchthis = true;
-               } elseif ( $wgUser->isWatched( $this->mTitle ) ) {
+               } elseif ( $user->isWatched( $this->mTitle ) ) {
                        # Already watched
                        $this->watchthis = true;
                }
-               if ( $wgUser->getOption( 'minordefault' ) && !$this->isNew ) {
+               if ( $user->getOption( 'minordefault' ) && !$this->isNew ) {
                        $this->minoredit = true;
                }
                if ( $this->textbox1 === false ) {
@@ -1064,9 +1078,11 @@ class EditPage {
         * @since 1.21
         */
        protected function getContentObject( $def_content = null ) {
-               global $wgOut, $wgRequest, $wgUser, $wgContLang;
+               global $wgContLang;
 
                $content = false;
+               $request = $this->context->getRequest();
+               $user = $this->context->getUser();
 
                // For message page not locally set, use the i18n message.
                // For other non-existent articles, use preload text if any.
@@ -1079,10 +1095,10 @@ class EditPage {
                        }
                        if ( $content === false ) {
                                # If requested, preload some text.
-                               $preload = $wgRequest->getVal( 'preload',
+                               $preload = $request->getVal( 'preload',
                                        // Custom preload text for new sections
                                        $this->section === 'new' ? 'MediaWiki:addsection-preload' : '' );
-                               $params = $wgRequest->getArray( 'preloadparams', [] );
+                               $params = $request->getArray( 'preloadparams', [] );
 
                                $content = $this->getPreloadedContent( $preload, $params );
                        }
@@ -1090,15 +1106,15 @@ class EditPage {
                } else {
                        if ( $this->section != '' ) {
                                // Get section edit text (returns $def_text for invalid sections)
-                               $orig = $this->getOriginalContent( $wgUser );
+                               $orig = $this->getOriginalContent( $user );
                                $content = $orig ? $orig->getSection( $this->section ) : null;
 
                                if ( !$content ) {
                                        $content = $def_content;
                                }
                        } else {
-                               $undoafter = $wgRequest->getInt( 'undoafter' );
-                               $undo = $wgRequest->getInt( 'undo' );
+                               $undoafter = $request->getInt( 'undoafter' );
+                               $undo = $request->getInt( 'undo' );
 
                                if ( $undo > 0 && $undoafter > 0 ) {
                                        $undorev = Revision::newFromId( $undo );
@@ -1118,8 +1134,8 @@ class EditPage {
                                                        $undoMsg = 'failure';
                                                } else {
                                                        $oldContent = $this->page->getContent( Revision::RAW );
-                                                       $popts = ParserOptions::newFromUserAndLang( $wgUser, $wgContLang );
-                                                       $newContent = $content->preSaveTransform( $this->mTitle, $wgUser, $popts );
+                                                       $popts = ParserOptions::newFromUserAndLang( $user, $wgContLang );
+                                                       $newContent = $content->preSaveTransform( $this->mTitle, $user, $popts );
 
                                                        if ( $newContent->equals( $oldContent ) ) {
                                                                # Tell the user that the undo results in no change,
@@ -1166,12 +1182,13 @@ class EditPage {
 
                                        // Messages: undo-success, undo-failure, undo-norev, undo-nochange
                                        $class = ( $undoMsg == 'success' ? '' : 'error ' ) . "mw-undo-{$undoMsg}";
-                                       $this->editFormPageTop .= $wgOut->parse( "<div class=\"{$class}\">" .
+                                       $this->editFormPageTop .= $this->context->getOutput()->parse(
+                                               "<div class=\"{$class}\">" .
                                                wfMessage( 'undo-' . $undoMsg )->plain() . '</div>', true, /* interface */true );
                                }
 
                                if ( $content === false ) {
-                                       $content = $this->getOriginalContent( $wgUser );
+                                       $content = $this->getOriginalContent( $user );
                                }
                        }
                }
@@ -1368,10 +1385,10 @@ class EditPage {
         * @private
         */
        function tokenOk( &$request ) {
-               global $wgUser;
                $token = $request->getVal( 'wpEditToken' );
-               $this->mTokenOk = $wgUser->matchEditToken( $token );
-               $this->mTokenOkExceptSuffix = $wgUser->matchEditTokenNoSuffix( $token );
+               $user = $this->context->getUser();
+               $this->mTokenOk = $user->matchEditToken( $token );
+               $this->mTokenOkExceptSuffix = $user->matchEditTokenNoSuffix( $token );
                return $this->mTokenOk;
        }
 
@@ -1402,7 +1419,7 @@ class EditPage {
                        $val = 'restored';
                }
 
-               $response = RequestContext::getMain()->getRequest()->response();
+               $response = $this->context->getRequest()->response();
                $response->setCookie( $postEditKey, $val, time() + self::POST_EDIT_COOKIE_DURATION, [
                        'httpOnly' => false,
                ] );
@@ -1410,15 +1427,13 @@ class EditPage {
 
        /**
         * Attempt submission
-        * @param array $resultDetails See docs for $result in internalAttemptSave
+        * @param array|bool $resultDetails See docs for $result in internalAttemptSave
         * @throws UserBlockedError|ReadOnlyError|ThrottledError|PermissionsError
         * @return Status The resulting status object.
         */
        public function attemptSave( &$resultDetails = false ) {
-               global $wgUser;
-
                # Allow bots to exempt some edits from bot flagging
-               $bot = $wgUser->isAllowed( 'bot' ) && $this->bot;
+               $bot = $this->context->getUser()->isAllowed( 'bot' ) && $this->bot;
                $status = $this->internalAttemptSave( $resultDetails, $bot );
 
                Hooks::run( 'EditPage::attemptSave:after', [ $this, $status, $resultDetails ] );
@@ -1436,8 +1451,6 @@ class EditPage {
         * @return bool False, if output is done, true if rest of the form should be displayed
         */
        private function handleStatus( Status $status, $resultDetails ) {
-               global $wgUser, $wgOut;
-
                /**
                 * @todo FIXME: once the interface for internalAttemptSave() is made
                 *   nicer, this should use the message in $status
@@ -1451,9 +1464,11 @@ class EditPage {
                        }
                }
 
+               $out = $this->context->getOutput();
+
                // "wpExtraQueryRedirect" is a hidden input to modify
                // after save URL and is not used by actual edit form
-               $request = RequestContext::getMain()->getRequest();
+               $request = $this->context->getRequest();
                $extraQueryRedirect = $request->getVal( 'wpExtraQueryRedirect' );
 
                switch ( $status->value ) {
@@ -1474,7 +1489,7 @@ class EditPage {
 
                        case self::AS_CANNOT_USE_CUSTOM_MODEL:
                        case self::AS_PARSE_ERROR:
-                               $wgOut->addWikiText( '<div class="error">' . "\n" . $status->getWikiText() . '</div>' );
+                               $out->addWikiText( '<div class="error">' . "\n" . $status->getWikiText() . '</div>' );
                                return true;
 
                        case self::AS_SUCCESS_NEW_ARTICLE:
@@ -1487,7 +1502,7 @@ class EditPage {
                                        }
                                }
                                $anchor = isset( $resultDetails['sectionanchor'] ) ? $resultDetails['sectionanchor'] : '';
-                               $wgOut->redirect( $this->mTitle->getFullURL( $query ) . $anchor );
+                               $out->redirect( $this->mTitle->getFullURL( $query ) . $anchor );
                                return false;
 
                        case self::AS_SUCCESS_UPDATE:
@@ -1515,7 +1530,7 @@ class EditPage {
                                        }
                                }
 
-                               $wgOut->redirect( $this->mTitle->getFullURL( $extraQuery ) . $sectionanchor );
+                               $out->redirect( $this->mTitle->getFullURL( $extraQuery ) . $sectionanchor );
                                return false;
 
                        case self::AS_SPAM_ERROR:
@@ -1523,7 +1538,7 @@ class EditPage {
                                return false;
 
                        case self::AS_BLOCKED_PAGE_FOR_USER:
-                               throw new UserBlockedError( $wgUser->getBlock() );
+                               throw new UserBlockedError( $this->context->getUser()->getBlock() );
 
                        case self::AS_IMAGE_REDIRECT_ANON:
                        case self::AS_IMAGE_REDIRECT_LOGGED:
@@ -1584,7 +1599,7 @@ class EditPage {
 
                // Run new style post-section-merge edit filter
                if ( !Hooks::run( 'EditFilterMergedContent',
-                               [ $this->mArticle->getContext(), $content, $status, $this->summary,
+                               [ $this->context, $content, $status, $this->summary,
                                $user, $this->minoredit ] )
                ) {
                        # Error messages etc. could be handled within the hook...
@@ -1669,10 +1684,11 @@ class EditPage {
         * time.
         */
        function internalAttemptSave( &$result, $bot = false ) {
-               global $wgUser, $wgRequest, $wgParser, $wgMaxArticleSize;
-               global $wgContentHandlerUseDB;
+               global $wgParser, $wgMaxArticleSize, $wgContentHandlerUseDB;
 
                $status = Status::newGood();
+               $user = $this->context->getUser();
+               $request = $this->context->getRequest();
 
                if ( !Hooks::run( 'EditPage::attemptSave', [ $this ] ) ) {
                        wfDebug( "Hook 'EditPage::attemptSave' aborted article saving\n" );
@@ -1681,11 +1697,11 @@ class EditPage {
                        return $status;
                }
 
-               $spam = $wgRequest->getText( 'wpAntispam' );
+               $spam = $request->getText( 'wpAntispam' );
                if ( $spam !== '' ) {
                        wfDebugLog(
                                'SimpleAntiSpam',
-                               $wgUser->getName() .
+                               $user->getName() .
                                ' editing "' .
                                $this->mTitle->getPrefixedText() .
                                '" submitted bogus field "' .
@@ -1714,9 +1730,9 @@ class EditPage {
                # Check image redirect
                if ( $this->mTitle->getNamespace() == NS_FILE &&
                        $textbox_content->isRedirect() &&
-                       !$wgUser->isAllowed( 'upload' )
+                       !$user->isAllowed( 'upload' )
                ) {
-                               $code = $wgUser->isAnon() ? self::AS_IMAGE_REDIRECT_ANON : self::AS_IMAGE_REDIRECT_LOGGED;
+                               $code = $user->isAnon() ? self::AS_IMAGE_REDIRECT_ANON : self::AS_IMAGE_REDIRECT_LOGGED;
                                $status->setResult( false, $code );
 
                                return $status;
@@ -1741,7 +1757,7 @@ class EditPage {
                }
                if ( $match !== false ) {
                        $result['spam'] = $match;
-                       $ip = $wgRequest->getIP();
+                       $ip = $request->getIP();
                        $pdbk = $this->mTitle->getPrefixedDBkey();
                        $match = str_replace( "\n", '', $match );
                        wfDebugLog( 'SpamRegex', "$ip spam regex hit [[$pdbk]]: \"$match\"" );
@@ -1764,10 +1780,10 @@ class EditPage {
                        return $status;
                }
 
-               if ( $wgUser->isBlockedFrom( $this->mTitle, false ) ) {
+               if ( $user->isBlockedFrom( $this->mTitle, false ) ) {
                        // Auto-block user's IP if the account was "hard" blocked
                        if ( !wfReadOnly() ) {
-                               $wgUser->spreadAnyEditBlock();
+                               $user->spreadAnyEditBlock();
                        }
                        # Check block state against master, thus 'false'.
                        $status->setResult( false, self::AS_BLOCKED_PAGE_FOR_USER );
@@ -1782,8 +1798,8 @@ class EditPage {
                        return $status;
                }
 
-               if ( !$wgUser->isAllowed( 'edit' ) ) {
-                       if ( $wgUser->isAnon() ) {
+               if ( !$user->isAllowed( 'edit' ) ) {
+                       if ( $user->isAnon() ) {
                                $status->setResult( false, self::AS_READ_ONLY_PAGE_ANON );
                                return $status;
                        } else {
@@ -1799,7 +1815,7 @@ class EditPage {
                                $status->fatal( 'editpage-cannot-use-custom-model' );
                                $status->value = self::AS_CANNOT_USE_CUSTOM_MODEL;
                                return $status;
-                       } elseif ( !$wgUser->isAllowed( 'editcontentmodel' ) ) {
+                       } elseif ( !$user->isAllowed( 'editcontentmodel' ) ) {
                                $status->setResult( false, self::AS_NO_CHANGE_CONTENT_MODEL );
                                return $status;
 
@@ -1810,7 +1826,7 @@ class EditPage {
 
                if ( $this->changeTags ) {
                        $changeTagsStatus = ChangeTags::canAddTagsAccompanyingChange(
-                               $this->changeTags, $wgUser );
+                               $this->changeTags, $user );
                        if ( !$changeTagsStatus->isOK() ) {
                                $changeTagsStatus->value = self::AS_CHANGE_TAG_ERROR;
                                return $changeTagsStatus;
@@ -1822,7 +1838,7 @@ class EditPage {
                        $status->value = self::AS_READ_ONLY_PAGE;
                        return $status;
                }
-               if ( $wgUser->pingLimiter() || $wgUser->pingLimiter( 'linkpurge', 0 ) ) {
+               if ( $user->pingLimiter() || $user->pingLimiter( 'linkpurge', 0 ) ) {
                        $status->fatal( 'actionthrottledtext' );
                        $status->value = self::AS_RATE_LIMITED;
                        return $status;
@@ -1842,7 +1858,7 @@ class EditPage {
 
                if ( $new ) {
                        // Late check for create permission, just in case *PARANOIA*
-                       if ( !$this->mTitle->userCan( 'create', $wgUser ) ) {
+                       if ( !$this->mTitle->userCan( 'create', $user ) ) {
                                $status->fatal( 'nocreatetext' );
                                $status->value = self::AS_NO_CREATE_PERMISSION;
                                wfDebug( __METHOD__ . ": no create permission\n" );
@@ -1866,7 +1882,7 @@ class EditPage {
                                return $status;
                        }
 
-                       if ( !$this->runPostMergeFilters( $textbox_content, $status, $wgUser ) ) {
+                       if ( !$this->runPostMergeFilters( $textbox_content, $status, $user ) ) {
                                return $status;
                        }
 
@@ -1902,7 +1918,7 @@ class EditPage {
                        ) {
                                $this->isConflict = true;
                                if ( $this->section == 'new' ) {
-                                       if ( $this->page->getUserText() == $wgUser->getName() &&
+                                       if ( $this->page->getUserText() == $user->getName() &&
                                                $this->page->getComment() == $this->newSectionSummary()
                                        ) {
                                                // Probably a duplicate submission of a new comment.
@@ -1918,7 +1934,7 @@ class EditPage {
                                } elseif ( $this->section == ''
                                        && Revision::userWasLastToEdit(
                                                DB_MASTER, $this->mTitle->getArticleID(),
-                                               $wgUser->getId(), $this->edittime
+                                               $user->getId(), $this->edittime
                                        )
                                ) {
                                        # Suppress edit conflict with self, except for section edits where merging is required.
@@ -1988,7 +2004,7 @@ class EditPage {
                                return $status;
                        }
 
-                       if ( !$this->runPostMergeFilters( $content, $status, $wgUser ) ) {
+                       if ( !$this->runPostMergeFilters( $content, $status, $user ) ) {
                                return $status;
                        }
 
@@ -2009,7 +2025,7 @@ class EditPage {
                                        return $status;
                                }
                        } elseif ( !$this->allowBlankSummary
-                               && !$content->equals( $this->getOriginalContent( $wgUser ) )
+                               && !$content->equals( $this->getOriginalContent( $user ) )
                                && !$content->isRedirect()
                                && md5( $this->summary ) == $this->autoSumm
                        ) {
@@ -2079,7 +2095,7 @@ class EditPage {
                        $this->summary,
                        $flags,
                        false,
-                       $wgUser,
+                       $user,
                        $content->getDefaultFormat(),
                        $this->changeTags
                );
@@ -2102,7 +2118,7 @@ class EditPage {
                $result['nullEdit'] = $doEditStatus->hasMessage( 'edit-no-change' );
                if ( $result['nullEdit'] ) {
                        // We don't know if it was a null edit until now, so increment here
-                       $wgUser->pingLimiter( 'linkpurge' );
+                       $user->pingLimiter( 'linkpurge' );
                }
                $result['redirect'] = $content->isRedirect();
 
@@ -2111,7 +2127,7 @@ class EditPage {
                // If the content model changed, add a log entry
                if ( $changingContentModel ) {
                        $this->addContentModelChangeLogEntry(
-                               $wgUser,
+                               $user,
                                $new ? false : $oldContentModel,
                                $this->contentModel,
                                $this->summary
@@ -2145,13 +2161,12 @@ class EditPage {
         * Register the change of watch status
         */
        protected function updateWatchlist() {
-               global $wgUser;
+               $user = $this->context->getUser();
 
-               if ( !$wgUser->isLoggedIn() ) {
+               if ( !$user->isLoggedIn() ) {
                        return;
                }
 
-               $user = $wgUser;
                $title = $this->mTitle;
                $watch = $this->watchthis;
                // Do this in its own transaction to reduce contention...
@@ -2266,29 +2281,32 @@ class EditPage {
        }
 
        function setHeaders() {
-               global $wgOut, $wgUser, $wgAjaxEditStash;
+               global $wgAjaxEditStash;
+
+               $out = $this->context->getOutput();
+               $user = $this->context->getUser();
 
-               $wgOut->addModules( 'mediawiki.action.edit' );
-               $wgOut->addModuleStyles( 'mediawiki.action.edit.styles' );
+               $out->addModules( 'mediawiki.action.edit' );
+               $out->addModuleStyles( 'mediawiki.action.edit.styles' );
 
-               if ( $wgUser->getOption( 'showtoolbar' ) ) {
+               if ( $user->getOption( 'showtoolbar' ) ) {
                        // The addition of default buttons is handled by getEditToolbar() which
                        // has its own dependency on this module. The call here ensures the module
                        // is loaded in time (it has position "top") for other modules to register
                        // buttons (e.g. extensions, gadgets, user scripts).
-                       $wgOut->addModules( 'mediawiki.toolbar' );
+                       $out->addModules( 'mediawiki.toolbar' );
                }
 
-               if ( $wgUser->getOption( 'uselivepreview' ) ) {
-                       $wgOut->addModules( 'mediawiki.action.edit.preview' );
+               if ( $user->getOption( 'uselivepreview' ) ) {
+                       $out->addModules( 'mediawiki.action.edit.preview' );
                }
 
-               if ( $wgUser->getOption( 'useeditwarning' ) ) {
-                       $wgOut->addModules( 'mediawiki.action.edit.editWarning' );
+               if ( $user->getOption( 'useeditwarning' ) ) {
+                       $out->addModules( 'mediawiki.action.edit.editWarning' );
                }
 
                # Enabled article-related sidebar, toplinks, etc.
-               $wgOut->setArticleRelated( true );
+               $out->setArticleRelated( true );
 
                $contextTitle = $this->getContextTitle();
                if ( $this->isConflict ) {
@@ -2311,10 +2329,10 @@ class EditPage {
                if ( $displayTitle === false ) {
                        $displayTitle = $contextTitle->getPrefixedText();
                }
-               $wgOut->setPageTitle( wfMessage( $msg, $displayTitle ) );
+               $out->setPageTitle( wfMessage( $msg, $displayTitle ) );
                # Transmit the name of the message to JavaScript for live preview
                # Keep Resources.php/mediawiki.action.edit.preview in sync with the possible keys
-               $wgOut->addJsConfigVars( [
+               $out->addJsConfigVars( [
                        'wgEditMessage' => $msg,
                        'wgAjaxEditStash' => $wgAjaxEditStash,
                ] );
@@ -2324,16 +2342,16 @@ class EditPage {
         * Show all applicable editing introductions
         */
        protected function showIntro() {
-               global $wgOut, $wgUser;
                if ( $this->suppressIntro ) {
                        return;
                }
 
+               $out = $this->context->getOutput();
                $namespace = $this->mTitle->getNamespace();
 
                if ( $namespace == NS_MEDIAWIKI ) {
                        # Show a warning if editing an interface message
-                       $wgOut->wrapWikiMsg( "<div class='mw-editinginterface'>\n$1\n</div>", 'editinginterface' );
+                       $out->wrapWikiMsg( "<div class='mw-editinginterface'>\n$1\n</div>", 'editinginterface' );
                        # If this is a default message (but not css or js),
                        # show a hint that it is translatable on translatewiki.net
                        if ( !$this->mTitle->hasContentModel( CONTENT_MODEL_CSS )
@@ -2341,7 +2359,7 @@ class EditPage {
                        ) {
                                $defaultMessageText = $this->mTitle->getDefaultMessageText();
                                if ( $defaultMessageText !== false ) {
-                                       $wgOut->wrapWikiMsg( "<div class='mw-translateinterface'>\n$1\n</div>",
+                                       $out->wrapWikiMsg( "<div class='mw-translateinterface'>\n$1\n</div>",
                                                'translateinterface' );
                                }
                        }
@@ -2353,11 +2371,11 @@ class EditPage {
                                # there must be a description url to show a hint to shared repo
                                if ( $descUrl ) {
                                        if ( !$this->mTitle->exists() ) {
-                                               $wgOut->wrapWikiMsg( "<div class=\"mw-sharedupload-desc-create\">\n$1\n</div>", [
+                                               $out->wrapWikiMsg( "<div class=\"mw-sharedupload-desc-create\">\n$1\n</div>", [
                                                                        'sharedupload-desc-create', $file->getRepo()->getDisplayName(), $descUrl
                                                ] );
                                        } else {
-                                               $wgOut->wrapWikiMsg( "<div class=\"mw-sharedupload-desc-edit\">\n$1\n</div>", [
+                                               $out->wrapWikiMsg( "<div class=\"mw-sharedupload-desc-edit\">\n$1\n</div>", [
                                                                        'sharedupload-desc-edit', $file->getRepo()->getDisplayName(), $descUrl
                                                ] );
                                        }
@@ -2373,12 +2391,12 @@ class EditPage {
                        $ip = User::isIP( $username );
                        $block = Block::newFromTarget( $user, $user );
                        if ( !( $user && $user->isLoggedIn() ) && !$ip ) { # User does not exist
-                               $wgOut->wrapWikiMsg( "<div class=\"mw-userpage-userdoesnotexist error\">\n$1\n</div>",
+                               $out->wrapWikiMsg( "<div class=\"mw-userpage-userdoesnotexist error\">\n$1\n</div>",
                                        [ 'userpage-userdoesnotexist', wfEscapeWikiText( $username ) ] );
                        } elseif ( !is_null( $block ) && $block->getType() != Block::TYPE_AUTO ) {
                                # Show log extract if the user is currently blocked
                                LogEventsList::showLogExtract(
-                                       $wgOut,
+                                       $out,
                                        'block',
                                        MWNamespace::getCanonicalName( NS_USER ) . ':' . $block->getTarget(),
                                        '',
@@ -2398,8 +2416,8 @@ class EditPage {
                        $helpLink = wfExpandUrl( Skin::makeInternalOrExternalUrl(
                                wfMessage( 'helppage' )->inContentLanguage()->text()
                        ) );
-                       if ( $wgUser->isLoggedIn() ) {
-                               $wgOut->wrapWikiMsg(
+                       if ( $this->context->getUser()->isLoggedIn() ) {
+                               $out->wrapWikiMsg(
                                        // Suppress the external link icon, consider the help url an internal one
                                        "<div class=\"mw-newarticletext plainlinks\">\n$1\n</div>",
                                        [
@@ -2408,7 +2426,7 @@ class EditPage {
                                        ]
                                );
                        } else {
-                               $wgOut->wrapWikiMsg(
+                               $out->wrapWikiMsg(
                                        // Suppress the external link icon, consider the help url an internal one
                                        "<div class=\"mw-newarticletextanon plainlinks\">\n$1\n</div>",
                                        [
@@ -2420,7 +2438,7 @@ class EditPage {
                }
                # Give a notice if the user is editing a deleted/moved page...
                if ( !$this->mTitle->exists() ) {
-                       LogEventsList::showLogExtract( $wgOut, [ 'delete', 'move' ], $this->mTitle,
+                       LogEventsList::showLogExtract( $out, [ 'delete', 'move' ], $this->mTitle,
                                '',
                                [
                                        'lim' => 10,
@@ -2441,9 +2459,8 @@ class EditPage {
                if ( $this->editintro ) {
                        $title = Title::newFromText( $this->editintro );
                        if ( $title instanceof Title && $title->exists() && $title->userCan( 'read' ) ) {
-                               global $wgOut;
                                // Added using template syntax, to take <noinclude>'s into account.
-                               $wgOut->addWikiTextTitleTidy(
+                               $this->context->getOutput()->addWikiTextTitleTidy(
                                        '<div class="mw-editintro">{{:' . $title->getFullText() . '}}</div>',
                                        $this->mTitle
                                );
@@ -2493,7 +2510,7 @@ class EditPage {
         * content.
         *
         * @param string|null|bool $text Text to unserialize
-        * @return Content The content object created from $text. If $text was false
+        * @return Content|bool|null The content object created from $text. If $text was false
         *   or null, false resp. null will be  returned instead.
         *
         * @throws MWException If unserializing the text results in a Content
@@ -2525,8 +2542,6 @@ class EditPage {
         * use the EditPage::showEditForm:fields hook instead.
         */
        function showEditForm( $formCallback = null ) {
-               global $wgOut, $wgUser;
-
                # need to parse the preview early so that we know which templates are used,
                # otherwise users with "show preview after edit box" will get a blank list
                # we parse this near the beginning so that setHeaders can do the title
@@ -2536,7 +2551,8 @@ class EditPage {
                        $previewOutput = $this->getPreviewText();
                }
 
-               Hooks::run( 'EditPage::showEditForm:initial', [ &$this, &$wgOut ] );
+               $out = $this->context->getOutput();
+               Hooks::run( 'EditPage::showEditForm:initial', [ &$this, &$out ] );
 
                $this->setHeaders();
 
@@ -2544,13 +2560,14 @@ class EditPage {
                        return;
                }
 
-               $wgOut->addHTML( $this->editFormPageTop );
+               $out->addHTML( $this->editFormPageTop );
 
-               if ( $wgUser->getOption( 'previewontop' ) ) {
+               $user = $this->context->getUser();
+               if ( $user->getOption( 'previewontop' ) ) {
                        $this->displayPreviewArea( $previewOutput, true );
                }
 
-               $wgOut->addHTML( $this->editFormTextTop );
+               $out->addHTML( $this->editFormTextTop );
 
                $showToolbar = true;
                if ( $this->wasDeletedSinceLastEdit() ) {
@@ -2559,14 +2576,14 @@ class EditPage {
                                // Add an confirmation checkbox and explanation.
                                $showToolbar = false;
                        } else {
-                               $wgOut->wrapWikiMsg( "<div class='error mw-deleted-while-editing'>\n$1\n</div>",
+                               $out->wrapWikiMsg( "<div class='error mw-deleted-while-editing'>\n$1\n</div>",
                                        'deletedwhileediting' );
                        }
                }
 
                // @todo add EditForm plugin interface and use it here!
                //       search for textarea1 and textares2, and allow EditForm to override all uses.
-               $wgOut->addHTML( Html::openElement(
+               $out->addHTML( Html::openElement(
                        'form',
                        [
                                'id' => self::EDITFORM_ID,
@@ -2579,11 +2596,11 @@ class EditPage {
 
                if ( is_callable( $formCallback ) ) {
                        wfWarn( 'The $formCallback parameter to ' . __METHOD__ . 'is deprecated' );
-                       call_user_func_array( $formCallback, [ &$wgOut ] );
+                       call_user_func_array( $formCallback, [ &$out ] );
                }
 
                // Add an empty field to trip up spambots
-               $wgOut->addHTML(
+               $out->addHTML(
                        Xml::openElement( 'div', [ 'id' => 'antispam-container', 'style' => 'display: none;' ] )
                        . Html::rawElement(
                                'label',
@@ -2602,7 +2619,7 @@ class EditPage {
                        . Xml::closeElement( 'div' )
                );
 
-               Hooks::run( 'EditPage::showEditForm:fields', [ &$this, &$wgOut ] );
+               Hooks::run( 'EditPage::showEditForm:fields', [ &$this, &$out ] );
 
                // Put these up at the top to ensure they aren't lost on early form submission
                $this->showFormBeforeText();
@@ -2616,7 +2633,7 @@ class EditPage {
                        $key = $comment === ''
                                ? 'confirmrecreate-noreason'
                                : 'confirmrecreate';
-                       $wgOut->addHTML(
+                       $out->addHTML(
                                '<div class="mw-confirm-recreate">' .
                                        wfMessage( $key, $username, "<nowiki>$comment</nowiki>" )->parse() .
                                Xml::checkLabel( wfMessage( 'recreate' )->text(), 'wpRecreate', 'wpRecreate', false,
@@ -2628,7 +2645,7 @@ class EditPage {
 
                # When the summary is hidden, also hide them on preview/show changes
                if ( $this->nosummary ) {
-                       $wgOut->addHTML( Html::hidden( 'nosummary', true ) );
+                       $out->addHTML( Html::hidden( 'nosummary', true ) );
                }
 
                # If a blank edit summary was previously provided, and the appropriate
@@ -2639,15 +2656,15 @@ class EditPage {
                # For a bit more sophisticated detection of blank summaries, hash the
                # automatic one and pass that in the hidden field wpAutoSummary.
                if ( $this->missingSummary || ( $this->section == 'new' && $this->nosummary ) ) {
-                       $wgOut->addHTML( Html::hidden( 'wpIgnoreBlankSummary', true ) );
+                       $out->addHTML( Html::hidden( 'wpIgnoreBlankSummary', true ) );
                }
 
                if ( $this->undidRev ) {
-                       $wgOut->addHTML( Html::hidden( 'wpUndidRevision', $this->undidRev ) );
+                       $out->addHTML( Html::hidden( 'wpUndidRevision', $this->undidRev ) );
                }
 
                if ( $this->selfRedirect ) {
-                       $wgOut->addHTML( Html::hidden( 'wpIgnoreSelfRedirect', true ) );
+                       $out->addHTML( Html::hidden( 'wpIgnoreSelfRedirect', true ) );
                }
 
                if ( $this->hasPresetSummary ) {
@@ -2658,27 +2675,27 @@ class EditPage {
                }
 
                $autosumm = $this->autoSumm ? $this->autoSumm : md5( $this->summary );
-               $wgOut->addHTML( Html::hidden( 'wpAutoSummary', $autosumm ) );
+               $out->addHTML( Html::hidden( 'wpAutoSummary', $autosumm ) );
 
-               $wgOut->addHTML( Html::hidden( 'oldid', $this->oldid ) );
-               $wgOut->addHTML( Html::hidden( 'parentRevId', $this->getParentRevId() ) );
+               $out->addHTML( Html::hidden( 'oldid', $this->oldid ) );
+               $out->addHTML( Html::hidden( 'parentRevId', $this->getParentRevId() ) );
 
-               $wgOut->addHTML( Html::hidden( 'format', $this->contentFormat ) );
-               $wgOut->addHTML( Html::hidden( 'model', $this->contentModel ) );
+               $out->addHTML( Html::hidden( 'format', $this->contentFormat ) );
+               $out->addHTML( Html::hidden( 'model', $this->contentModel ) );
 
                if ( $this->section == 'new' ) {
                        $this->showSummaryInput( true, $this->summary );
-                       $wgOut->addHTML( $this->getSummaryPreview( true, $this->summary ) );
+                       $out->addHTML( $this->getSummaryPreview( true, $this->summary ) );
                }
 
-               $wgOut->addHTML( $this->editFormTextBeforeContent );
+               $out->addHTML( $this->editFormTextBeforeContent );
 
-               if ( !$this->isCssJsSubpage && $showToolbar && $wgUser->getOption( 'showtoolbar' ) ) {
-                       $wgOut->addHTML( EditPage::getEditToolbar( $this->mTitle ) );
+               if ( !$this->isCssJsSubpage && $showToolbar && $user->getOption( 'showtoolbar' ) ) {
+                       $out->addHTML( EditPage::getEditToolbar( $this->mTitle ) );
                }
 
                if ( $this->blankArticle ) {
-                       $wgOut->addHTML( Html::hidden( 'wpIgnoreBlankArticle', true ) );
+                       $out->addHTML( Html::hidden( 'wpIgnoreBlankArticle', true ) );
                }
 
                if ( $this->isConflict ) {
@@ -2696,7 +2713,7 @@ class EditPage {
                        $this->showContentForm();
                }
 
-               $wgOut->addHTML( $this->editFormTextAfterContent );
+               $out->addHTML( $this->editFormTextAfterContent );
 
                $this->showStandardInputs();
 
@@ -2706,19 +2723,19 @@ class EditPage {
 
                $this->showEditTools();
 
-               $wgOut->addHTML( $this->editFormTextAfterTools . "\n" );
+               $out->addHTML( $this->editFormTextAfterTools . "\n" );
 
-               $wgOut->addHTML( Html::rawElement( 'div', [ 'class' => 'templatesUsed' ],
+               $out->addHTML( Html::rawElement( 'div', [ 'class' => 'templatesUsed' ],
                        Linker::formatTemplates( $this->getTemplates(), $this->preview, $this->section != '' ) ) );
 
-               $wgOut->addHTML( Html::rawElement( 'div', [ 'class' => 'hiddencats' ],
+               $out->addHTML( Html::rawElement( 'div', [ 'class' => 'hiddencats' ],
                        Linker::formatHiddenCategories( $this->page->getHiddenCategories() ) ) );
 
                if ( $this->mParserOutput ) {
-                       $wgOut->setLimitReportData( $this->mParserOutput->getLimitReportData() );
+                       $out->setLimitReportData( $this->mParserOutput->getLimitReportData() );
                }
 
-               $wgOut->addModules( 'mediawiki.action.edit.collapsibleFooter' );
+               $out->addModules( 'mediawiki.action.edit.collapsibleFooter' );
 
                if ( $this->isConflict ) {
                        try {
@@ -2731,7 +2748,7 @@ class EditPage {
                                        $this->contentFormat,
                                        $ex->getMessage()
                                );
-                               $wgOut->addWikiText( '<div class="error">' . $msg->text() . '</div>' );
+                               $out->addWikiText( '<div class="error">' . $msg->text() . '</div>' );
                        }
                }
 
@@ -2745,14 +2762,14 @@ class EditPage {
                } else {
                        $mode = 'text';
                }
-               $wgOut->addHTML( Html::hidden( 'mode', $mode, [ 'id' => 'mw-edit-mode' ] ) );
+               $out->addHTML( Html::hidden( 'mode', $mode, [ 'id' => 'mw-edit-mode' ] ) );
 
                // Marker for detecting truncated form data.  This must be the last
                // parameter sent in order to be of use, so do not move me.
-               $wgOut->addHTML( Html::hidden( 'wpUltimateParam', true ) );
-               $wgOut->addHTML( $this->editFormTextBottom . "\n</form>\n" );
+               $out->addHTML( Html::hidden( 'wpUltimateParam', true ) );
+               $out->addHTML( $this->editFormTextBottom . "\n</form>\n" );
 
-               if ( !$wgUser->getOption( 'previewontop' ) ) {
+               if ( !$user->getOption( 'previewontop' ) ) {
                        $this->displayPreviewArea( $previewOutput, false );
                }
 
@@ -2778,21 +2795,23 @@ class EditPage {
         * @return bool
         */
        protected function showHeader() {
-               global $wgOut, $wgUser, $wgMaxArticleSize, $wgLang;
-               global $wgAllowUserCss, $wgAllowUserJs;
+               global $wgMaxArticleSize, $wgAllowUserCss, $wgAllowUserJs;
+
+               $out = $this->context->getOutput();
+               $user = $this->context->getUser();
 
                if ( $this->mTitle->isTalkPage() ) {
-                       $wgOut->addWikiMsg( 'talkpagetext' );
+                       $out->addWikiMsg( 'talkpagetext' );
                }
 
                // Add edit notices
                $editNotices = $this->mTitle->getEditNotices( $this->oldid );
                if ( count( $editNotices ) ) {
-                       $wgOut->addHTML( implode( "\n", $editNotices ) );
+                       $out->addHTML( implode( "\n", $editNotices ) );
                } else {
                        $msg = wfMessage( 'editnotice-notext' );
                        if ( !$msg->isDisabled() ) {
-                               $wgOut->addHTML(
+                               $out->addHTML(
                                        '<div class="mw-editnotice-notext">'
                                        . $msg->parseAsBlock()
                                        . '</div>'
@@ -2801,14 +2820,14 @@ class EditPage {
                }
 
                if ( $this->isConflict ) {
-                       $wgOut->wrapWikiMsg( "<div class='mw-explainconflict'>\n$1\n</div>", 'explainconflict' );
+                       $out->wrapWikiMsg( "<div class='mw-explainconflict'>\n$1\n</div>", 'explainconflict' );
                        $this->editRevId = $this->page->getLatest();
                } else {
                        if ( $this->section != '' && !$this->isSectionEditSupported() ) {
                                // We use $this->section to much before this and getVal('wgSection') directly in other places
                                // at this point we can't reset $this->section to '' to fallback to non-section editing.
                                // Someone is welcome to try refactoring though
-                               $wgOut->showErrorPage( 'sectioneditnotsupported-title', 'sectioneditnotsupported-text' );
+                               $out->showErrorPage( 'sectioneditnotsupported-title', 'sectioneditnotsupported-text' );
                                return false;
                        }
 
@@ -2822,31 +2841,31 @@ class EditPage {
                        }
 
                        if ( $this->missingComment ) {
-                               $wgOut->wrapWikiMsg( "<div id='mw-missingcommenttext'>\n$1\n</div>", 'missingcommenttext' );
+                               $out->wrapWikiMsg( "<div id='mw-missingcommenttext'>\n$1\n</div>", 'missingcommenttext' );
                        }
 
                        if ( $this->missingSummary && $this->section != 'new' ) {
-                               $wgOut->wrapWikiMsg( "<div id='mw-missingsummary'>\n$1\n</div>", 'missingsummary' );
+                               $out->wrapWikiMsg( "<div id='mw-missingsummary'>\n$1\n</div>", 'missingsummary' );
                        }
 
                        if ( $this->missingSummary && $this->section == 'new' ) {
-                               $wgOut->wrapWikiMsg( "<div id='mw-missingcommentheader'>\n$1\n</div>", 'missingcommentheader' );
+                               $out->wrapWikiMsg( "<div id='mw-missingcommentheader'>\n$1\n</div>", 'missingcommentheader' );
                        }
 
                        if ( $this->blankArticle ) {
-                               $wgOut->wrapWikiMsg( "<div id='mw-blankarticle'>\n$1\n</div>", 'blankarticle' );
+                               $out->wrapWikiMsg( "<div id='mw-blankarticle'>\n$1\n</div>", 'blankarticle' );
                        }
 
                        if ( $this->selfRedirect ) {
-                               $wgOut->wrapWikiMsg( "<div id='mw-selfredirect'>\n$1\n</div>", 'selfredirect' );
+                               $out->wrapWikiMsg( "<div id='mw-selfredirect'>\n$1\n</div>", 'selfredirect' );
                        }
 
                        if ( $this->hookError !== '' ) {
-                               $wgOut->addWikiText( $this->hookError );
+                               $out->addWikiText( $this->hookError );
                        }
 
                        if ( !$this->checkUnicodeCompliantBrowser() ) {
-                               $wgOut->addWikiMsg( 'nonunicodebrowser' );
+                               $out->addWikiMsg( 'nonunicodebrowser' );
                        }
 
                        if ( $this->section != 'new' ) {
@@ -2854,13 +2873,13 @@ class EditPage {
                                if ( $revision ) {
                                        // Let sysop know that this will make private content public if saved
 
-                                       if ( !$revision->userCan( Revision::DELETED_TEXT, $wgUser ) ) {
-                                               $wgOut->wrapWikiMsg(
+                                       if ( !$revision->userCan( Revision::DELETED_TEXT, $user ) ) {
+                                               $out->wrapWikiMsg(
                                                        "<div class='mw-warning plainlinks'>\n$1\n</div>\n",
                                                        'rev-deleted-text-permission'
                                                );
                                        } elseif ( $revision->isDeleted( Revision::DELETED_TEXT ) ) {
-                                               $wgOut->wrapWikiMsg(
+                                               $out->wrapWikiMsg(
                                                        "<div class='mw-warning plainlinks'>\n$1\n</div>\n",
                                                        'rev-deleted-text-view'
                                                );
@@ -2868,25 +2887,25 @@ class EditPage {
 
                                        if ( !$revision->isCurrent() ) {
                                                $this->mArticle->setOldSubtitle( $revision->getId() );
-                                               $wgOut->addWikiMsg( 'editingold' );
+                                               $out->addWikiMsg( 'editingold' );
                                        }
                                } elseif ( $this->mTitle->exists() ) {
                                        // Something went wrong
 
-                                       $wgOut->wrapWikiMsg( "<div class='errorbox'>\n$1\n</div>\n",
+                                       $out->wrapWikiMsg( "<div class='errorbox'>\n$1\n</div>\n",
                                                [ 'missing-revision', $this->oldid ] );
                                }
                        }
                }
 
                if ( wfReadOnly() ) {
-                       $wgOut->wrapWikiMsg(
+                       $out->wrapWikiMsg(
                                "<div id=\"mw-read-only-warning\">\n$1\n</div>",
                                [ 'readonlywarning', wfReadOnlyReason() ]
                        );
-               } elseif ( $wgUser->isAnon() ) {
+               } elseif ( $user->isAnon() ) {
                        if ( $this->formtype != 'preview' ) {
-                               $wgOut->wrapWikiMsg(
+                               $out->wrapWikiMsg(
                                        "<div id='mw-anon-edit-warning' class='warningbox'>\n$1\n</div>",
                                        [ 'anoneditwarning',
                                                // Log-in link
@@ -2900,7 +2919,7 @@ class EditPage {
                                        ]
                                );
                        } else {
-                               $wgOut->wrapWikiMsg( "<div id=\"mw-anon-preview-warning\" class=\"warningbox\">\n$1</div>",
+                               $out->wrapWikiMsg( "<div id=\"mw-anon-preview-warning\" class=\"warningbox\">\n$1</div>",
                                        'anonpreviewwarning'
                                );
                        }
@@ -2908,22 +2927,25 @@ class EditPage {
                        if ( $this->isCssJsSubpage ) {
                                # Check the skin exists
                                if ( $this->isWrongCaseCssJsPage ) {
-                                       $wgOut->wrapWikiMsg(
+                                       $out->wrapWikiMsg(
                                                "<div class='error' id='mw-userinvalidcssjstitle'>\n$1\n</div>",
                                                [ 'userinvalidcssjstitle', $this->mTitle->getSkinFromCssJsSubpage() ]
                                        );
                                }
-                               if ( $this->getTitle()->isSubpageOf( $wgUser->getUserPage() ) ) {
+                               if ( $this->getTitle()->isSubpageOf( $user->getUserPage() ) ) {
+                                       $out->wrapWikiMsg( '<div class="mw-usercssjspublic">$1</div>',
+                                               $this->isCssSubpage ? 'usercssispublic' : 'userjsispublic'
+                                       );
                                        if ( $this->formtype !== 'preview' ) {
                                                if ( $this->isCssSubpage && $wgAllowUserCss ) {
-                                                       $wgOut->wrapWikiMsg(
+                                                       $out->wrapWikiMsg(
                                                                "<div id='mw-usercssyoucanpreview'>\n$1\n</div>",
                                                                [ 'usercssyoucanpreview' ]
                                                        );
                                                }
 
                                                if ( $this->isJsSubpage && $wgAllowUserJs ) {
-                                                       $wgOut->wrapWikiMsg(
+                                                       $out->wrapWikiMsg(
                                                                "<div id='mw-userjsyoucanpreview'>\n$1\n</div>",
                                                                [ 'userjsyoucanpreview' ]
                                                        );
@@ -2943,7 +2965,7 @@ class EditPage {
                                # Then it must be protected based on static groups (regular)
                                $noticeMsg = 'protectedpagewarning';
                        }
-                       LogEventsList::showLogExtract( $wgOut, 'protect', $this->mTitle, '',
+                       LogEventsList::showLogExtract( $out, 'protect', $this->mTitle, '',
                                [ 'lim' => 1, 'msgKey' => [ $noticeMsg ] ] );
                }
                if ( $this->mTitle->isCascadeProtected() ) {
@@ -2959,10 +2981,10 @@ class EditPage {
                                }
                        }
                        $notice .= '</div>';
-                       $wgOut->wrapWikiMsg( $notice, [ 'cascadeprotectedwarning', $cascadeSourcesCount ] );
+                       $out->wrapWikiMsg( $notice, [ 'cascadeprotectedwarning', $cascadeSourcesCount ] );
                }
                if ( !$this->mTitle->exists() && $this->mTitle->getRestrictions( 'create' ) ) {
-                       LogEventsList::showLogExtract( $wgOut, 'protect', $this->mTitle, '',
+                       LogEventsList::showLogExtract( $out, 'protect', $this->mTitle, '',
                                [ 'lim' => 1,
                                        'showIfEmpty' => false,
                                        'msgKey' => [ 'titleprotectedwarning' ],
@@ -2973,20 +2995,21 @@ class EditPage {
                        $this->contentLength = strlen( $this->textbox1 );
                }
 
+               $lang = $this->context->getLanguage();
                if ( $this->tooBig || $this->contentLength > $wgMaxArticleSize * 1024 ) {
-                       $wgOut->wrapWikiMsg( "<div class='error' id='mw-edit-longpageerror'>\n$1\n</div>",
+                       $out->wrapWikiMsg( "<div class='error' id='mw-edit-longpageerror'>\n$1\n</div>",
                                [
                                        'longpageerror',
-                                       $wgLang->formatNum( round( $this->contentLength / 1024, 3 ) ),
-                                       $wgLang->formatNum( $wgMaxArticleSize )
+                                       $lang->formatNum( round( $this->contentLength / 1024, 3 ) ),
+                                       $lang->formatNum( $wgMaxArticleSize )
                                ]
                        );
                } else {
                        if ( !wfMessage( 'longpage-hint' )->isDisabled() ) {
-                               $wgOut->wrapWikiMsg( "<div id='mw-edit-longpage-hint'>\n$1\n</div>",
+                               $out->wrapWikiMsg( "<div id='mw-edit-longpage-hint'>\n$1\n</div>",
                                        [
                                                'longpage-hint',
-                                               $wgLang->formatSize( strlen( $this->textbox1 ) ),
+                                               $lang->formatSize( strlen( $this->textbox1 ) ),
                                                strlen( $this->textbox1 )
                                        ]
                                );
@@ -3051,7 +3074,6 @@ class EditPage {
         * @param string $summary The text of the summary to display
         */
        protected function showSummaryInput( $isSubjectPreview, $summary = "" ) {
-               global $wgOut;
                # Add a class if 'missingsummary' is triggered to allow styling of the summary line
                $summaryClass = $this->missingSummary ? 'mw-summarymissed' : 'mw-summary';
                if ( $isSubjectPreview ) {
@@ -3070,7 +3092,7 @@ class EditPage {
                        [ 'class' => $summaryClass ],
                        []
                );
-               $wgOut->addHTML( "{$label} {$input}" );
+               $this->context->getOutput()->addHTML( "{$label} {$input}" );
        }
 
        /**
@@ -3102,9 +3124,9 @@ class EditPage {
        }
 
        protected function showFormBeforeText() {
-               global $wgOut;
                $section = htmlspecialchars( $this->section );
-               $wgOut->addHTML( <<<HTML
+               $out = $this->context->getOutput();
+               $out->addHTML( <<<HTML
 <input type='hidden' value="{$section}" name="wpSection"/>
 <input type='hidden' value="{$this->starttime}" name="wpStarttime" />
 <input type='hidden' value="{$this->edittime}" name="wpEdittime" />
@@ -3114,12 +3136,11 @@ class EditPage {
 HTML
                );
                if ( !$this->checkUnicodeCompliantBrowser() ) {
-                       $wgOut->addHTML( Html::hidden( 'safemode', '1' ) );
+                       $out->addHTML( Html::hidden( 'safemode', '1' ) );
                }
        }
 
        protected function showFormAfterText() {
-               global $wgOut, $wgUser;
                /**
                 * To make it harder for someone to slip a user a page
                 * which submits an edit form to the wiki without their
@@ -3132,7 +3153,10 @@ HTML
                 * include the constant suffix to prevent editing from
                 * broken text-mangling proxies.
                 */
-               $wgOut->addHTML( "\n" . Html::hidden( "wpEditToken", $wgUser->getEditToken() ) . "\n" );
+               $token = $this->context->getUser()->getEditToken();
+               $this->context->getOutput()->addHTML(
+                       "\n" . Html::hidden( "wpEditToken", $token ) . "\n"
+               );
        }
 
        /**
@@ -3202,8 +3226,6 @@ HTML
        }
 
        protected function showTextbox( $text, $name, $customAttribs = [] ) {
-               global $wgOut, $wgUser;
-
                $wikitext = $this->safeUnicodeOutput( $text );
                if ( strval( $wikitext ) !== '' ) {
                        // Ensure there's a newline at the end, otherwise adding lines
@@ -3213,11 +3235,12 @@ HTML
                        $wikitext .= "\n";
                }
 
+               $user = $this->context->getUser();
                $attribs = $customAttribs + [
                        'accesskey' => ',',
                        'id' => $name,
-                       'cols' => $wgUser->getIntOption( 'cols' ),
-                       'rows' => $wgUser->getIntOption( 'rows' ),
+                       'cols' => $user->getIntOption( 'cols' ),
+                       'rows' => $user->getIntOption( 'rows' ),
                        // Avoid PHP notices when appending preferences
                        // (appending allows customAttribs['style'] to still work).
                        'style' => ''
@@ -3227,11 +3250,10 @@ HTML
                $attribs['lang'] = $pageLang->getHtmlCode();
                $attribs['dir'] = $pageLang->getDir();
 
-               $wgOut->addHTML( Html::textarea( $name, $wikitext, $attribs ) );
+               $this->context->getOutput()->addHTML( Html::textarea( $name, $wikitext, $attribs ) );
        }
 
        protected function displayPreviewArea( $previewOutput, $isOnTop = false ) {
-               global $wgOut;
                $classes = [];
                if ( $isOnTop ) {
                        $classes[] = 'ontop';
@@ -3243,7 +3265,8 @@ HTML
                        $attribs['style'] = 'display: none;';
                }
 
-               $wgOut->addHTML( Xml::openElement( 'div', $attribs ) );
+               $out = $this->context->getOutput();
+               $out->addHTML( Xml::openElement( 'div', $attribs ) );
 
                if ( $this->formtype == 'preview' ) {
                        $this->showPreview( $previewOutput );
@@ -3252,10 +3275,10 @@ HTML
                        $pageViewLang = $this->mTitle->getPageViewLanguage();
                        $attribs = [ 'lang' => $pageViewLang->getHtmlCode(), 'dir' => $pageViewLang->getDir(),
                                'class' => 'mw-content-' . $pageViewLang->getDir() ];
-                       $wgOut->addHTML( Html::rawElement( 'div', $attribs ) );
+                       $out->addHTML( Html::rawElement( 'div', $attribs ) );
                }
 
-               $wgOut->addHTML( '</div>' );
+               $out->addHTML( '</div>' );
 
                if ( $this->formtype == 'diff' ) {
                        try {
@@ -3267,7 +3290,7 @@ HTML
                                        $this->contentFormat,
                                        $ex->getMessage()
                                );
-                               $wgOut->addWikiText( '<div class="error">' . $msg->text() . '</div>' );
+                               $out->addWikiText( '<div class="error">' . $msg->text() . '</div>' );
                        }
                }
        }
@@ -3279,14 +3302,14 @@ HTML
         * @param string $text The HTML to be output for the preview.
         */
        protected function showPreview( $text ) {
-               global $wgOut;
                if ( $this->mTitle->getNamespace() == NS_CATEGORY ) {
                        $this->mArticle->openShowCategory();
                }
+               $out = $this->context->getOutput();
                # This hook seems slightly odd here, but makes things more
                # consistent for extensions.
-               Hooks::run( 'OutputPageBeforeHTML', [ &$wgOut, &$text ] );
-               $wgOut->addHTML( $text );
+               Hooks::run( 'OutputPageBeforeHTML', [ &$out, &$text ] );
+               $out->addHTML( $text );
                if ( $this->mTitle->getNamespace() == NS_CATEGORY ) {
                        $this->mArticle->closeShowCategory();
                }
@@ -3300,7 +3323,7 @@ HTML
         * save and then make a comparison.
         */
        function showDiff() {
-               global $wgUser, $wgContLang, $wgOut;
+               global $wgContLang;
 
                $oldtitlemsg = 'currentrev';
                # if message does not exist, show diff against the preloaded default
@@ -3331,8 +3354,9 @@ HTML
                        ContentHandler::runLegacyHooks( 'EditPageGetDiffText', [ $this, &$newContent ] );
                        Hooks::run( 'EditPageGetDiffContent', [ $this, &$newContent ] );
 
-                       $popts = ParserOptions::newFromUserAndLang( $wgUser, $wgContLang );
-                       $newContent = $newContent->preSaveTransform( $this->mTitle, $wgUser, $popts );
+                       $user = $this->context->getUser();
+                       $popts = ParserOptions::newFromUserAndLang( $user, $wgContLang );
+                       $newContent = $newContent->preSaveTransform( $this->mTitle, $user, $popts );
                }
 
                if ( ( $oldContent && !$oldContent->isEmpty() ) || ( $newContent && !$newContent->isEmpty() ) ) {
@@ -3356,7 +3380,7 @@ HTML
                        $difftext = '';
                }
 
-               $wgOut->addHTML( '<div id="wikiDiff">' . $difftext . '</div>' );
+               $this->context->getOutput()->addHTML( '<div id="wikiDiff">' . $difftext . '</div>' );
        }
 
        /**
@@ -3365,8 +3389,7 @@ HTML
        protected function showHeaderCopyrightWarning() {
                $msg = 'editpage-head-copy-warn';
                if ( !wfMessage( $msg )->isDisabled() ) {
-                       global $wgOut;
-                       $wgOut->wrapWikiMsg( "<div class='editpage-head-copywarn'>\n$1\n</div>",
+                       $this->context->getOutput()->wrapWikiMsg( "<div class='editpage-head-copywarn'>\n$1\n</div>",
                                'editpage-head-copy-warn' );
                }
        }
@@ -3383,16 +3406,15 @@ HTML
                $msg = 'editpage-tos-summary';
                Hooks::run( 'EditPageTosSummary', [ $this->mTitle, &$msg ] );
                if ( !wfMessage( $msg )->isDisabled() ) {
-                       global $wgOut;
-                       $wgOut->addHTML( '<div class="mw-tos-summary">' );
-                       $wgOut->addWikiMsg( $msg );
-                       $wgOut->addHTML( '</div>' );
+                       $out = $this->context->getOutput();
+                       $out->addHTML( '<div class="mw-tos-summary">' );
+                       $out->addWikiMsg( $msg );
+                       $out->addHTML( '</div>' );
                }
        }
 
        protected function showEditTools() {
-               global $wgOut;
-               $wgOut->addHTML( '<div class="mw-editTools">' .
+               $this->context->getOutput()->addHTML( '<div class="mw-editTools">' .
                        wfMessage( 'edittools' )->inContentLanguage()->parse() .
                        '</div>' );
        }
@@ -3452,24 +3474,24 @@ HTML
        }
 
        protected function showStandardInputs( &$tabindex = 2 ) {
-               global $wgOut;
-               $wgOut->addHTML( "<div class='editOptions'>\n" );
+               $out = $this->context->getOutput();
+               $out->addHTML( "<div class='editOptions'>\n" );
 
                if ( $this->section != 'new' ) {
                        $this->showSummaryInput( false, $this->summary );
-                       $wgOut->addHTML( $this->getSummaryPreview( false, $this->summary ) );
+                       $out->addHTML( $this->getSummaryPreview( false, $this->summary ) );
                }
 
                $checkboxes = $this->getCheckboxes( $tabindex,
                        [ 'minor' => $this->minoredit, 'watch' => $this->watchthis ] );
-               $wgOut->addHTML( "<div class='editCheckboxes'>" . implode( $checkboxes, "\n" ) . "</div>\n" );
+               $out->addHTML( "<div class='editCheckboxes'>" . implode( $checkboxes, "\n" ) . "</div>\n" );
 
                // Show copyright warning.
-               $wgOut->addWikiText( $this->getCopywarn() );
-               $wgOut->addHTML( $this->editFormTextAfterWarn );
+               $out->addWikiText( $this->getCopywarn() );
+               $out->addHTML( $this->editFormTextAfterWarn );
 
-               $wgOut->addHTML( "<div class='editButtons'>\n" );
-               $wgOut->addHTML( implode( $this->getEditButtons( $tabindex ), "\n" ) . "\n" );
+               $out->addHTML( "<div class='editButtons'>\n" );
+               $out->addHTML( implode( $this->getEditButtons( $tabindex ), "\n" ) . "\n" );
 
                $cancel = $this->getCancelLink();
                if ( $cancel !== '' ) {
@@ -3489,13 +3511,13 @@ HTML
                        wfMessage( 'word-separator' )->escaped() .
                        wfMessage( 'newwindow' )->parse();
 
-               $wgOut->addHTML( "      <span class='cancelLink'>{$cancel}</span>\n" );
-               $wgOut->addHTML( "      <span class='editHelp'>{$edithelp}</span>\n" );
-               $wgOut->addHTML( "</div><!-- editButtons -->\n" );
+               $out->addHTML( "        <span class='cancelLink'>{$cancel}</span>\n" );
+               $out->addHTML( "        <span class='editHelp'>{$edithelp}</span>\n" );
+               $out->addHTML( "</div><!-- editButtons -->\n" );
 
-               Hooks::run( 'EditPage::showStandardInputs:options', [ $this, $wgOut, &$tabindex ] );
+               Hooks::run( 'EditPage::showStandardInputs:options', [ $this, $out, &$tabindex ] );
 
-               $wgOut->addHTML( "</div><!-- editOptions -->\n" );
+               $out->addHTML( "</div><!-- editOptions -->\n" );
        }
 
        /**
@@ -3503,10 +3525,9 @@ HTML
         * If you want to use another entry point to this function, be careful.
         */
        protected function showConflict() {
-               global $wgOut;
-
-               if ( Hooks::run( 'EditPageBeforeConflictDiff', [ &$this, &$wgOut ] ) ) {
-                       $stats = $wgOut->getContext()->getStats();
+               $out = $this->context->getOutput();
+               if ( Hooks::run( 'EditPageBeforeConflictDiff', [ &$this, &$out ] ) ) {
+                       $stats = $this->context->getStats();
                        $stats->increment( 'edit.failures.conflict' );
                        // Only include 'standard' namespaces to avoid creating unknown numbers of statsd metrics
                        if (
@@ -3516,7 +3537,7 @@ HTML
                                $stats->increment( 'edit.failures.conflict.byNamespaceId.' . $this->mTitle->getNamespace() );
                        }
 
-                       $wgOut->wrapWikiMsg( '<h2>$1</h2>', "yourdiff" );
+                       $out->wrapWikiMsg( '<h2>$1</h2>', "yourdiff" );
 
                        $content1 = $this->toEditContent( $this->textbox1 );
                        $content2 = $this->toEditContent( $this->textbox2 );
@@ -3529,7 +3550,7 @@ HTML
                                wfMessage( 'storedversion' )->text()
                        );
 
-                       $wgOut->wrapWikiMsg( '<h2>$1</h2>', "yourtext" );
+                       $out->wrapWikiMsg( '<h2>$1</h2>', "yourtext" );
                        $this->showTextbox2();
                }
        }
@@ -3642,10 +3663,10 @@ HTML
         * @return string
         */
        function getPreviewText() {
-               global $wgOut, $wgRawHtml, $wgLang;
-               global $wgAllowUserCss, $wgAllowUserJs;
+               global $wgRawHtml, $wgAllowUserCss, $wgAllowUserJs;
 
-               $stats = $wgOut->getContext()->getStats();
+               $stats = $this->context->getStats();
+               $out = $this->context->getOutput();
 
                if ( $wgRawHtml && !$this->mTokenOk ) {
                        // Could be an offsite preview attempt. This is very unsafe if
@@ -3655,7 +3676,7 @@ HTML
                                // Do not put big scary notice, if previewing the empty
                                // string, which happens when you initially edit
                                // a category page, due to automatic preview-on-open.
-                               $parsedNote = $wgOut->parse( "<div class='previewnote'>" .
+                               $parsedNote = $out->parse( "<div class='previewnote'>" .
                                        wfMessage( 'session_fail_preview_html' )->text() . "</div>", true, /* interface */true );
                        }
                        $stats->increment( 'edit.failures.session_loss' );
@@ -3677,7 +3698,7 @@ HTML
 
                        # provide a anchor link to the editform
                        $continueEditing = '<span class="mw-continue-editing">' .
-                               '[[#' . self::EDITFORM_ID . '|' . $wgLang->getArrow() . ' ' .
+                               '[[#' . self::EDITFORM_ID . '|' . $this->context->getLanguage()->getArrow() . ' ' .
                                wfMessage( 'continue-editing' )->text() . ']]</span>';
                        if ( $this->mTriedSave && !$this->mTokenOk ) {
                                if ( $this->mTokenOkExceptSuffix ) {
@@ -3743,7 +3764,7 @@ HTML
                        $parserOutput = $parserResult['parserOutput'];
                        $previewHTML = $parserResult['html'];
                        $this->mParserOutput = $parserOutput;
-                       $wgOut->addParserOutputMetadata( $parserOutput );
+                       $out->addParserOutputMetadata( $parserOutput );
 
                        if ( count( $parserOutput->getWarnings() ) ) {
                                $note .= "\n\n" . implode( "\n\n", $parserOutput->getWarnings() );
@@ -3769,7 +3790,7 @@ HTML
 
                $previewhead = "<div class='previewnote'>\n" .
                        '<h2 id="mw-previewheader">' . wfMessage( 'preview' )->escaped() . "</h2>" .
-                       $wgOut->parse( $note, true, /* interface */true ) . $conflict . "</div>\n";
+                       $out->parse( $note, true, /* interface */true ) . $conflict . "</div>\n";
 
                $pageViewLang = $this->mTitle->getPageViewLanguage();
                $attribs = [ 'lang' => $pageViewLang->getHtmlCode(), 'dir' => $pageViewLang->getDir(),
@@ -3784,7 +3805,7 @@ HTML
         * @return ParserOptions
         */
        protected function getPreviewParserOptions() {
-               $parserOptions = $this->page->makeParserOptions( $this->mArticle->getContext() );
+               $parserOptions = $this->page->makeParserOptions( $this->context );
                $parserOptions->setIsPreview( true );
                $parserOptions->setIsSectionPreview( !is_null( $this->section ) && $this->section !== '' );
                $parserOptions->enableLimitReport();
@@ -3795,17 +3816,17 @@ HTML
         * Parse the page for a preview. Subclasses may override this class, in order
         * to parse with different options, or to otherwise modify the preview HTML.
         *
-        * @param Content @content The page content
-        * @return Associative array with keys:
+        * @param Content $content The page content
+        * @return array with keys:
         *   - parserOutput: The ParserOutput object
         *   - html: The HTML to be displayed
         */
        protected function doPreviewParse( Content $content ) {
-               global $wgUser;
+               $user = $this->context->getUser();
                $parserOptions = $this->getPreviewParserOptions();
-               $pstContent = $content->preSaveTransform( $this->mTitle, $wgUser, $parserOptions );
+               $pstContent = $content->preSaveTransform( $this->mTitle, $user, $parserOptions );
                $scopedCallback = $parserOptions->setupFakeRevision(
-                       $this->mTitle, $pstContent, $wgUser );
+                       $this->mTitle, $pstContent, $user );
                $parserOutput = $pstContent->getParserOutput( $this->mTitle, null, $parserOptions );
                ScopedCallback::consume( $scopedCallback );
                $parserOutput->setEditSectionTokens( false ); // no section edit links
@@ -3981,15 +4002,16 @@ HTML
         * @return array
         */
        public function getCheckboxes( &$tabindex, $checked ) {
-               global $wgUser, $wgUseMediaWikiUIEverywhere;
+               global $wgUseMediaWikiUIEverywhere;
 
                $checkboxes = [];
+               $user = $this->context->getUser();
 
                // don't show the minor edit checkbox if it's a new page or section
                if ( !$this->isNew ) {
                        $checkboxes['minor'] = '';
                        $minorLabel = wfMessage( 'minoredit' )->parse();
-                       if ( $wgUser->isAllowed( 'minoredit' ) ) {
+                       if ( $user->isAllowed( 'minoredit' ) ) {
                                $attribs = [
                                        'tabindex' => ++$tabindex,
                                        'accesskey' => wfMessage( 'accesskey-minoredit' )->text(),
@@ -4013,7 +4035,7 @@ HTML
 
                $watchLabel = wfMessage( 'watchthis' )->parse();
                $checkboxes['watch'] = '';
-               if ( $wgUser->isLoggedIn() ) {
+               if ( $user->isLoggedIn() ) {
                        $attribs = [
                                'tabindex' => ++$tabindex,
                                'accesskey' => wfMessage( 'accesskey-watch' )->text(),
@@ -4047,13 +4069,19 @@ HTML
        public function getEditButtons( &$tabindex ) {
                $buttons = [];
 
+               $labelAsPublish = $this->mArticle->getContext()->getConfig()->get( 'EditButtonPublishNotSave' );
+               if ( $labelAsPublish ) {
+                       $buttonLabelKey = $this->isNew ? 'publishpage' : 'publishchanges';
+               } else {
+                       $buttonLabelKey = $this->isNew ? 'savearticle' : 'savechanges';
+               }
+               $buttonLabel = wfMessage( $buttonLabelKey )->text();
                $attribs = [
                        'id' => 'wpSave',
                        'name' => 'wpSave',
                        'tabindex' => ++$tabindex,
                ] + Linker::tooltipAndAccesskeyAttribs( 'save' );
-               $buttons['save'] = Html::submitButton( wfMessage( 'savearticle' )->text(),
-                       $attribs, [ 'mw-ui-constructive' ] );
+               $buttons['save'] = Html::submitButton( $buttonLabel, $attribs, [ 'mw-ui-constructive' ] );
 
                ++$tabindex; // use the same for preview and live preview
                $attribs = [
@@ -4082,15 +4110,14 @@ HTML
         * they have attempted to edit a nonexistent section.
         */
        function noSuchSectionPage() {
-               global $wgOut;
-
-               $wgOut->prepareErrorPage( wfMessage( 'nosuchsectiontitle' ) );
+               $out = $this->context->getOutput();
+               $out->prepareErrorPage( wfMessage( 'nosuchsectiontitle' ) );
 
                $res = wfMessage( 'nosuchsectiontext', $this->section )->parseAsBlock();
                Hooks::run( 'EditPageNoSuchSection', [ &$this, &$res ] );
-               $wgOut->addHTML( $res );
+               $out->addHTML( $res );
 
-               $wgOut->returnToMain( false, $this->mTitle );
+               $out->returnToMain( false, $this->mTitle );
        }
 
        /**
@@ -4099,28 +4126,28 @@ HTML
         * @param string|array|bool $match Text (or array of texts) which triggered one or more filters
         */
        public function spamPageWithContent( $match = false ) {
-               global $wgOut, $wgLang;
                $this->textbox2 = $this->textbox1;
 
                if ( is_array( $match ) ) {
-                       $match = $wgLang->listToText( $match );
+                       $match = $this->context->getLanguage()->listToText( $match );
                }
-               $wgOut->prepareErrorPage( wfMessage( 'spamprotectiontitle' ) );
+               $out = $this->context->getOutput();
+               $out->prepareErrorPage( wfMessage( 'spamprotectiontitle' ) );
 
-               $wgOut->addHTML( '<div id="spamprotected">' );
-               $wgOut->addWikiMsg( 'spamprotectiontext' );
+               $out->addHTML( '<div id="spamprotected">' );
+               $out->addWikiMsg( 'spamprotectiontext' );
                if ( $match ) {
-                       $wgOut->addWikiMsg( 'spamprotectionmatch', wfEscapeWikiText( $match ) );
+                       $out->addWikiMsg( 'spamprotectionmatch', wfEscapeWikiText( $match ) );
                }
-               $wgOut->addHTML( '</div>' );
+               $out->addHTML( '</div>' );
 
-               $wgOut->wrapWikiMsg( '<h2>$1</h2>', "yourdiff" );
+               $out->wrapWikiMsg( '<h2>$1</h2>', "yourdiff" );
                $this->showDiff();
 
-               $wgOut->wrapWikiMsg( '<h2>$1</h2>', "yourtext" );
+               $out->wrapWikiMsg( '<h2>$1</h2>', "yourtext" );
                $this->showTextbox2();
 
-               $wgOut->addReturnTo( $this->getContextTitle(), [ 'action' => 'edit' ] );
+               $out->addReturnTo( $this->getContextTitle(), [ 'action' => 'edit' ] );
        }
 
        /**
@@ -4130,9 +4157,9 @@ HTML
         * @return bool
         */
        private function checkUnicodeCompliantBrowser() {
-               global $wgBrowserBlackList, $wgRequest;
+               global $wgBrowserBlackList;
 
-               $currentbrowser = $wgRequest->getHeader( 'User-Agent' );
+               $currentbrowser = $this->context->getRequest()->getHeader( 'User-Agent' );
                if ( $currentbrowser === false ) {
                        // No User-Agent header sent? Trust it by default...
                        return true;
index 361058b..65638f2 100644 (file)
@@ -189,31 +189,24 @@ class FileDeleteForm {
                        );
                        $page = WikiPage::factory( $title );
                        $dbw = wfGetDB( DB_MASTER );
-                       try {
-                               $dbw->startAtomic( __METHOD__ );
-                               // delete the associated article first
-                               $error = '';
-                               $deleteStatus = $page->doDeleteArticleReal( $reason, $suppress, 0, false, $error, $user );
-                               // doDeleteArticleReal() returns a non-fatal error status if the page
-                               // or revision is missing, so check for isOK() rather than isGood()
-                               if ( $deleteStatus->isOK() ) {
-                                       $status = $file->delete( $reason, $suppress, $user );
-                                       if ( $status->isOK() ) {
-                                               $status->value = $deleteStatus->value; // log id
-                                               $dbw->endAtomic( __METHOD__ );
-                                       } else {
-                                               // Page deleted but file still there? rollback page delete
-                                               wfGetLBFactory()->rollbackMasterChanges( __METHOD__ );
-                                       }
-                               } else {
-                                       // Done; nothing changed
+                       $dbw->startAtomic( __METHOD__ );
+                       // delete the associated article first
+                       $error = '';
+                       $deleteStatus = $page->doDeleteArticleReal( $reason, $suppress, 0, false, $error, $user );
+                       // doDeleteArticleReal() returns a non-fatal error status if the page
+                       // or revision is missing, so check for isOK() rather than isGood()
+                       if ( $deleteStatus->isOK() ) {
+                               $status = $file->delete( $reason, $suppress, $user );
+                               if ( $status->isOK() ) {
+                                       $status->value = $deleteStatus->value; // log id
                                        $dbw->endAtomic( __METHOD__ );
+                               } else {
+                                       // Page deleted but file still there? rollback page delete
+                                       wfGetLBFactory()->rollbackMasterChanges( __METHOD__ );
                                }
-                       } catch ( Exception $e ) {
-                               // Rollback before returning to prevent UI from displaying
-                               // incorrect "View or restore N deleted edits?"
-                               $dbw->rollback( __METHOD__ );
-                               throw $e;
+                       } else {
+                               // Done; nothing changed
+                               $dbw->endAtomic( __METHOD__ );
                        }
                }
 
index 7cb75bb..8c01448 100644 (file)
@@ -627,6 +627,17 @@ class Html {
         * @return string Raw HTML
         */
        public static function inlineStyle( $contents, $media = 'all' ) {
+               // Don't escape '>' since that is used
+               // as direct child selector.
+               // Remember, in css, there is no "x" for hexadecimal escapes, and
+               // the space immediately after an escape sequence is swallowed.
+               $contents = strtr( $contents, [
+                       '<' => '\3C ',
+                       // CDATA end tag for good measure, but the main security
+                       // is from escaping the '<'.
+                       ']]>' => '\5D\5D\3E '
+               ] );
+
                if ( preg_match( '/[<&]/', $contents ) ) {
                        $contents = "/*<![CDATA[*/$contents/*]]>*/";
                }
index 7dac0ec..2a00900 100644 (file)
@@ -286,6 +286,16 @@ class MediaWiki {
                                // may still be a wikipage redirect to another article or URL.
                                $article = $this->initializeArticle();
                                if ( is_object( $article ) ) {
+                                       $url = $request->getFullRequestURL(); // requested URL
+                                       if (
+                                               $request->getMethod() === 'GET' &&
+                                               $url === $article->getTitle()->getCanonicalURL() &&
+                                               $article->checkTouched() &&
+                                               $output->checkLastModified( $article->getTouched() )
+                                       ) {
+                                               wfDebug( __METHOD__ . ": done 304\n" );
+                                               return;
+                                       }
                                        $this->performAction( $article, $requestTitle );
                                } elseif ( is_string( $article ) ) {
                                        $output->redirect( $article );
@@ -803,10 +813,10 @@ class MediaWiki {
         */
        public function triggerJobs() {
                $jobRunRate = $this->config->get( 'JobRunRate' );
-               if ( $jobRunRate <= 0 || wfReadOnly() ) {
-                       return;
-               } elseif ( $this->getTitle()->isSpecial( 'RunJobs' ) ) {
+               if ( $this->getTitle()->isSpecial( 'RunJobs' ) ) {
                        return; // recursion guard
+               } elseif ( $jobRunRate <= 0 || wfReadOnly() ) {
+                       return;
                }
 
                if ( $jobRunRate < 1 ) {
@@ -843,7 +853,7 @@ class MediaWiki {
                        $query, $this->config->get( 'SecretKey' ) );
 
                $errno = $errstr = null;
-               $info = wfParseUrl( $this->config->get( 'Server' ) );
+               $info = wfParseUrl( $this->config->get( 'CanonicalServer' ) );
                MediaWiki\suppressWarnings();
                $host = $info['host'];
                $port = 80;
@@ -872,7 +882,8 @@ class MediaWiki {
                        return;
                }
 
-               $url = wfAppendQuery( wfScript( 'index' ), $query );
+               $special = SpecialPageFactory::getPage( 'RunJobs' );
+               $url = $special->getPageTitle()->getCanonicalURL( $query );
                $req = (
                        "POST $url HTTP/1.1\r\n" .
                        "Host: {$info['host']}\r\n" .
@@ -883,7 +894,7 @@ class MediaWiki {
                $runJobsLogger->info( "Running $n job(s) via '$url'" );
                // Send a cron API request to be performed in the background.
                // Give up if this takes too long to send (which should be rare).
-               stream_set_timeout( $sock, 1 );
+               stream_set_timeout( $sock, 2 );
                $bytes = fwrite( $sock, $req );
                if ( $bytes !== strlen( $req ) ) {
                        $runJobsLogger->error( "Failed to start cron API (socket write error)" );
index ac5fbe0..49891df 100644 (file)
@@ -28,6 +28,7 @@ use WatchedItemQueryService;
 use SkinFactory;
 use TitleFormatter;
 use TitleParser;
+use VirtualRESTServiceClient;
 use MediaWiki\Interwiki\InterwikiLookup;
 
 /**
@@ -571,6 +572,14 @@ class MediaWikiServices extends ServiceContainer {
                return $this->getService( 'TitleParser' );
        }
 
+       /**
+        * @since 1.28
+        * @return VirtualRESTServiceClient
+        */
+       public function getVirtualRESTServiceClient() {
+               return $this->getService( 'VirtualRESTServiceClient' );
+       }
+
        ///////////////////////////////////////////////////////////////////////////
        // NOTE: When adding a service getter here, don't forget to add a test
        // case for it in MediaWikiServicesTest::provideGetters() and in
index bc3305a..d17f234 100644 (file)
@@ -297,19 +297,25 @@ class MovePage {
 
                if ( $protected ) {
                        # Protect the redirect title as the title used to be...
-                       $dbw->insertSelect( 'page_restrictions', 'page_restrictions',
-                               [
-                                       'pr_page' => $redirid,
-                                       'pr_type' => 'pr_type',
-                                       'pr_level' => 'pr_level',
-                                       'pr_cascade' => 'pr_cascade',
-                                       'pr_user' => 'pr_user',
-                                       'pr_expiry' => 'pr_expiry'
-                               ],
+                       $res = $dbw->select(
+                               'page_restrictions',
+                               '*',
                                [ 'pr_page' => $pageid ],
                                __METHOD__,
-                               [ 'IGNORE' ]
+                               'FOR UPDATE'
                        );
+                       $rowsInsert = [];
+                       foreach ( $res as $row ) {
+                               $rowsInsert[] = [
+                                       'pr_page' => $redirid,
+                                       'pr_type' => $row->pr_type,
+                                       'pr_level' => $row->pr_level,
+                                       'pr_cascade' => $row->pr_cascade,
+                                       'pr_user' => $row->pr_user,
+                                       'pr_expiry' => $row->pr_expiry
+                               ];
+                       }
+                       $dbw->insert( 'page_restrictions', $rowsInsert, __METHOD__, [ 'IGNORE' ] );
 
                        // Build comment for log
                        $comment = wfMessage(
index eb3040c..c7499b1 100644 (file)
@@ -2852,7 +2852,6 @@ class OutputPage extends ContextSource {
 
        private function isUserJsPreview() {
                return $this->getConfig()->get( 'AllowUserJs' )
-                       && $this->getUser()->isLoggedIn()
                        && $this->getTitle()
                        && $this->getTitle()->isJsSubpage()
                        && $this->userCanPreview();
@@ -2860,7 +2859,6 @@ class OutputPage extends ContextSource {
 
        private function isUserCssPreview() {
                return $this->getConfig()->get( 'AllowUserCss' )
-                       && $this->getUser()->isLoggedIn()
                        && $this->getTitle()
                        && $this->getTitle()->isCssSubpage()
                        && $this->userCanPreview();
@@ -3097,6 +3095,11 @@ class OutputPage extends ContextSource {
                }
 
                $user = $this->getUser();
+
+               if ( !$user->isLoggedIn() ) {
+                       // Anons have predictable edit tokens
+                       return false;
+               }
                if ( !$user->matchEditToken( $request->getVal( 'wpEditToken' ) ) ) {
                        return false;
                }
index dd68102..0039d2b 100644 (file)
@@ -117,6 +117,9 @@ class Pingback {
         *
         * This is public so we can display it in the installer
         *
+        * Developers: If you're adding a new piece of data to this, please ensure
+        * that you update https://www.mediawiki.org/wiki/Manual:$wgPingback
+        *
         * @return array
         */
        public function getSystemInfo() {
index 21c6377..33569e6 100644 (file)
@@ -209,6 +209,24 @@ return [
                return $services->getService( '_MediaWikiTitleCodec' );
        },
 
+       'VirtualRESTServiceClient' => function( MediaWikiServices $services ) {
+               $config = $services->getMainConfig()->get( 'VirtualRestConfig' );
+
+               $vrsClient = new VirtualRESTServiceClient( new MultiHttpClient( [] ) );
+               foreach ( $config['paths'] as $prefix => $serviceConfig ) {
+                       $class = $serviceConfig['class'];
+                       // Merge in the global defaults
+                       $constructArg = isset( $serviceConfig['options'] )
+                               ? $serviceConfig['options']
+                               : [];
+                       $constructArg += $config['global'];
+                       // Make the VRS service available at the mount point
+                       $vrsClient->mount( $prefix, [ 'class' => $class, 'config' => $constructArg ] );
+               }
+
+               return $vrsClient;
+       },
+
        ///////////////////////////////////////////////////////////////////////////
        // NOTE: When adding a service here, don't forget to add a getter function
        // in the MediaWikiServices class. The convenience getter should just call
index ed445cc..2021e0a 100644 (file)
@@ -2271,13 +2271,17 @@ class Title implements LinkTarget {
         * @return array List of errors
         */
        private function checkUserBlock( $action, $user, $errors, $rigor, $short ) {
+               global $wgEmailConfirmToEdit, $wgBlockDisablesLogin;
                // Account creation blocks handled at userlogin.
                // Unblocking handled in SpecialUnblock
                if ( $rigor === 'quick' || in_array( $action, [ 'createaccount', 'unblock' ] ) ) {
                        return $errors;
                }
 
-               global $wgEmailConfirmToEdit;
+               // Optimize for a very common case
+               if ( $action === 'read' && !$wgBlockDisablesLogin ) {
+                       return $errors;
+               }
 
                if ( $wgEmailConfirmToEdit && !$user->isEmailConfirmed() ) {
                        $errors[] = [ 'confirmedittext' ];
@@ -2434,6 +2438,7 @@ class Title implements LinkTarget {
                        $checks = [
                                'checkPermissionHooks',
                                'checkReadPermissions',
+                               'checkUserBlock', // for wgBlockDisablesLogin
                        ];
                # Don't call checkSpecialsAndNSPermissions or checkCSSandJSPermissions
                # here as it will lead to duplicate error messages. This is okay to do
index 35fad4a..83b5d93 100644 (file)
@@ -36,6 +36,12 @@ class ApiParse extends ApiBase {
        /** @var Content $pstContent */
        private $pstContent = null;
 
+       private function checkReadPermissions( Title $title ) {
+               if ( !$title->userCan( 'read', $this->getUser() ) ) {
+                       $this->dieUsage( "You don't have permission to view this page", 'permissiondenied' );
+               }
+       }
+
        public function execute() {
                // The data is hot but user-dependent, like page views, so we set vary cookies
                $this->getMain()->setCacheMode( 'anon-public-user-private' );
@@ -102,6 +108,8 @@ class ApiParse extends ApiBase {
                                if ( !$rev ) {
                                        $this->dieUsage( "There is no revision ID $oldid", 'missingrev' );
                                }
+
+                               $this->checkReadPermissions( $rev->getTitle() );
                                if ( !$rev->userCan( Revision::DELETED_TEXT, $this->getUser() ) ) {
                                        $this->dieUsage( "You don't have permission to view deleted revisions", 'permissiondenied' );
                                }
@@ -161,6 +169,8 @@ class ApiParse extends ApiBase {
                                if ( !$titleObj || !$titleObj->exists() ) {
                                        $this->dieUsage( "The page you specified doesn't exist", 'missingtitle' );
                                }
+
+                               $this->checkReadPermissions( $titleObj );
                                $wgTitle = $titleObj;
 
                                if ( isset( $prop['revid'] ) ) {
index cfc1e46..9b45b91 100644 (file)
@@ -328,7 +328,6 @@ class ApiQueryUsers extends ApiQueryBase {
                        ],
                        'attachedwiki' => null,
                        'users' => [
-                               ApiBase::PARAM_TYPE => 'user',
                                ApiBase::PARAM_ISMULTI => true
                        ],
                        'token' => [
index ac817ba..f7ce552 100644 (file)
@@ -405,6 +405,7 @@ class ApiUpload extends ApiBase {
                        $code = $overrideCode;
                }
                if ( $moreExtraData ) {
+                       $extraData = $extraData ?: [];
                        $extraData += $moreExtraData;
                }
                $this->dieUsage( $msg, $code, 0, $extraData );
index 7970800..938c098 100644 (file)
@@ -6,6 +6,7 @@
                ]
        },
        "apihelp-main-param-action": "Kurį veiksmą atlikti.",
+       "apihelp-main-param-curtimestamp": "Prie rezultato pridėti dabartinę laiko žymę.",
        "apihelp-block-description": "Blokuoti vartotoją.",
        "apihelp-block-param-reason": "Blokavimo priežastis.",
        "apihelp-block-param-nocreate": "Neleisti kurti paskyrų.",
        "apihelp-edit-param-text": "Puslapio turinys.",
        "apihelp-edit-param-minor": "Smulkus pakeitimas.",
        "apihelp-edit-param-notminor": "Nesmulkus pakeitimas.",
+       "apihelp-edit-param-createonly": "Neredaguoti puslapio jei jis jau egzistuoja.",
+       "apihelp-edit-param-watch": "Pridėti puslapį į dabartinio vartotojo stebimųjų sąrašą.",
+       "apihelp-edit-param-unwatch": "Pašalinti puslapį iš dabartinio vartotojo stebimųjų sąrašo.",
        "apihelp-edit-param-redirect": "Automatiškai išspręsti peradresavimus.",
        "apihelp-edit-example-edit": "Redaguoti puslapį.",
        "apihelp-emailuser-description": "Siųsti el. laišką naudotojui.",
+       "apihelp-emailuser-param-target": "El. laiško gavėjas.",
        "apihelp-expandtemplates-param-title": "Puslapio pavadinimas.",
+       "apihelp-feedcontributions-param-feedformat": "Srauto formatas.",
+       "apihelp-feedcontributions-param-year": "Nuo metų (ir anksčiau).",
+       "apihelp-feedcontributions-param-month": "Nuo mėnesio (ir anksčiau).",
+       "apihelp-feedcontributions-param-tagfilter": "Filtruoti įnašus, kurie turi šias žymes.",
+       "apihelp-feedcontributions-param-deletedonly": "Rodyti tik ištrintus įnašus.",
+       "apihelp-feedcontributions-param-hideminor": "Slėpti nedidelius pakeitimus.",
+       "apihelp-feedrecentchanges-param-feedformat": "Srauto formatas.",
+       "apihelp-feedrecentchanges-param-from": "Rodyti pakeitimus nuo tada.",
        "apihelp-feedrecentchanges-param-hideminor": "Slėpti smulkius pakeitimus.",
        "apihelp-feedrecentchanges-param-hidebots": "Slėpti robotų pakeitimus.",
        "apihelp-feedrecentchanges-param-hideanons": "Slėpti vartotojų anonimų pakeitimus.",
        "apihelp-feedrecentchanges-param-hideliu": "Slėpti užsiregistravusių vartotojų pakeitimus.",
        "apihelp-feedrecentchanges-param-hidemyself": "Slėpti pakeitimus, atliktus dabartinio vartotojo.",
        "apihelp-feedrecentchanges-param-tagfilter": "Filtruoti pagal žymę.",
+       "apihelp-feedrecentchanges-param-target": "Rodyti tik keitimus puslapiuose, pasiekiamuose iš šio puslapio.",
        "apihelp-feedrecentchanges-example-simple": "Parodyti naujausius keitimus.",
+       "apihelp-feedwatchlist-param-feedformat": "Srauto formatas.",
        "apihelp-filerevert-param-comment": "Įkėlimo komentaras.",
+       "apihelp-help-example-recursive": "Visa pagalba viename puslapyje.",
+       "apihelp-help-example-help": "Pačio pagalbos modulio pagalba.",
+       "apihelp-imagerotate-description": "Pasukti viena ar daugiau paveikslėlių.",
+       "apihelp-imagerotate-param-rotation": "Kiek laipsnių pasukti paveikslėlį pagal laikrodžio rodyklę.",
+       "apihelp-imagerotate-example-generator": "Pasukti visus paveikslėlius <kbd>Category:Flip</kbd> <kbd>180</kbd> laipsnių.",
        "apihelp-import-param-xml": "XML failas įkeltas.",
        "apihelp-login-param-name": "Vartotojo vardas.",
        "apihelp-login-param-password": "Slaptažodis.",
        "apihelp-login-param-domain": "Domenas (neprivaloma).",
+       "apihelp-login-example-login": "Prisijungti.",
+       "apihelp-logout-description": "Atsijungti ir išvalyti sesijos duomenis.",
+       "apihelp-logout-example-logout": "Atjungti dabartinė vartotoją.",
+       "apihelp-managetags-example-delete": "Ištrinti <kbd>vandlaism</kbd> žymę su priežastimi <kbd>Misspelt</kbd>",
+       "apihelp-managetags-example-activate": "Aktyvuoti žymę pavadinimu <kbd>spam</kbd> su priežastimi <kbd>For use in edit patrolling</kbd>",
+       "apihelp-managetags-example-deactivate": "Išjungti žymę pavadinimu <kbd>spam</kbd> su priežastimi <kbd>No longer required</kbd>",
+       "apihelp-mergehistory-description": "Sujungti puslapio istorijas.",
+       "apihelp-mergehistory-param-reason": "Istorijos sujungimo priežastis.",
+       "apihelp-mergehistory-example-merge": "Sujungti visą <kbd>Oldpage</kbd> istoriją į <kbd>Newpage</kbd>.",
+       "apihelp-move-description": "Perkelti puslapį.",
+       "apihelp-move-param-to": "Pavadinimas, į kuri pervadinamas puslapis.",
+       "apihelp-move-param-reason": "Pervadinimo priežastis.",
+       "apihelp-move-param-movetalk": "Pervadinti aptarimo puslapį, jei jis egzistuoja.",
+       "apihelp-move-param-noredirect": "Nekurti nukreipimo.",
+       "apihelp-move-param-watch": "Pridėti puslapį ir nukreipimą į dabartinio vartotojo stebimųjų sąrašą.",
+       "apihelp-move-param-unwatch": "Pašalinti puslapį ir nukreipimą iš dabartinio vartotojo stebimųjų sąrašo.",
+       "apihelp-move-param-ignorewarnings": "Ignuoruoti bet kokius įspėjimus.",
+       "apihelp-move-example-move": "Perkelti <kbd>Badtitle</kbd> į <kbd>Goodtitle</kbd> nepaliekant nukreipimo.",
+       "apihelp-opensearch-description": "Ieškoti viki naudojant OpenSearch protokolą.",
+       "apihelp-opensearch-param-limit": "Maksimalus grąžinamas rezultatų skaičius.",
+       "apihelp-opensearch-example-te": "Rasti puslapius prasidedančius su <kbd>Te</kbd>.",
+       "apihelp-options-example-reset": "Nustatyti visus pageidavimus iš naujo.",
+       "apihelp-options-example-change": "Keisti <kbd>skin</kbd> ir <kbd>hideminor</kbd> pageidavimus.",
+       "apihelp-options-example-complex": "Nustatyti visus pageidavimus iš naujo, tada nustatyti <kbd>skin</kbd> ir <kbd>nickname</kbd>.",
+       "apihelp-paraminfo-description": "Gauti informaciją apie API modulius.",
+       "apihelp-protect-example-protect": "Apsaugoti puslapį.",
+       "apihelp-query+allcategories-param-dir": "Rūšiavimo kryptis.",
+       "apihelp-query+allcategories-param-min": "Gražinti tik kategorijas, kuriuose yra bent tiek narių.",
+       "apihelp-query+allcategories-param-max": "Gražinti tik kategorijas, kuriuose yra iki tiek narių.",
+       "apihelp-query+allcategories-param-limit": "Kiek kategorijų gražinti.",
+       "apihelp-query+allcategories-paramvalue-prop-size": "Prideda puslapių kategorijoje skaičių.",
        "apihelp-query+alldeletedrevisions-example-user": "Sąrašas paskutinių 50 ištrintų indėlių pagal vartotoją\n<kbd>Pavyzdys</kbd>.",
+       "apihelp-query+alllinks-paramvalue-prop-title": "Prideda nuorodos pavadinimą.",
+       "apihelp-query+alllinks-param-limit": "Kiek objektų iš viso gražinti.",
+       "apihelp-query+allmessages-param-lang": "Gražinti pranešimus šia kalba.",
        "apihelp-query+allrevisions-param-namespace": "Rodyti puslapius tik šioje vardų srityje.",
        "apihelp-query+backlinks-example-simple": "Rodyti nuorodas <kbd>Pagrindinis puslapis</kbd>.",
+       "apihelp-query+blocks-paramvalue-prop-id": "Prideda bloko ID.",
+       "apihelp-query+blocks-paramvalue-prop-user": "Prideda užblokuoto vartotojo vardą.",
+       "apihelp-query+blocks-paramvalue-prop-userid": "Prideda užblokuoto vartotojo ID.",
+       "apihelp-query+blocks-paramvalue-prop-by": "Prideda užblokuoto vartotojo vardą.",
+       "apihelp-query+blocks-paramvalue-prop-byid": "Prideda užblokuoto vartotojo ID.",
+       "apihelp-query+blocks-paramvalue-prop-timestamp": "Prideda blokavimo laiko žymę.",
+       "apihelp-query+blocks-paramvalue-prop-expiry": "Prideda blokavimo pabaigos laiko žymes.",
+       "apihelp-query+blocks-paramvalue-prop-reason": "Prideda blokavimo priežastį.",
+       "apihelp-query+blocks-paramvalue-prop-range": "Prideda blokavimo paveiktų IP adresų diapazoną.",
        "apihelp-query+watchlist-paramvalue-type-external": "Išoriniai keitimai.",
        "apihelp-query+watchlist-paramvalue-type-new": "Puslapio sukūrimai.",
        "apihelp-stashedit-param-title": "Puslapio pavadinimas buvo redaguotas.",
index fe254af..881c8c2 100644 (file)
@@ -49,6 +49,8 @@ abstract class Collation {
                switch ( $collationName ) {
                        case 'uppercase':
                                return new UppercaseCollation;
+                       case 'numeric':
+                               return new NumericUppercaseCollation;
                        case 'identity':
                                return new IdentityCollation;
                        case 'uca-default':
diff --git a/includes/collation/NumericUppercaseCollation.php b/includes/collation/NumericUppercaseCollation.php
new file mode 100644 (file)
index 0000000..4bf2f73
--- /dev/null
@@ -0,0 +1,58 @@
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+/**
+ * Collation that orders text with numbers "naturally", so that 'Foo 1' < 'Foo 2' < 'Foo 12'.
+ *
+ * Note that this only works in terms of sequences of digits, and the behavior for decimal fractions
+ * or pretty-formatted numbers may be unexpected.
+ *
+ * @since 1.28
+ */
+class NumericUppercaseCollation extends UppercaseCollation {
+       public function getSortKey( $string ) {
+               $sortkey = parent::getSortKey( $string );
+
+               // For each sequence of digits, insert the digit '0' and then the length of the sequence
+               // (encoded in two bytes) before it. That's all folks, it sorts correctly now! The '0' ensures
+               // correct position (where digits would normally sort), then the length will be compared putting
+               // shorter numbers before longer ones; if identical, then the characters will be compared, which
+               // generates the correct results for numbers of equal length.
+               $sortkey = preg_replace_callback( '/\d+/', function ( $matches ) {
+                       $len = strlen( $matches[0] );
+                       // This allows sequences of up to 65536 numeric characters to be handled correctly. One byte
+                       // would allow only for 256, which doesn't feel future-proof.
+                       $prefix = chr( floor( $len / 256 ) ) . chr( $len % 256 );
+                       return '0' . $prefix . $matches[0];
+               }, $sortkey );
+
+               return $sortkey;
+       }
+
+       public function getFirstLetter( $string ) {
+               if ( preg_match( '/^\d/', $string ) ) {
+                       // Note that we pass 0 and 9 as normal params, not numParams(). This only works for 0-9
+                       // and not localised digits, so we don't want them to be converted.
+                       return wfMessage( 'category-header-numerals' )->params( 0, 9 )->text();
+               } else {
+                       return parent::getFirstLetter( $string );
+               }
+       }
+}
index 40d9277..14c8182 100644 (file)
@@ -86,7 +86,7 @@ class JsonContent extends TextContent {
                        return $this;
                }
 
-               return new static( $this->beautifyJSON() );
+               return new static( self::normalizeLineEndings( $this->beautifyJSON() ) );
        }
 
        /**
index de650b9..7bb4def 100644 (file)
@@ -147,9 +147,28 @@ class TextContent extends AbstractContent {
                }
        }
 
+       /**
+        * Do a "\r\n" -> "\n" and "\r" -> "\n" transformation
+        * as well as trim trailing whitespace
+        *
+        * This was formerly part of Parser::preSaveTransform, but
+        * for non-wikitext content models they probably still want
+        * to normalize line endings without all of the other PST
+        * changes.
+        *
+        * @since 1.28
+        * @param $text
+        * @return string
+        */
+       public static function normalizeLineEndings( $text ) {
+               return str_replace( [ "\r\n", "\r" ], "\n", rtrim( $text ) );
+       }
+
        /**
         * Returns a Content object with pre-save transformations applied.
-        * This implementation just trims trailing whitespace and normalizes newlines.
+        *
+        * At a minimum, subclasses should make sure to call TextContent::normalizeLineEndings()
+        * either directly or part of Parser::preSaveTransform().
         *
         * @param Title $title
         * @param User $user
@@ -159,8 +178,7 @@ class TextContent extends AbstractContent {
         */
        public function preSaveTransform( Title $title, User $user, ParserOptions $popts ) {
                $text = $this->getNativeData();
-               $pst = rtrim( $text );
-               $pst = str_replace( [ "\r\n", "\r" ], "\n", $pst );
+               $pst = self::normalizeLineEndings( $text );
 
                return ( $text === $pst ) ? $this : new static( $pst, $this->getModel() );
        }
index a63819d..9296728 100644 (file)
@@ -138,7 +138,6 @@ class WikitextContent extends TextContent {
 
                $text = $this->getNativeData();
                $pst = $wgParser->preSaveTransform( $text, $title, $user, $popts );
-               rtrim( $pst );
 
                return ( $text === $pst ) ? $this : new static( $pst );
        }
index 3cd09e2..577c98d 100644 (file)
@@ -129,25 +129,11 @@ class CloneDatabase {
         */
        public static function changePrefix( $prefix ) {
                global $wgDBprefix;
-               wfGetLBFactory()->forEachLB( [ 'CloneDatabase', 'changeLBPrefix' ], [ $prefix ] );
+               wfGetLBFactory()->forEachLB( function( $lb ) use ( $prefix ) {
+                       $lb->forEachOpenConnection( function ( $db ) use ( $prefix ) {
+                               $db->tablePrefix( $prefix );
+                       } );
+               } );
                $wgDBprefix = $prefix;
        }
-
-       /**
-        * @param LoadBalancer $lb
-        * @param string $prefix
-        * @return void
-        */
-       public static function changeLBPrefix( $lb, $prefix ) {
-               $lb->forEachOpenConnection( [ 'CloneDatabase', 'changeDBPrefix' ], [ $prefix ] );
-       }
-
-       /**
-        * @param DatabaseBase $db
-        * @param string $prefix
-        * @return void
-        */
-       public static function changeDBPrefix( $db, $prefix ) {
-               $db->tablePrefix( $prefix );
-       }
 }
index 186a87b..6ddc9f7 100644 (file)
 abstract class DatabaseBase implements IDatabase {
        /** Number of times to re-try an operation in case of deadlock */
        const DEADLOCK_TRIES = 4;
-
        /** Minimum time to wait before retry, in microseconds */
        const DEADLOCK_DELAY_MIN = 500000;
-
        /** Maximum time to wait before retry */
        const DEADLOCK_DELAY_MAX = 1500000;
 
+       /** How long before it is worth doing a dummy query to test the connection */
+       const PING_TTL = 1.0;
+
+       /** @var string SQL query */
        protected $mLastQuery = '';
+       /** @var bool */
        protected $mDoneWrites = false;
+       /** @var string|bool */
        protected $mPHPError = false;
-
-       protected $mServer, $mUser, $mPassword, $mDBname;
+       /** @var string */
+       protected $mServer;
+       /** @var string */
+       protected $mUser;
+       /** @var string */
+       protected $mPassword;
+       /** @var string */
+       protected $mDBname;
 
        /** @var BagOStuff APC cache */
        protected $srvCache;
 
        /** @var resource Database connection */
        protected $mConn = null;
+       /** @var bool */
        protected $mOpened = false;
 
        /** @var array[] List of (callable, method name) */
@@ -61,20 +72,27 @@ abstract class DatabaseBase implements IDatabase {
        /** @var bool Whether to suppress triggering of post-commit callbacks */
        protected $suppressPostCommitCallbacks = false;
 
+       /** @var string */
        protected $mTablePrefix;
+       /** @var string */
        protected $mSchema;
+       /** @var integer */
        protected $mFlags;
+       /** @var bool */
        protected $mForeign;
+       /** @var array */
        protected $mLBInfo = [];
+       /** @var bool|null */
        protected $mDefaultBigSelects = null;
+       /** @var array|bool */
        protected $mSchemaVars = false;
        /** @var array */
        protected $mSessionVars = [];
-
+       /** @var array|null */
        protected $preparedArgs;
-
+       /** @var string|bool|null Stashed value of html_errors INI setting */
        protected $htmlErrors;
-
+       /** @var string */
        protected $delimiter = ';';
 
        /**
@@ -177,6 +195,9 @@ abstract class DatabaseBase implements IDatabase {
         */
        protected $allViews = null;
 
+       /** @var float UNIX timestamp */
+       protected $lastPing = 0.0;
+
        /** @var TransactionProfiler */
        protected $trxProfiler;
 
@@ -786,8 +807,8 @@ abstract class DatabaseBase implements IDatabase {
                $priorWritesPending = $this->writesOrCallbacksPending();
                $this->mLastQuery = $sql;
 
-               $isWriteQuery = $this->isWriteQuery( $sql );
-               if ( $isWriteQuery ) {
+               $isWrite = $this->isWriteQuery( $sql );
+               if ( $isWrite ) {
                        $reason = $this->getReadOnlyReason();
                        if ( $reason !== false ) {
                                throw new DBReadOnlyError( $this, "Database is read-only: $reason" );
@@ -820,31 +841,12 @@ abstract class DatabaseBase implements IDatabase {
                }
 
                # Keep track of whether the transaction has write queries pending
-               if ( $this->mTrxLevel && !$this->mTrxDoneWrites && $isWriteQuery ) {
+               if ( $this->mTrxLevel && !$this->mTrxDoneWrites && $isWrite ) {
                        $this->mTrxDoneWrites = true;
                        $this->getTransactionProfiler()->transactionWritingIn(
                                $this->mServer, $this->mDBname, $this->mTrxShortId );
                }
 
-               $isMaster = !is_null( $this->getLBInfo( 'master' ) );
-               # generalizeSQL will probably cut down the query to reasonable
-               # logging size most of the time. The substr is really just a sanity check.
-               if ( $isMaster ) {
-                       $queryProf = 'query-m: ' . substr( DatabaseBase::generalizeSQL( $sql ), 0, 255 );
-                       $totalProf = 'DatabaseBase::query-master';
-               } else {
-                       $queryProf = 'query: ' . substr( DatabaseBase::generalizeSQL( $sql ), 0, 255 );
-                       $totalProf = 'DatabaseBase::query';
-               }
-               # Include query transaction state
-               $queryProf .= $this->mTrxShortId ? " [TRX#{$this->mTrxShortId}]" : "";
-
-               $profiler = Profiler::instance();
-               if ( !$profiler instanceof ProfilerStub ) {
-                       $totalProfSection = $profiler->scopedProfileIn( $totalProf );
-                       $queryProfSection = $profiler->scopedProfileIn( $queryProf );
-               }
-
                if ( $this->debug() ) {
                        wfDebugLog( 'queries', sprintf( "%s: %s", $this->mDBname, $commentedSql ) );
                }
@@ -852,15 +854,8 @@ abstract class DatabaseBase implements IDatabase {
                # Avoid fatals if close() was called
                $this->assertOpen();
 
-               # Do the query and handle errors
-               $startTime = microtime( true );
-               $ret = $this->doQuery( $commentedSql );
-               $queryRuntime = microtime( true ) - $startTime;
-               # Log the query time and feed it into the DB trx profiler
-               $this->getTransactionProfiler()->recordQueryCompletion(
-                       $queryProf, $startTime, $isWriteQuery, $this->affectedRows() );
-
-               MWDebug::query( $sql, $fname, $isMaster, $queryRuntime );
+               # Send the query to the server
+               $ret = $this->doProfiledQuery( $sql, $commentedSql, $isWrite, $fname );
 
                # Try reconnecting if the connection was lost
                if ( false === $ret && $this->wasErrorReissuable() ) {
@@ -881,12 +876,7 @@ abstract class DatabaseBase implements IDatabase {
                                        $this->reportQueryError( $lastError, $lastErrno, $sql, $fname );
                                } else {
                                        # Should be safe to silently retry the query
-                                       $startTime = microtime( true );
-                                       $ret = $this->doQuery( $commentedSql );
-                                       $queryRuntime = microtime( true ) - $startTime;
-                                       # Log the query time and feed it into the DB trx profiler
-                                       $this->getTransactionProfiler()->recordQueryCompletion(
-                                               $queryProf, $startTime, $isWriteQuery, $this->affectedRows() );
+                                       $ret = $this->doProfiledQuery( $sql, $commentedSql, $isWrite, $fname );
                                }
                        } else {
                                wfDebug( "Failed\n" );
@@ -911,16 +901,47 @@ abstract class DatabaseBase implements IDatabase {
 
                $res = $this->resultObject( $ret );
 
-               // Destroy profile sections in the opposite order to their creation
-               ScopedCallback::consume( $queryProfSection );
-               ScopedCallback::consume( $totalProfSection );
+               return $res;
+       }
 
-               if ( $isWriteQuery && $this->mTrxLevel ) {
-                       $this->mTrxWriteDuration += $queryRuntime;
-                       $this->mTrxWriteCallers[] = $fname;
+       private function doProfiledQuery( $sql, $commentedSql, $isWrite, $fname ) {
+               $isMaster = !is_null( $this->getLBInfo( 'master' ) );
+               # generalizeSQL() will probably cut down the query to reasonable
+               # logging size most of the time. The substr is really just a sanity check.
+               if ( $isMaster ) {
+                       $queryProf = 'query-m: ' . substr( DatabaseBase::generalizeSQL( $sql ), 0, 255 );
+               } else {
+                       $queryProf = 'query: ' . substr( DatabaseBase::generalizeSQL( $sql ), 0, 255 );
                }
 
-               return $res;
+               # Include query transaction state
+               $queryProf .= $this->mTrxShortId ? " [TRX#{$this->mTrxShortId}]" : "";
+
+               $profiler = Profiler::instance();
+               if ( !( $profiler instanceof ProfilerStub ) ) {
+                       $queryProfSection = $profiler->scopedProfileIn( $queryProf );
+               }
+
+               $startTime = microtime( true );
+               $ret = $this->doQuery( $commentedSql );
+               $queryRuntime = microtime( true ) - $startTime;
+
+               unset( $queryProfSection ); // profile out (if set)
+
+               if ( $ret !== false ) {
+                       $this->lastPing = $startTime;
+                       if ( $isWrite && $this->mTrxLevel ) {
+                               $this->mTrxWriteDuration += $queryRuntime;
+                               $this->mTrxWriteCallers[] = $fname;
+                       }
+               }
+
+               $this->getTransactionProfiler()->recordQueryCompletion(
+                       $queryProf, $startTime, $isWrite, $this->affectedRows()
+               );
+               MWDebug::query( $sql, $fname, $isMaster, $queryRuntime );
+
+               return $ret;
        }
 
        private function canRecoverFromDisconnect( $sql, $priorWritesPending ) {
@@ -2926,6 +2947,9 @@ abstract class DatabaseBase implements IDatabase {
        }
 
        public function ping() {
+               if ( $this->isOpen() && ( microtime( true ) - $this->lastPing ) < self::PING_TTL ) {
+                       return true;
+               }
                try {
                        // This will reconnect if possible, or error out if not
                        $this->query( "SELECT 1 AS ping", __METHOD__ );
@@ -2939,8 +2963,18 @@ abstract class DatabaseBase implements IDatabase {
         * @return bool
         */
        protected function reconnect() {
-               # Stub. Not essential to override.
-               return true;
+               $this->closeConnection();
+               $this->mOpened = false;
+               $this->mConn = false;
+               try {
+                       $this->open( $this->mServer, $this->mUser, $this->mPassword, $this->mDBname );
+                       $this->lastPing = microtime( true );
+                       $ok = true;
+               } catch ( DBConnectionError $e ) {
+                       $ok = false;
+               }
+
+               return $ok;
        }
 
        public function getSessionLagStatus() {
index 9528220..93814d2 100644 (file)
@@ -616,15 +616,6 @@ abstract class DatabaseMysqlBase extends Database {
                return strlen( $name ) && $name[0] == '`' && substr( $name, -1, 1 ) == '`';
        }
 
-       function reconnect() {
-               $this->closeConnection();
-               $this->mOpened = false;
-               $this->mConn = false;
-               $this->open( $this->mServer, $this->mUser, $this->mPassword, $this->mDBname );
-
-               return true;
-       }
-
        function getLag() {
                if ( $this->getLagDetectionMethod() === 'pt-heartbeat' ) {
                        return $this->getLagFromPtHeartbeat();
index f045acf..13a0879 100644 (file)
@@ -1090,7 +1090,7 @@ class LoadBalancer {
        public function approveMasterChanges( array $options ) {
                $limit = isset( $options['maxWriteDuration'] ) ? $options['maxWriteDuration'] : 0;
                $this->forEachOpenMasterConnection( function ( DatabaseBase $conn ) use ( $limit ) {
-                       // If atomic section or explicit transactions are still open, some caller must have
+                       // If atomic sections or explicit transactions are still open, some caller must have
                        // caught an exception but failed to properly rollback any changes. Detect that and
                        // throw and error (causing rollback).
                        if ( $conn->explicitTrxActive() ) {
@@ -1108,6 +1108,14 @@ class LoadBalancer {
                                        wfMessage( 'transaction-duration-limit-exceeded', $time, $limit )->text()
                                );
                        }
+                       // If a connection sits idle while slow queries execute on another, that connection
+                       // may end up dropped before the commit round is reached. Ping servers to detect this.
+                       if ( $conn->writesOrCallbacksPending() && !$conn->ping() ) {
+                               throw new DBTransactionError(
+                                       $conn,
+                                       "A connection to the {$conn->getDBname()} database was lost before commit."
+                               );
+                       }
                } );
        }
 
index 5b84ca9..281ac24 100644 (file)
@@ -155,18 +155,3 @@ abstract class DataUpdate implements DeferrableUpdate {
                return $remaining;
        }
 }
-
-/**
- * Interface that marks a DataUpdate as enqueuable via the JobQueue
- *
- * Such updates must be representable using IJobSpecification, so that
- * they can be serialized into jobs and enqueued for later execution
- *
- * @since 1.27
- */
-interface EnqueueableDataUpdate {
-       /**
-        * @return array (wiki => wiki ID, job => IJobSpecification)
-        */
-       public function getAsJobSpecification();
-}
diff --git a/includes/deferred/EnqueueableDataUpdate.php b/includes/deferred/EnqueueableDataUpdate.php
new file mode 100644 (file)
index 0000000..ffeb740
--- /dev/null
@@ -0,0 +1,15 @@
+<?php
+/**
+ * Interface that marks a DataUpdate as enqueuable via the JobQueue
+ *
+ * Such updates must be representable using IJobSpecification, so that
+ * they can be serialized into jobs and enqueued for later execution
+ *
+ * @since 1.27
+ */
+interface EnqueueableDataUpdate {
+       /**
+        * @return array (wiki => wiki ID, job => IJobSpecification)
+        */
+       public function getAsJobSpecification();
+}
index 4f40c38..5e02c5c 100644 (file)
@@ -304,7 +304,7 @@ class LinksUpdate extends SqlDataUpdate implements EnqueueableDataUpdate {
         * @param array $cats
         */
        function invalidateCategories( $cats ) {
-               $this->invalidatePages( NS_CATEGORY, array_keys( $cats ) );
+               PurgeJobUtils::invalidatePages( $this->mDb, NS_CATEGORY, array_keys( $cats ) );
        }
 
        /**
@@ -323,7 +323,7 @@ class LinksUpdate extends SqlDataUpdate implements EnqueueableDataUpdate {
         * @param array $images
         */
        function invalidateImageDescriptions( $images ) {
-               $this->invalidatePages( NS_FILE, array_keys( $images ) );
+               PurgeJobUtils::invalidatePages( $this->mDb, NS_FILE, array_keys( $images ) );
        }
 
        /**
index 9740cbe..ff06915 100644 (file)
@@ -98,53 +98,4 @@ abstract class SqlDataUpdate extends DataUpdate {
                        $this->mHasTransaction = false;
                }
        }
-
-       /**
-        * Invalidate the cache of a list of pages from a single namespace.
-        * This is intended for use by subclasses.
-        *
-        * @param int $namespace Namespace number
-        * @param array $dbkeys
-        */
-       protected function invalidatePages( $namespace, array $dbkeys ) {
-               if ( $dbkeys === [] ) {
-                       return;
-               }
-
-               $dbw = $this->mDb;
-               $dbw->onTransactionPreCommitOrIdle( function() use ( $dbw, $namespace, $dbkeys ) {
-                       /**
-                        * 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
-                        */
-                       $now = $dbw->timestamp();
-                       $ids = $dbw->selectFieldValues( 'page',
-                               'page_id',
-                               [
-                                       'page_namespace' => $namespace,
-                                       'page_title' => $dbkeys,
-                                       'page_touched < ' . $dbw->addQuotes( $now )
-                               ],
-                               __METHOD__
-                       );
-
-                       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__
-                       );
-               } );
-       }
 }
index c9aad43..cccf71a 100644 (file)
@@ -144,33 +144,38 @@ abstract class DBLockManager extends QuorumLockManager {
         * @param string $lockDb
         * @return IDatabase
         * @throws DBError
+        * @throws UnexpectedValueException
         */
        protected function getConnection( $lockDb ) {
                if ( !isset( $this->conns[$lockDb] ) ) {
-                       $db = null;
                        if ( $lockDb === 'localDBMaster' ) {
-                               $db = $this->getLocalLB()->getConnection( DB_MASTER, [], $this->domain );
+                               $lb = $this->getLocalLB();
+                               $db = $lb->getConnection( DB_MASTER, [], $this->domain );
+                               # Do not mess with settings if the LoadBalancer is the main singleton
+                               # to avoid clobbering the settings of handles from wfGetDB( DB_MASTER ).
+                               $init = ( wfGetLB() !== $lb );
                        } elseif ( isset( $this->dbServers[$lockDb] ) ) {
                                $config = $this->dbServers[$lockDb];
                                $db = DatabaseBase::factory( $config['type'], $config );
+                               $init = true;
+                       } else {
+                               throw new UnexpectedValueException( "No server called '$lockDb'." );
                        }
-                       if ( !$db ) {
-                               return null; // config error?
+
+                       if ( $init ) {
+                               $db->clearFlag( DBO_TRX );
+                               # If the connection drops, try to avoid letting the DB rollback
+                               # and release the locks before the file operations are finished.
+                               # This won't handle the case of DB server restarts however.
+                               $options = [];
+                               if ( $this->lockExpiry > 0 ) {
+                                       $options['connTimeout'] = $this->lockExpiry;
+                               }
+                               $db->setSessionOptions( $options );
+                               $this->initConnection( $lockDb, $db );
                        }
+
                        $this->conns[$lockDb] = $db;
-                       $this->conns[$lockDb]->clearFlag( DBO_TRX );
-                       # If the connection drops, try to avoid letting the DB rollback
-                       # and release the locks before the file operations are finished.
-                       # This won't handle the case of DB server restarts however.
-                       $options = [];
-                       if ( $this->lockExpiry > 0 ) {
-                               $options['connTimeout'] = $this->lockExpiry;
-                       }
-                       $this->conns[$lockDb]->setSessionOptions( $options );
-                       $this->initConnection( $lockDb, $this->conns[$lockDb] );
-               }
-               if ( !$this->conns[$lockDb]->trxLevel() ) {
-                       $this->conns[$lockDb]->begin( __METHOD__ ); // start transaction
                }
 
                return $this->conns[$lockDb];
@@ -239,205 +244,3 @@ abstract class DBLockManager extends QuorumLockManager {
                }
        }
 }
-
-/**
- * MySQL version of DBLockManager that supports shared locks.
- *
- * All lock servers must have the innodb table defined in locking/filelocks.sql.
- * All locks are non-blocking, which avoids deadlocks.
- *
- * @ingroup LockManager
- */
-class MySqlLockManager extends DBLockManager {
-       /** @var array Mapping of lock types to the type actually used */
-       protected $lockTypeMap = [
-               self::LOCK_SH => self::LOCK_SH,
-               self::LOCK_UW => self::LOCK_SH,
-               self::LOCK_EX => self::LOCK_EX
-       ];
-
-       protected function getLocalLB() {
-               // Use a separate connection so releaseAllLocks() doesn't rollback the main trx
-               return wfGetLBFactory()->newMainLB( $this->domain );
-       }
-
-       protected function initConnection( $lockDb, IDatabase $db ) {
-               # Let this transaction see lock rows from other transactions
-               $db->query( "SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;" );
-       }
-
-       /**
-        * Get a connection to a lock DB and acquire locks on $paths.
-        * This does not use GET_LOCK() per http://bugs.mysql.com/bug.php?id=1118.
-        *
-        * @see DBLockManager::getLocksOnServer()
-        * @param string $lockSrv
-        * @param array $paths
-        * @param string $type
-        * @return Status
-        */
-       protected function doGetLocksOnServer( $lockSrv, array $paths, $type ) {
-               $status = Status::newGood();
-
-               $db = $this->getConnection( $lockSrv ); // checked in isServerUp()
-
-               $keys = []; // list of hash keys for the paths
-               $data = []; // list of rows to insert
-               $checkEXKeys = []; // list of hash keys that this has no EX lock on
-               # Build up values for INSERT clause
-               foreach ( $paths as $path ) {
-                       $key = $this->sha1Base36Absolute( $path );
-                       $keys[] = $key;
-                       $data[] = [ 'fls_key' => $key, 'fls_session' => $this->session ];
-                       if ( !isset( $this->locksHeld[$path][self::LOCK_EX] ) ) {
-                               $checkEXKeys[] = $key;
-                       }
-               }
-
-               # Block new writers (both EX and SH locks leave entries here)...
-               $db->insert( 'filelocks_shared', $data, __METHOD__, [ 'IGNORE' ] );
-               # Actually do the locking queries...
-               if ( $type == self::LOCK_SH ) { // reader locks
-                       $blocked = false;
-                       # Bail if there are any existing writers...
-                       if ( count( $checkEXKeys ) ) {
-                               $blocked = $db->selectField( 'filelocks_exclusive', '1',
-                                       [ 'fle_key' => $checkEXKeys ],
-                                       __METHOD__
-                               );
-                       }
-                       # Other prospective writers that haven't yet updated filelocks_exclusive
-                       # will recheck filelocks_shared after doing so and bail due to this entry.
-               } else { // writer locks
-                       $encSession = $db->addQuotes( $this->session );
-                       # Bail if there are any existing writers...
-                       # This may detect readers, but the safe check for them is below.
-                       # Note: if two writers come at the same time, both bail :)
-                       $blocked = $db->selectField( 'filelocks_shared', '1',
-                               [ 'fls_key' => $keys, "fls_session != $encSession" ],
-                               __METHOD__
-                       );
-                       if ( !$blocked ) {
-                               # Build up values for INSERT clause
-                               $data = [];
-                               foreach ( $keys as $key ) {
-                                       $data[] = [ 'fle_key' => $key ];
-                               }
-                               # Block new readers/writers...
-                               $db->insert( 'filelocks_exclusive', $data, __METHOD__ );
-                               # Bail if there are any existing readers...
-                               $blocked = $db->selectField( 'filelocks_shared', '1',
-                                       [ 'fls_key' => $keys, "fls_session != $encSession" ],
-                                       __METHOD__
-                               );
-                       }
-               }
-
-               if ( $blocked ) {
-                       foreach ( $paths as $path ) {
-                               $status->fatal( 'lockmanager-fail-acquirelock', $path );
-                       }
-               }
-
-               return $status;
-       }
-
-       /**
-        * @see QuorumLockManager::releaseAllLocks()
-        * @return Status
-        */
-       protected function releaseAllLocks() {
-               $status = Status::newGood();
-
-               foreach ( $this->conns as $lockDb => $db ) {
-                       if ( $db->trxLevel() ) { // in transaction
-                               try {
-                                       $db->rollback( __METHOD__ ); // finish transaction and kill any rows
-                               } catch ( DBError $e ) {
-                                       $status->fatal( 'lockmanager-fail-db-release', $lockDb );
-                               }
-                       }
-               }
-
-               return $status;
-       }
-}
-
-/**
- * PostgreSQL version of DBLockManager that supports shared locks.
- * All locks are non-blocking, which avoids deadlocks.
- *
- * @ingroup LockManager
- */
-class PostgreSqlLockManager extends DBLockManager {
-       /** @var array Mapping of lock types to the type actually used */
-       protected $lockTypeMap = [
-               self::LOCK_SH => self::LOCK_SH,
-               self::LOCK_UW => self::LOCK_SH,
-               self::LOCK_EX => self::LOCK_EX
-       ];
-
-       protected function doGetLocksOnServer( $lockSrv, array $paths, $type ) {
-               $status = Status::newGood();
-               if ( !count( $paths ) ) {
-                       return $status; // nothing to lock
-               }
-
-               $db = $this->getConnection( $lockSrv ); // checked in isServerUp()
-               $bigints = array_unique( array_map(
-                       function ( $key ) {
-                               return Wikimedia\base_convert( substr( $key, 0, 15 ), 16, 10 );
-                       },
-                       array_map( [ $this, 'sha1Base16Absolute' ], $paths )
-               ) );
-
-               // Try to acquire all the locks...
-               $fields = [];
-               foreach ( $bigints as $bigint ) {
-                       $fields[] = ( $type == self::LOCK_SH )
-                               ? "pg_try_advisory_lock_shared({$db->addQuotes( $bigint )}) AS K$bigint"
-                               : "pg_try_advisory_lock({$db->addQuotes( $bigint )}) AS K$bigint";
-               }
-               $res = $db->query( 'SELECT ' . implode( ', ', $fields ), __METHOD__ );
-               $row = $res->fetchRow();
-
-               if ( in_array( 'f', $row ) ) {
-                       // Release any acquired locks if some could not be acquired...
-                       $fields = [];
-                       foreach ( $row as $kbigint => $ok ) {
-                               if ( $ok === 't' ) { // locked
-                                       $bigint = substr( $kbigint, 1 ); // strip off the "K"
-                                       $fields[] = ( $type == self::LOCK_SH )
-                                               ? "pg_advisory_unlock_shared({$db->addQuotes( $bigint )})"
-                                               : "pg_advisory_unlock({$db->addQuotes( $bigint )})";
-                               }
-                       }
-                       if ( count( $fields ) ) {
-                               $db->query( 'SELECT ' . implode( ', ', $fields ), __METHOD__ );
-                       }
-                       foreach ( $paths as $path ) {
-                               $status->fatal( 'lockmanager-fail-acquirelock', $path );
-                       }
-               }
-
-               return $status;
-       }
-
-       /**
-        * @see QuorumLockManager::releaseAllLocks()
-        * @return Status
-        */
-       protected function releaseAllLocks() {
-               $status = Status::newGood();
-
-               foreach ( $this->conns as $lockDb => $db ) {
-                       try {
-                               $db->query( "SELECT pg_advisory_unlock_all()", __METHOD__ );
-                       } catch ( DBError $e ) {
-                               $status->fatal( 'lockmanager-fail-db-release', $lockDb );
-                       }
-               }
-
-               return $status;
-       }
-}
diff --git a/includes/filebackend/lockmanager/MySqlLockManager.php b/includes/filebackend/lockmanager/MySqlLockManager.php
new file mode 100644 (file)
index 0000000..0536091
--- /dev/null
@@ -0,0 +1,125 @@
+<?php
+/**
+ * MySQL version of DBLockManager that supports shared locks.
+ *
+ * All lock servers must have the innodb table defined in locking/filelocks.sql.
+ * All locks are non-blocking, which avoids deadlocks.
+ *
+ * @ingroup LockManager
+ */
+class MySqlLockManager extends DBLockManager {
+       /** @var array Mapping of lock types to the type actually used */
+       protected $lockTypeMap = [
+               self::LOCK_SH => self::LOCK_SH,
+               self::LOCK_UW => self::LOCK_SH,
+               self::LOCK_EX => self::LOCK_EX
+       ];
+
+       protected function getLocalLB() {
+               // Use a separate connection so releaseAllLocks() doesn't rollback the main trx
+               return wfGetLBFactory()->newMainLB( $this->domain );
+       }
+
+       protected function initConnection( $lockDb, IDatabase $db ) {
+               # Let this transaction see lock rows from other transactions
+               $db->query( "SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;" );
+               # Do everything in a transaction as it all gets rolled back eventually
+               $db->startAtomic( __CLASS__ );
+       }
+
+       /**
+        * Get a connection to a lock DB and acquire locks on $paths.
+        * This does not use GET_LOCK() per http://bugs.mysql.com/bug.php?id=1118.
+        *
+        * @see DBLockManager::getLocksOnServer()
+        * @param string $lockSrv
+        * @param array $paths
+        * @param string $type
+        * @return Status
+        */
+       protected function doGetLocksOnServer( $lockSrv, array $paths, $type ) {
+               $status = Status::newGood();
+
+               $db = $this->getConnection( $lockSrv ); // checked in isServerUp()
+
+               $keys = []; // list of hash keys for the paths
+               $data = []; // list of rows to insert
+               $checkEXKeys = []; // list of hash keys that this has no EX lock on
+               # Build up values for INSERT clause
+               foreach ( $paths as $path ) {
+                       $key = $this->sha1Base36Absolute( $path );
+                       $keys[] = $key;
+                       $data[] = [ 'fls_key' => $key, 'fls_session' => $this->session ];
+                       if ( !isset( $this->locksHeld[$path][self::LOCK_EX] ) ) {
+                               $checkEXKeys[] = $key;
+                       }
+               }
+
+               # Block new writers (both EX and SH locks leave entries here)...
+               $db->insert( 'filelocks_shared', $data, __METHOD__, [ 'IGNORE' ] );
+               # Actually do the locking queries...
+               if ( $type == self::LOCK_SH ) { // reader locks
+                       $blocked = false;
+                       # Bail if there are any existing writers...
+                       if ( count( $checkEXKeys ) ) {
+                               $blocked = $db->selectField( 'filelocks_exclusive', '1',
+                                       [ 'fle_key' => $checkEXKeys ],
+                                       __METHOD__
+                               );
+                       }
+                       # Other prospective writers that haven't yet updated filelocks_exclusive
+                       # will recheck filelocks_shared after doing so and bail due to this entry.
+               } else { // writer locks
+                       $encSession = $db->addQuotes( $this->session );
+                       # Bail if there are any existing writers...
+                       # This may detect readers, but the safe check for them is below.
+                       # Note: if two writers come at the same time, both bail :)
+                       $blocked = $db->selectField( 'filelocks_shared', '1',
+                               [ 'fls_key' => $keys, "fls_session != $encSession" ],
+                               __METHOD__
+                       );
+                       if ( !$blocked ) {
+                               # Build up values for INSERT clause
+                               $data = [];
+                               foreach ( $keys as $key ) {
+                                       $data[] = [ 'fle_key' => $key ];
+                               }
+                               # Block new readers/writers...
+                               $db->insert( 'filelocks_exclusive', $data, __METHOD__ );
+                               # Bail if there are any existing readers...
+                               $blocked = $db->selectField( 'filelocks_shared', '1',
+                                       [ 'fls_key' => $keys, "fls_session != $encSession" ],
+                                       __METHOD__
+                               );
+                       }
+               }
+
+               if ( $blocked ) {
+                       foreach ( $paths as $path ) {
+                               $status->fatal( 'lockmanager-fail-acquirelock', $path );
+                       }
+               }
+
+               return $status;
+       }
+
+       /**
+        * @see QuorumLockManager::releaseAllLocks()
+        * @return Status
+        */
+       protected function releaseAllLocks() {
+               $status = Status::newGood();
+
+               foreach ( $this->conns as $lockDb => $db ) {
+                       if ( $db->trxLevel() ) { // in transaction
+                               try {
+                                       $db->rollback( __METHOD__ ); // finish transaction and kill any rows
+                               } catch ( DBError $e ) {
+                                       $status->fatal( 'lockmanager-fail-db-release', $lockDb );
+                               }
+                       }
+               }
+
+               return $status;
+       }
+}
diff --git a/includes/filebackend/lockmanager/PostgreSqlLockManager.php b/includes/filebackend/lockmanager/PostgreSqlLockManager.php
new file mode 100644 (file)
index 0000000..d55b5ae
--- /dev/null
@@ -0,0 +1,79 @@
+<?php
+/**
+ * PostgreSQL version of DBLockManager that supports shared locks.
+ * All locks are non-blocking, which avoids deadlocks.
+ *
+ * @ingroup LockManager
+ */
+class PostgreSqlLockManager extends DBLockManager {
+       /** @var array Mapping of lock types to the type actually used */
+       protected $lockTypeMap = [
+               self::LOCK_SH => self::LOCK_SH,
+               self::LOCK_UW => self::LOCK_SH,
+               self::LOCK_EX => self::LOCK_EX
+       ];
+
+       protected function doGetLocksOnServer( $lockSrv, array $paths, $type ) {
+               $status = Status::newGood();
+               if ( !count( $paths ) ) {
+                       return $status; // nothing to lock
+               }
+
+               $db = $this->getConnection( $lockSrv ); // checked in isServerUp()
+               $bigints = array_unique( array_map(
+                       function ( $key ) {
+                               return Wikimedia\base_convert( substr( $key, 0, 15 ), 16, 10 );
+                       },
+                       array_map( [ $this, 'sha1Base16Absolute' ], $paths )
+               ) );
+
+               // Try to acquire all the locks...
+               $fields = [];
+               foreach ( $bigints as $bigint ) {
+                       $fields[] = ( $type == self::LOCK_SH )
+                               ? "pg_try_advisory_lock_shared({$db->addQuotes( $bigint )}) AS K$bigint"
+                               : "pg_try_advisory_lock({$db->addQuotes( $bigint )}) AS K$bigint";
+               }
+               $res = $db->query( 'SELECT ' . implode( ', ', $fields ), __METHOD__ );
+               $row = $res->fetchRow();
+
+               if ( in_array( 'f', $row ) ) {
+                       // Release any acquired locks if some could not be acquired...
+                       $fields = [];
+                       foreach ( $row as $kbigint => $ok ) {
+                               if ( $ok === 't' ) { // locked
+                                       $bigint = substr( $kbigint, 1 ); // strip off the "K"
+                                       $fields[] = ( $type == self::LOCK_SH )
+                                               ? "pg_advisory_unlock_shared({$db->addQuotes( $bigint )})"
+                                               : "pg_advisory_unlock({$db->addQuotes( $bigint )})";
+                               }
+                       }
+                       if ( count( $fields ) ) {
+                               $db->query( 'SELECT ' . implode( ', ', $fields ), __METHOD__ );
+                       }
+                       foreach ( $paths as $path ) {
+                               $status->fatal( 'lockmanager-fail-acquirelock', $path );
+                       }
+               }
+
+               return $status;
+       }
+
+       /**
+        * @see QuorumLockManager::releaseAllLocks()
+        * @return Status
+        */
+       protected function releaseAllLocks() {
+               $status = Status::newGood();
+
+               foreach ( $this->conns as $lockDb => $db ) {
+                       try {
+                               $db->query( "SELECT pg_advisory_unlock_all()", __METHOD__ );
+                       } catch ( DBError $e ) {
+                               $status->fatal( 'lockmanager-fail-db-release', $lockDb );
+                       }
+               }
+
+               return $status;
+       }
+}
index 6095aee..4121ecb 100644 (file)
@@ -81,10 +81,12 @@ class RedisLockManager extends QuorumLockManager {
        protected function getLocksOnServer( $lockSrv, array $pathsByType ) {
                $status = Status::newGood();
 
+               $pathList = call_user_func_array( 'array_merge', array_values( $pathsByType ) );
+
                $server = $this->lockServers[$lockSrv];
                $conn = $this->redisPool->getConnection( $server );
                if ( !$conn ) {
-                       foreach ( array_merge( array_values( $pathsByType ) ) as $path ) {
+                       foreach ( $pathList as $path ) {
                                $status->fatal( 'lockmanager-fail-acquirelock', $path );
                        }
 
@@ -157,7 +159,7 @@ LUA;
                }
 
                if ( $res === false ) {
-                       foreach ( array_merge( array_values( $pathsByType ) ) as $path ) {
+                       foreach ( $pathList as $path ) {
                                $status->fatal( 'lockmanager-fail-acquirelock', $path );
                        }
                } else {
@@ -172,10 +174,12 @@ LUA;
        protected function freeLocksOnServer( $lockSrv, array $pathsByType ) {
                $status = Status::newGood();
 
+               $pathList = call_user_func_array( 'array_merge', array_values( $pathsByType ) );
+
                $server = $this->lockServers[$lockSrv];
                $conn = $this->redisPool->getConnection( $server );
                if ( !$conn ) {
-                       foreach ( array_merge( array_values( $pathsByType ) ) as $path ) {
+                       foreach ( $pathList as $path ) {
                                $status->fatal( 'lockmanager-fail-releaselock', $path );
                        }
 
@@ -225,7 +229,7 @@ LUA;
                }
 
                if ( $res === false ) {
-                       foreach ( array_merge( array_values( $pathsByType ) ) as $path ) {
+                       foreach ( $pathList as $path ) {
                                $status->fatal( 'lockmanager-fail-releaselock', $path );
                        }
                } else {
index 91d628c..40141c9 100644 (file)
@@ -2216,8 +2216,9 @@ class LocalFileDeleteBatch {
        }
 
        protected function doDBInserts() {
+               $now = time();
                $dbw = $this->file->repo->getMasterDB();
-               $encTimestamp = $dbw->addQuotes( $dbw->timestamp() );
+               $encTimestamp = $dbw->addQuotes( $dbw->timestamp( $now ) );
                $encUserId = $dbw->addQuotes( $this->user->getId() );
                $encReason = $dbw->addQuotes( $this->reason );
                $encGroup = $dbw->addQuotes( 'deleted' );
@@ -2239,15 +2240,15 @@ class LocalFileDeleteBatch {
                }
 
                if ( $deleteCurrent ) {
-                       $concat = $dbw->buildConcat( [ "img_sha1", $encExt ] );
-                       $where = [ 'img_name' => $this->file->getName() ];
-                       $dbw->insertSelect( 'filearchive', 'image',
+                       $dbw->insertSelect(
+                               'filearchive',
+                               'image',
                                [
                                        'fa_storage_group' => $encGroup,
                                        'fa_storage_key' => $dbw->conditional(
                                                [ 'img_sha1' => '' ],
                                                $dbw->addQuotes( '' ),
-                                               $concat
+                                               $dbw->buildConcat( [ "img_sha1", $encExt ] )
                                        ),
                                        'fa_deleted_user' => $encUserId,
                                        'fa_deleted_timestamp' => $encTimestamp,
@@ -2268,44 +2269,56 @@ class LocalFileDeleteBatch {
                                        'fa_user' => 'img_user',
                                        'fa_user_text' => 'img_user_text',
                                        'fa_timestamp' => 'img_timestamp',
-                                       'fa_sha1' => 'img_sha1',
-                               ], $where, __METHOD__ );
+                                       'fa_sha1' => 'img_sha1'
+                               ],
+                               [ 'img_name' => $this->file->getName() ],
+                               __METHOD__
+                       );
                }
 
                if ( count( $oldRels ) ) {
-                       $concat = $dbw->buildConcat( [ "oi_sha1", $encExt ] );
-                       $where = [
-                               'oi_name' => $this->file->getName(),
-                               'oi_archive_name' => array_keys( $oldRels ) ];
-                       $dbw->insertSelect( 'filearchive', 'oldimage',
+                       $res = $dbw->select(
+                               'oldimage',
+                               OldLocalFile::selectFields(),
                                [
-                                       'fa_storage_group' => $encGroup,
-                                       'fa_storage_key' => $dbw->conditional(
-                                               [ 'oi_sha1' => '' ],
-                                               $dbw->addQuotes( '' ),
-                                               $concat
-                                       ),
-                                       'fa_deleted_user' => $encUserId,
-                                       'fa_deleted_timestamp' => $encTimestamp,
-                                       'fa_deleted_reason' => $encReason,
-                                       'fa_deleted' => $this->suppress ? $bitfield : 'oi_deleted',
-
-                                       'fa_name' => 'oi_name',
-                                       'fa_archive_name' => 'oi_archive_name',
-                                       'fa_size' => 'oi_size',
-                                       'fa_width' => 'oi_width',
-                                       'fa_height' => 'oi_height',
-                                       'fa_metadata' => 'oi_metadata',
-                                       'fa_bits' => 'oi_bits',
-                                       'fa_media_type' => 'oi_media_type',
-                                       'fa_major_mime' => 'oi_major_mime',
-                                       'fa_minor_mime' => 'oi_minor_mime',
-                                       'fa_description' => 'oi_description',
-                                       'fa_user' => 'oi_user',
-                                       'fa_user_text' => 'oi_user_text',
-                                       'fa_timestamp' => 'oi_timestamp',
-                                       'fa_sha1' => 'oi_sha1',
-                               ], $where, __METHOD__ );
+                                       'oi_name' => $this->file->getName(),
+                                       'oi_archive_name' => array_keys( $oldRels )
+                               ],
+                               __METHOD__,
+                               [ 'FOR UPDATE' ]
+                       );
+                       $rowsInsert = [];
+                       foreach ( $res as $row ) {
+                               $rowsInsert[] = [
+                                       // Deletion-specific fields
+                                       'fa_storage_group' => 'deleted',
+                                       'fa_storage_key' => ( $row->oi_sha1 === '' )
+                                               ? ''
+                                               : "{$row->oi_sha1}{$dotExt}",
+                                       'fa_deleted_user' => $this->user->getId(),
+                                       'fa_deleted_timestamp' => $dbw->timestamp( $now ),
+                                       'fa_deleted_reason' => $this->reason,
+                                       // Counterpart fields
+                                       'fa_deleted' => $this->suppress ? $bitfield : $row->oi_deleted,
+                                       'fa_name' => $row->oi_name,
+                                       'fa_archive_name' => $row->oi_archive_name,
+                                       'fa_size' => $row->oi_size,
+                                       'fa_width' => $row->oi_width,
+                                       'fa_height' => $row->oi_height,
+                                       'fa_metadata' => $row->oi_metadata,
+                                       'fa_bits' => $row->oi_bits,
+                                       'fa_media_type' => $row->oi_media_type,
+                                       'fa_major_mime' => $row->oi_major_mime,
+                                       'fa_minor_mime' => $row->oi_minor_mime,
+                                       'fa_description' => $row->oi_description,
+                                       'fa_user' => $row->oi_user,
+                                       'fa_user_text' => $row->oi_user_text,
+                                       'fa_timestamp' => $row->oi_timestamp,
+                                       'fa_sha1' => $row->oi_sha1
+                               ];
+                       }
+
+                       $dbw->insert( 'filearchive', $rowsInsert, __METHOD__ );
                }
        }
 
@@ -2596,8 +2609,9 @@ class LocalFileRestoreBatch {
 
                                // The live (current) version cannot be hidden!
                                if ( !$this->unsuppress && $row->fa_deleted ) {
-                                       $storeBatch[] = [ $deletedUrl, 'public', $destRel ];
-                                       $this->cleanupBatch[] = $row->fa_storage_key;
+                                       $status->fatal( 'undeleterevdel' );
+                                       $this->file->unlock();
+                                       return $status;
                                }
                        } else {
                                $archiveName = $row->fa_archive_name;
index ff37e24..3c88594 100644 (file)
@@ -354,6 +354,26 @@ class HTMLForm extends ContextSource {
                $this->mFieldTree = $loadedDescriptor;
        }
 
+       /**
+        * @param string $fieldname
+        * @return bool
+        */
+       public function hasField( $fieldname ) {
+               return isset( $this->mFlatFields[$fieldname] );
+       }
+
+       /**
+        * @param string $fieldname
+        * @return HTMLFormField
+        * @throws DomainException on invalid field name
+        */
+       public function getField( $fieldname ) {
+               if ( !$this->hasField( $fieldname ) ) {
+                       throw new DomainException( __METHOD__ . ': no field named ' . $fieldname );
+               }
+               return $this->mFlatFields[$fieldname];
+       }
+
        /**
         * Set format in which to display the form
         *
index e553218..089213c 100644 (file)
@@ -9,15 +9,23 @@
 trait HTMLFormElement {
 
        protected $hideIf = null;
+       protected $modules = null;
 
        public function initializeHTMLFormElement( array $config = [] ) {
                // Properties
                $this->hideIf = isset( $config['hideIf'] ) ? $config['hideIf'] : null;
+               $this->modules = isset( $config['modules'] ) ? $config['modules'] : [];
 
                // Initialization
                if ( $this->hideIf ) {
                        $this->addClasses( [ 'mw-htmlform-hide-if' ] );
                }
+               if ( $this->modules ) {
+                       // JS code must be able to read this before infusing (before OOjs UI is even loaded),
+                       // so we put this in a separate attribute (not with the rest of the config).
+                       // And it's not needed anymore after infusing, so we don't put it in JS config at all.
+                       $this->setAttributes( [ 'data-mw-modules' => implode( ',', $this->modules ) ] );
+               }
                $this->registerConfigCallback( function( &$config ) {
                        if ( $this->hideIf !== null ) {
                                $config['hideIf'] = $this->hideIf;
index 3319d3b..25b4cca 100644 (file)
@@ -61,7 +61,7 @@ abstract class HTMLFormField {
         * @return bool
         */
        public function canDisplayErrors() {
-               return true;
+               return $this->hasVisibleOutput();
        }
 
        /**
@@ -455,10 +455,6 @@ abstract class HTMLFormField {
                        $this->mFilterCallback = $params['filter-callback'];
                }
 
-               if ( isset( $params['flatlist'] ) ) {
-                       $this->mClass .= ' mw-htmlform-flatlist';
-               }
-
                if ( isset( $params['hidelabel'] ) ) {
                        $this->mShowEmptyLabels = false;
                }
@@ -626,8 +622,10 @@ abstract class HTMLFormField {
                        'infusable' => $infusable,
                ];
 
+               $preloadModules = false;
+
                if ( $infusable && $this->shouldInfuseOOUI() ) {
-                       $this->mParent->getOutput()->addModules( 'mediawiki.htmlform.ooui' );
+                       $preloadModules = true;
                        $config['classes'][] = 'mw-htmlform-field-autoinfuse';
                }
 
@@ -638,10 +636,17 @@ abstract class HTMLFormField {
                }
 
                if ( $this->mHideIf ) {
-                       $this->mParent->getOutput()->addModules( 'mediawiki.htmlform.ooui' );
+                       $preloadModules = true;
                        $config['hideIf'] = $this->mHideIf;
                }
 
+               $config['modules'] = $this->getOOUIModules();
+
+               if ( $preloadModules ) {
+                       $this->mParent->getOutput()->addModules( 'mediawiki.htmlform.ooui' );
+                       $this->mParent->getOutput()->addModules( $this->getOOUIModules() );
+               }
+
                return $this->getFieldLayoutOOUI( $inputField, $config );
        }
 
@@ -677,6 +682,16 @@ abstract class HTMLFormField {
                return $this->getHelpText() !== null;
        }
 
+       /**
+        * Get the list of extra ResourceLoader modules which must be loaded client-side before it's
+        * possible to infuse this field's OOjs UI widget.
+        *
+        * @return string[]
+        */
+       protected function getOOUIModules() {
+               return [];
+       }
+
        /**
         * Get the complete raw fields for the input, including help text,
         * labels, and whatever.
index a231b2f..c9fcb09 100644 (file)
@@ -4,6 +4,29 @@
  * Multi-select field
  */
 class HTMLMultiSelectField extends HTMLFormField implements HTMLNestedFilterable {
+       /**
+        * @param array $params
+        *   In adition to the usual HTMLFormField parameters, this can take the following fields:
+        *   - dropdown: If given, the options will be displayed inside a dropdown with a text field that
+        *     can be used to filter them. This is desirable mostly for very long lists of options.
+        *     This only works for users with JavaScript support and falls back to the list of checkboxes.
+        *   - flatlist: If given, the options will be displayed on a single line (wrapping to following
+        *     lines if necessary), rather than each one on a line of its own. This is desirable mostly
+        *     for very short lists of concisely labelled options.
+        */
+       public function __construct( $params ) {
+               parent::__construct( $params );
+
+               // For backwards compatibility, also handle the old way with 'cssclass' => 'mw-chosen'
+               if ( isset( $params['dropdown'] ) || strpos( $this->mClass, 'mw-chosen' ) !== false ) {
+                       $this->mClass .= ' mw-htmlform-dropdown';
+               }
+
+               if ( isset( $params['flatlist'] ) ) {
+                       $this->mClass .= ' mw-htmlform-flatlist';
+               }
+       }
+
        function validate( $value, $alldata ) {
                $p = parent::validate( $value, $alldata );
 
@@ -28,6 +51,10 @@ class HTMLMultiSelectField extends HTMLFormField implements HTMLNestedFilterable
        }
 
        function getInputHTML( $value ) {
+               if ( isset( $this->mParams['dropdown'] ) ) {
+                       $this->mParent->getOutput()->addModules( 'jquery.chosen' );
+               }
+
                $value = HTMLFormField::forceToStringRecursive( $value );
                $html = $this->formatOptions( $this->getOptions(), $value );
 
index 976befe..f9f035d 100644 (file)
@@ -4,6 +4,21 @@
  * Radio checkbox fields.
  */
 class HTMLRadioField extends HTMLFormField {
+       /**
+        * @param array $params
+        *   In adition to the usual HTMLFormField parameters, this can take the following fields:
+        *   - flatlist: If given, the options will be displayed on a single line (wrapping to following
+        *     lines if necessary), rather than each one on a line of its own. This is desirable mostly
+        *     for very short lists of concisely labelled options.
+        */
+       public function __construct( $params ) {
+               parent::__construct( $params );
+
+               if ( isset( $params['flatlist'] ) ) {
+                       $this->mClass .= ' mw-htmlform-flatlist';
+               }
+       }
+
        function validate( $value, $alldata ) {
                $p = parent::validate( $value, $alldata );
 
index ffa2500..230790d 100644 (file)
@@ -34,6 +34,11 @@ class HTMLSelectNamespace extends HTMLFormField {
                ] );
        }
 
+       protected function getOOUIModules() {
+               // FIXME: NamespaceInputWidget should be in its own module (probably?)
+               return [ 'mediawiki.widgets' ];
+       }
+
        protected function shouldInfuseOOUI() {
                return true;
        }
index 5d5d765..a15b90e 100644 (file)
@@ -73,7 +73,6 @@ class HTMLTitleTextField extends HTMLTextField {
        }
 
        protected function getInputWidget( $params ) {
-               $this->mParent->getOutput()->addModules( 'mediawiki.widgets' );
                if ( $this->mParams['namespace'] !== false ) {
                        $params['namespace'] = $this->mParams['namespace'];
                }
@@ -85,6 +84,11 @@ class HTMLTitleTextField extends HTMLTextField {
                return true;
        }
 
+       protected function getOOUIModules() {
+               // FIXME: TitleInputWidget should be in its own module
+               return [ 'mediawiki.widgets' ];
+       }
+
        public function getInputHtml( $value ) {
                // add mw-searchInput class to enable search suggestions for non-OOUI, too
                $this->mClass .= 'mw-searchInput';
index f21b53d..14b5e59 100644 (file)
@@ -40,8 +40,6 @@ class HTMLUserTextField extends HTMLTextField {
        }
 
        protected function getInputWidget( $params ) {
-               $this->mParent->getOutput()->addModules( 'mediawiki.widgets.UserInputWidget' );
-
                return new UserInputWidget( $params );
        }
 
@@ -49,6 +47,10 @@ class HTMLUserTextField extends HTMLTextField {
                return true;
        }
 
+       protected function getOOUIModules() {
+               return [ 'mediawiki.widgets.UserInputWidget' ];
+       }
+
        public function getInputHtml( $value ) {
                // add the required module and css class for user suggestions in non-OOUI mode
                $this->mParent->getOutput()->addModules( 'mediawiki.userSuggest' );
index f97b6dc..cc197c8 100644 (file)
@@ -56,8 +56,8 @@
        "config-env-php": "A PHP verziója: $1",
        "config-env-hhvm": "HHVM verziója: $1",
        "config-unicode-using-intl": "A rendszer Unicode normalizálására az [http://pecl.php.net/intl intl PECL kiterjesztést] használja.",
-       "config-unicode-pure-php-warning": "'''Figyelmeztetés''': Az Unicode normalizáláshoz szükséges [http://pecl.php.net/intl intl PECL kiterjesztés] nem érhető el, helyette a lassú, PHP alapú implementáció lesz használva.\nHa nagy látogatottságú oldalt üzemeltetsz, itt találhatsz további információkat [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations a témáról].",
-       "config-unicode-update-warning": "'''Figyelmeztetés''': Az Unicode normalizáláshoz szükséges burkolókönyvtár [http://site.icu-project.org/ ICU projekt] függvénykönyvtárának régebbi változatát használja.\nHa ügyelni kívánsz a Unicode használatára, fontold meg a [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations frissítését].",
+       "config-unicode-pure-php-warning": "<strong>Figyelmeztetés:</strong> Az Unicode normalizáláshoz szükséges [http://pecl.php.net/intl intl PECL kiterjesztés] nem érhető el, helyette a lassú, PHP-alapú implementáció lesz használatban.\nHa nagy látogatottságú oldalt üzemeltetsz, [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations itt] találhatsz további információkat a témáról.",
+       "config-unicode-update-warning": "<strong>Figyelmeztetés:</strong> Az Unicode normalizáláshoz szükséges burkolókönyvtár [http://site.icu-project.org/ ICU projekt] függvénykönyvtárának régebbi változatát használja.\nHa ügyelni kívánsz a Unicode használatára, fontold meg a [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations frissítését].",
        "config-no-db": "Nem sikerült egyetlen használható adatbázis-illesztőprogramot sem találni. Telepítened kell egyet a PHP-hez.\nA következő {{PLURAL:$2|adatbázistípus támogatott|adatbázistípusok támogatottak}}: $1.\n\nHa a PHP-t magad fordítottad, konfiguráld újra úgy, hogy engedélyezve legyen egy adatbáziskliens, pl. a <code>./configure --with-mysql</code> parancs használatával.\nHa a PHP-t Debian vagy Ubuntu csomaggal telepítetted, akkor szükséged lesz például a php5-mysql csomagra is.",
        "config-outdated-sqlite": "<strong>Figyelmeztetés:</strong> SQLite $1 verziód van, ami alacsonyabb a legalább szükséges $2 verziónál. Az SQLite nem lesz elérhető.",
        "config-no-fts3": "'''Figyelmeztetés''': Az SQLite [//sqlite.org/fts3.html FTS3 modul] nélkül lett fordítva, a keresési funkciók nem fognak működni ezen a rendszeren.",
        "config-ns-site-name": "Ugyanaz, mint a wiki neve: $1",
        "config-ns-other": "Más (meg kell adni)",
        "config-ns-other-default": "SajátWiki",
-       "config-project-namespace-help": "A Wikipédia példáját követve számos wiki elkülöníti egy '''projekt névtérbe''' az irányelveit a tartalommal rendelkező lapoktól\nAz ebben a névtérben található lapok nevei egy előtaggal kezdődnek, amit itt adhatsz meg.\nÁltalában az előtag a wiki nevéből származik, de nem tartalmazhat írásjeleket, például „#”-t vagy „:”-t.",
+       "config-project-namespace-help": "A Wikipédia példáját követve számos wiki elkülöníti az irányelveit a tartalmi lapoktól egy '''projektnévtérbe'''.\nAz ebben a névtérben található lapok nevei egy előtaggal kezdődnek, amit itt adhatsz meg.\nÁltalában az előtag a wiki nevéből származik, de nem tartalmazhat írásjeleket, például „#”-t vagy „:”-t.",
        "config-ns-invalid": "A megadott névtér („<nowiki>$1</nowiki>”) érvénytelen.\nVálassz másik projektnévteret!",
        "config-ns-conflict": "A megadott névtér („<nowiki>$1</nowiki>”) ütközik az egyik alapértelmezett MediaWiki-névtérrel.\nVálassz másik projektnévteret!",
        "config-admin-box": "Adminisztrátori fiók",
        "config-logo": "A logó URL-címe:",
        "config-logo-help": "A MediaWiki alapértelmezett felülete helyet ad egy 135×160 pixeles logónak a bal felső sarokban.\nTölts fel egy megfelelő méretű képet, majd írd be ide az URL-címét!\n\nHasználhatsz  <code>$wgStylePath</code>-t vagy <code>$wgScriptPath</code>-t, ha más méretű a logód.\n\nHa nem szeretnél logót használni, egyszerűen hagyd üresen a mezőt.",
        "config-instantcommons": "Instant Commons engedélyezése",
-       "config-instantcommons-help": "Az [https://www.mediawiki.org/wiki/InstantCommons Instant Commons] lehetővé teszi, hogy a wikin használhassák a [https://commons.wikimedia.org/ Wikimedia Commons] oldalon található képeket, hangokat és más médiafájlokat.\nA használatához a MediaWikinek internethozzáférésre van szüksége.\n\nA funkcióról és hogy hogyan állítható be más wikik esetén [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:$wgForeignFileRepos a kézikönyvben] találhatsz további információkat.",
+       "config-instantcommons-help": "Az [https://www.mediawiki.org/wiki/InstantCommons Instant Commons] lehetővé teszi, hogy a wikin használhassák a [https://commons.wikimedia.org/ Wikimédia Commons] oldalon található képeket, hangokat és más médiafájlokat.\nA használatához a MediaWikinek internet-hozzáférésre van szüksége.\n\nA funkcióról és hogy hogyan állítható be más wikik esetén [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:$wgForeignFileRepos a kézikönyvben] találhatsz további információkat.",
        "config-cc-error": "A Creative Commons-licencválasztó nem tért vissza eredménnyel.\nAdd meg kézzel a licencet.",
        "config-cc-again": "Válassz újra…",
        "config-cc-not-chosen": "Válaszd ki a kívánt Creative Commons licencet, majd kattints a „proceed”!",
        "config-install-mainpage": "Kezdőlap létrehozása az alapértelmezett tartalommal",
        "config-install-extension-tables": "Táblák létrehozása az engedélyezett kiterjesztésekhez",
        "config-install-mainpage-failed": "Nemsikerült létrehozni a kezdőlapot: $1",
-       "config-install-done": "'''Gratulálunk!'''\nA MediaWiki telepítése sikeresen befejeződött.\n\nA telepítő elkészítette a <code>LocalSettings.php</code> fájlt, amely tartalmazza az összes beállítást.\n\nEzt le kell tölteni, majd elhelyezni a wiki telepítési könyvtárába (az a könyvtár, ahol az index.php is található).\n\nA letöltés automatikusan elindul. Ha mégsem indulna el, vagy megszakítottad, az alábbi linkre kattintva újra letöltheted:\n\n$3\n\n'''Megjegyzés''': Ha ezt most nem teszed meg, és kilépsz a telepítésből, az elkészített konfigurációs fájlt nem tudod elérni a későbbiekben.\n\nHa végeztél a fájl elhelyezésével, '''[$2 beléphetsz a wikibe]'''.",
+       "config-install-done": "<strong>Gratulálunk!</strong>\nA MediaWiki telepítése sikeresen befejeződött.\n\nA telepítő elkészítette a <code>LocalSettings.php</code> fájlt, amely tartalmazza az összes beállítást.\n\nEzt le kell tölteni, majd elhelyezni a wiki telepítési könyvtárába (az a könyvtár, ahol az index.php is található).\n\nA letöltés automatikusan elindul. Ha mégsem indulna el, vagy megszakítottad, az alábbi linkre kattintva újra letöltheted:\n\n$3\n\n<strong>Megjegyzés:</strong> Ha ezt most nem teszed meg, és kilépsz a telepítésből, az elkészített konfigurációs fájlt nem tudod elérni a későbbiekben.\n\nHa végeztél a fájl elhelyezésével, <strong>[$2 beléphetsz a wikibe]</strong>.",
        "config-download-localsettings": "<code>LocalSettings.php</code> letöltése",
        "config-help": "segítség",
        "config-help-tooltip": "kattints a kibontáshoz",
        "config-nofile": "\"$1\" fájl nem található. Törölve lett?",
        "config-extension-link": "Tudtad, hogy a wikid támogat [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Extensions kiterjesztéseket]?\n\nBöngészhetsz [https://www.mediawiki.org/wiki/Special:MyLanguage/Category:Extensions_by_category kiterjesztéseket kategóriánként] vagy válogathatsz a [https://www.mediawiki.org/wiki/Extension_Matrix kiterjesztésmátrixból] az összes kiterjesztés áttekintéséhez.",
-       "mainpagetext": "'''A MediaWiki telepítése sikeresen befejeződött.'''",
+       "mainpagetext": "<strong>A MediaWiki telepítése sikeresen befejeződött.</strong>",
        "mainpagedocfooter": "Ha segítségre van szükséged a wikiszoftver használatához, akkor keresd fel a [https://meta.wikimedia.org/wiki/Help:Contents User's Guide] oldalt.\n\n== Alapok (angol nyelven) ==\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings Beállítások listája]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ MediaWiki GyIK]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki-kiadások levelezőlistája]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation#Translation_resources A MediaWiki fordítása a saját nyelvedre]"
 }
index fa2af13..22cceb4 100644 (file)
        "config-install-user": "Kuriamas duomenų bazės naudotojas",
        "config-install-user-alreadyexists": "Naudotojas „$1“ jau yra",
        "config-install-user-create-failed": "Nepavyko sukurti naudotojo „$1“: $2",
+       "config-install-user-missing": "Nurodytas vartotojas „$1“ neegzistuoja.",
+       "config-install-user-missing-create": "Nurodytas vartotojas „$1“ neegzistuoja.\nPrašome pažymėti „sukurti paskyrą“ laukelį žemiau jei norite jį sukurti.",
        "config-install-tables": "Kuriamos lentelės",
+       "config-install-tables-exist": "<strong>Įspėjimas:</strong> MediaWiki lentelės, atrodo, jau egzistuoja.\nKūrimas praleidžiamas.",
+       "config-install-tables-failed": "<strong>Klaida:</strong> Lentelės sukūrimas nepavyko dėl šios klaidos: $1",
+       "config-install-interwiki-list": "Nepavyko perskaityti failo <code>interwiki.list</code>.",
+       "config-install-interwiki-exists": "<strong>Įspėjimas:</strong> Interwiki lentelė, atrodo, jau turi įrašų.\nNumatytasis sąrašas praleidžiamas.",
        "config-install-stats": "Inicijuojamos statistikos",
        "config-install-keys": "Generuojami slapti raktai",
        "config-install-sysop": "Administratoriaus vartotojo paskyra kuriama",
        "config-help": "pagalba",
        "config-help-tooltip": "spustelėkite išplėtimui",
        "config-nofile": "Failas \"$1\" nerastas. Ar jis buvo ištrintas?",
-       "mainpagetext": "'''MediaWiki sėkmingai įdiegta.'''",
+       "mainpagetext": "<strong>MediaWiki sėkmingai įdiegta.</strong>",
        "mainpagedocfooter": "Informacijos apie wiki programinės įrangos naudojimą, ieškokite [https://meta.wikimedia.org/wiki/Help:Contents žinyne].\n\n== Pradžiai ==\n\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings Konfigūracijos nustatymų sąrašas]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ MediaWiki DUK]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki pranešimai paštu apie naujas versijas]"
 }
index f2f56d0..08aa8c1 100644 (file)
        "config-subscribe": "Theo dõi [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce danh sách thư thông báo phát hành].",
        "config-subscribe-help": "thông báo an ninh.\nBạn nên đồng ý với nó và cập nhật bản cài đặt MediaWiki của bạn khi phiên bản mới xuất hiện.",
        "config-subscribe-noemail": "Bạn đã cố gắng để đăng ký vào danh sách thư thông báo phát hành mà không cung cấp một địa chỉ thư điện tử nào cả.\nVui lòng cung cấp một địa chỉ thư điện tử nếu bạn muốn đăng ký vào danh sách thư.",
+       "config-pingback": "Chia sẻ dữ liệu về bản cài đặt này với nhóm phát triển MediaWiki.",
        "config-almost-done": "Bạn gần như đã hoàn tất!\nBây giờ bạn có thể bỏ qua cấu hình còn lại và cài đặt wiki ngay bây giờ.",
        "config-optional-continue": "Hỏi tôi về thêm chi tiết.",
        "config-optional-skip": "Chán quá, cài đặt wiki rỗi.",
index 479ec32..3a1da13 100644 (file)
@@ -245,10 +245,7 @@ class JobQueueDB extends JobQueue {
                                count( $rowSet ) + count( $rowList ) - count( $rows )
                        );
                } catch ( DBError $e ) {
-                       if ( $flags & self::QOS_ATOMIC ) {
-                               $dbw->rollback( $method );
-                       }
-                       throw $e;
+                       $this->throwDBException( $e );
                }
                if ( $flags & self::QOS_ATOMIC ) {
                        $dbw->endAtomic( $method );
@@ -264,7 +261,6 @@ class JobQueueDB extends JobQueue {
        protected function doPop() {
                $dbw = $this->getMasterDB();
                try {
-                       $dbw->commit( __METHOD__, 'flush' ); // flush existing transaction
                        $autoTrx = $dbw->getFlag( DBO_TRX ); // get current setting
                        $dbw->clearFlag( DBO_TRX ); // make each query its own transaction
                        $scopedReset = new ScopedCallback( function () use ( $dbw, $autoTrx ) {
@@ -460,7 +456,6 @@ class JobQueueDB extends JobQueue {
 
                $dbw = $this->getMasterDB();
                try {
-                       $dbw->commit( __METHOD__, 'flush' ); // flush existing transaction
                        $autoTrx = $dbw->getFlag( DBO_TRX ); // get current setting
                        $dbw->clearFlag( DBO_TRX ); // make each query its own transaction
                        $scopedReset = new ScopedCallback( function () use ( $dbw, $autoTrx ) {
index 4b906a7..5f48dca 100644 (file)
@@ -21,6 +21,7 @@
  * @ingroup JobQueue
  */
 
+use MediaWiki\MediaWikiServices;
 use MediaWiki\Logger\LoggerFactory;
 use Psr\Log\LoggerAwareInterface;
 use Psr\Log\LoggerInterface;
@@ -122,7 +123,8 @@ class JobRunner implements LoggerAwareInterface {
                }
 
                // Flush any pending DB writes for sanity
-               wfGetLBFactory()->commitAll( __METHOD__ );
+               $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
+               $lbFactory->commitAll( __METHOD__ );
 
                // Catch huge single updates that lead to slave lag
                $trxProfiler = Profiler::instance()->getTransactionProfiler();
@@ -176,9 +178,11 @@ class JobRunner implements LoggerAwareInterface {
                                        $backoffs = $this->syncBackoffDeltas( $backoffs, $backoffDeltas, $wait );
                                }
 
+                               $lbFactory->commitMasterChanges( __METHOD__ ); // flush any JobQueueDB writes
                                $info = $this->executeJob( $job, $stats, $popTime );
                                if ( $info['status'] !== false || !$job->allowRetries() ) {
                                        $group->ack( $job ); // succeeded or job cannot be retried
+                                       $lbFactory->commitMasterChanges( __METHOD__ ); // flush any JobQueueDB writes
                                }
 
                                // Back off of certain jobs for a while (for throttling and for errors)
@@ -212,7 +216,7 @@ class JobRunner implements LoggerAwareInterface {
                                $timePassed = microtime( true ) - $lastCheckTime;
                                if ( $timePassed >= self::LAG_CHECK_PERIOD || $timePassed < 0 ) {
                                        try {
-                                               wfGetLBFactory()->waitForReplication( [
+                                               $lbFactory->waitForReplication( [
                                                        'ifWritesSince' => $lastCheckTime,
                                                        'timeout' => self::MAX_ALLOWED_LAG
                                                ] );
@@ -257,6 +261,7 @@ class JobRunner implements LoggerAwareInterface {
                $msg = $job->toString() . " STARTING";
                $this->logger->debug( $msg );
                $this->debugCallback( $msg );
+               $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
 
                // Run the job...
                $rssStart = $this->getMaxRssKb();
@@ -284,7 +289,7 @@ class JobRunner implements LoggerAwareInterface {
                // Commit all outstanding connections that are in a transaction
                // to get a fresh repeatable read snapshot on every connection.
                // Note that jobs are still responsible for handling slave lag.
-               wfGetLBFactory()->commitAll( __METHOD__ );
+               $lbFactory->commitAll( __METHOD__ );
                // Clear out title cache data from prior snapshots
                LinkCache::singleton()->clear();
                $timeMs = intval( ( microtime( true ) - $jobStartTime ) * 1000 );
@@ -528,17 +533,6 @@ class JobRunner implements LoggerAwareInterface {
                        $lb->waitForAll( $pos );
                }
 
-               $fname = __METHOD__;
-               // Re-ping all masters with transactions. This throws DBError if some
-               // connection died while waiting on locks/slaves, triggering a rollback.
-               wfGetLBFactory()->forEachLB( function( LoadBalancer $lb ) use ( $fname ) {
-                       $lb->forEachOpenConnection( function( IDatabase $conn ) use ( $fname ) {
-                               if ( $conn->writesOrCallbacksPending() ) {
-                                       $conn->ping();
-                               }
-                       } );
-               } );
-
                // Actually commit the DB master changes
                wfGetLBFactory()->commitMasterChanges( __METHOD__ );
 
diff --git a/includes/jobqueue/utils/PurgeJobUtils.php b/includes/jobqueue/utils/PurgeJobUtils.php
new file mode 100644 (file)
index 0000000..329bc23
--- /dev/null
@@ -0,0 +1,71 @@
+<?php
+/**
+ * Base code for update jobs that put some secondary data extracted
+ * from article content into the database.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+class PurgeJobUtils {
+       /**
+        * Invalidate the cache of a list of pages from a single namespace.
+        * This is intended for use by subclasses.
+        *
+        * @param IDatabase $dbw
+        * @param int $namespace Namespace number
+        * @param array $dbkeys
+        */
+       public static function invalidatePages( IDatabase $dbw, $namespace, array $dbkeys ) {
+               if ( $dbkeys === [] ) {
+                       return;
+               }
+
+               $dbw->onTransactionPreCommitOrIdle( function() use ( $dbw, $namespace, $dbkeys ) {
+                       // 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.
+                       $now = $dbw->timestamp();
+                       $ids = $dbw->selectFieldValues(
+                               'page',
+                               'page_id',
+                               [
+                                       'page_namespace' => $namespace,
+                                       'page_title' => $dbkeys,
+                                       'page_touched < ' . $dbw->addQuotes( $now )
+                               ],
+                               __METHOD__
+                       );
+
+                       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__
+                       );
+               } );
+       }
+}
index f304bd9..0864e5c 100644 (file)
@@ -45,9 +45,9 @@
  */
 class VirtualRESTServiceClient {
        /** @var MultiHttpClient */
-       protected $http;
-       /** @var VirtualRESTService[] Map of (prefix => VirtualRESTService) */
-       protected $instances = [];
+       private $http;
+       /** @var array Map of (prefix => VirtualRESTService|array) */
+       private $instances = [];
 
        const VALID_MOUNT_REGEX = '#^/[0-9a-z]+/([0-9a-z]+/)*$#';
 
@@ -61,15 +61,24 @@ class VirtualRESTServiceClient {
        /**
         * Map a prefix to service handler
         *
+        * If $instance is in array, it must have these keys:
+        *   - class : string; fully qualified VirtualRESTService class name
+        *   - config : array; map of parameters that is the first __construct() argument
+        *
         * @param string $prefix Virtual path
-        * @param VirtualRESTService $instance
+        * @param VirtualRESTService|array $instance Service or info to yield the service
         */
-       public function mount( $prefix, VirtualRESTService $instance ) {
+       public function mount( $prefix, $instance ) {
                if ( !preg_match( self::VALID_MOUNT_REGEX, $prefix ) ) {
                        throw new UnexpectedValueException( "Invalid service mount point '$prefix'." );
                } elseif ( isset( $this->instances[$prefix] ) ) {
                        throw new UnexpectedValueException( "A service is already mounted on '$prefix'." );
                }
+               if ( !( $instance instanceof VirtualRESTService ) ) {
+                       if ( !isset( $instance['class'] ) || !isset( $instance['config'] ) ) {
+                               throw new UnexpectedValueException( "Missing 'class' or 'config' ('$prefix')." );
+                       }
+               }
                $this->instances[$prefix] = $instance;
        }
 
@@ -104,7 +113,7 @@ class VirtualRESTServiceClient {
                };
 
                $matches = []; // matching prefixes (mount points)
-               foreach ( $this->instances as $prefix => $service ) {
+               foreach ( $this->instances as $prefix => $unused ) {
                        if ( strpos( $path, $prefix ) === 0 ) {
                                $matches[] = $prefix;
                        }
@@ -112,8 +121,8 @@ class VirtualRESTServiceClient {
                usort( $matches, $cmpFunc );
 
                // Return the most specific prefix and corresponding service
-               return isset( $matches[0] )
-                       ? [ $matches[0], $this->instances[$matches[0]] ]
+               return $matches
+                       ? [ $matches[0], $this->getInstance( $matches[0] ) ]
                        : [ null, null ];
        }
 
@@ -216,7 +225,7 @@ class VirtualRESTServiceClient {
                        // defer the original or to set a proxy response to the original.
                        $newReplaceReqsByService = [];
                        foreach ( $replaceReqsByService as $prefix => $servReqs ) {
-                               $service = $this->instances[$prefix];
+                               $service = $this->getInstance( $prefix );
                                foreach ( $service->onRequests( $servReqs, $idFunc ) as $index => $req ) {
                                        // Services use unique IDs for replacement requests
                                        if ( isset( $servReqs[$index] ) || isset( $origPending[$index] ) ) {
@@ -237,8 +246,6 @@ class VirtualRESTServiceClient {
                                        $checkReqIndexesByPrefix[$prefix][$index] = 1;
                                }
                        }
-                       // Update index of requests to inspect for replacement
-                       $replaceReqsByService = $newReplaceReqsByService;
                        // Run the actual work HTTP requests
                        foreach ( $this->http->runMulti( $executeReqs ) as $index => $ranReq ) {
                                $doneReqs[$index] = $ranReq;
@@ -252,7 +259,7 @@ class VirtualRESTServiceClient {
                        // forced by setting 'response' rather than actually be sent over the wire.
                        $newReplaceReqsByService = [];
                        foreach ( $checkReqIndexesByPrefix as $prefix => $servReqIndexes ) {
-                               $service = $this->instances[$prefix];
+                               $service = $this->getInstance( $prefix );
                                // $doneReqs actually has the requests (with 'response' set)
                                $servReqs = array_intersect_key( $doneReqs, $servReqIndexes );
                                foreach ( $service->onResponses( $servReqs, $idFunc ) as $index => $req ) {
@@ -290,4 +297,26 @@ class VirtualRESTServiceClient {
 
                return $responses;
        }
+
+       /**
+        * @param string $prefix
+        * @return VirtualRESTService
+        */
+       private function getInstance( $prefix ) {
+               if ( !isset( $this->instances[$prefix] ) ) {
+                       throw new RunTimeException( "No service registered at prefix '{$prefix}'." );
+               }
+
+               if ( !( $this->instances[$prefix] instanceof VirtualRESTService ) ) {
+                       $config = $this->instances[$prefix]['config'];
+                       $class = $this->instances[$prefix]['class'];
+                       $service = new $class( $config );
+                       if ( !( $service instanceof VirtualRESTService ) ) {
+                               throw new UnexpectedValueException( "Registered service has the wrong class." );
+                       }
+                       $this->instances[$prefix] = $service;
+               }
+
+               return $this->instances[$prefix];
+       }
 }
index 5556dd8..9ab28aa 100644 (file)
@@ -21,6 +21,8 @@
  * @ingroup Cache
  */
 
+use \MediaWiki\MediaWikiServices;
+
 /**
  * Class to store objects in the database
  *
@@ -276,6 +278,7 @@ class SqlBagOStuff extends BagOStuff {
                        if ( isset( $dataRows[$key] ) ) { // HIT?
                                $row = $dataRows[$key];
                                $this->debug( "get: retrieved data; expiry time is " . $row->exptime );
+                               $db = null;
                                try {
                                        $db = $this->getDB( $row->serverIndex );
                                        if ( $this->isExpired( $db, $row->exptime ) ) { // MISS
@@ -284,7 +287,7 @@ class SqlBagOStuff extends BagOStuff {
                                                $values[$key] = $this->unserialize( $db->decodeBlob( $row->value ) );
                                        }
                                } catch ( DBQueryError $e ) {
-                                       $this->handleWriteError( $e, $row->serverIndex );
+                                       $this->handleWriteError( $e, $db, $row->serverIndex );
                                }
                        } else { // MISS
                                $this->debug( 'get: no matching rows' );
@@ -306,10 +309,11 @@ class SqlBagOStuff extends BagOStuff {
                $result = true;
                $exptime = (int)$expiry;
                foreach ( $keysByTable as $serverIndex => $serverKeys ) {
+                       $db = null;
                        try {
                                $db = $this->getDB( $serverIndex );
                        } catch ( DBError $e ) {
-                               $this->handleWriteError( $e, $serverIndex );
+                               $this->handleWriteError( $e, $db, $serverIndex );
                                $result = false;
                                continue;
                        }
@@ -342,7 +346,7 @@ class SqlBagOStuff extends BagOStuff {
                                                __METHOD__
                                        );
                                } catch ( DBError $e ) {
-                                       $this->handleWriteError( $e, $serverIndex );
+                                       $this->handleWriteError( $e, $db, $serverIndex );
                                        $result = false;
                                }
 
@@ -364,6 +368,7 @@ class SqlBagOStuff extends BagOStuff {
 
        protected function cas( $casToken, $key, $value, $exptime = 0 ) {
                list( $serverIndex, $tableName ) = $this->getTableByKey( $key );
+               $db = null;
                try {
                        $db = $this->getDB( $serverIndex );
                        $exptime = intval( $exptime );
@@ -394,7 +399,7 @@ class SqlBagOStuff extends BagOStuff {
                                __METHOD__
                        );
                } catch ( DBQueryError $e ) {
-                       $this->handleWriteError( $e, $serverIndex );
+                       $this->handleWriteError( $e, $db, $serverIndex );
 
                        return false;
                }
@@ -404,6 +409,7 @@ class SqlBagOStuff extends BagOStuff {
 
        public function delete( $key ) {
                list( $serverIndex, $tableName ) = $this->getTableByKey( $key );
+               $db = null;
                try {
                        $db = $this->getDB( $serverIndex );
                        $db->delete(
@@ -411,7 +417,7 @@ class SqlBagOStuff extends BagOStuff {
                                [ 'keyname' => $key ],
                                __METHOD__ );
                } catch ( DBError $e ) {
-                       $this->handleWriteError( $e, $serverIndex );
+                       $this->handleWriteError( $e, $db, $serverIndex );
                        return false;
                }
 
@@ -420,6 +426,7 @@ class SqlBagOStuff extends BagOStuff {
 
        public function incr( $key, $step = 1 ) {
                list( $serverIndex, $tableName ) = $this->getTableByKey( $key );
+               $db = null;
                try {
                        $db = $this->getDB( $serverIndex );
                        $step = intval( $step );
@@ -455,7 +462,7 @@ class SqlBagOStuff extends BagOStuff {
                                $newValue = null;
                        }
                } catch ( DBError $e ) {
-                       $this->handleWriteError( $e, $serverIndex );
+                       $this->handleWriteError( $e, $db, $serverIndex );
                        return null;
                }
 
@@ -473,6 +480,7 @@ class SqlBagOStuff extends BagOStuff {
 
        public function changeTTL( $key, $expiry = 0 ) {
                list( $serverIndex, $tableName ) = $this->getTableByKey( $key );
+               $db = null;
                try {
                        $db = $this->getDB( $serverIndex );
                        $db->update(
@@ -485,7 +493,7 @@ class SqlBagOStuff extends BagOStuff {
                                return false;
                        }
                } catch ( DBError $e ) {
-                       $this->handleWriteError( $e, $serverIndex );
+                       $this->handleWriteError( $e, $db, $serverIndex );
                        return false;
                }
 
@@ -542,6 +550,7 @@ class SqlBagOStuff extends BagOStuff {
         */
        public function deleteObjectsExpiringBefore( $timestamp, $progressCallback = false ) {
                for ( $serverIndex = 0; $serverIndex < $this->numServers; $serverIndex++ ) {
+                       $db = null;
                        try {
                                $db = $this->getDB( $serverIndex );
                                $dbTimestamp = $db->timestamp( $timestamp );
@@ -604,7 +613,7 @@ class SqlBagOStuff extends BagOStuff {
                                        }
                                }
                        } catch ( DBError $e ) {
-                               $this->handleWriteError( $e, $serverIndex );
+                               $this->handleWriteError( $e, $db, $serverIndex );
                                return false;
                        }
                }
@@ -618,13 +627,14 @@ class SqlBagOStuff extends BagOStuff {
         */
        public function deleteAll() {
                for ( $serverIndex = 0; $serverIndex < $this->numServers; $serverIndex++ ) {
+                       $db = null;
                        try {
                                $db = $this->getDB( $serverIndex );
                                for ( $i = 0; $i < $this->shards; $i++ ) {
                                        $db->delete( $this->getTableNameByShard( $i ), '*', __METHOD__ );
                                }
                        } catch ( DBError $e ) {
-                               $this->handleWriteError( $e, $serverIndex );
+                               $this->handleWriteError( $e, $db, $serverIndex );
                                return false;
                        }
                }
@@ -694,18 +704,19 @@ class SqlBagOStuff extends BagOStuff {
         * Handle a DBQueryError which occurred during a write operation.
         *
         * @param DBError $exception
+        * @param IDatabase|null $db DB handle or null if connection failed
         * @param int $serverIndex
+        * @throws Exception
         */
-       protected function handleWriteError( DBError $exception, $serverIndex ) {
-               if ( $exception instanceof DBConnectionError ) {
+       protected function handleWriteError( DBError $exception, IDatabase $db = null, $serverIndex ) {
+               if ( !$db ) {
                        $this->markServerDown( $exception, $serverIndex );
-               }
-               if ( $exception->db && $exception->db->wasReadOnlyError() ) {
-                       if ( $exception->db->trxLevel() ) {
-                               try {
-                                       $exception->db->rollback( __METHOD__ );
-                               } catch ( DBError $e ) {
-                               }
+               } elseif ( $db->wasReadOnlyError() ) {
+                       if ( $db->trxLevel() && $this->usesMainDB() ) {
+                               // Errors like deadlocks and connection drops already cause rollback.
+                               // For consistency, we have no choice but to throw an error and trigger
+                               // complete rollback if the main DB is also being used as the cache DB.
+                               throw $exception;
                        }
                }
 
@@ -725,7 +736,7 @@ class SqlBagOStuff extends BagOStuff {
         * @param DBError $exception
         * @param int $serverIndex
         */
-       protected function markServerDown( $exception, $serverIndex ) {
+       protected function markServerDown( DBError $exception, $serverIndex ) {
                unset( $this->conns[$serverIndex] ); // bug T103435
 
                if ( isset( $this->connFailureTimes[$serverIndex] ) ) {
@@ -762,11 +773,19 @@ class SqlBagOStuff extends BagOStuff {
                }
        }
 
+       /**
+        * @return bool Whether the main DB is used, e.g. wfGetDB( DB_MASTER )
+        */
+       protected function usesMainDB() {
+               return !$this->serverInfos;
+       }
+
        protected function waitForSlaves() {
-               if ( !$this->serverInfos ) {
+               if ( $this->usesMainDB() ) {
                        // Main LB is used; wait for any slaves to catch up
                        try {
-                               wfGetLBFactory()->waitForReplication( [ 'wiki' => wfWikiID() ] );
+                               $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
+                               $lbFactory->waitForReplication( [ 'wiki' => wfWikiID() ] );
                                return true;
                        } catch ( DBReplicationWaitError $e ) {
                                return false;
index 6396aaa..b3a97f7 100644 (file)
@@ -543,13 +543,8 @@ class Article implements Page {
                                }
                        }
 
-                       # Is it client cached?
-                       if ( $outputPage->checkLastModified( $timestamp ) ) {
-                               wfDebug( __METHOD__ . ": done 304\n" );
-
-                               return;
-                       # Try file cache
-                       } elseif ( $wgUseFileCache && $this->tryFileCache() ) {
+                       # Try to stream the output from file cache
+                       if ( $wgUseFileCache && $this->tryFileCache() ) {
                                wfDebug( __METHOD__ . ": done file cache\n" );
                                # tell wgOut that output is taken care of
                                $outputPage->disable();
index 0344756..4066501 100644 (file)
@@ -504,13 +504,13 @@ class WikiPage implements Page, IDBAccessObject {
 
        /**
         * Loads page_touched and returns a value indicating if it should be used
-        * @return bool True if not a redirect
+        * @return bool True if this page exists and is not a redirect
         */
        public function checkTouched() {
                if ( !$this->mDataLoaded ) {
                        $this->loadPageData();
                }
-               return !$this->mIsRedirect;
+               return ( $this->mId && !$this->mIsRedirect );
        }
 
        /**
@@ -1770,7 +1770,6 @@ class WikiPage implements Page, IDBAccessObject {
                        $revisionId = $revision->insertOn( $dbw );
                        // Update page_latest and friends to reflect the new revision
                        if ( !$this->updateRevisionOn( $dbw, $revision, null, $meta['oldIsRedirect'] ) ) {
-                               $dbw->rollback( __METHOD__ ); // sanity; this should never happen
                                throw new MWException( "Failed to update page row to use new revision." );
                        }
 
@@ -1920,7 +1919,6 @@ class WikiPage implements Page, IDBAccessObject {
                $revisionId = $revision->insertOn( $dbw );
                // Update the page record with revision data
                if ( !$this->updateRevisionOn( $dbw, $revision, 0 ) ) {
-                       $dbw->rollback( __METHOD__ ); // sanity; this should never happen
                        throw new MWException( "Failed to update page row to use new revision." );
                }
 
@@ -2875,6 +2873,10 @@ class WikiPage implements Page, IDBAccessObject {
                        return $status;
                }
 
+               // Given the lock above, we can be confident in the title and page ID values
+               $namespace = $this->getTitle()->getNamespace();
+               $dbKey = $this->getTitle()->getDBkey();
+
                // At this point we are now comitted to returning an OK
                // status unless some DB query error or other exception comes up.
                // This way callers don't have to call rollback() if $status is bad
@@ -2895,54 +2897,50 @@ class WikiPage implements Page, IDBAccessObject {
                        $bitfield = 'rev_deleted';
                }
 
-               /**
-                * For now, shunt the revision data into the archive table.
-                * Text is *not* removed from the text table; bulk storage
-                * is left intact to avoid breaking block-compression or
-                * immutable storage schemes.
-                *
-                * For backwards compatibility, note that some older archive
-                * table entries will have ar_text and ar_flags fields still.
-                *
-                * In the future, we may keep revisions and mark them with
-                * the rev_deleted field, which is reserved for this purpose.
-                */
-
-               $row = [
-                       'ar_namespace'  => 'page_namespace',
-                       'ar_title'      => 'page_title',
-                       'ar_comment'    => 'rev_comment',
-                       'ar_user'       => 'rev_user',
-                       'ar_user_text'  => 'rev_user_text',
-                       'ar_timestamp'  => 'rev_timestamp',
-                       'ar_minor_edit' => 'rev_minor_edit',
-                       'ar_rev_id'     => 'rev_id',
-                       'ar_parent_id'  => 'rev_parent_id',
-                       'ar_text_id'    => 'rev_text_id',
-                       'ar_text'       => '\'\'', // Be explicit to appease
-                       'ar_flags'      => '\'\'', // MySQL's "strict mode"...
-                       'ar_len'        => 'rev_len',
-                       'ar_page_id'    => 'page_id',
-                       'ar_deleted'    => $bitfield,
-                       'ar_sha1'       => 'rev_sha1',
-               ];
+               // For now, shunt the revision data into the archive table.
+               // Text is *not* removed from the text table; bulk storage
+               // is left intact to avoid breaking block-compression or
+               // immutable storage schemes.
+               // In the future, we may keep revisions and mark them with
+               // the rev_deleted field, which is reserved for this purpose.
 
-               if ( $wgContentHandlerUseDB ) {
-                       $row['ar_content_model'] = 'rev_content_model';
-                       $row['ar_content_format'] = 'rev_content_format';
-               }
-
-               // Copy all the page revisions into the archive table
-               $dbw->insertSelect(
-                       'archive',
-                       [ 'page', 'revision' ],
-                       $row,
-                       [
-                               'page_id' => $id,
-                               'page_id = rev_page'
-                       ],
-                       __METHOD__
+               // Get all of the page revisions
+               $res = $dbw->select(
+                       'revision',
+                       Revision::selectFields(),
+                       [ 'rev_page' => $id ],
+                       __METHOD__,
+                       'FOR UPDATE'
                );
+               // Build their equivalent archive rows
+               $rowsInsert = [];
+               foreach ( $res as $row ) {
+                       $rowInsert = [
+                               'ar_namespace'  => $namespace,
+                               'ar_title'      => $dbKey,
+                               'ar_comment'    => $row->rev_comment,
+                               'ar_user'       => $row->rev_user,
+                               'ar_user_text'  => $row->rev_user_text,
+                               'ar_timestamp'  => $row->rev_timestamp,
+                               'ar_minor_edit' => $row->rev_minor_edit,
+                               'ar_rev_id'     => $row->rev_id,
+                               'ar_parent_id'  => $row->rev_parent_id,
+                               'ar_text_id'    => $row->rev_text_id,
+                               'ar_text'       => '',
+                               'ar_flags'      => '',
+                               'ar_len'        => $row->rev_len,
+                               'ar_page_id'    => $id,
+                               'ar_deleted'    => $bitfield,
+                               'ar_sha1'       => $row->rev_sha1,
+                       ];
+                       if ( $wgContentHandlerUseDB ) {
+                               $rowInsert['ar_content_model'] = $row->rev_content_model;
+                               $rowInsert['ar_content_format'] = $row->rev_content_format;
+                       }
+                       $rowsInsert[] = $rowInsert;
+               }
+               // Copy them into the archive table
+               $dbw->insert( 'archive', $rowsInsert, __METHOD__ );
                // Save this so we can pass it to the ArticleDeleteComplete hook.
                $archivedRevisionCount = $dbw->affectedRows();
 
index 4f579a9..b116bd4 100644 (file)
@@ -2158,7 +2158,7 @@ class Parser {
                                $might_be_img = true;
                                $text = $m[2];
                                if ( strpos( $m[1], '%' ) !== false ) {
-                                       $m[1] = rawurldecode( $m[1] );
+                                       $m[1] = str_replace( [ '<', '>' ], [ '&lt;', '&gt;' ], rawurldecode( $m[1] ) );
                                }
                                $trail = "";
                        } else { # Invalid form; output directly
@@ -4364,7 +4364,11 @@ class Parser {
                $this->startParse( $title, $options, self::OT_WIKI, $clearState );
                $this->setUser( $user );
 
-               $text = str_replace( [ "\r\n", "\r" ], "\n", $text );
+               // We still normalize line endings for backwards-compatibility
+               // with other code that just calls PST, but this should already
+               // be handled in TextContent subclasses
+               $text = TextContent::normalizeLineEndings( $text );
+
                if ( $options->getPreSaveTransform() ) {
                        $text = $this->pstPass2( $text, $user );
                }
@@ -4442,9 +4446,6 @@ class Parser {
                        $text = preg_replace( $p2, '[[\\1]]', $text );
                }
 
-               # Trim trailing whitespace
-               $text = rtrim( $text );
-
                return $text;
        }
 
index 85b8dc3..68f0d00 100644 (file)
@@ -536,7 +536,7 @@ abstract class AuthManagerSpecialPage extends SpecialPage {
                $form->setAction( $this->getFullTitle()->getFullURL( $this->getPreservedParams() ) );
                $form->addHiddenField( $this->getTokenName(), $this->getToken()->toString() );
                $form->addHiddenField( 'authAction', $this->authAction );
-               $form->suppressDefaultSubmit( !$this->needsSubmitButton( $formDescriptor ) );
+               $form->suppressDefaultSubmit( !$this->needsSubmitButton( $requests ) );
 
                return $form;
        }
@@ -554,24 +554,38 @@ abstract class AuthManagerSpecialPage extends SpecialPage {
        }
 
        /**
-        * Returns true if the form has fields which take values. If all available providers use the
-        * redirect flow, the form might contain nothing but submit buttons, in which case we should
-        * not add an extra submit button which does nothing.
+        * Returns true if the form built from the given AuthenticationRequests has fields which take
+        * values. If all available providers use the redirect flow, the form might contain nothing
+        * but submit buttons, in which case we should not add an extra submit button which does nothing.
         *
-        * @param array $formDescriptor A HTMLForm descriptor
+        * @param AuthenticationRequest[] $requests An array of AuthenticationRequests from which the
+        *  form will be built
         * @return bool
         */
-       protected function needsSubmitButton( $formDescriptor ) {
-               return (bool)array_filter( $formDescriptor, function ( $item ) {
-                       $class = false;
-                       if ( array_key_exists( 'class', $item ) ) {
-                               $class = $item['class'];
-                       } elseif ( array_key_exists( 'type', $item ) ) {
-                               $class = HTMLForm::$typeMappings[$item['type']];
+       protected function needsSubmitButton( array $requests ) {
+               foreach ( $requests as $req ) {
+                       if ( $req->required === AuthenticationRequest::PRIMARY_REQUIRED &&
+                               $this->doesRequestNeedsSubmitButton( $req )
+                       ) {
+                               return true;
                        }
-                       return !is_a( $class, \HTMLInfoField::class, true ) &&
-                               !is_a( $class, \HTMLSubmitField::class, true );
-               } );
+               }
+               return false;
+       }
+
+       /**
+        * Checks if the given AuthenticationRequest needs a submit button or not.
+        *
+        * @param AuthenticationRequest $req The request to check
+        * @return bool
+        */
+       protected function doesRequestNeedsSubmitButton( AuthenticationRequest $req ) {
+               foreach ( $req->getFieldInfo() as $field => $info ) {
+                       if ( $info['type'] === 'button' ) {
+                               return false;
+                       }
+               }
+               return true;
        }
 
        /**
index 8a2e0d6..22c38cb 100644 (file)
@@ -585,7 +585,7 @@ abstract class LoginSignupSpecialPage extends AuthManagerSpecialPage {
                $this->fakeTemplate = $fakeTemplate; // FIXME there should be a saner way to pass this to the hook
                // this will call onAuthChangeFormFields()
                $formDescriptor = static::fieldInfoToFormDescriptor( $requests, $fieldInfo, $this->authAction );
-               $this->postProcessFormDescriptor( $formDescriptor );
+               $this->postProcessFormDescriptor( $formDescriptor, $requests );
 
                $context = $this->getContext();
                if ( $context->getRequest() !== $this->getRequest() ) {
@@ -616,36 +616,6 @@ abstract class LoginSignupSpecialPage extends AuthManagerSpecialPage {
                        $form->setId( 'userlogin2' );
                }
 
-               // add pre/post text
-               // header used by ConfirmEdit, CondfirmAccount, Persona, WikimediaIncubator, SemanticSignup
-               // should be above the error message but HTMLForm doesn't support that
-               $form->addHeaderText( $fakeTemplate->get( 'header' ) );
-
-               // FIXME the old form used this for error/warning messages which does not play well with
-               // HTMLForm (maybe it could with a subclass?); for now only display it for signups
-               // (where the JS username validation needs it) and alway empty
-               if ( $this->isSignup() ) {
-                       // used by the mediawiki.special.userlogin.signup.js module
-                       $statusAreaAttribs = [ 'id' => 'mw-createacct-status-area' ];
-                       // $statusAreaAttribs += $msg ? [ 'class' => "{$msgType}box" ] : [ 'style' => 'display: none;' ];
-                       $form->addHeaderText( Html::element( 'div', $statusAreaAttribs ) );
-               }
-
-               // header used by MobileFrontend
-               $form->addHeaderText( $fakeTemplate->get( 'formheader' ) );
-
-               // blank signup footer for site customization
-               if ( $this->isSignup() && $this->showExtraInformation() ) {
-                       // Use signupend-https for HTTPS requests if it's not blank, signupend otherwise
-                       $signupendMsg = $this->msg( 'signupend' );
-                       $signupendHttpsMsg = $this->msg( 'signupend-https' );
-                       if ( !$signupendMsg->isDisabled() ) {
-                               $signupendText = ( $usingHTTPS && !$signupendHttpsMsg->isBlank() )
-                                       ? $signupendHttpsMsg ->parse() : $signupendMsg->parse();
-                               $form->addPostText( Html::rawElement( 'div', [ 'id' => 'signupend' ], $signupendText ) );
-                       }
-               }
-
                // warning header for non-standard workflows (e.g. security reauthentication)
                if ( !$this->isSignup() && $this->getUser()->isLoggedIn() ) {
                        $reauthMessage = $this->securityLevel ? 'userlogin-reauth' : 'userlogin-loggedin';
@@ -653,52 +623,6 @@ abstract class LoginSignupSpecialPage extends AuthManagerSpecialPage {
                                $this->msg( $reauthMessage )->params( $this->getUser()->getName() )->parse() ) );
                }
 
-               if ( !$this->isSignup() && $this->showExtraInformation() ) {
-                       $passwordReset = new PasswordReset( $this->getConfig(), AuthManager::singleton() );
-                       if ( $passwordReset->isAllowed( $this->getUser() ) ) {
-                               $form->addFooterText( Html::rawElement(
-                                       'div',
-                                       [ 'class' => 'mw-ui-vform-field mw-form-related-link-container' ],
-                                       Linker::link(
-                                               SpecialPage::getTitleFor( 'PasswordReset' ),
-                                               $this->msg( 'userlogin-resetpassword-link' )->escaped()
-                                       )
-                               ) );
-                       }
-
-                       // Don't show a "create account" link if the user can't.
-                       if ( $this->showCreateAccountLink() ) {
-                               // link to the other action
-                               $linkTitle = $this->getTitleFor( $this->isSignup() ? 'Userlogin' :'CreateAccount' );
-                               $linkq = $this->getReturnToQueryStringFragment();
-                               // Pass any language selection on to the mode switch link
-                               if ( $wgLoginLanguageSelector && $this->mLanguage ) {
-                                       $linkq .= '&uselang=' . $this->mLanguage;
-                               }
-
-                               $loggedIn = $this->getUser()->isLoggedIn();
-                               $createOrLoginHtml = Html::rawElement( 'div',
-                                       [ 'id' => 'mw-createaccount' . ( !$loggedIn ? '-cta' : '' ),
-                                               'class' => ( $loggedIn ? 'mw-form-related-link-container' : 'mw-ui-vform-field' ) ],
-                                       ( $loggedIn ? '' : $this->msg( 'userlogin-noaccount' )->escaped() )
-                                       . Html::element( 'a',
-                                               [
-                                                       'id' => 'mw-createaccount-join' . ( $loggedIn ? '-loggedin' : '' ),
-                                                       'href' => $linkTitle->getLocalURL( $linkq ),
-                                                       'class' => ( $loggedIn ? '' : 'mw-ui-button' ),
-                                                       'tabindex' => 100,
-                                               ],
-                                               $this->msg(
-                                                       ( $this->getUser()->isLoggedIn() ?
-                                                               'userlogin-createanother' :
-                                                               'userlogin-joinproject'
-                                                       ) )->escaped()
-                                       )
-                               );
-                               $form->addFooterText( $createOrLoginHtml );
-                       }
-               }
-
                $form->suppressDefaultSubmit();
 
                $this->authForm = $form;
@@ -837,7 +761,7 @@ abstract class LoginSignupSpecialPage extends AuthManagerSpecialPage {
                array $requests, array $fieldInfo, array &$formDescriptor, $action
        ) {
                $coreFieldDescriptors = $this->getFieldDefinitions( $this->fakeTemplate );
-               $specialFields = array_merge( [ 'extraInput', 'linkcontainer', 'entryError' ],
+               $specialFields = array_merge( [ 'extraInput' ],
                        array_keys( $this->fakeTemplate->getExtraInputDefinitions() ) );
 
                // keep the ordering from getCoreFieldDescriptors() where there is no explicit weight
@@ -846,14 +770,19 @@ abstract class LoginSignupSpecialPage extends AuthManagerSpecialPage {
                                $formDescriptor[$fieldName] : [];
 
                        // remove everything that is not in the fieldinfo, is not marked as a supplemental field
-                       // to something in the fieldinfo, and is not a generic or B/C field or a submit button
+                       // to something in the fieldinfo, is not B/C for the pre-AuthManager templates,
+                       // and is not an info field or a submit button
                        if (
                                !isset( $fieldInfo[$fieldName] )
                                && (
                                        !isset( $coreField['baseField'] )
                                        || !isset( $fieldInfo[$coreField['baseField']] )
-                               ) && !in_array( $fieldName, $specialFields, true )
-                               && ( !isset( $coreField['type'] ) || $coreField['type'] !== 'submit' )
+                               )
+                               && !in_array( $fieldName, $specialFields, true )
+                               && (
+                                       !isset( $coreField['type'] )
+                                       || !in_array( $coreField['type'], [ 'submit', 'info' ], true )
+                               )
                        ) {
                                $coreFieldDescriptors[$fieldName] = null;
                                continue;
@@ -892,13 +821,12 @@ abstract class LoginSignupSpecialPage extends AuthManagerSpecialPage {
         * @return array
         */
        protected function getFieldDefinitions( $template ) {
-               global $wgEmailConfirmToEdit;
+               global $wgEmailConfirmToEdit, $wgLoginLanguageSelector;
 
                $isLoggedIn = $this->getUser()->isLoggedIn();
                $continuePart = $this->isContinued() ? 'continue-' : '';
                $anotherPart = $isLoggedIn ? 'another-' : '';
-               $expiration = $this->getRequest()->getSession()->getProvider()
-                       ->getRememberUserDuration();
+               $expiration = $this->getRequest()->getSession()->getProvider()->getRememberUserDuration();
                $expirationDays = ceil( $expiration / ( 3600 * 24 ) );
                $secureLoginLink = '';
                if ( $this->mSecureLoginUrl ) {
@@ -910,6 +838,14 @@ abstract class LoginSignupSpecialPage extends AuthManagerSpecialPage {
 
                if ( $this->isSignup() ) {
                        $fieldDefinitions = [
+                               'statusarea' => [
+                                       // used by the mediawiki.special.userlogin.signup.js module for error display
+                                       // FIXME merge this with HTMLForm's normal status (error) area
+                                       'type' => 'info',
+                                       'raw' => true,
+                                       'default' => Html::element( 'div', [ 'id' => 'mw-createacct-status-area' ] ),
+                                       'weight' => -105,
+                               ],
                                'username' => [
                                        'label-message' => 'userlogin-yourname',
                                        // FIXME help-message does not match old formatting
@@ -1056,6 +992,7 @@ abstract class LoginSignupSpecialPage extends AuthManagerSpecialPage {
                                ],
                        ];
                }
+
                $fieldDefinitions['username'] += [
                        'type' => 'text',
                        'name' => 'wpName',
@@ -1072,6 +1009,19 @@ abstract class LoginSignupSpecialPage extends AuthManagerSpecialPage {
                        // 'required' => true,
                ];
 
+               if ( $template->get( 'header' ) || $template->get( 'formheader' ) ) {
+                       // B/C for old extensions that haven't been converted to AuthManager (or have been
+                       // but somebody is using the old version) and still use templates via the
+                       // UserCreateForm/UserLoginForm hook.
+                       // 'header' used by ConfirmEdit, CondfirmAccount, Persona, WikimediaIncubator, SemanticSignup
+                       // 'formheader' used by MobileFrontend
+                       $fieldDefinitions['header'] = [
+                               'type' => 'info',
+                               'raw' => true,
+                               'default' => $template->get( 'header' ) ?: $template->get( 'formheader' ),
+                               'weight' => - 110,
+                       ];
+               }
                if ( $this->mEntryError ) {
                        $fieldDefinitions['entryError'] = [
                                'type' => 'info',
@@ -1082,9 +1032,77 @@ abstract class LoginSignupSpecialPage extends AuthManagerSpecialPage {
                                'weight' => -100,
                        ];
                }
-
                if ( !$this->showExtraInformation() ) {
-                       unset( $fieldDefinitions['linkcontainer'] );
+                       unset( $fieldDefinitions['linkcontainer'], $fieldDefinitions['signupend'] );
+               }
+               if ( $this->isSignup() && $this->showExtraInformation() ) {
+                       // blank signup footer for site customization
+                       // uses signupend-https for HTTPS requests if it's not blank, signupend otherwise
+                       $signupendMsg = $this->msg( 'signupend' );
+                       $signupendHttpsMsg = $this->msg( 'signupend-https' );
+                       if ( !$signupendMsg->isDisabled() ) {
+                               $usingHTTPS = $this->getRequest()->getProtocol() === 'https';
+                               $signupendText = ( $usingHTTPS && !$signupendHttpsMsg->isBlank() )
+                                       ? $signupendHttpsMsg ->parse() : $signupendMsg->parse();
+                               $fieldDefinitions['signupend'] = [
+                                       'type' => 'info',
+                                       'raw' => true,
+                                       'default' => Html::rawElement( 'div', [ 'id' => 'signupend' ], $signupendText ),
+                                       'weight' => 225,
+                               ];
+                       }
+               }
+               if ( !$this->isSignup() && $this->showExtraInformation() ) {
+                       $passwordReset = new PasswordReset( $this->getConfig(), AuthManager::singleton() );
+                       if ( $passwordReset->isAllowed( $this->getUser() ) ) {
+                               $fieldDefinitions['passwordReset'] = [
+                                       'type' => 'info',
+                                       'raw' => true,
+                                       'cssclass' => 'mw-form-related-link-container',
+                                       'default' => Linker::link(
+                                               SpecialPage::getTitleFor( 'PasswordReset' ),
+                                               $this->msg( 'userlogin-resetpassword-link' )->escaped()
+                                       ),
+                                       'weight' => 230,
+                               ];
+                       }
+
+                       // Don't show a "create account" link if the user can't.
+                       if ( $this->showCreateAccountLink() ) {
+                               // link to the other action
+                               $linkTitle = $this->getTitleFor( $this->isSignup() ? 'Userlogin' :'CreateAccount' );
+                               $linkq = $this->getReturnToQueryStringFragment();
+                               // Pass any language selection on to the mode switch link
+                               if ( $wgLoginLanguageSelector && $this->mLanguage ) {
+                                       $linkq .= '&uselang=' . $this->mLanguage;
+                               }
+                               $loggedIn = $this->getUser()->isLoggedIn();
+
+                               $fieldDefinitions['createOrLogin'] = [
+                                       'type' => 'info',
+                                       'raw' => true,
+                                       'linkQuery' => $linkq,
+                                       'default' => function ( $params ) use ( $loggedIn, $linkTitle ) {
+                                               return Html::rawElement( 'div',
+                                                       [ 'id' => 'mw-createaccount' . ( !$loggedIn ? '-cta' : '' ),
+                                                               'class' => ( $loggedIn ? 'mw-form-related-link-container' : 'mw-ui-vform-field' ) ],
+                                                       ( $loggedIn ? '' : $this->msg( 'userlogin-noaccount' )->escaped() )
+                                                       . Html::element( 'a',
+                                                               [
+                                                                       'id' => 'mw-createaccount-join' . ( $loggedIn ? '-loggedin' : '' ),
+                                                                       'href' => $linkTitle->getLocalURL( $params['linkQuery'] ),
+                                                                       'class' => ( $loggedIn ? '' : 'mw-ui-button' ),
+                                                                       'tabindex' => 100,
+                                                               ],
+                                                               $this->msg(
+                                                                       $loggedIn ? 'userlogin-createanother' : 'userlogin-joinproject'
+                                                               )->escaped()
+                                                       )
+                                               );
+                                       },
+                                       'weight' => 235,
+                               ];
+                       }
                }
 
                $fieldDefinitions = $this->getBCFieldDefinitions( $fieldDefinitions, $template );
@@ -1237,7 +1255,7 @@ abstract class LoginSignupSpecialPage extends AuthManagerSpecialPage {
        /**
         * @param array $formDescriptor
         */
-       protected function postProcessFormDescriptor( &$formDescriptor ) {
+       protected function postProcessFormDescriptor( &$formDescriptor, $requests ) {
                // Pre-fill username (if not creating an account, T46775).
                if (
                        isset( $formDescriptor['username'] ) &&
@@ -1255,7 +1273,7 @@ abstract class LoginSignupSpecialPage extends AuthManagerSpecialPage {
 
                // don't show a submit button if there is nothing to submit (i.e. the only form content
                // is other submit buttons, for redirect flows)
-               if ( !$this->needsSubmitButton( $formDescriptor ) ) {
+               if ( !$this->needsSubmitButton( $requests ) ) {
                        unset( $formDescriptor['createaccount'], $formDescriptor['loginattempt'] );
                }
 
index d98504d..ff70848 100644 (file)
@@ -149,7 +149,7 @@ class SpecialChangeCredentials extends AuthManagerSpecialPage {
                return $form;
        }
 
-       protected function needsSubmitButton( $formDescriptor ) {
+       protected function needsSubmitButton( array $requests ) {
                // Change/remove forms show are built from a single AuthenticationRequest and do not allow
                // for redirect flow; they always need a submit button.
                return true;
index 2b43a49..73beafc 100644 (file)
@@ -119,7 +119,12 @@ class SpecialCreateAccount extends LoginSignupSpecialPage {
                                } else {
                                        $out->addWikiMsg( 'accountcreatedtext', $user->getName() );
                                }
-                               $out->addReturnTo( $this->getPageTitle() );
+
+                               $rt = Title::newFromText( $this->mReturnTo );
+                               $out->addReturnTo(
+                                       ( $rt && !$rt->isExternal() ) ? $rt : $this->getPageTitle(),
+                                       wfCgiToArray( $this->mReturnToQuery )
+                               );
                                return;
                        }
                }
index 3e66ab0..fe97739 100644 (file)
@@ -201,6 +201,7 @@ class SpecialExport extends SpecialPage {
                                'buttontype' => 'submit',
                                'buttonname' => 'addcat',
                                'buttondefault' => $this->msg( 'export-addcat' )->text(),
+                               'hide-if' => [ '===', 'exportall', '1' ],
                        ],
                ];
                if ( $config->get( 'ExportFromNamespaces' ) ) {
@@ -216,6 +217,7 @@ class SpecialExport extends SpecialPage {
                                        'buttontype' => 'submit',
                                        'buttonname' => 'addns',
                                        'buttondefault' => $this->msg( 'export-addns' )->text(),
+                                       'hide-if' => [ '===', 'exportall', '1' ],
                                ],
                        ];
                }
@@ -240,6 +242,7 @@ class SpecialExport extends SpecialPage {
                                'nodata' => true,
                                'rows' => 10,
                                'default' => $page,
+                               'hide-if' => [ '===', 'exportall', '1' ],
                        ],
                ];
 
index d11fbe6..9cb6d4b 100644 (file)
@@ -91,7 +91,7 @@ class SpecialMyLanguage extends RedirectSpecialArticle {
 
                $uiCode = $this->getLanguage()->getCode();
                $proposed = $base->getSubpage( $uiCode );
-               if ( $uiCode !== $this->getConfig()->get( 'LanguageCode' ) && $proposed && $proposed->exists() ) {
+               if ( $proposed && $proposed->exists() && $uiCode !== $base->getPageLanguage()->getCode() ) {
                        return $proposed;
                } elseif ( $provided && $provided->exists() ) {
                        return $provided;
index ce5533f..e1e2049 100644 (file)
@@ -40,7 +40,6 @@ class SpecialRunJobs extends UnlistedSpecialPage {
 
        public function execute( $par = '' ) {
                $this->getOutput()->disable();
-
                if ( wfReadOnly() ) {
                        // HTTP 423 Locked
                        HttpStatus::header( 423 );
index 83cfa40..a9ccc4e 100644 (file)
@@ -3134,6 +3134,7 @@ class User implements IDBAccessObject {
        public function getRights() {
                if ( is_null( $this->mRights ) ) {
                        $this->mRights = self::getGroupPermissions( $this->getEffectiveGroups() );
+                       Hooks::run( 'UserGetRights', [ $this, &$this->mRights ] );
 
                        // Deny any rights denied by the user's session, unless this
                        // endpoint has no sessions.
@@ -3144,9 +3145,24 @@ class User implements IDBAccessObject {
                                }
                        }
 
-                       Hooks::run( 'UserGetRights', [ $this, &$this->mRights ] );
                        // Force reindexation of rights when a hook has unset one of them
                        $this->mRights = array_values( array_unique( $this->mRights ) );
+
+                       // If block disables login, we should also remove any
+                       // extra rights blocked users might have, in case the
+                       // blocked user has a pre-existing session (T129738).
+                       // This is checked here for cases where people only call
+                       // $user->isAllowed(). It is also checked in Title::checkUserBlock()
+                       // to give a better error message in the common case.
+                       $config = RequestContext::getMain()->getConfig();
+                       if (
+                               $this->isLoggedIn() &&
+                               $config->get( 'BlockDisablesLogin' ) &&
+                               $this->isBlocked()
+                       ) {
+                               $anon = new User;
+                               $this->mRights = array_intersect( $this->mRights, $anon->getRights() );
+                       }
                }
                return $this->mRights;
        }
@@ -3961,7 +3977,6 @@ class User implements IDBAccessObject {
                $noPass = PasswordFactory::newInvalidPassword()->toString();
 
                $dbw = wfGetDB( DB_MASTER );
-               $inWrite = $dbw->writesOrCallbacksPending();
                $seqVal = $dbw->nextSequenceValue( 'user_user_id_seq' );
                $dbw->insert( 'user',
                        [
@@ -3980,25 +3995,17 @@ class User implements IDBAccessObject {
                        [ 'IGNORE' ]
                );
                if ( !$dbw->affectedRows() ) {
-                       // The queries below cannot happen in the same REPEATABLE-READ snapshot.
-                       // Handle this by COMMIT, if possible, or by LOCK IN SHARE MODE otherwise.
-                       if ( $inWrite ) {
-                               // Can't commit due to pending writes that may need atomicity.
-                               // This may cause some lock contention unlike the case below.
-                               $options = [ 'LOCK IN SHARE MODE' ];
-                               $flags = self::READ_LOCKING;
-                       } else {
-                               // Often, this case happens early in views before any writes when
-                               // using CentralAuth. It's should be OK to commit and break the snapshot.
-                               $dbw->commit( __METHOD__, 'flush' );
-                               $options = [];
-                               $flags = self::READ_LATEST;
-                       }
-                       $this->mId = $dbw->selectField( 'user', 'user_id',
-                               [ 'user_name' => $this->mName ], __METHOD__, $options );
+                       // Use locking reads to bypass any REPEATABLE-READ snapshot.
+                       $this->mId = $dbw->selectField(
+                               'user',
+                               'user_id',
+                               [ 'user_name' => $this->mName ],
+                               __METHOD__,
+                               [ 'LOCK IN SHARE MODE' ]
+                       );
                        $loaded = false;
                        if ( $this->mId ) {
-                               if ( $this->loadFromDatabase( $flags ) ) {
+                               if ( $this->loadFromDatabase( self::READ_LOCKING ) ) {
                                        $loaded = true;
                                }
                        }
index ffb7053..a6e47c8 100644 (file)
@@ -20,6 +20,8 @@
  * @file
  * @ingroup Maintenance
  */
+use \MediaWiki\MediaWikiServices;
+
 class BatchRowWriter {
        /**
         * @var IDatabase $db The database to write to
@@ -54,7 +56,8 @@ class BatchRowWriter {
         *  names to update values to apply to the row.
         */
        public function write( array $updates ) {
-               $this->db->begin();
+               $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
+               $ticket = $lbFactory->getEmptyTransactionTicket( __METHOD__ );
 
                foreach ( $updates as $update ) {
                        $this->db->update(
@@ -65,7 +68,6 @@ class BatchRowWriter {
                        );
                }
 
-               $this->db->commit();
-               wfGetLBFactory()->waitForReplication();
+               $lbFactory->commitAndWaitForReplication( __METHOD__, $ticket );
        }
 }
index 8b39d77..cb3b4b8 100644 (file)
@@ -1036,6 +1036,7 @@ class Language {
         *    xin  n (month number) in Iranian calendar
         *    xiy  y (two digit year) in Iranian calendar
         *    xiY  Y (full year) in Iranian calendar
+        *    xit  t (days in month) in Iranian calendar
         *
         *    xjj  j (day number) in Hebrew calendar
         *    xjF  F (month name) in Hebrew calendar
@@ -1331,6 +1332,13 @@ class Language {
                                        }
                                        $num = substr( $iranian[0], -2 );
                                        break;
+                               case 'xit':
+                                       $usedIranianYear = true;
+                                       if ( !$iranian ) {
+                                               $iranian = self::tsToIranian( $ts );
+                                       }
+                                       $num = self::$IRANIAN_DAYS[$iranian[1] - 1];
+                                       break;
                                case 'a':
                                        $usedAMPM = true;
                                        $s .= intval( substr( $ts, 8, 2 ) ) < 12 ? 'am' : 'pm';
index 7bdc097..d6534b6 100644 (file)
        "passwordreset-emailtext-user": "Gebruiker $1 op die webtuiste {{SITENAME}} het u gebruikersgegewens vir {{SITENAME}} ($4) opgevra.\nDie volgende {{PLURAL:$3|gebruiker is|gebruikers is}} aan die e-posadres gekoppel:\n\n$2\n\n{{PLURAL:$3|Die tydelike wagwoord verval|Hierdie tydelike wagwoorde verval}} oor {{PLURAL:$5|een dag|$5 dae}}.\nMeld asseblief aan en verander u wagwoord nou. As u dit nie versoek het nie, of as u die oorspronklike wagwoord nog ken en dit nie wil verander nie, ignoreer die berig en hou aan om u ou wagwoord te gebruik.",
        "passwordreset-emailelement": "Gebruikersnaam: \n$1\n\nTydelike wagwoord: \n$2",
        "passwordreset-emailsentemail": "'n E-pos is gestuur om u wagwoord te herstel.",
-       "passwordreset-emailsent-capture": "'n E-pos vir die herstel van 'n wagwoord is gestuur. Dit word hieronder vertoon.",
-       "passwordreset-emailerror-capture": "'n E-pos vir die herstel van 'n wagwoord is saamgestel. Dit word hieronder vertoon. Die uitstuur daarvan na die {{GENDER:$2|gebruiker}} het egter gefaal: $1",
        "changeemail": "Wysig E-posadres",
        "changeemail-header": "Wysig rekening se e-posadres",
        "changeemail-no-info": "U moet aangemeld wees om regstreeks toegang tot die bladsy te kry.",
        "undo-nochange": "Die wysiging is klaarblyklik reeds teruggerol.",
        "undo-summary": "Rol weergawe $1 deur [[Special:Contributions/$2|$2]] ([[User talk:$2|bespreek]]) terug.",
        "undo-summary-username-hidden": "Rol weergawe $1 deur 'n versteekte gebruiker terug",
-       "cantcreateaccounttitle": "Kan nie rekening skep nie",
        "cantcreateaccount-text": "Die registrasie van nuwe rekeninge vanaf die IP-adres ('''$1''') is geblok deur [[User:$3|$3]].\n\nDie rede verskaf deur $3 is ''$2''",
        "viewpagelogs": "Bekyk logboeke vir hierdie bladsy",
        "nohistory": "Daar is geen wysigingsgeskiedenis vir hierdie bladsy nie.",
        "mediastatistics-header-total": "Alle lêers",
        "json-error-syntax": "Sintaksfout",
        "headline-anchor-title": "Skakel na die afdeling",
-       "special-characters-group-latin": "Latyns",
-       "special-characters-group-latinextended": "Latyns uitgebreid",
+       "special-characters-group-latin": "Latyn",
+       "special-characters-group-latinextended": "Latyn uitgebrei",
        "special-characters-group-ipa": "IFA",
        "special-characters-group-symbols": "Simbole",
        "special-characters-group-greek": "Grieks",
+       "special-characters-group-greekextended": "Grieks uitgebrei",
        "special-characters-group-cyrillic": "Cyrillies",
        "special-characters-group-arabic": "Arabies",
        "special-characters-group-arabicextended": "Arabies uitgebrei",
        "special-characters-group-gujarati": "Gujarati",
        "special-characters-group-devanagari": "Devanagari",
        "special-characters-group-thai": "Thai",
-       "special-characters-group-lao": "Lao",
+       "special-characters-group-lao": "Laosiaans",
        "special-characters-group-khmer": "Khmer",
        "special-characters-title-minus": "minusteken",
        "mw-widgets-dateinput-no-date": "Geen datum gekies nie",
index 5e8b4b2..3b40a05 100644 (file)
        "activeusers-hidebots": "أخف البوتات",
        "activeusers-hidesysops": "أخف الإداريين",
        "activeusers-noresult": "لم يعثر على أي مستخدمين",
-       "activeusers-submit": "لعرض المستخدمين النشطين",
+       "activeusers-submit": "عرض المستخدمين النشطين",
        "listgrouprights": "صلاحيات مجموعات المستخدمين",
        "listgrouprights-summary": "التالي قائمة بمجموعات المستخدمين المعرفة في هذا الويكي، بصلاحياتهم المصاحبة.\nربما تكون هناك [[{{MediaWiki:Listgrouprights-helppage}}|معلومات إضافية]] حول الصلاحيات المنفردة.",
        "listgrouprights-key": "عنوان:\n* <span class=\"listgrouprights-granted\">صلاحية ممنوحة</span>\n* <span class=\"listgrouprights-revoked\">صلاحية مسحوبة</span>",
index e5a0f8d..51f186e 100644 (file)
        "tagline": "Зьвесткі з {{GRAMMAR:родны|{{SITENAME}}}}",
        "help": "Дапамога",
        "search": "Пошук",
-       "search-ignored-headings": " #<!-- не зьмяняйце гэты радок --> <pre>\n# Загалоўкі, якія мусіць ігнараваць пошукавы рухавік.\n# Зьмены будуць ужытыя па наступным індэксаваньні старонкі.\n# Вы можаце змусіць пераіндэксаваць старонку пустым рэдагаваньнем.\n# Сынтакс наступны:\n#   * Усё, што пачынаецца з \"#\" — камэнтар\n#   * Усякі непусты радок — загаловак, які трэба ігнараваць\nКрыніцы\nВонкавыя спасылкі\nГлядзіце таксама\n #</pre> <!-- не зьмяняйце гэты радок -->",
+       "search-ignored-headings": " #<!-- не зьмяняйце гэты радок --> <pre>\n# Загалоўкі, якія мусіць ігнараваць пошукавы рухавік.\n# Зьмены будуць ужытыя па наступным індэксаваньні старонкі.\n# Вы можаце змусіць пераіндэксаваць старонку пустым рэдагаваньнем.\n# Сынтакс наступны:\n#   * Усё, што пачынаецца з «#» і да канца радку — камэнтар\n#   * Усякі непусты радок — загаловак, які трэба ігнараваць\nКрыніцы\nВонкавыя спасылкі\nГлядзіце таксама\n #</pre> <!-- не зьмяняйце гэты радок -->",
        "searchbutton": "Пошук",
        "go": "Старонка",
        "searcharticle": "Старонка",
        "grant-group-high-volume": "Выкананьне дзеяньняў з высокай інтэнсіўнасьцю",
        "grant-group-customization": "Налады і перавагі",
        "grant-group-administration": "Выкананьне адміністрацыйных дзеяньняў",
+       "grant-group-private-information": "Доступ да прыватных зьвестак пра вас",
        "grant-group-other": "Розная актыўнасьць",
        "grant-blockusers": "Блякаваньне і разблякаваньне ўдзельнікаў",
        "grant-createaccount": "Стварыць рахункі",
        "grant-highvolume": "Рэдагаваньне з высокай інтэнсіўнасьцю",
        "grant-oversight": "Хаваньне ўдзельнікаў і вэрсіяў старонак",
        "grant-patrol": "Патруляваньне зьменаў старонак",
+       "grant-privateinfo": "Доступ да прыватных зьвестак",
        "grant-protect": "Абарона і зьняцьце абароны старонак",
        "grant-rollback": "Адкат зьменаў старонак",
        "grant-sendemail": "Адпраўка лістоў электроннай пошты іншым удзельнікам",
        "uploadstash-errclear": "Не атрымалася ачысьціць файлы.",
        "uploadstash-refresh": "Абнавіць сьпіс файлаў.",
        "uploadstash-thumbnail": "прагляд мініятуры",
+       "uploadstash-exception": "Не магу захаваць загрузку ў сховішчы ($1): «$2».",
        "invalid-chunk-offset": "Няслушнае зрушэньне фрагмэнту",
        "img-auth-accessdenied": "Доступ забаронены",
        "img-auth-nopathinfo": "Адсутнічае PATH_INFO.\nВаш сэрвэр не ўстаноўлены на пропуск гэтай інфармацыі.\nМагчма, ён працуе праз CGI і не падтрымлівае img_auth.\nГлядзіце https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Image_Authorization.",
        "undeletedrevisions": "{{PLURAL:$1|адноўленая $1 вэрсія|адноўленыя $1 вэрсіі|адноўленыя $1 вэрсіяў}}",
        "undeletedrevisions-files": "адноўленыя $1 {{PLURAL:$1|вэрсія|вэрсіі|вэрсіяў}} і $2 {{PLURAL:$2|файл|файлы|файлаў}}",
        "undeletedfiles": "{{PLURAL:$1|адноўлены $1 файл|адноўленыя $1 файлы|адноўленыя $1 файлаў}}",
-       "cannotundelete": "Ð\9fамÑ\8bлка Ð°Ð´Ð½Ð°Ñ\9eленÑ\8cня:\n$1",
+       "cannotundelete": "Ð\9dекаÑ\82оÑ\80Ñ\8bÑ\8f Ð°Ð±Ð¾ Ñ\9eÑ\81е Ð°Ð´Ð½Ð°Ñ\9eленÑ\8cнÑ\96 Ð½Ðµ Ð±Ñ\8bлÑ\96 Ð²Ñ\8bкананÑ\8bя:\n$1",
        "undeletedpage": "'''Старонка $1 была адноўленая'''\n\nГлядзіце [[Special:Log/delete|журнал выдаленьняў]] для прагляду апошніх выдаленьняў і аднаўненьняў.",
        "undelete-header": "Глядзіце [[Special:Log/delete|журнал выдаленьняў]] для прагляду апошніх выдаленьняў.",
        "undelete-search-title": "Пошук выдаленых старонак",
        "sp-contributions-newbies-sub": "Унёсак пачынаючых",
        "sp-contributions-newbies-title": "Унёсак удзельнікаў з новых рахункаў",
        "sp-contributions-blocklog": "журнал блякаваньняў",
-       "sp-contributions-suppresslog": "выдалены ўнёсак удзельніка",
-       "sp-contributions-deleted": "выдалены ўнёсак удзельніка",
+       "sp-contributions-suppresslog": "выдалены ўнёсак {{GENDER:$1|удзельніка|удзельніцы}}",
+       "sp-contributions-deleted": "выдалены ўнёсак {{GENDER:$1|удзельніка|удзельніцы}}",
        "sp-contributions-uploads": "загрузкі",
        "sp-contributions-logs": "журналы падзеяў",
        "sp-contributions-talk": "гутаркі",
        "log-action-filter-import": "Тып імпарту:",
        "log-action-filter-move": "Тып пераносу:",
        "log-action-filter-all": "Усе",
+       "log-action-filter-block-block": "Заблякаваць",
+       "log-action-filter-block-reblock": "Зьмяненьне блякаваньня",
+       "log-action-filter-block-unblock": "Разблякаваць",
+       "log-action-filter-contentmodel-change": "Зьмена мадэлі зьместу",
+       "log-action-filter-delete-delete": "Выдаленьне старонкі",
+       "log-action-filter-delete-restore": "Аднаўленьне старонкі",
+       "log-action-filter-delete-event": "Выдаленьне журналу",
+       "log-action-filter-delete-revision": "Выдаленьне вэрсіі",
+       "log-action-filter-managetags-create": "Стварэньне цэтлікаў",
+       "log-action-filter-managetags-delete": "Выдаленьне цэтлікаў",
+       "log-action-filter-managetags-activate": "Актывацыя цэтлікаў",
+       "log-action-filter-managetags-deactivate": "Дэактывацыя цэтлікаў",
+       "log-action-filter-newusers-autocreate": "Аўтаматычнае стварэньне",
+       "log-action-filter-patrol-autopatrol": "Аўтаматычнае патруляваньне",
+       "log-action-filter-protect-protect": "Абарона",
+       "log-action-filter-protect-unprotect": "Зьняцьце абароны",
+       "log-action-filter-rights-autopromote": "Аўтаматычнае зьмяненьне",
+       "log-action-filter-upload-upload": "Новая загрузка",
+       "authmanager-realname-label": "Сапраўднае імя",
+       "authmanager-provider-temporarypassword": "Часовы пароль",
        "changecredentials": "Зьмена ўліковых зьвестак",
        "removecredentials": "Выдаленьне ўліковых зьвестак",
        "removecredentials-submit": "Выдаліць уліковыя зьвесткі",
index 5252461..42348b0 100644 (file)
        "tagline": "З пляцоўкі {{SITENAME}}",
        "help": "Даведка",
        "search": "Знайсці",
+       "search-ignored-headings": " #<!-- не змяняйце гэты радок --> <pre>\n# Загалоўкі, якія будзе ігнараваць рухавік пошуку.\n# Змены набудуць моц па наступным індэксаванні старонкі.\n# Вы можаце змусіць пераіндэксаванне старонкі, зрабіўшы пустое рэдагаванне.\n# Сінтаксіс наступны:\n#   * Усё ад сімвала \"#\" да канца радка - каментарый.\n#   * Кожны непусты радок - дакладны загаловак, які трэба ігнараваць, з рэгістрам і інш.\nКрыніцы\nСпасылкі\nГл. таксама\n #</pre> <!-- не змяняйце гэты радок -->",
        "searchbutton": "Знайсці",
        "go": "Пераход",
        "searcharticle": "Артыкул",
        "nocookiesnew": "Рахунак быў створаны, але ў сістэму вы не ўвайшлі. {{SITENAME}} карыстаецца квіткамі (кукі), каб апрацоўваць уваходы ўдзельнікаў, а гэтая функцыянальнасць адключана ў вашым браўзеры. Уключыце квіткі ў браўзеры, тады ўваходзьце са сваімі новымі імем удзельніка і паролем.",
        "nocookieslogin": "{{SITENAME}} карыстаецца квіткамі (кукі), каб пазнаваць удзельнікаў. У вашым браўзеры квіткі не дазволены. Дазвольце іх працу і паспрабуйце ізноў.",
        "nocookiesfornew": "Уліковы запіс карыстальніка не быў створаны, бо мы не змаглі пацвердзіць яго крыніцы. \nУпэўніцеся, што кукі ўключаныя, абнавіце старонку і паспрабуйце яшчэ раз.",
-       "createacct-loginerror": "Уліковы запіс быў паспяхова створаны, але Вы не змаглі аўтарызавацц аўтаматычна. Калі ласка, перайдзіце да старонкі [[Адмысловае:Імя_ўдзельніка|ручной аўтарызацыі]].",
+       "createacct-loginerror": "Уліковы запіс быў паспяхова створаны, але Вы не змаглі аўтарызавацца аўтаматычна. Калі ласка, перайдзіце да старонкі [[Special:UserLogin|ручной аўтарызацыі]].",
        "noname": "Вы не вызначылі правільнага імя ўдзельніка.",
        "loginsuccesstitle": "Паспяховы ўваход у сістэму",
        "loginsuccess": "<strong>Цяпер Вы ўвайшлі на {{SITENAME}} як \"$1\".</strong>",
        "passwordreset-emailelement": "Імя ўдзельніка: \n$1\n\nЧасовы пароль: \n$2",
        "passwordreset-emailsentemail": "Калі гэты адрас электроннай пошты злучаны з вашым уліковым запісам, будзе адпраўлены ліст пра скід пароля.",
        "passwordreset-emailsentusername": "Калі ёсць адрас электроннай пошты, злучаны з гэтым імем удзельніка, то будзе дасланы ліст пра скід пароля.",
+       "passwordreset-emailsent-capture2": "{{PLURAL:$1|Электронны ліст|электронныя лісты}} скіду пароля адпраўлены. {{PLURAL:$1|Імя ўдзельніка і пароль|Спіс імён удзельнікаў і паролі}} паказаны ніжэй.",
        "passwordreset-invalideamil": "Няслушны адрас электроннай пошты",
        "passwordreset-nodata": "Не былі пададзены ні імя ўдзельніка, ні адрас электроннай пошты",
        "changeemail": "Змяніць або выдаліць адрас электроннай пошты",
index 8a60608..608f042 100644 (file)
        "lineno": "Xeta $1:",
        "compareselectedversions": "Rewizyonanê weçineyan pêver ke",
        "showhideselectedversions": "Revizyonanê weçinıtan bımocne/bınımne",
-       "editundo": "peyser bı",
+       "editundo": "peyser bıya",
        "diff-empty": "(Babetna niyo)",
        "diff-multi-sameuser": "(Terefê eyni karberi ra {{PLURAL:$1|yew revizyono miyanên nêmocno|$1 revizyonê miyanêni nêmocnê}})",
        "diff-multi-otherusers": "(Terefê {{PLURAL:$2|yew karberi|$2 karberan}} ra {{PLURAL:$1|yew revizyono miyanên nêmocno|$1 revizyonê miyanêni nêmocnê}})",
index fd75693..6b1e366 100644 (file)
        "versionrequiredtext": "ये पाना प्रयोग गर्नका लागि MediaWiki $1 संस्करण चाहिन्छ ।\nहेर  [[Special:Version|version page]]",
        "ok": "भयो",
        "retrievedfrom": " \"$1\" बठे निकालिया",
-       "youhavenewmessages": "तमखी लेखा($3)मी $1 ($2) छ ।",
+       "youhavenewmessages": "तमखी लेखा($3)मी $1($2) छ ।",
        "youhavenewmessagesfromusers": "तमखी लेखा {{PLURAL:$3|प्रयोगकर्ता|$3 प्रयोगकर्तान}}($2)बठे$1",
        "youhavenewmessagesmanyusers": "तमलाई धेरै प्रयोगकर्ताहरू($2) बठे $1 छ ।",
        "newmessageslinkplural": "{{PLURAL:$1|एक नौलो रैबार|999=नौला रैबारहरू}}",
        "title-invalid-interwiki": "अनुरोध गरियाको शिर्षकमी अन्तर विकि लिङ्क छ जइलाई शिर्षकमी प्रयोग गद्द नाइपाइनो ।",
        "title-invalid-talk-namespace": "निवेदन गरियाको पानाको शिर्षकले उपलब्ध नभएका कुरडी पानालाई सन्दर्भको रूपमी राख्याको छ ।",
        "title-invalid-characters": "निवेदन गरियाको यै पानाको शिर्षकमी अवैध अक्षर रयाको छः \"$1\" ।",
+       "title-invalid-leading-colon": "निवेदन गरिया पृष्ठको शिर्षकको शुरूमी अवैध कोलोन रया छ ।",
+       "perfcached": "तलका डाटाहरू क्याचमी रया कुराहरू हुन्। अपटुडेट नहुन लाई सक्दान। बर्ति {{PLURAL:$1|नतिजा|$1 नतिजाहरू}} क्याचमी उपलब्ध छ।",
+       "perfcachedts": "तलतिरको आँकडा क्याच हो र $1 पहिला अद्यतन गरिया थ्यो। येई क्याचमी उपलब्ध {{PLURAL:$4|एउटा कारण हो|$4 कारणहरू हुन्}}।",
        "viewsource": "स्रोत हेर",
        "viewsource-title": " $1 को स्रोत हेर",
        "actionthrottled": "कार्य रोकिईयो",
        "prefs-rc": "नौला परिवर्तनहरू",
        "prefs-watchlist": "मेरो ध्यान सूची",
        "prefs-editwatchlist": "अवलोकनसूची सम्पादन",
+       "prefs-editwatchlist-label": "आफना अवलोकनसूचीमी रया इन्ट्रीलाई सम्पादन गर:",
+       "prefs-editwatchlist-edit": "आफना अवलोकनसूचीमी रया शीर्षकलाई धेकाउन्या तथा हटाउन्या",
        "prefs-editwatchlist-raw": "कच्चा अवलोकनसूची सम्पादन गद्दा",
        "prefs-editwatchlist-clear": "तमरो अवलोकनसूची मेटा",
        "prefs-watchlist-days": "ध्यान सूचीमी धेकाउने दिनहरू:",
+       "prefs-watchlist-days-max": "भौत $1 {{PLURAL:$1|दिन|दिन}}",
+       "prefs-watchlist-edits": "उच्चतम परिवर्तन संख्या बढाइएको निगरानी सूचीमी  धकाउनका लागि :",
        "prefs-watchlist-edits-max": "सबै है ज्यादा संख्या : १०००",
        "prefs-watchlist-token": "अवलोकन सूची टोकन:",
        "prefs-misc": "साधारण",
        "prefs-resetpass": "पासवर्ड परिवर्तन गर",
-       "prefs-changeemail": "à¤\87मà¥\87ल à¤ªà¤°à¤¿à¤µà¤°à¥\8dतन à¤\97रà¥\8dनà¥\8dया",
+       "prefs-changeemail": "à¤\87मà¥\87ल à¤ à¥\87à¤\97ाना à¤¬à¤¦à¥\87ल à¤µà¤¾ à¤¹à¤\9fा",
        "prefs-setemail": "इमेल ठेगाना प्रविष्ट गर्न्या",
        "prefs-email": "इमेल  विकल्पहरू",
        "prefs-rendering": "स्वरुप",
        "saveprefs": "संग्रह",
+       "restoreprefs": "सबै पूर्वनिर्धारित स्थिती कायम गर्ने(सबै खण्डहरूमी)",
        "prefs-editing": "सम्पादन",
        "rows": "हरफहरू :",
        "columns": "स्तम्भहरू :",
        "right-sendemail": "अन्य प्रयोगकर्तानलाई इमेल पठाउन्या",
        "grant-editmycssjs": "तमरो प्रयोगकर्ता CSS/JavaScript सम्पादन गर",
        "grant-editmyoptions": "तमरा प्रयोगकर्ता अभिरूचीहरूलाई सम्पादन गर",
+       "grant-editmywatchlist": "तमरो अवलोकनसूची सम्पादन गर",
+       "grant-editpage": "भैरया पृष्ठहरू सम्पादन गर",
+       "grant-editprotected": "सुरक्षित पृष्ठ सम्पादन",
+       "grant-highvolume": "उच्च मात्रा सम्पादन",
+       "grant-basic": "आधारभूत अधिकार",
+       "grant-viewdeleted": "नयाँ फाइलहरू अपलोड\nसम्पादन गर",
+       "grant-viewmywatchlist": "आफनो अबलोकन सुची हेर",
        "newuserlogpage": "प्रयोगकर्ता श्रृजना लग",
        "action-move-subpages": "यै पानाको रे यैका उपपानाको नाम बदल्न्या",
        "action-unwatchedpages": "कसैले ध्यान नराख्याका पाननको सूची हेद्या",
index e1c37c8..892ac2c 100644 (file)
        "linkaccounts-submit": "Link accounts",
        "unlinkaccounts": "Unlink accounts",
        "unlinkaccounts-success": "The account was unlinked.",
-       "authenticationdatachange-ignored": "The authentication data change was not handled. Maybe no provider was configured?"
+       "authenticationdatachange-ignored": "The authentication data change was not handled. Maybe no provider was configured?",
+       "userjsispublic": "Please note: JavaScript subpages should not contain confidential data as they are viewable by other users.",
+       "usercssispublic": "Please note: CSS subpages should not contain confidential data as they are viewable by other users."
 }
index 10e56f2..13eb863 100644 (file)
        "actionfailed": "Ago malsukcesis",
        "deletedtext": "\"$1\" estas forigita.\nVidu la paĝon $2 por registro de lastatempaj forigoj.",
        "dellogpage": "Protokolo pri forigoj",
-       "dellogpagetext": "Jen listo de la plej lastaj forigoj el la datumaro.\nĈiuj tempoj sekvas la horzonon UTC.",
+       "dellogpagetext": "Jen listo de la plej lastaj forigoj.",
        "deletionlog": "protokolo pri forigoj",
        "reverted": "Malfaris al antaŭa revisio",
        "deletecomment": "Kialo:",
index ac4112f..cfc4e4d 100644 (file)
        "undo-success": "این ویرایش را می‌توان خنثی کرد.\nلطفاً تفاوت زیر را بررسی کنید تا تأیید کنید که این چیزی است که می‌خواهید انجام دهید، سپس تغییرات زیر را ذخیره کنید تا خنثی‌سازی ویرایش را به پایان ببرید.",
        "undo-failure": "به علت تعارض با ویرایش‌های میانی، این ویرایش را نمی‌توان خنثی کرد.",
        "undo-norev": "این ویرایش را نمی‌توان خنثی کرد چون وجود ندارد یا حذف شده‌است.",
-       "undo-nochange": "به نظر می‌رسد ویرایش از پیش واگردانی شده است.",
+       "undo-nochange": "به نظر می‌رسد ویرایش از پیش خنثی‌سازی شده است.",
        "undo-summary": "خنثی‌سازی ویرایش $1 توسط [[Special:Contributions/$2|$2]] ([[User talk:$2|بحث]])",
        "undo-summary-username-hidden": "خنثی‌سازی نسخهٔ $1 به دست یک کاربر پنهان‌شده",
        "cantcreateaccount-text": "امكان ساختن حساب کاربری از این این نشانی آی‌پی ('''$1''') توسط [[User:$3|$3]] سلب شده است.\n\nدلیل ارائه شده توسط $3 چنین است: $2",
index 55d067f..5710a6f 100644 (file)
        "shortpages": "Pages courtes",
        "longpages": "Pages longues",
        "deadendpages": "Pages en impasse",
-       "deadendpagestext": "Les pages suivantes ne contiennent aucun lien vers d'autres pages du wiki.",
+       "deadendpagestext": "Les pages suivantes ne contiennent aucun lien vers d'autres pages dans le wiki {{SITENAME}}.",
        "protectedpages": "Pages protégées",
        "protectedpages-indef": "Uniquement les protections indéfinies",
        "protectedpages-summary": "Cette page liste les pages existantes actuellement protégées. Pour une liste des titres protégés contre la création, voir [[{{#special:ProtectedTitles}}|{{int:protectedtitles}}]].",
        "move": "Renommer",
        "movethispage": "Renommer cette page",
        "unusedimagestext": "Les fichiers suivants existent, mais ne sont inclus dans aucune page.\nVeuillez noter que d’autres sites peuvent accéder à ces fichiers à l’aide de liens directs (URLs), et donc qu’un fichier peut être listé ici alors qu’il est utilisé par ces sites.",
-       "unusedcategoriestext": "Les catégories suivantes existent mais aucune page ou catégorie ne les utilise.",
+       "unusedcategoriestext": "Les pages de catégories suivantes existent, mais aucune page ou catégorie ne les utilise.",
        "notargettitle": "Pas de cible",
        "notargettext": "Vous n'avez pas indiqué une page ou un utilisateur sur lequel vous souhaitez effectuer cette action.",
        "nopagetitle": "Page cible inexistante",
        "querypage-disabled": "Cette page spéciale est désactivée pour des raisons de performances.",
        "apihelp": "Aide de l’API",
        "apihelp-no-such-module": "Le module « $1 » est introuvable.",
-       "apisandbox": "Bac à sable API",
+       "apisandbox": "Bac à sable de l'API",
        "apisandbox-jsonly": "Le bac à sable de l'API nécessite JavaScript",
        "apisandbox-api-disabled": "L'API est désactivé sur ce site.",
        "apisandbox-intro": "Utilisez cette page pour expérimenter l’<strong>API webservice de MediaWiki</strong>.\nReportez-vous à [[mw:API:Main page|la documentation de l’API]] pour plus de détails sur l’utilisation de l’API. Exemple: [https://www.mediawiki.org/wiki/API#A_simple_example obtenir le contenu d'une page principale]. Choisissez une option pour voir d'autres exemples.",
        "emailccsubject": "Copie de votre message à $1 : $2",
        "emailsent": "Courriel envoyé",
        "emailsenttext": "Votre message a été envoyé par courriel.",
-       "emailuserfooter": "Ce courriel a été envoyé par « $1 » à « $2 » par la fonction « {{int:emailuser}} » de {{SITENAME}}.",
-       "usermessage-summary": "A laissé un message système.",
+       "emailuserfooter": "Ce courriel a été {{GENDER:$1|envoyé}} par « $1 » à « {{GENDER:$2|$2}} » par la fonction « {{int:emailuser}} » de {{SITENAME}}.",
+       "usermessage-summary": "Laisser un message système.",
        "usermessage-editor": "Messager du système",
        "watchlist": "Liste de suivi",
        "mywatchlist": "Liste de suivi",
        "unwatch": "Ne plus suivre",
        "unwatchthispage": "Ne plus suivre",
        "notanarticle": "Ce n'est pas une page de contenu",
-       "notvisiblerev": "La version a été supprimée",
+       "notvisiblerev": "La dernière version relue par un utilisateur différent, a été supprimée",
        "watchlist-details": "{{PLURAL:$1|$1 page|$1 pages}} dans votre liste de suivi, sans compter les pages de discussion.",
        "wlheader-enotif": "La notification par courriel est activée.",
-       "wlheader-showupdated": "Les pages qui ont été modifiées depuis votre dernière visite sont affichées en '''gras'''.",
+       "wlheader-showupdated": "Les pages qui ont été modifiées depuis votre dernière visite sont affichées en <strong>gras</strong>.",
        "wlnote": "Ci-dessous {{PLURAL:$1|figure la dernière modification effectuée|figurent les <strong>$1</strong> dernières modifications effectuées}} durant {{PLURAL:$2|la dernière heure|les <strong>$2</strong> dernières heures}}, jusqu'au $3, $4.",
        "wlshowlast": "Montrer les dernières $1 heures, les derniers $2 jours",
        "watchlist-hide": "Masquer",
index 28f11c0..f70f581 100644 (file)
        "listingcontinuesabbrev": "samb.",
        "index-category": "Kaca kaindhèksan",
        "noindex-category": "Kaca ora kaindhèksan",
-       "broken-file-category": "Kaca mawa pranala berkas rusak",
+       "broken-file-category": "Kaca mawa pranala barkas rusak",
        "about": "Bab",
        "article": "Kaca isi",
        "newwindow": "(buka mawa jendhéla anyar)",
        "no-null-revision": "Ora isa nggawe revisi 'null' anyar kanggo kaca \"$1\"",
        "badtitle": "Sesirah ala",
        "badtitletext": "Sesirahing kaca sing dikarepaké ora sah, suwung, utawa salah nggayut nyang sesirah antarabasa utawa antarawiki.\nIku mungkin ngandhut pralambang siji utawa luwih sing ora kena dianggo tumrap sesirah iki.",
-       "perfcached": "Data iki mung dijupuk saka papan singgahan lan mungkin ora kaanyaran. Maksimum {{PLURAL:$1|sak asil|$1 asil}} sumadhiya nèng papan singgahan.",
-       "perfcachedts": "Data iki mung dijupuk saka papan singgahan lan mungkin dianyari pungkasan $1. Maksimum {{PLURAL:$4|sak asil|$4 asil}} sumadhiya nèng papan singgahan.",
+       "perfcached": "Data ing ngisor iki kasimpen ing telih lan mungkin durung dianyari. Paling akèh ana {{PLURAL:$1|sakasil|$1 kasil}} sumadhiya ing telih iku.",
+       "perfcachedts": "Data ing ngisor iki kasimpen ing telih, lan pungkasan dianyari $1. Paling akèh ana {{PLURAL:$4|sakasil|$4 kasil}} sumadhiya ing telih iku.",
        "querypage-no-updates": "Update saka kaca iki lagi dipatèni. Data sing ana ing kéné saiki ora bisa bakal dibalèni unggah manèh.",
        "viewsource": "Deleng sumber",
        "viewsource-title": "Delok sumberé $1",
        "anoneditwarning": "<strong>Penget:</strong> Panjenengan boten mlebet log. Alamat IP Panjenengan badhe katingal dening publik manawi Panjenengan ngayahi ewah-ewahan. Manawi Panjenengan  <strong>[$1 mlebet log]</strong> utawai <strong>[$2 damel akun]</strong>, suntingan Panjenengan badhe kaatribusekaken dhumateng  nama pangangge Panjenengan, lan rupi-rupi  kauntungan sanesipun.",
        "anonpreviewwarning": "''Sampéyan durung mlebu log. Nyimpen bakal nyathet alamat IP Sampéyan nèng riwayat sunting kaca iki.''",
        "missingsummary": "'''Pènget:''' Panjenengan ora nglebokaké ringkesan panyuntingan. Menawa panjenengan mencèt tombol Simpen manèh, suntingan panjenengan bakal kasimpen tanpa ringkesan panyuntingan.",
+       "selfredirect": "<strong>Pélik:</strong> Sampéyan ngalih kaca iki iya nyang kaca iki dhéwé.\nSampéyan mungkin salah wènèh tujuan kanggo alihan utawa salah mbesut kaca.\nYèn sampéyan ngeklik \"{{int:savearticle}}\" manèh, kaca alihan bakal digawé.",
        "missingcommenttext": "Mangga isi tanggapan ing ngisor iki.",
        "missingcommentheader": "'''Pangéling:''' Sampéyan durung nyadhiyakaké judhul/jejer kanggo tanggepan iki.\nYèn Sampéyan klik \"{{int:savearticle}}\" manèh, suntingan Sampéyan bakal kasimpen tanpa kuwi.",
        "summary-preview": "Pratuduh tingkesan:",
        "searchrelated": "magepokan",
        "searchall": "kabèh",
        "showingresults": "Ing ngisor iki dituduhaké {{PLURAL:$1|'''1''' kasil|'''$1''' kasil}}, wiwitané saking #<strong>$2</strong>.",
+       "showingresultsinrange": "Nuduhaké nganti {{PLURAL:$1|<strong>1</strong> kasil|<strong>$1</strong> kasil}} sajeroning penthangan #<strong>$2</strong> tekan #<strong>$3</strong>.",
        "search-showingresults": "{{PLURAL:$4|Asil <strong>$1</strong> dari <strong>$3</strong>|Asil <strong>$1 - $2</strong> saking <strong>$3</strong>}}",
        "search-nonefound": "Ora ana kasil sing cocog karo pitakonan (''query'').",
        "powersearch-legend": "Panggolèkan sabanjuré (''advance search'')",
        "prefs-searchoptions": "Golèk",
        "prefs-namespaces": "Ruang jeneng / Bilik jeneng",
        "default": "baku",
-       "prefs-files": "Berkas",
+       "prefs-files": "Barkas",
        "prefs-custom-css": "CSS priangga",
        "prefs-custom-js": "JavaScript priangga",
        "prefs-common-css-js": "CSS/JS didumaké kanggo kabèh kulit:",
        "right-move-rootuserpages": "Ngalih kaca panganggo oyod",
        "right-movefile": "Mindhah berkas",
        "right-suppressredirect": "Aja nggawé pangalihan saka kaca sing lawas yèn mindhah sawijining kaca",
-       "right-upload": "Ngunggahaké berkas-berkas",
+       "right-upload": "Unggah barkas",
        "right-reupload": "Tindhihana sawijining berkas sing wis ana",
        "right-reupload-own": "Nimpa sawijining berkas sing wis ana lan diunggahaké déning panganggo sing padha",
        "right-reupload-shared": "Timpanana berkas-berkas ing khazanah binagi sacara lokal",
        "upload-prohibited": "Jenis berkas sing dilarang: $1.",
        "uploadlogpage": "Log pangunggahan",
        "uploadlogpagetext": "Ing ngisor iki kapacak log pangunggahan berkas sing anyar dhéwé.\nMangga mirsani [[Special:NewFiles|galeri berkas-berkas anyar]] kanggo pratélan visual.",
-       "filename": "Jeneng berkas",
+       "filename": "Jeneng barkas",
        "filedesc": "Tingkesan",
        "fileuploadsummary": "Ringkesan:",
        "filereuploadsummary": "Owah-owahan berkas:",
        "file-deleted-duplicate": "Sawijining berkas persis berkas iki ([[:$1]]) wis tau dibusak. Mangga panjenengan priksani sajarah pambusakan berkas kasebut sadurungé nerusaké ngunggahaké berkas kuwi manèh.",
        "uploadwarning": "Pèngetan pangunggahan berkas",
        "uploadwarning-text": "Mangga owah katrangan berkas nèng ngisor lan coba manèh.",
-       "savefile": "Simpen berkas",
+       "savefile": "Simpen barkas",
        "uploaddisabled": "Nuwun sèwu, fasilitas pangunggahan dipatèni.",
        "copyuploaddisabled": "Ngunggah mawa URL dipatèni.",
        "uploaddisabledtext": "Pangunggahan berkas ora diidinaké.",
        "uploadscripted": "Berkas iki ngandhut HTML utawa kode sing bisa diinterpretasi salah déning panjlajah wèb.",
        "uploadvirus": "Berkas iki ngamot virus! Détil: $1",
        "uploadjava": "Berkas kuwi berkas ZIP sing kaisi berkas .class Java.\nNgungga berkas Java ora dililakaké amarga bisa nyebabaké ngluwèhaké wates kamanan.",
-       "upload-source": "Berkas sumber",
+       "upload-source": "Barkas sumber",
        "sourcefilename": "Jeneng berkas sumber:",
        "sourceurl": "URL sumber:",
        "destfilename": "Jeneng berkas sing dituju",
        "brokenredirectstext": "Pengalihan ing ngisor iki tumuju menyang kaca sing ora ana:",
        "brokenredirects-edit": "besut",
        "brokenredirects-delete": "busak",
-       "withoutinterwiki": "Kaca tanpa pranala antarbasa",
-       "withoutinterwiki-summary": "Kaca-kaca iki ora nduwé pranala menyang vèrsi ing  basa liyané:",
+       "withoutinterwiki": "Kaca tanpa pranala basa",
+       "withoutinterwiki-summary": "Kaca-kaca ing ngisor iki ora nggayut nyang vèrsi basa liyané.",
        "withoutinterwiki-legend": "Préfiks",
        "withoutinterwiki-submit": "Tuduhna",
        "fewestrevisions": "Artikel mawa owah-owahan sithik dhéwé",
        "mostlinkedtemplates": "Kaca paling akèh transklusi",
        "mostcategories": "Kaca sing kategoriné akèh dhéwé",
        "mostimages": "Berkas sing kerep dhéwé dienggo",
-       "mostinterwikis": "Halaman dengan interwiki terbanyak",
+       "mostinterwikis": "Kaca mawa interwiki paling akèh",
        "mostrevisions": "Kaca mawa pangowahan sing akèh dhéwé",
        "prefixindex": "Kabèh kaca mawa ater-ater",
        "prefixindex-namespace": "Kabèh kaca mawa ater-ater (bilik jeneng $1)",
        "shortpages": "Kaca cendhak",
        "longpages": "Kaca dawa",
        "deadendpages": "Kaca-kaca buntu (tanpa pranala)",
-       "deadendpagestext": "kaca-kaca iki ora nduwé pranala tekan ngendi waé ing wiki iki..",
+       "deadendpagestext": "Kaca-kaca ing ngisor iki ora nggayut nyang kaca liya ing {{SITENAME}}.",
        "protectedpages": "Kaca sing direksa",
        "protectedpages-indef": "Namung pangreksan ora langgeng waé",
        "protectedpages-cascade": "Amung kaca rineksan kang runtut",
        "delete-legend": "Busak",
        "historywarning": "'''Pènget''': Kaca sing bakal panjenengan busak ana sajarahé kanthi $1 {{PLURAL:$1|révisi|révisi}}:",
        "confirmdeletetext": "Panjenengan bakal mbusak kaca utawa berkas iki minangka permanèn karo kabèh sajarahé saka basis data. Pastèkna dhisik menawa panjenengan pancèn nggayuh iki, ngerti kabèh akibat lan konsekwènsiné, lan apa sing bakal panjenengan tumindak iku cocog karo [[{{MediaWiki:Policy-url}}|kawicaksanan {{SITENAME}}]].",
-       "actioncomplete": "Proses tuntas",
+       "actioncomplete": "Kasil diayahi",
        "actionfailed": "Tindakan gagal",
-       "deletedtext": "\"$1\" sampun kabusak. Coba pirsani $2 kanggé log paling énggal kaca ingkang kabusak.",
+       "deletedtext": "\"$1\" wis dibusak. \nDelenga $2 minangka rekamaning busak-busakan pungkasan.",
        "dellogpage": "Log busak",
        "dellogpagetext": "Ing ngisor iki kapacak log pambusakan kaca sing anyar dhéwé.",
        "deletionlog": "Cathetan sing dibusak",
        "whatlinkshere": "Sing nggayut mréné",
        "whatlinkshere-title": "Kaca mawa pranala nggayut \"$1\"",
        "whatlinkshere-page": "Kaca:",
-       "linkshere": "Kaca-kaca iki nduwé pranala menyang '''[[:$1]]''':",
+       "linkshere": "Kaca-kaca ing ngisor iki nggayut nyang '''[[:$1]]''':",
        "nolinkshere": "Ora ana kaca sing nduwé pranala menyang '''[[:$1]]'''.",
        "nolinkshere-ns": " Ora ana kaca sing nduwé pranala menyang '''[[:$1]]''' ing bilik jeneng sing kapilih.",
        "isredirect": "kaca lih-lihan",
        "import-interwiki-history": "Tuladen kabèh vèrsi lawas saka kaca iki",
        "import-interwiki-templates": "Katutna kabèh cithakan",
        "import-interwiki-submit": "Impor",
-       "import-upload-filename": "Jeneng berkas:",
+       "import-upload-filename": "Jeneng barkas:",
        "import-comment": "Komentar:",
        "importtext": "Mangga èkspor berkas saka wiki sumber nganggo [[Special:Export|prangkat èkspor]].\nSimpen nèng komputer Sampéyan lan unggaha nèng kéné.",
        "importstart": "Ngimpor kaca...",
        "exif-subjectlocation": "Lokasi subjèk",
        "exif-exposureindex": "Indhèks pajanan",
        "exif-sensingmethod": "Métodhe pangindran",
-       "exif-filesource": "Sumber berkas",
+       "exif-filesource": "Sumber barkas",
        "exif-scenetype": "Tipe panyawangan",
        "exif-customrendered": "Prosès nggawé gambar",
        "exif-exposuremode": "Modhe pajanan",
        "redirect-user": "ID panganggo",
        "redirect-page": "ID kaca",
        "redirect-revision": "Revisi kaca",
-       "redirect-file": "Jeneng berkas",
+       "redirect-file": "Jeneng barkas",
        "redirect-not-exists": "Nilai ora ditemokaké",
        "fileduplicatesearch": "Golèk berkas duplikat",
        "fileduplicatesearch-summary": "Golèk duplikat berkas adhedhasar biji hash-é.",
index f56536b..f9bb80c 100644 (file)
        "recentchanges-feed-description": "Verfollegt mat dësem Feed déi rezent Ännerungen op {{SITENAME}}.",
        "recentchanges-label-newpage": "Mat dëser Ännerung gouf eng nei Säit ugeluecht",
        "recentchanges-label-minor": "Dëst ass eng kleng Ännerung",
-       "recentchanges-label-bot": "Dës Ännerung gouf vun engem Bot gemaacht",
+       "recentchanges-label-bot": "Dës Ännerung gouf vun engem Bot gemaach",
        "recentchanges-label-unpatrolled": "Dës Ännerung gouf nach net nogekuckt",
        "recentchanges-label-plusminus": "D'Gréisst vun der Säit huet sech ëm déi Zuel vu Bytes geännert",
        "recentchanges-legend-heading": "<strong>Legend:</strong>",
index 1b78704..86a2ba4 100644 (file)
        "show": "Rodyti",
        "minoreditletter": "S",
        "newpageletter": "N",
-       "boteditletter": "R",
+       "boteditletter": "r",
        "number_of_watching_users_pageview": "[$1 {{PLURAL:$1|stebintis naudotojas|stebintys naudotojai|stebinčių naudotojų}}]",
        "rc_categories": "Riboti kategorijoms (atskirkite su „|“)",
        "rc_categories_any": "Bet kuris iš pasirinktųjų",
        "log-action-filter-rights-autopromote": "Automatinis keitimas",
        "log-action-filter-upload-upload": "Naujas įkėlimas",
        "log-action-filter-upload-overwrite": "Kelti iš naujo",
+       "authmanager-create-disabled": "Paskyros kūrimas yra išjungtas.",
+       "authmanager-create-from-login": "Norėdami sukurti paskyrą užpildykite laukelius žemiau.",
        "authmanager-authplugin-setpass-failed-title": "Slaptažodžio keitimas nepavyko",
        "authmanager-authplugin-setpass-bad-domain": "Negalimas domenas.",
        "authmanager-autocreate-noperm": "Automatinis paskyros kūrimas neleidžiamas.",
index ecd1cf0..89742eb 100644 (file)
        "botpasswords-label-resetpassword": "Atiestatīt paroli",
        "botpasswords-label-restrictions": "Lietošanas ierobežojumi:",
        "botpasswords-label-grants-column": "Piešķirts",
+       "botpasswords-created-title": "Bota parole izveidota",
+       "botpasswords-updated-title": "Bota parole atjaunināta",
        "botpasswords-deleted-title": "Bota parole dzēsta",
        "resetpass_forbidden": "Paroles nav iespējams nomainīt",
        "resetpass-no-info": "Jums ir nepieciešams ieiet, lai tūlīt piekļūtu šai lapai.",
        "passwordreset-emailsentemail": "Paroles atiestatīšanas e-pasts ir nosūtīts.",
        "passwordreset-nosuchcaller": "Izsaucējs nepastāv: $1",
        "passwordreset-invalideamil": "Nederīga e-pasta adrese",
-       "changeemail": "Mainīt e-pasta adresi",
+       "changeemail": "Mainīt vai noņemt e-pasta adresi",
        "changeemail-header": "Mainīt konta e-pasta adresi",
        "changeemail-oldemail": "Pašreizējā e-pasta adrese:",
        "changeemail-newemail": "Jaunā e-pasta adrese:",
        "mergehistory-submit": "Apvienot versijas",
        "mergehistory-empty": "Neviena versija nevar tikt apvienota",
        "mergehistory-fail": "Nav iespējams apvienot hronoloģiju, lūdzu, pārbaudiet vēlreiz lapu un laika parametrus.",
+       "mergehistory-fail-bad-timestamp": "Laika zīmogs ir nederīgs.",
+       "mergehistory-fail-invalid-source": "Avota lapa ir nederīga.",
+       "mergehistory-fail-invalid-dest": "Mērķa lapa ir nederīga.",
        "mergehistory-no-source": "Avota lapa $1 nepastāv.",
        "mergehistory-no-destination": "Mērķa lapa $1 nepastāv.",
        "mergehistory-invalid-source": "Avota lapas nosaukumam jābūt derīgam.",
        "sp-contributions-newbies": "Rādīt jauno lietotāju devumu",
        "sp-contributions-newbies-sub": "Jaunie lietotāji",
        "sp-contributions-blocklog": "Bloķēšanas reģistrs",
-       "sp-contributions-deleted": "dzēstais dalībnieka devums",
+       "sp-contributions-deleted": "dzēstais {{GENDER:$1|dalībnieka|dalībnieces}} devums",
        "sp-contributions-uploads": "augšupielādes",
        "sp-contributions-logs": "reģistri",
        "sp-contributions-talk": "diskusija",
index 459c47b..fe9097d 100644 (file)
@@ -18,7 +18,8 @@
                        "Milicevic01",
                        "Macofe",
                        "Nemo bis",
-                       "Matma Rex"
+                       "Matma Rex",
+                       "Kaldari"
                ]
        },
        "tog-underline": "Потцртување на врски:",
        "hidden-category-category": "Скриени категории",
        "category-subcat-count": "{{PLURAL:$2|Оваа категорија ја содржи само следнава поткатегорија.|Оваа категорија {{PLURAL:$1|ја содржи следнава поткатегорија|ги содржи следниве $1 поткатегории}} од вкупно $2.}}",
        "category-subcat-count-limited": "Оваа категорија {{PLURAL:$1|ја содржи следнава поткатегорија|ги содржи следниве $1 поткатегории}}.",
-       "category-article-count": "{{#ifeq:$2|Оваа категорија содржи само една страница.|{{PLURAL:$1|Прикажана е една|Прикажани се $1}} од вкупно $2 страници во категоријата.}}",
+       "category-article-count": "{{PLURAL:$2|Оваа категорија содржи само една страница.|{{PLURAL:$1|Прикажана е една|Прикажани се $1}} од вкупно $2 страници во категоријата.}}",
        "category-article-count-limited": "{{PLURAL:$1|Следната страница е|Следните $1 страници се}} во оваа категорија.",
-       "category-file-count": "{{#ifeq:$2|Оваа категорија содржи само една податотека.|{{PLURAL:$1|Прикажана е една|Прикажани се $1}} од вкупно $2 податотеки во категоријата.}}",
+       "category-file-count": "{{PLURAL:$2|Оваа категорија содржи само една податотека.|{{PLURAL:$1|Прикажана е една|Прикажани се $1}} од вкупно $2 податотеки во категоријата.}}",
        "category-file-count-limited": "{{PLURAL:$1|Следнава податотека е|Следниве $1 податотеки се}} во оваа категорија.",
        "listingcontinuesabbrev": "продолжува",
        "index-category": "Индексирани страници",
index 21bf1d8..482171a 100644 (file)
        "shown-title": "စာမျက်နှာတစ်ခုလျှင် ရလဒ် $1 {{PLURAL:$1|ခု|ခု}} ပြရန်",
        "viewprevnext": "($1 {{int:မှ}} $2) အထိကြား ရလဒ် ($3) ခုကို ကြည့်ရန်",
        "searchmenu-exists": "'''ဤဝီကီတွင် \"[[:$1]]\" အမည်နှင့် စာမျက်နှာတစ်ခုရှိသည်။'''",
-       "searchmenu-new": "<strong>á\80¤á\80\9dá\80®á\80\80á\80®á\80\90á\80½á\80\84á\80º \"[[:$1]]\" á\80\85á\80¬á\80\99á\80»á\80\80á\80ºá\80\94á\80¾á\80¬á\80\80á\80­á\80¯ á\80\96á\80\94á\80ºá\80\90á\80®á\80¸á\80\95á\80«!</strong> {{PLURAL:$2|0=|á\80\9eá\80\84á\80·á\80ºá\80\9bá\80¾á\80¬á\80\96á\80½á\80±á\80\99á\80¾á\80¯á\80\94á\80¾á\80\84á\80·á\80º á\80\85á\80¬á\80\99á\80»á\80\80á\80ºá\80\94á\80¾á\80¬á\80\80á\80­á\80¯á\80\9cá\80\8aá\80ºá\80¸ á\80\80á\80¼á\80\8aá\80·á\80ºá\80\95á\80«á\81\8b\80\9bá\80¾á\80¬á\80\96á\80½á\80±á\80\99á\80¾á\80¯ á\80\9bá\80\9cá\80\92á\80ºá\80\99á\80»á\80¬á\80¸á\80\80á\80­á\80¯á\80\9cá\80\8aá\80ºá\80¸ á\80\80á\80¼á\80\8aá\80ºá\80«ပါ။}}",
+       "searchmenu-new": "<strong>á\80¤á\80\9dá\80®á\80\80á\80®á\80\90á\80½á\80\84á\80º \"[[:$1]]\" á\80\85á\80¬á\80\99á\80»á\80\80á\80ºá\80\94á\80¾á\80¬á\80\80á\80­á\80¯ á\80\96á\80\94á\80ºá\80\90á\80®á\80¸á\80\95á\80«!</strong> {{PLURAL:$2|0=|á\80\9eá\80\84á\80·á\80ºá\80\9bá\80¾á\80¬á\80\96á\80½á\80±á\80\99á\80¾á\80¯á\80\94á\80¾á\80\84á\80·á\80º á\80\85á\80¬á\80\99á\80»á\80\80á\80ºá\80\94á\80¾á\80¬á\80\80á\80­á\80¯á\80\9cá\80\8aá\80ºá\80¸ á\80\80á\80¼á\80\8aá\80·á\80ºá\80\95á\80«á\81\8b\80\9bá\80¾á\80¬á\80\96á\80½á\80±á\80\99á\80¾á\80¯ á\80\9bá\80\9cá\80\92á\80ºá\80\99á\80»á\80¬á\80¸á\80\80á\80­á\80¯á\80\9cá\80\8aá\80ºá\80¸ á\80\80á\80¼á\80\8aá\80·á\80ºပါ။}}",
        "searchprofile-articles": "မာတိကာစာမျက်နှာများ",
        "searchprofile-images": "မာလတီမီဒီယာ",
        "searchprofile-everything": "အားလုံး",
index 6cd436a..3fc66f5 100644 (file)
        "expand_templates_remove_comments": "Kommentaren rutnehmen",
        "expand_templates_generate_xml": "XML-Parser-Boom wiesen",
        "expand_templates_preview": "Vörschau",
-       "pagelang-language": "Spraak"
+       "pagelang-language": "Spraak",
+       "special-characters-group-latin": "Latiensch",
+       "special-characters-group-latinextended": "Latiensch verwiedert",
+       "special-characters-group-ipa": "Internatschonal Phoneetsch Alphabet",
+       "special-characters-group-symbols": "Symbolen",
+       "special-characters-group-greek": "Greeksch",
+       "special-characters-group-greekextended": "Greeksch verwiedert",
+       "special-characters-group-cyrillic": "Kyrillisch",
+       "special-characters-group-arabic": "Araabsch",
+       "special-characters-group-arabicextended": "Araabsch verwiedert",
+       "special-characters-group-persian": "Persisch",
+       "special-characters-group-hebrew": "Hebrääsch",
+       "special-characters-group-bangla": "Bengaalsch",
+       "special-characters-group-tamil": "Tamilsch",
+       "special-characters-group-telugu": "Telugu",
+       "special-characters-group-sinhala": "Singaleesch",
+       "special-characters-group-gujarati": "Gujarati",
+       "special-characters-group-devanagari": "Devanagari",
+       "special-characters-group-thai": "Thailändsch",
+       "special-characters-group-lao": "Laotisch",
+       "special-characters-group-khmer": "Khmer"
 }
index 2a1628f..10d005d 100644 (file)
        "recreate": "Attopprett",
        "confirm_purge_button": "OK",
        "confirm-purge-top": "Vil du slette tenarane sin mellomlagra versjon av denne sida?",
-       "confirm-purge-bottom": "Reinsing av ei side slettar mellomlageret og tvinger fram den nyaste versjonen.",
+       "confirm-purge-bottom": "Reinsing av ei side slettar mellomlageret og tvingar fram den nyaste versjonen.",
        "confirm-watch-button": "OK",
        "confirm-watch-top": "Legg denne sida til i overvakingslista di?",
        "confirm-unwatch-button": "OK",
index 0a13f29..33fc57f 100644 (file)
        "passwordreset-emailtext-user": "L'utilizaire $1 sus {{SITENAME}} a demandat una reïnicializacion de vòstre senhal per {{SITENAME}} ($4). {{PLURAL:$3|Lo compte d'utilizaire seguent es associat|Los comptes d'utilizaires seguents son associats}} a aquesta adreça de corrièr electronic :\n\n$2\n\n{{PLURAL:$3|Aqueste senhal temporari expirarà|Aquestes senhals temporaris expiraràn}} dins {{PLURAL:$5|un jorn|$5 jorns}}. Ara, vos cal vos connectar e causir un senhal novèl. Se aquesta demanda proven pas de vos, o que vos sètz remembrat de vòstre senhal inicial, e que lo volètz pas mai modificar, podètz ignorar aqueste messatge e contunhar d'utilizar vòstre ancian senhal.",
        "passwordreset-emailelement": "Utilizaire: \n$1\n\nSenhal temporari: \n$2",
        "passwordreset-emailsentemail": "Un corrièr electronic de reïnicializacion de senhal es estat mandat.",
-       "passwordreset-emailsent-capture": "Un corrièr electronic de reïnicializacion senhal es estat mandat, qu'es afichat çaijós.",
-       "passwordreset-emailerror-capture": "Un corrièr electronic de reïnicializacion de senhal es estat generat, qu'es afichat çaijós, mas lo mandadís a l'{{GENDER:$2|utilizaire}} a fracassat : $1",
        "changeemail": "Cambiar o suprimir l'adreça electronica",
        "changeemail-header": "Cambiar l'adreça electronica del compte",
        "changeemail-no-info": "Vos cal èsser connectat per aver accès a aquesta pagina.",
        "minoredit": "Aquò es un cambiament menor",
        "watchthis": "Seguir aquesta pagina",
        "savearticle": "Salvar",
+       "publishchanges": "Publicar las modificacions",
        "preview": "Previsualizar",
-       "showpreview": "Previsualizacion",
+       "showpreview": "Previsualizar",
        "showdiff": "Veire los cambiaments",
        "blankarticle": "<strong>Atencion :</strong> La pagina que creatz es voida.\nSe clicatz tornarmai sus « {{int:savearticle}} », la pagina serà creada sens cap de contengut.",
        "anoneditwarning": "<strong>Atencion :<strong> sètz pas connectat.\nVòstra adreça IP serà visibla per tot lo monde se fasètz de modificacions. Se <strong>[$1 vos connectatz]</strong> o <strong>[$2 creatz un compte]</strong>, vòstras modificacions seràn atribuidas a vòstre nom d’utilizaire, entre autres avantatges.",
        "undo-nochange": "Sembla que la modificacion es ja estada anullada.",
        "undo-summary": "Anullacion de las modificacions $1 de [[Special:Contributions/$2|$2]] ([[User talk:$2|discutir]] | [[Special:Contributions/$2|{{MediaWiki:Contribslink}}]])",
        "undo-summary-username-hidden": "Anullar la revision $1 per un utilizaire amagat",
-       "cantcreateaccounttitle": "Podètz pas crear de compte.",
        "cantcreateaccount-text": "La creacion de compte dempuèi aquesta adreça IP ('''$1''') es estada blocada per [[User:$3|$3]].\n\nLa rason balhada per $3 èra ''$2''.",
        "cantcreateaccount-range-text": "La creacion de compte dempuèi las adreças IP dins la plaja <strong>$1</strong>, que compren vòstra agreça IP (<strong>$4</strong>) son estadas blocadas per [[User:$3|$3]].\n\nLo motiu provesit per $3 es <em>$2</em>",
        "viewpagelogs": "Vejatz las operacions per aquesta pagina",
        "tooltip-ca-nstab-category": "Vejatz la pagina de la categoria",
        "tooltip-minoredit": "Marcar mas modificacions coma un cambiament menor",
        "tooltip-save": "Salvar vòstras modificacions",
+       "tooltip-publish": "Publicar vòstras modificacions",
        "tooltip-preview": "Mercé de previsualizar vòstras modificacions abans de salvar!",
        "tooltip-diff": "Aficha los cambiaments qu'avètz aportats al tèxte",
        "tooltip-compareselectedversions": "Afichar las diferéncias entre doas versions d'aquesta pagina",
index 8108c7a..2989d3b 100644 (file)
        "botpasswords-insert-failed": "Falhou ao adicionar o nome do robô \"$1\". Já foi adicionado?",
        "botpasswords-update-failed": "Falha ao atualizar o nome do robô \"$1\". Será que foi eliminado?",
        "botpasswords-created-title": "Criada palavra-passe para o robô",
-       "botpasswords-created-body": "O robô palavra-passe para o nome do robô \"$1\" do utilizador \"$2\" foi criado.",
+       "botpasswords-created-body": "A palavra-passe de robô para o robô \"$1\" do utilizador \"$2\" foi criada.",
        "botpasswords-updated-title": "A palavra-passe de robô foi actualizada.",
        "botpasswords-updated-body": "O robô palavra-passe para o nome do robô \"$1\" do utilizador \"$2\" foi atualizado.",
        "botpasswords-deleted-title": "Palavra-passe de robô eliminada",
index 8368ac5..4377326 100644 (file)
        "prefs-watchlist-token": "వీక్షణాజాబితా టోకెను:",
        "prefs-misc": "ఇతరత్రా",
        "prefs-resetpass": "సంకేతపదాన్ని మార్చుకోండి",
-       "prefs-changeemail": "ఈ-మెయిలు చిరునామా మార్పు",
+       "prefs-changeemail": "ఈ-మెయిలు చిరునామా మార్పు లేదా తొలగింపు",
        "prefs-setemail": "ఓ ఈమెయిల్ చిరునామా ఇవ్వండి",
        "prefs-email": "ఈ-మెయిల్ ఎంపికలు",
        "prefs-rendering": "రూపురేఖలు",
        "saveprefs": "భద్రపరచు",
        "restoreprefs": "అప్రమేయ అమరికలను పునఃస్థాపించు (అన్ని విభాగాల్లోనూ)",
-       "prefs-editing": "సవరిసà±\8dà°¤à±\81à°¨à±\8dనారు",
+       "prefs-editing": "దిదà±\8dà°¦à±\81బాà°\9fà±\8dà°²ు",
        "rows": "అడ్డు వరుసలు:",
        "columns": "నిలువు వరుసలు:",
        "searchresultshead": "వెతుకు",
-       "stub-threshold": "<a href=\"#\" class=\"stub\">మొలక లింకు</a> ఫార్మాటింగు కొరకు హద్దు (బైట్లు):",
+       "stub-threshold": "మొలక లింకు ఫార్మాటింగు కొరకు హద్దు ($1):",
        "stub-threshold-sample-link": "నమూనా",
        "stub-threshold-disabled": "అచేతనం",
        "recentchangesdays": "ఇటీవలి మార్పులు లో చూపించవలసిన రోజులు:",
        "prefs-help-recentchangescount": "ఇది ఇటీవలి మార్పులు, పేజీ చరిత్రలు, మరియు చిట్టాలకు వర్తిస్తుంది.",
        "prefs-help-watchlist-token2": "మీ వీక్షణజాబితా యొక్క జాలవడ్డింపుకు చెందిన రహస్య తాళమిది.\nఈ తాళం తెలిసిన ఎవరైనా మీ వీక్షణజాబితాను చదవగలుగుతారు. అందుచేత దీన్ని ఎవరికీ ఇవ్వకండి.\n[[Special:ResetTokens|దాన్ని మార్చాలంటే ఇక్కడ నొక్కండి]].",
        "savedprefs": "మీ అభిరుచులను భద్రపరిచాం.",
+       "savedrights": "{{GENDER:$1|$1}} వాడుకరి హక్కులను భద్రపరచాం.",
        "timezonelegend": "కాల మండలం:",
        "localtime": "స్థానిక సమయం:",
        "timezoneuseserverdefault": "వికీ అప్రమేయాన్ని ఉపయోగించు ($1)",
        "badsig": "సంతకం చెల్లనిది.\nHTML ట్యాగులను ఒకసారి సరిచూసుకోండి.",
        "badsiglength": "మీ సంతకం చాలా పెద్దగా ఉంది.\nఇది తప్పనిసరిగా $1 {{PLURAL:$1|అక్షరం|అక్షరాల}} లోపులోనే ఉండాలి.",
        "yourgender": "మిమ్మల్ని మీరు ఎలా వర్ణించుకుంటారు?",
-       "gender-unknown": "à°µà±\86à°²à±\8dలడిà°\82à°\9aడానిà°\95à°¿ à°¨à±\87à°¨à±\81 à°\87à°·à±\8dà°\9fపడà°\9fà±\8dà°²à±\87à°¦à±\81",
+       "gender-unknown": "సాఫà±\8dà°\9fà±\81à°µà±\87à°°à±\81 à°®à°¿à°®à±\8dమలà±\8dని à°\89à°²à±\8dà°²à±\87à°\96à°¿à°\82à°\9aà±\87 à°¸à°\82దరà±\8dà°­à°\82à°²à±\8b, à°µà±\80à°²à±\88à°¨à°\82తవరà°\95à±\81 à°²à°¿à°\82à°\97 à°¤à°\9fà°¸à±\8dథతనà±\81 à°\85వలà°\82బిసà±\8dà°¤à±\81à°\82ది",
        "gender-male": "అతను వికీ పేజీలను సరిదిద్దుతాడు",
        "gender-female": "ఆమె వికీ పేజీలను సరిదిద్దుతుంది",
        "prefs-help-gender": "ఈ అభిరుచిని అమర్చుకోవడం ఐచ్చికం.\nమిమ్మల్ని సంబోధించేప్పుడూ మిమ్మల్ని పేర్కొనేప్పుడూ వ్యాకరణపరంగా సరైన లింగాన్ని  వాడటానికి ఈ విలువ ఉపయోగపడుతుంది.\nఈ సమాచారం బహిరంగం.",
        "prefs-tokenwatchlist": "టోకెన్",
        "prefs-diffs": "తేడాలు",
        "prefs-help-prefershttps": "ఈ అభిరుచి మీరు పైసారి లాగినైనపుడు అమలౌతుంది.",
+       "prefswarning-warning": "మీ అభిరుచులలో మీరు చేసిన మార్పులను ఇంకా భద్రపరచలేదు. మీరు \"$1\" ను నొక్కకుండా ఈ పేజీని వదలి వెళ్తే, మీ అభిరుచులు భద్రం కావు.",
        "prefs-tabs-navigation-hint": "చిట్కా: ట్యాబుల జాబితాలో ఓ ట్యాబు నుండి మరోదానికి వెళ్ళేందుకు కుడి ఎడమ బాణాల కీలను వాడవచ్చు.",
        "userrights": "వాడుకరి హక్కుల నిర్వహణ",
        "userrights-lookup-user": "వాడుకరి సమూహాలను నిర్వహించండి",
        "userrights-user-editname": "వాడుకరిపేరును ఇవ్వండి:",
-       "editusergroup": "వాడుకరి గుంపులను మార్చు",
-       "editinguser": "వాడుకరి '''[[User:$1|$1]]''' $2 యొక్క వాడుకరి హక్కులను మారుస్తున్నారు",
+       "editusergroup": "{{GENDER:$1|వాడుకరి}} గుంపులను మార్చు",
+       "editinguser": "{{GENDER:$1|వాడుకరి}} <strong>[[వాడుకరి:$1|$1]]</strong> $2 యొక్క వాడుకరి హక్కులను మారుస్తున్నారు",
        "userrights-editusergroup": "వాడుకరి సమూహాలను మార్చండి",
-       "saveusergroups": "వాడుకరి గుంపులను భద్రపరచు",
+       "saveusergroups": "{{GENDER:$1|వాడుకరి}} గుంపులను భద్రపరచు",
        "userrights-groupsmember": "సభ్యులు:",
        "userrights-groupsmember-auto": "సంభావిత సభ్యులు:",
        "userrights-groups-help": "ఈ వాడుకరి ఏయే గుంపులలో ఉండాలో మీరు మార్చవచ్చు.\n* టిక్కు పెట్టివుంటే ఆ గుంపులో ఈ వాడుకరి ఉన్నట్టు.\n* టిక్కు లేకుంటే ఆ గుంపులో ఈ వాడుకరి లేనట్టు.\n* * ఉంటే ఒకసారి ఆ గుంపుని చేర్చాక మీరు తీసివేయలేరు, లేదా తీసివేసాక తిరిగి చేర్చలేరు.",
        "userrights-changeable-col": "మీరు మార్చదగిన గుంపులు",
        "userrights-unchangeable-col": "మీరు మార్చలేని గుంపులు",
        "userrights-conflict": "వాడుకరి హక్కుల మార్పులలో ఘర్షణ! మీ మార్పులను సమీక్షించి, నిర్ధారించండి.",
-       "userrights-removed-self": "à°®à±\80 à°¹à°\95à±\8dà°\95à±\81లనà±\81 à°®à±\80à°°à±\81 à°µà°¿à°\9cయవà°\82à°¤à°\82à°\97à°¾ à°¤à±\8aà°²à°\97à°¿à°\82à°\9aà±\81à°\95à±\81à°¨à±\8dనారà±\81. à°¤à°¦à±\8dవారా, à°\88 à°ªà±\87à°\9cà±\80ని à°\9aà±\82డడానిà°\95à°¿ à°®à±\80à°\95à±\81 à°\87à°\95 à°\85à°¨à±\81మతి à°²à±\87à°¦ు.",
+       "userrights-removed-self": "à°®à±\80 à°¹à°\95à±\8dà°\95à±\81లనà±\81 à°®à±\80à°°à±\81 à°¤à±\8aà°²à°\97à°¿à°\82à°\9aà±\81à°\95à±\81à°¨à±\8dనారà±\81. à°\87à°\95, à°®à±\80à°°à±\80 à°ªà±\87à°\9cà±\80ని à°\9aà±\82à°¡à°²à±\87à°°ు.",
        "group": "గుంపు:",
        "group-user": "వాడుకరులు",
        "group-autoconfirmed": "ఆటోమాటిగ్గా నిర్ధారించబడిన వాడుకరులు",
        "right-managechangetags": "డేటాబేసులో [[Special:Tags|ట్యాగుల]]ను సృష్టించడం, తొలగించడం",
        "right-applychangetags": "తన మార్పులతో [[Special:Tags|ట్యాగుల]]ను ఆపాదించడం",
        "right-changetags": "విడి కూర్పులకు, చిట్టా పద్దులకు ఏవైనా [[Special:Tags|ట్యాగుల]]ను చేర్చడం, తొలగించడం",
+       "right-deletechangetags": "[[ప్రత్యేక:Tags|ట్యాగులను]] డేటాబేసు నుండి తొలగించు",
+       "grant-generic": "\"$1\" హక్కుల కట్ట",
+       "grant-group-email": "ఈమెయిలు పంపించడం",
+       "grant-group-administration": "నిర్వాహక చర్యలు చేపట్టడం",
+       "grant-group-private-information": "మీ గోపనీయ డేటాను చూడడం",
        "newuserlogpage": "కొత్త వాడుకరుల చిట్టా",
        "newuserlogpagetext": "ఇది వాడుకరి నమోదుల చిట్టా.",
        "rightslog": "వాడుకరుల హక్కుల మార్పుల చిట్టా",
index 14059a5..2afd362 100644 (file)
        "changepassword-throttled": "Bạn đã thử đăng nhập gần đây nhiều lần quá. Xin chờ $1 trước khi bạn thử lần nữa.",
        "botpasswords": "Mật khẩu Bot",
        "botpasswords-summary": "<em>Mật khẩu bot</em> cho phép truy cập một tài khoản người dùng qua API mà không sử dụng thông tin chứng nhận chính của tài khoản. Các quyền người dùng có thể bị hạn chế khi đăng nhập dùng mật khẩu bot.\n\nNếu bạn không hiểu tại sao cần sử dụng mật khẩu bot, có lẽ bạn không nên sử dụng nó. Không ai bao giờ có lý do chính đáng để yêu cầu bạn tạo ra một mật khẩu bot và cung cấp nó cho họ.",
-       "botpasswords-disabled": "Mật khẩu Bot bị vô hiệu hoá.",
+       "botpasswords-disabled": "Mật khẩu bot bị vô hiệu hóa.",
        "botpasswords-no-central-id": "Để sử dụng mật khẩu bot, bạn phải đăng nhập vào một tài khoản tập trung.",
        "botpasswords-existing": "Mật khẩu bot hiện tại",
        "botpasswords-createnew": "Tạo một mật khẩu mới bot",
        "grant-group-high-volume": "Hoạt động với tần số cao",
        "grant-group-customization": "Tùy biến và tùy chọn",
        "grant-group-administration": "Thực hiện các hành động bảo quản",
+       "grant-group-private-information": "Truy cập dữ liệu cá nhân của bạn",
        "grant-group-other": "Hoạt động khác",
        "grant-blockusers": "Cấm và bỏ cấm người dùng",
        "grant-createaccount": "Mở tài khoản",
        "grant-highvolume": "Sửa đổi tốc độ cao",
        "grant-oversight": "Ẩn người dùng và phiên bản",
        "grant-patrol": "Tuần tra các thay đổi trang",
+       "grant-privateinfo": "Truy cập dữ liệu cá nhân",
        "grant-protect": "Khóa và mở khóa các trang",
        "grant-rollback": "Lùi một loạt thay đổi vào một trang",
        "grant-sendemail": "Gửi thư điện tử cho người dùng khác",
        "uploadstash-errclear": "Việc dọn sạch các tập tin bị thất bại.",
        "uploadstash-refresh": "Làm mới danh sách tập tin",
        "uploadstash-thumbnail": "xem hình thu nhỏ",
+       "uploadstash-exception": "Không thể lưu tập tin vào hàng đợi tải lên ($1): “$2”.",
        "invalid-chunk-offset": "Khúc lệch (chunk offset) không hợp lệ",
        "img-auth-accessdenied": "Không cho phép truy cập",
        "img-auth-nopathinfo": "Thiếu PATH_INFO.\nMáy chủ của bạn không được thiết lập để truyền thông tin này.\nCó thể do nó dựa trên CGI và không hỗ trợ img_auth.\nXem [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Image_Authorization hướng dẫn điều khiển truy cập hình ảnh].",
        "watchnologin": "Chưa đăng nhập",
        "addwatch": "Thêm vào danh sách theo dõi",
        "addedwatchtext": "“[[:$1]]” cùng trang thảo luận đã vào [[Special:Watchlist|danh sách theo dõi]] của bạn.",
+       "addedwatchtext-talk": "“[[:$1]]” cùng trang đi kèm đã vào [[Special:Watchlist|danh sách theo dõi]] của bạn.",
        "addedwatchtext-short": "Trang “$1” đã được thêm vào danh sách theo dõi của bạn.",
        "removewatch": "Gỡ khỏi danh sách theo dõi",
        "removedwatchtext": "“[[:$1]]” cùng trang thảo luận đã được đưa ra khỏi [[Special:Watchlist|danh sách theo dõi]] của bạn.",
+       "removedwatchtext-talk": "“[[:$1]]” cùng trang đi kèm đã được đưa ra khỏi [[Special:Watchlist|danh sách theo dõi]] của bạn.",
        "removedwatchtext-short": "Trang “$1” đã được xóa khỏi danh sách theo dõi của bạn.",
        "watch": "Theo dõi",
        "watchthispage": "Theo dõi trang này",
        "undeletehistorynoadmin": "Trang này đã bị xóa.\nLý do xóa trang được hiển thị dưới đây, cùng với thông tin về những người đã sửa đổi trang này trước khi bị xóa.\nChỉ có bảo quản viên mới xem được văn bản đầy đủ của những phiên bản trang bị xóa.",
        "undelete-revision": "Phiên bản đã xóa của $1 (vào lúc $4 tại $5) do $3 sửa đổi:",
        "undeleterevision-missing": "Phiên bản này không hợp lệ hay không tồn tại. Đây có thể là một địa chỉ sai, hoặc là phiên bản đã được phục hồi hoặc đã xóa khỏi kho lưu trữ.",
+       "undeleterevision-duplicate-revid": "Không thể phục hồi {{PLURAL:$1|một phiên bản|$1 phiên bản}} vì <code>rev_id</code> của {{PLURAL:$1|nó|chúng}} đã được sử dụng.",
        "undelete-nodiff": "Không tìm thấy phiên bản cũ hơn.",
        "undeletebtn": "Phục hồi",
        "undeletelink": "xem lại/phục hồi",
        "undeletedrevisions": "$1 phiên bản được phục hồi",
        "undeletedrevisions-files": "$1 phiên bản và $2 tập tin đã được phục hồi",
        "undeletedfiles": "$1 tập tin đã được phục hồi",
-       "cannotundelete": "Phục hồi thất bại:\n$1",
+       "cannotundelete": "Phục hồi bị thất bại một phần hoặc hoàn toàn:\n$1",
        "undeletedpage": "'''$1 đã được khôi phục'''\n\nXem nhật trình xóa và phục hồi các trang gần đây tại [[Special:Log/delete|nhật trình xóa]].",
        "undelete-header": "Xem các trang bị xóa gần đây tại [[Special:Log/delete|nhật trình xóa]].",
        "undelete-search-title": "Tìm kiếm trang đã bị xóa",
        "sp-contributions-newbies-sub": "Các thành viên mới",
        "sp-contributions-newbies-title": "Đóng góp của các thành viên mới",
        "sp-contributions-blocklog": "nhật trình cấm",
-       "sp-contributions-suppresslog": "đóng góp của người dùng đã bị xóa hẳn",
-       "sp-contributions-deleted": "đóng góp đã bị xóa của thành viên",
+       "sp-contributions-suppresslog": "đóng góp của {{GENDER:$1}}người dùng đã bị xóa hẳn",
+       "sp-contributions-deleted": "đóng góp đã bị xóa của {{GENDER:$1}}thành viên",
        "sp-contributions-uploads": "tập tin tải lên",
        "sp-contributions-logs": "nhật trình",
        "sp-contributions-talk": "thảo luận",
index 1fa701e..2d04f63 100644 (file)
@@ -250,8 +250,8 @@ $namespaceNames = [
        NS_MEDIA            => 'Médiá',
        NS_SPECIAL          => 'Špeciálne',
        NS_TALK             => 'Diskusia',
-       NS_USER             => 'Redaktor',
-       NS_USER_TALK        => 'Diskusia_s_redaktorom',
+       NS_USER             => 'Užívateľ',
+       NS_USER_TALK        => 'Diskusia_s_užívateľom',
        NS_PROJECT_TALK     => 'Diskusia_k_{{GRAMMAR:datív|$1}}',
        NS_FILE             => 'Súbor',
        NS_FILE_TALK        => 'Diskusia_k_súboru',
@@ -267,6 +267,8 @@ $namespaceNames = [
 
 $namespaceAliases = [
        "Komentár"               => NS_TALK,
+       'Redaktor'               => NS_USER,
+       'Diskusia_s_redaktorom'  => NS_USER_TALK,
        "Komentár_k_redaktorovi" => NS_USER_TALK,
        "Komentár_k_Wikipédii"   => NS_PROJECT_TALK,
        'Obrázok' => NS_FILE,
@@ -275,6 +277,11 @@ $namespaceAliases = [
        "Komentár_k_MediaWiki"   => NS_MEDIAWIKI_TALK,
 ];
 
+$namespaceGenderAliases = [
+       NS_USER => [ 'male' => 'Užívateľ', 'female' => 'Užívateľka' ],
+       NS_USER_TALK => [ 'male' => 'Diskusia_s_užívateľom', 'female' => 'Diskusia_s_užívateľkou' ],
+];
+
 $separatorTransformTable = [
        ',' => "\xc2\xa0",
        '.' => ','
index 8fd25a6..d6a9ba8 100644 (file)
@@ -596,6 +596,8 @@ class NamespaceConflictChecker extends Maintenance {
 
                $this->db->delete( 'page', [ 'page_id' => $id ], __METHOD__ );
 
+               $this->commitTransaction( $this->db, __METHOD__ );
+
                /* Call LinksDeletionUpdate to delete outgoing links from the old title,
                 * and update category counts.
                 *
@@ -605,9 +607,8 @@ class NamespaceConflictChecker extends Maintenance {
                 * accidentally introduce an assumption of title validity to the code we
                 * are calling.
                 */
-               $update = new LinksDeletionUpdate( $wikiPage );
-               $update->doUpdate();
-               $this->commitTransaction( $this->db, __METHOD__ );
+               $updates = [ new LinksDeletionUpdate( $wikiPage ) ];
+               DataUpdate::runUpdates( $updates );
 
                return true;
        }
index 9cf7b2b..bd34a50 100644 (file)
@@ -8,7 +8,7 @@ class ValidateRegistrationFile extends Maintenance {
                $this->addArg( 'path', 'Path to extension.json/skin.json file.', true );
        }
        public function execute() {
-               if ( !class_exists( 'JsonSchema\Uri\UriRetriever' ) ) {
+               if ( !class_exists( 'JsonSchema\Validato' ) ) {
                        $this->error( 'The JsonSchema library cannot be found, please install it through composer.', 1 );
                }
 
@@ -38,11 +38,8 @@ class ValidateRegistrationFile extends Maintenance {
                        $this->output( "Warning: $path is using a deprecated schema, and should be updated to "
                                . ExtensionRegistry::MANIFEST_VERSION . "\n" );
                }
-               $retriever = new JsonSchema\Uri\UriRetriever();
-               $schema = $retriever->retrieve( 'file://' . $schemaPath );
-
-               $validator = new JsonSchema\Validator();
-               $validator->check( $data, $schema );
+               $validator = new JsonSchema\Validator;
+               $validator->check( $data, (object) [ '$ref' => 'file://' . $schemaPath ] );
                if ( $validator->isValid() ) {
                        $this->output( "$path validates against the version $version schema!\n" );
                } else {
index 1edb9f2..ac60e8f 100644 (file)
                                        }
 
                                } else if ( $collapsible.parent().is( 'li' ) &&
-                                       $collapsible.parent().children( '.mw-collapsible' ).size() === 1 &&
-                                       $collapsible.find( '> .mw-collapsible-toggle' ).size() === 0
+                                       $collapsible.parent().children( '.mw-collapsible' ).length === 1 &&
+                                       $collapsible.find( '> .mw-collapsible-toggle' ).length === 0
                                ) {
                                        // special case of one collapsible in <li> tag
                                        $toggleLink = buildDefaultToggleLink();
index b4edc50..0035601 100644 (file)
@@ -33,8 +33,8 @@
        // Standalone icons
        //
        // Markup:
-       // <div class="mw-ui-icon mw-ui-icon-element mw-ui-icon-ok">OK</div><br/>
-       // <div class="mw-ui-icon mw-ui-icon-element mw-ui-icon-ok mw-ui-button mw-ui-progressive">OK</div><br/>
+       // <div class="mw-ui-icon mw-ui-icon-element mw-ui-icon-ok">OK</div><br>
+       // <div class="mw-ui-icon mw-ui-icon-element mw-ui-icon-ok mw-ui-button mw-ui-progressive">OK</div><br>
        // <button class="mw-ui-icon mw-ui-icon-ok mw-ui-icon-element mw-ui-button mw-ui-quiet" title="">Close</button>
        //
        // Styleguide 6.1.1.
                        margin-right: @iconGutterWidth;
                }
        }
-}
+
+       // Icons small for elements like indicators
+       //
+       // Markup:
+       // <div class="mw-ui-icon mw-ui-icon-small mw-ui-icon-help"></div>
+       //
+       // Styleguide 6.1.3
+       &.mw-ui-icon-small:before {
+               background-size: 66.67% auto; // 66.67% of 24px equals 16px
+       }
+}
\ No newline at end of file
index 0bdf02e..946823d 100644 (file)
                        $.each( response.query.pages, function ( index, page ) {
                                var title = new ForeignTitle( page.title ).getPrefixedText();
                                cache.existenceCache[ title ] = !page.missing;
+                               if ( !queue[ title ] ) {
+                                       // Debugging for T139130
+                                       throw new Error( 'No queue for "' + title + '", requested "' + titles.join( '|' ) + '"' );
+                               }
                                queue[ title ].resolve( cache.existenceCache[ title ] );
                        } );
                } );
index 8efbc69..f2e0f4d 100644 (file)
@@ -2,30 +2,24 @@
  * HTMLForm enhancements:
  * Infuse some OOjs UI HTMLForm fields (those which benefit from always being infused).
  */
-( function ( mw ) {
+( function ( mw, $ ) {
 
        mw.hook( 'htmlform.enhance' ).add( function ( $root ) {
-               var $oouiNodes, modules;
+               var $oouiNodes, modules, extraModules;
 
                $oouiNodes = $root.find( '.mw-htmlform-field-autoinfuse' );
                if ( $oouiNodes.length ) {
                        // The modules are preloaded (added server-side in HTMLFormField, and the individual fields
                        // which need extra ones), but this module doesn't depend on them. Wait until they're loaded.
                        modules = [ 'mediawiki.htmlform.ooui' ];
-                       if ( $oouiNodes.filter( '.mw-htmlform-field-HTMLTitleTextField' ).length ) {
-                               // FIXME: TitleInputWidget should be in its own module
-                               modules.push( 'mediawiki.widgets' );
-                       }
-                       if ( $oouiNodes.filter( '.mw-htmlform-field-HTMLUserTextField' ).length ) {
-                               modules.push( 'mediawiki.widgets.UserInputWidget' );
-                       }
-                       if (
-                               $oouiNodes.filter( '.mw-htmlform-field-HTMLSelectNamespace' ).length ||
-                               $oouiNodes.filter( '.mw-htmlform-field-HTMLSelectNamespaceWithButton' ).length
-                       ) {
-                               // FIXME: NamespaceInputWidget should be in its own module (probably?)
-                               modules.push( 'mediawiki.widgets' );
-                       }
+                       $oouiNodes.each( function () {
+                               var data = $( this ).data( 'mw-modules' );
+                               if ( data ) {
+                                       // We can trust this value, 'data-mw-*' attributes are banned from user content in Sanitizer
+                                       extraModules = data.split( ',' );
+                                       modules.push.apply( modules, extraModules );
+                               }
+                       } );
                        mw.loader.using( modules ).done( function () {
                                $oouiNodes.each( function () {
                                        OO.ui.infuse( this );
@@ -35,4 +29,4 @@
 
        } );
 
-}( mediaWiki ) );
+}( mediaWiki, jQuery ) );
index 6460ed1..cb717af 100644 (file)
 
        mw.hook( 'htmlform.enhance' ).add( function ( $root ) {
                $root.find( '.mw-htmlform-hide-if' ).each( function () {
-                       var v, i, fields, test, func, spec, self, modules,
+                       var v, i, fields, test, func, spec, self, modules, data,extraModules,
                                $el = $( this );
 
                        modules = [];
                        if ( $el.is( '[data-ooui]' ) ) {
                                modules.push( 'mediawiki.htmlform.ooui' );
-                               if ( $el.filter( '.mw-htmlform-field-HTMLTitleTextField' ).length ) {
-                                       // FIXME: TitleInputWidget should be in its own module
-                                       modules.push( 'mediawiki.widgets' );
-                               }
-                               if ( $el.filter( '.mw-htmlform-field-HTMLUserTextField' ).length ) {
-                                       modules.push( 'mediawiki.widgets.UserInputWidget' );
-                               }
-                               if (
-                                       $el.filter( '.mw-htmlform-field-HTMLSelectNamespace' ).length ||
-                                       $el.filter( '.mw-htmlform-field-HTMLSelectNamespaceWithButton' ).length
-                               ) {
-                                       // FIXME: NamespaceInputWidget should be in its own module (probably?)
-                                       modules.push( 'mediawiki.widgets' );
+                               data = $el.data( 'mw-modules' );
+                               if ( data ) {
+                                       // We can trust this value, 'data-mw-*' attributes are banned from user content in Sanitizer
+                                       extraModules = data.split( ',' );
+                                       modules.push.apply( modules, extraModules );
                                }
                        }
 
index 3cdab3c..a8786ef 100644 (file)
@@ -6,7 +6,7 @@
 
        function addMulti( $oldContainer, $container ) {
                var name = $oldContainer.find( 'input:first-child' ).attr( 'name' ),
-                       oldClass = ( ' ' + $oldContainer.attr( 'class' ) + ' ' ).replace( /(mw-htmlform-field-HTMLMultiSelectField|mw-chosen)/g, '' ),
+                       oldClass = ( ' ' + $oldContainer.attr( 'class' ) + ' ' ).replace( /(mw-htmlform-field-HTMLMultiSelectField|mw-chosen|mw-htmlform-dropdown)/g, '' ),
                        $select = $( '<select>' ),
                        dataPlaceholder = mw.message( 'htmlform-chosen-placeholder' );
                oldClass = $.trim( oldClass );
@@ -53,9 +53,9 @@
        }
 
        mw.hook( 'htmlform.enhance' ).add( function ( $root ) {
-               if ( $root.find( '.mw-chosen' ).length ) {
+               if ( $root.find( '.mw-htmlform-dropdown' ).length ) {
                        mw.loader.using( 'jquery.chosen', function () {
-                               $root.find( '.mw-chosen' ).each( function () {
+                               $root.find( '.mw-htmlform-dropdown' ).each( function () {
                                        var type = this.nodeName.toLowerCase(),
                                                $converted = convertCheckboxesToMulti( $( this ), type );
                                        $converted.find( '.htmlform-chzn-select' ).chosen( { width: 'auto' } );
index 11784fb..491564a 100644 (file)
                         * State machine:
                         *
                         * - `registered`:
-                        *    The module is known to the system but not yet requested.
+                        *    The module is known to the system but not yet required.
                         *    Meta data is registered via mw.loader#register. Calls to that method are
                         *    generated server-side by the startup module.
                         * - `loading`:
-                        *    The module is requested through mw.loader (either directly or as dependency of
-                        *    another module). The client will be fetching module contents from the server.
+                        *    The module was required through mw.loader (either directly or as dependency of
+                        *    another module). The client will fetch module contents from the server.
                         *    The contents are then stashed in the registry via mw.loader#implement.
                         * - `loaded`:
-                        *    The module has been requested from the server and stashed via mw.loader#implement.
-                        *    If the module has no more dependencies in-fight, the module will be executed
-                        *    right away. Otherwise execution is deferred, controlled via #handlePending.
+                        *    The module has been loaded from the server and stashed via mw.loader#implement.
+                        *    If the module has no more dependencies in-flight, the module will be executed
+                        *    immediately. Otherwise execution is deferred, controlled via #handlePending.
                         * - `executing`:
                         *    The module is being executed.
                         * - `ready`:
                                //
                                sources = {},
 
-                               // List of modules which will be loaded as when ready
-                               batch = [],
-
-                               // Pending queueModuleScript() requests
+                               // For queueModuleScript()
                                handlingPendingRequests = false,
                                pendingRequests = [],
 
                                /**
                                 * List of callback jobs waiting for modules to be ready.
                                 *
-                                * Jobs are created by #request() and run by #handlePending().
+                                * Jobs are created by #enqueue() and run by #handlePending().
                                 *
                                 * Typically when a job is created for a module, the job's dependencies contain
-                                * both the module being requested and all its recursive dependencies.
+                                * both the required module and all its recursive dependencies.
                                 *
                                 * Format:
                                 *
                        }
 
                        /**
-                        * Adds all dependencies to the queue with optional callbacks to be run
-                        * when the dependencies are ready or fail
+                        * Add one or more modules to the module load queue.
+                        *
+                        * See also #work().
                         *
                         * @private
                         * @param {string|string[]} dependencies Module name or array of string module names
                         * @param {Function} [ready] Callback to execute when all dependencies are ready
                         * @param {Function} [error] Callback to execute when any dependency fails
                         */
-                       function request( dependencies, ready, error ) {
+                       function enqueue( dependencies, ready, error ) {
                                // Allow calling by single module name
                                if ( typeof dependencies === 'string' ) {
                                        dependencies = [ dependencies ];
                        }
 
                        /**
-                        * Load modules from load.php
+                        * Make a network request to load modules from the server.
                         *
                         * @private
                         * @param {Object} moduleMap Module map, see #buildModulesString
                         * @param {string} sourceLoadScript URL of load.php
                         */
                        function doRequest( moduleMap, currReqBase, sourceLoadScript ) {
-                               var request = $.extend(
+                               var query = $.extend(
                                        { modules: buildModulesString( moduleMap ) },
                                        currReqBase
                                );
-                               request = sortQuery( request );
-                               addScript( sourceLoadScript + '?' + $.param( request ) );
+                               query = sortQuery( query );
+                               addScript( sourceLoadScript + '?' + $.param( query ) );
                        }
 
                        /**
                                } );
                        }
 
+                       /**
+                        * Create network requests for a batch of modules.
+                        *
+                        * This is an internal method for #work(). This must not be called directly
+                        * unless the modules are already registered, and no request is in progress,
+                        * and the module state has already been set to `loading`.
+                        *
+                        * @private
+                        * @param {string[]} batch
+                        */
+                       function batchRequest( batch ) {
+                               var reqBase, splits, maxQueryLength, b, bSource, bGroup, bSourceGroup,
+                                       source, group, i, modules, sourceLoadScript,
+                                       currReqBase, currReqBaseLength, moduleMap, l,
+                                       lastDotIndex, prefix, suffix, bytesAdded;
+
+                               if ( !batch.length ) {
+                                       return;
+                               }
+
+                               // Always order modules alphabetically to help reduce cache
+                               // misses for otherwise identical content.
+                               batch.sort();
+
+                               // Build a list of query parameters common to all requests
+                               reqBase = {
+                                       skin: mw.config.get( 'skin' ),
+                                       lang: mw.config.get( 'wgUserLanguage' ),
+                                       debug: mw.config.get( 'debug' )
+                               };
+                               maxQueryLength = mw.config.get( 'wgResourceLoaderMaxQueryLength', 2000 );
+
+                               // Split module list by source and by group.
+                               splits = {};
+                               for ( b = 0; b < batch.length; b++ ) {
+                                       bSource = registry[ batch[ b ] ].source;
+                                       bGroup = registry[ batch[ b ] ].group;
+                                       if ( !hasOwn.call( splits, bSource ) ) {
+                                               splits[ bSource ] = {};
+                                       }
+                                       if ( !hasOwn.call( splits[ bSource ], bGroup ) ) {
+                                               splits[ bSource ][ bGroup ] = [];
+                                       }
+                                       bSourceGroup = splits[ bSource ][ bGroup ];
+                                       bSourceGroup.push( batch[ b ] );
+                               }
+
+                               for ( source in splits ) {
+
+                                       sourceLoadScript = sources[ source ];
+
+                                       for ( group in splits[ source ] ) {
+
+                                               // Cache access to currently selected list of
+                                               // modules for this group from this source.
+                                               modules = splits[ source ][ group ];
+
+                                               currReqBase = $.extend( {
+                                                       version: getCombinedVersion( modules )
+                                               }, reqBase );
+                                               // For user modules append a user name to the query string.
+                                               if ( group === 'user' && mw.config.get( 'wgUserName' ) !== null ) {
+                                                       currReqBase.user = mw.config.get( 'wgUserName' );
+                                               }
+                                               currReqBaseLength = $.param( currReqBase ).length;
+                                               // We may need to split up the request to honor the query string length limit,
+                                               // so build it piece by piece.
+                                               l = currReqBaseLength + 9; // '&modules='.length == 9
+
+                                               moduleMap = {}; // { prefix: [ suffixes ] }
+
+                                               for ( i = 0; i < modules.length; i++ ) {
+                                                       // Determine how many bytes this module would add to the query string
+                                                       lastDotIndex = modules[ i ].lastIndexOf( '.' );
+
+                                                       // If lastDotIndex is -1, substr() returns an empty string
+                                                       prefix = modules[ i ].substr( 0, lastDotIndex );
+                                                       suffix = modules[ i ].slice( lastDotIndex + 1 );
+
+                                                       bytesAdded = hasOwn.call( moduleMap, prefix )
+                                                               ? suffix.length + 3 // '%2C'.length == 3
+                                                               : modules[ i ].length + 3; // '%7C'.length == 3
+
+                                                       // If the url would become too long, create a new one,
+                                                       // but don't create empty requests
+                                                       if ( maxQueryLength > 0 && !$.isEmptyObject( moduleMap ) && l + bytesAdded > maxQueryLength ) {
+                                                               // This url would become too long, create a new one, and start the old one
+                                                               doRequest( moduleMap, currReqBase, sourceLoadScript );
+                                                               moduleMap = {};
+                                                               l = currReqBaseLength + 9;
+                                                               mw.track( 'resourceloader.splitRequest', { maxQueryLength: maxQueryLength } );
+                                                       }
+                                                       if ( !hasOwn.call( moduleMap, prefix ) ) {
+                                                               moduleMap[ prefix ] = [];
+                                                       }
+                                                       moduleMap[ prefix ].push( suffix );
+                                                       l += bytesAdded;
+                                               }
+                                               // If there's anything left in moduleMap, request that too
+                                               if ( !$.isEmptyObject( moduleMap ) ) {
+                                                       doRequest( moduleMap, currReqBase, sourceLoadScript );
+                                               }
+                                       }
+                               }
+                       }
+
                        /* Public Members */
                        return {
                                /**
                                addStyleTag: newStyleTag,
 
                                /**
-                                * Batch-request queued dependencies from the server.
+                                * Start loading of all queued module dependencies.
                                 *
                                 * @protected
                                 */
                                work: function () {
-                                       var     reqBase, splits, maxQueryLength, q, b, bSource, bGroup, bSourceGroup,
-                                               source, concatSource, origBatch, group, i, modules, sourceLoadScript,
-                                               currReqBase, currReqBaseLength, moduleMap, l,
-                                               lastDotIndex, prefix, suffix, bytesAdded;
-
-                                       // Build a list of request parameters common to all requests.
-                                       reqBase = {
-                                               skin: mw.config.get( 'skin' ),
-                                               lang: mw.config.get( 'wgUserLanguage' ),
-                                               debug: mw.config.get( 'debug' )
-                                       };
-                                       // Split module batch by source and by group.
-                                       splits = {};
-                                       maxQueryLength = mw.config.get( 'wgResourceLoaderMaxQueryLength', 2000 );
+                                       var q, batch, concatSource, origBatch;
+
+                                       batch = [];
 
                                        // Appends a list of modules from the queue to the batch
                                        for ( q = 0; q < queue.length; q++ ) {
-                                               // Only request modules which are registered
+                                               // Only load modules which are registered
                                                if ( hasOwn.call( registry, queue[ q ] ) && registry[ queue[ q ] ].state === 'registered' ) {
                                                        // Prevent duplicate entries
                                                        if ( $.inArray( queue[ q ], batch ) === -1 ) {
                                                }
                                        }
 
-                                       // Early exit if there's nothing to load...
-                                       if ( !batch.length ) {
-                                               return;
-                                       }
-
-                                       // The queue has been processed into the batch, clear up the queue.
+                                       // Now that the queue has been processed into a batch, clear up the queue.
+                                       // This MUST happen before we initiate any network request. Else it's possible
+                                       // that a script will be locally cached, instantly load, and work the queue
+                                       // again; all before we've cleared it causing each request to include modules
+                                       // which are already loaded.
                                        queue = [];
 
-                                       // Always order modules alphabetically to help reduce cache
-                                       // misses for otherwise identical content.
-                                       batch.sort();
-
-                                       // Split batch by source and by group.
-                                       for ( b = 0; b < batch.length; b++ ) {
-                                               bSource = registry[ batch[ b ] ].source;
-                                               bGroup = registry[ batch[ b ] ].group;
-                                               if ( !hasOwn.call( splits, bSource ) ) {
-                                                       splits[ bSource ] = {};
-                                               }
-                                               if ( !hasOwn.call( splits[ bSource ], bGroup ) ) {
-                                                       splits[ bSource ][ bGroup ] = [];
-                                               }
-                                               bSourceGroup = splits[ bSource ][ bGroup ];
-                                               bSourceGroup.push( batch[ b ] );
-                                       }
-
-                                       // Clear the batch - this MUST happen before we append any
-                                       // script elements to the body or it's possible that a script
-                                       // will be locally cached, instantly load, and work the batch
-                                       // again, all before we've cleared it causing each request to
-                                       // include modules which are already loaded.
-                                       batch = [];
-
-                                       for ( source in splits ) {
-
-                                               sourceLoadScript = sources[ source ];
-
-                                               for ( group in splits[ source ] ) {
-
-                                                       // Cache access to currently selected list of
-                                                       // modules for this group from this source.
-                                                       modules = splits[ source ][ group ];
-
-                                                       currReqBase = $.extend( {
-                                                               version: getCombinedVersion( modules )
-                                                       }, reqBase );
-                                                       // For user modules append a user name to the request.
-                                                       if ( group === 'user' && mw.config.get( 'wgUserName' ) !== null ) {
-                                                               currReqBase.user = mw.config.get( 'wgUserName' );
-                                                       }
-                                                       currReqBaseLength = $.param( currReqBase ).length;
-                                                       // We may need to split up the request to honor the query string length limit,
-                                                       // so build it piece by piece.
-                                                       l = currReqBaseLength + 9; // '&modules='.length == 9
-
-                                                       moduleMap = {}; // { prefix: [ suffixes ] }
-
-                                                       for ( i = 0; i < modules.length; i++ ) {
-                                                               // Determine how many bytes this module would add to the query string
-                                                               lastDotIndex = modules[ i ].lastIndexOf( '.' );
-
-                                                               // If lastDotIndex is -1, substr() returns an empty string
-                                                               prefix = modules[ i ].substr( 0, lastDotIndex );
-                                                               suffix = modules[ i ].slice( lastDotIndex + 1 );
-
-                                                               bytesAdded = hasOwn.call( moduleMap, prefix )
-                                                                       ? suffix.length + 3 // '%2C'.length == 3
-                                                                       : modules[ i ].length + 3; // '%7C'.length == 3
-
-                                                               // If the request would become too long, create a new one,
-                                                               // but don't create empty requests
-                                                               if ( maxQueryLength > 0 && !$.isEmptyObject( moduleMap ) && l + bytesAdded > maxQueryLength ) {
-                                                                       // This request would become too long, create a new one
-                                                                       // and fire off the old one
-                                                                       doRequest( moduleMap, currReqBase, sourceLoadScript );
-                                                                       moduleMap = {};
-                                                                       l = currReqBaseLength + 9;
-                                                                       mw.track( 'resourceloader.splitRequest', { maxQueryLength: maxQueryLength } );
-                                                               }
-                                                               if ( !hasOwn.call( moduleMap, prefix ) ) {
-                                                                       moduleMap[ prefix ] = [];
-                                                               }
-                                                               moduleMap[ prefix ].push( suffix );
-                                                               l += bytesAdded;
-                                                       }
-                                                       // If there's anything left in moduleMap, request that too
-                                                       if ( !$.isEmptyObject( moduleMap ) ) {
-                                                               doRequest( moduleMap, currReqBase, sourceLoadScript );
-                                                       }
-                                               }
-                                       }
+                                       batchRequest( batch );
                                },
 
                                /**
                                /**
                                 * Implement a module given the components that make up the module.
                                 *
-                                * When #load or #using requests one or more modules, the server
+                                * When #load() or #using() requests one or more modules, the server
                                 * response contain calls to this function.
                                 *
                                 * @param {string} module Name of module
                                                        dependencies
                                                );
                                        } else {
-                                               // Not all dependencies are ready: queue up a request
-                                               request( dependencies, function () {
+                                               // Not all dependencies are ready, add to the load queue
+                                               enqueue( dependencies, function () {
                                                        deferred.resolve( mw.loader.require );
                                                }, deferred.reject );
                                        }
                                        if ( allReady( filtered ) || anyFailed( filtered ) ) {
                                                return;
                                        }
-                                       // Since some modules are not yet ready, queue up a request.
-                                       request( filtered, undefined, undefined );
+                                       // Some modules are not yet ready, add to module load queue.
+                                       enqueue( filtered, undefined, undefined );
                                },
 
                                /**
index d6d2b29..3e9fef8 100644 (file)
@@ -2824,7 +2824,7 @@ parsoid
 !! wikitext
 {{echo|[{{fullurl:{{FULLPAGENAME}}|action=edit}} bar]}}
 !! html
-<p typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"[{{fullurl:{{FULLPAGENAME}}|action=edit}} bar]"}},"i":0}}]}'>[Main_Page bar]</p>
+<p typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"[{{fullurl:{{FULLPAGENAME}}|action=edit}} bar]"}},"i":0}}]}'>[Main Page bar]</p>
 !! end
 
 !! test
@@ -3173,58 +3173,19 @@ foo
 
 !!end
 
-!!test
+!! test
 4. Indent-Pre and extension tags
 !! wikitext
- a <gallery>
-File:foobar.jpg
-</gallery>
-!! html
- a <ul class="gallery mw-gallery-traditional">
-               <li class="gallerybox" style="width: 155px"><div style="width: 155px">
-                       <div class="thumb" style="width: 150px;"><div style="margin:68px auto;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="Foobar.jpg" src="http://example.com/images/thumb/3/3a/Foobar.jpg/120px-Foobar.jpg" width="120" height="14" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/180px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/240px-Foobar.jpg 2x" /></a></div></div>
-                       <div class="gallerytext">
-                       </div>
-               </div></li>
-</ul>
-
-!! html+tidy
-<p>a</p>
-<ul class="gallery mw-gallery-traditional">
-<li class="gallerybox" style="width: 155px">
-<div style="width: 155px">
-<div class="thumb" style="width: 150px;">
-<div style="margin:68px auto;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="Foobar.jpg" src="http://example.com/images/thumb/3/3a/Foobar.jpg/120px-Foobar.jpg" width="120" height="14" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/180px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/240px-Foobar.jpg 2x" /></a></div>
-</div>
-<div class="gallerytext"></div>
-</div>
-</li>
-</ul>
-!!end
+ a <tag />
+!! html/php
+ a <pre>
+NULL
+array (
+)
+</pre>
 
-!! test
-Table wikitext syntax outside wiki-tables
-!! wikitext
-a
-! not a table heading
-|- not a table row
-| not a table cell
-| class="foo bar" | baz
-b
-|}
-|-
-c
-!! html
-<p>a
-! not a table heading
-|- not a table row
-| not a table cell
-| class="foo bar" | baz
-b
-|}
-|-
-c
-</p>
+!! html/parsoid
+ a <pre typeof="mw:Extension/tag" about="#mwt2" data-parsoid='{}' data-mw='{"name":"tag","attrs":{},"body":null}'></pre>
 !! end
 
 !!test
@@ -4865,13 +4826,20 @@ And again with mixed protocols: [ftp://example.com?url=http://example.com link]
 </p>
 !!end
 
+# Since Parsoid is starting to emit canonical wikitext for links,
+# [http://example.com http://example.com] will not RT back to that
+# form anymore.
 !! test
 External links: URL in text
+!! options
+parsoid=wt2html
 !! wikitext
 URL in text: [http://example.com http://example.com]
-!! html
+!! html/php
 <p>URL in text: <a rel="nofollow" class="external free" href="http://example.com">http://example.com</a>
 </p>
+!! html/parsoid
+<p>URL in text: <a rel="mw:ExtLink" href="http://example.com">http://example.com</a></p>
 !! end
 
 !! test
@@ -7619,11 +7587,14 @@ Just a test of an article title containing a percent.
 Link containing % (not as a hex sequence)
 !! wikitext
 [[7% Solution]]
+[[7% Solution|7%25 Solution]]
 !! html/php
 <p><a href="/wiki/7%25_Solution" title="7% Solution">7% Solution</a>
+<a href="/wiki/7%25_Solution" title="7% Solution">7%25 Solution</a>
 </p>
 !! html/parsoid
-<p><a rel="mw:WikiLink" href="./7%25_Solution" title="7% Solution">7% Solution</a></p>
+<p><a rel="mw:WikiLink" href="./7%25_Solution" title="7% Solution">7% Solution</a>
+<a rel="mw:WikiLink" href="./7%25_Solution" title="7% Solution">7%25 Solution</a></p>
 !! end
 
 # note that the parsoid HTML is identical to the previous test output,
@@ -7635,11 +7606,14 @@ Link containing % as a single hex sequence interpreted to char
 parsoid=wt2wt,wt2html,html2html
 !! wikitext
 [[7%25 Solution]]
+[[7%25 Solution|7%25 Solution]]
 !! html/php
 <p><a href="/wiki/7%25_Solution" title="7% Solution">7% Solution</a>
+<a href="/wiki/7%25_Solution" title="7% Solution">7%25 Solution</a>
 </p>
 !! html/parsoid
-<p><a rel="mw:WikiLink" href="./7%25_Solution" title="7% Solution">7% Solution</a></p>
+<p><a rel="mw:WikiLink" href="./7%25_Solution" title="7% Solution">7% Solution</a>
+<a rel="mw:WikiLink" href="./7%25_Solution" title="7% Solution">7%25 Solution</a></p>
 !!end
 
 !! test
@@ -8004,6 +7978,28 @@ Link with multiple ":" in a subpage-supporting namespace (bug 63636)
 <p><a rel="mw:WikiLink" href="./User:Foo/Test/63636:Bar" title="User:Foo/Test/63636:Bar">Test</a></p>
 !! end
 
+## Mainly a sanity check for Parsoid
+!! test
+Handle title parsing for subpages
+!! options
+title=[[/123123]]
+!! wikitext
+123
+!! html/parsoid
+<p>123</p>
+!! end
+
+## FIXME: Add a working php section here
+!! test
+Link to a subpage from a namespace other than main
+!! options
+title=[[User:test]]
+!! wikitext
+[[/123]]
+!! html/parsoid
+<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
 Purely hash wikilink
 !! options
@@ -8515,18 +8511,25 @@ language=ln
 !! test
 Parsoid bug 53221: Wikilinks should be properly entity-escaped
 !! options
-parsoid=html2wt
+parsoid={ "modes": ["html2wt"], "suppressErrors": true }
 !! html/parsoid
 <p>He&amp;nbsp;llo <a href="Foo" rel="mw:WikiLink">He&amp;nbsp;llo</a></p>
 <p>He&amp;nbsp;llo <a href="He&amp;nbsp;llo" rel="mw:WikiLink">He&amp;nbsp;llo</a></p>
 !! wikitext
 He&amp;nbsp;llo [[Foo|He&amp;nbsp;llo]]
 
-He&amp;nbsp;llo [[He&amp;nbsp;llo]]
+He&amp;nbsp;llo He&amp;nbsp;llo
+!! html/php
+<p>He&amp;nbsp;llo <a href="/wiki/Foo" title="Foo">He&amp;nbsp;llo</a>
+</p><p>He&amp;nbsp;llo He&amp;nbsp;llo
+</p>
 !! end
 
+# html2wt will fail because of title normalization without data-parsoid
 !! test
 Parsoid: handle constructor well
+!! options
+parsoid=wt2html,wt2wt
 !! wikitext
 [[constructor]]
 
@@ -8536,9 +8539,9 @@ Parsoid: handle constructor well
 </p><p><a href="/index.php?title=Constructor:foo&amp;action=edit&amp;redlink=1" class="new" title="Constructor:foo (page does not exist)">constructor:foo</a>
 </p>
 !! html/parsoid
-<p><a rel="mw:WikiLink" href="./Constructor" title="Constructor" data-parsoid="{&quot;stx&quot;:&quot;simple&quot;,&quot;a&quot;:{&quot;href&quot;:&quot;./Constructor&quot;},&quot;sa&quot;:{&quot;href&quot;:&quot;constructor&quot;}}">constructor</a></p>
+<p><a rel="mw:WikiLink" href="./Constructor" title="Constructor" data-parsoid='{"stx":"simple","a":{"href":"./Constructor"},"sa":{"href":"constructor"}}'>constructor</a></p>
 
-<p><a rel="mw:WikiLink" href="./Foo" title="Foo" data-parsoid="{&quot;stx&quot;:&quot;simple&quot;,&quot;a&quot;:{&quot;href&quot;:&quot;./Foo&quot;},&quot;sa&quot;:{&quot;href&quot;:&quot;constructor:foo&quot;}}">constructor:foo</a></p>
+<p><a rel="mw:WikiLink" href="./Constructor:foo" title="Constructor:foo" data-parsoid='{"stx":"simple","a":{"href":"./Constructor:foo"},"sa":{"href":"constructor:foo"}}'>constructor:foo</a></p>
 !! end
 
 !! article
@@ -10140,7 +10143,7 @@ Parsoid: Page property magic word with magic word contents
 !! wikitext
 {{DISPLAYTITLE:''{{PAGENAME}}''}}
 !! html/parsoid
-<meta property="mw:PageProp/displaytitle" content="Main_Page" about="#mwt2" typeof="mw:ExpandedAttrs" data-parsoid='{"src":"{{DISPLAYTITLE:&#39;&#39;{{PAGENAME}}&#39;&#39;}}"}' data-mw='{"attribs":[[{"txt":"content"},{"html":"&lt;i data-parsoid=&#39;{\"dsr\":[15,31,2,2]}&#39;>&lt;span about=\"#mwt1\" typeof=\"mw:Transclusion\" data-parsoid=&#39;{\"pi\":[[]],\"dsr\":[17,29,null,null]}&#39; data-mw=&#39;{\"parts\":[{\"template\":{\"target\":{\"wt\":\"PAGENAME\",\"function\":\"pagename\"},\"params\":{},\"i\":0}}]}&#39;>Main_Page&lt;/span>&lt;/i>"}]]}'/>
+<meta property="mw:PageProp/displaytitle" content="Main Page" about="#mwt2" typeof="mw:ExpandedAttrs" data-parsoid='{"src":"{{DISPLAYTITLE:&#39;&#39;{{PAGENAME}}&#39;&#39;}}"}' data-mw='{"attribs":[[{"txt":"content"},{"html":"&lt;i data-parsoid=&#39;{\"dsr\":[15,31,2,2]}&#39;>&lt;span about=\"#mwt1\" typeof=\"mw:Transclusion\" data-parsoid=&#39;{\"pi\":[[]],\"dsr\":[17,29,null,null]}&#39; data-mw=&#39;{\"parts\":[{\"template\":{\"target\":{\"wt\":\"PAGENAME\",\"function\":\"pagename\"},\"params\":{},\"i\":0}}]}&#39;>Main Page&lt;/span>&lt;/i>"}]]}'/>
 !! end
 
 !! test
@@ -10970,7 +10973,7 @@ foo {{''}} baz
 <p>foo bar baz
 </p>
 !! html/parsoid
-<p>foo <span about="#mwt1" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"&#39;&#39;"},"params":{},"i":0}}]}'>bar</span> baz
+<p>foo <span about="#mwt1" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"&#39;&#39;","href":"./Template:&#39;&#39;"},"params":{},"i":0}}]}'>bar</span> baz
 </p>
 !! end
 
@@ -11444,6 +11447,33 @@ parsoid=wt2html
 </tbody></table>
 !!end
 
+!! test
+Table wikitext syntax outside wiki-tables
+!! wikitext
+a
+|+ not a caption
+! not a table heading
+|- not a table row
+| not a table cell
+| class="foo bar" | baz
+b
+|}
+|-
+c
+!! html
+<p>a
+|+ not a caption
+! not a table heading
+|- not a table row
+| not a table cell
+| class="foo bar" | baz
+b
+|}
+|-
+c
+</p>
+!! end
+
 ###
 ### Testing parsing of templates where a template arg
 ### has the same name as the template itself.
@@ -13955,6 +13985,17 @@ Escape HTML special chars in image alt text
 <p><span class="mw-default-size" typeof="mw:Image" data-parsoid='{"optList":[{"ck":"caption","ak":"&amp; &lt; > \""}]}' data-mw='{"caption":"&amp;amp; &amp;lt; > \""}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"},"sa":{}}'><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"220","width":"1941"},"sa":{"resource":"File:Foobar.jpg"}}'/></a></span></p>
 !! end
 
+!! test
+Entities in file name and attributes
+!! wikitext
+[[File:7%25 solution.gif|manualthumb=7%25 solution.gif|link=7%25 solution|[[7%25 solution]]]]
+!! html/php
+<p><a href="/index.php?title=Special:Upload&amp;wpDestFile=7%25_solution.gif" class="new" title="File:7% solution.gif">7% solution</a>
+</p>
+!! html/parsoid
+<p><span class="mw-default-size" typeof="mw:Error mw:Image" data-parsoid='{"optList":[{"ck":"bogus","ak":"manualthumb=7%25 solution.gif"},{"ck":"link","ak":"link=7%25 solution"},{"ck":"caption","ak":"[[7%25 solution]]"}]}' data-mw='{"errors":[{"key":"missing-image","message":"This image does not exist."}],"caption":"&lt;a rel=\"mw:WikiLink\" href=\"./7%25_solution\" title=\"7% solution\" data-parsoid=&#39;{\"stx\":\"simple\",\"a\":{\"href\":\"./7%25_solution\"},\"sa\":{\"href\":\"7%25 solution\"},\"dsr\":[74,91,2,2]}&#39;>7% solution&lt;/a>"}'><a href="./7%25_solution" data-parsoid='{"a":{"href":"./7%25_solution"},"sa":{"href":"link=7%25 solution"}}'><img resource="./File:7%25_solution.gif" src="./Special:FilePath/7%25_solution.gif" height="220" width="220" data-parsoid='{"a":{"resource":"./File:7%25_solution.gif","height":"220","width":"220"},"sa":{"resource":"File:7%25 solution.gif"}}'/></a></span></p>
+!! end
+
 !! test
 BUG 499: Alt text should have &#1234;, not &amp;1234;
 !! wikitext
@@ -14920,6 +14961,14 @@ parsoid=wt2html
 <link rel="mw:PageProp/Category" href="./Category:Baz" data-parsoid='{"stx":"simple","a":{"href":"./Category:Baz"},"sa":{"href":"Category:Baz"}}'/>
 !! end
 
+!! test
+Category links with multiple namespaces
+!! wikitext
+[[Category:Project:Foo]]
+!! html/parsoid
+<link rel="mw:PageProp/Category" href="./Category:Project:Foo" />
+!! end
+
 !! test
 Parsoid: Serialize link to category page with colon escape
 !! options
@@ -19329,10 +19378,14 @@ subpage title=[[Subpage test/L1/L2]]
 </p>
 !! end
 
+# This is wt2html only in Parsoid because we add <nowiki>
+# because of {{..}} and we don't expect to fix that to
+# eliminate the nowikis selective for {{..}} markup.
 !! test
 Non-transclusion because of too many up levels
 !! options
 subpage title=[[Subpage test/L1/L2/L3]]
+parsoid=wt2html
 !! wikitext
 {{../../../../More than parent}}
 !! html
@@ -20036,10 +20089,14 @@ Nested: -{zh-hans:Hi -{zh-cn:China;zh-sg:Singapore;}-;zh-hant:Hello -{zh-tw:Taiw
 </p>
 !! end
 
+# Since Parsoid is starting to emit canonical wikitext for links,
+# [http://example.com http://example.com] will not RT back to that
+# form anymore.
 !! test
 Proper conversion of text in external links
 !! options
 language=sr variant=sr-ec
+parsoid=wt2html
 !! wikitext
 http://www.google.com
 gopher://www.google.com
@@ -20048,7 +20105,7 @@ gopher://www.google.com
 [https://www.google.com irc://www.google.com]
 [ftp://www.google.com www.google.com/ftp://dir]
 [//www.google.com www.google.com]
-!! html
+!! html/php
 <p><a rel="nofollow" class="external free" href="http://www.google.com">http://www.google.com</a>
 <a rel="nofollow" class="external free" href="gopher://www.google.com">gopher://www.google.com</a>
 <a rel="nofollow" class="external free" href="http://www.google.com">http://www.google.com</a>
@@ -20057,6 +20114,14 @@ gopher://www.google.com
 <a rel="nofollow" class="external text" href="ftp://www.google.com">www.гоогле.цом/фтп://дир</a>
 <a rel="nofollow" class="external text" href="//www.google.com">www.гоогле.цом</a>
 </p>
+!! html/parsoid
+<p><a rel="mw:ExtLink" href="http://www.google.com">http://www.google.com</a>
+<a rel="mw:ExtLink" href="gopher://www.google.com">gopher://www.google.com</a>
+<a rel="mw:ExtLink" href="http://www.google.com">http://www.google.com</a>
+<a rel="mw:ExtLink" href="gopher://www.google.com">gopher://www.google.com</a>
+<a rel="mw:ExtLink" href="https://www.google.com">irc://www.google.com</a>
+<a rel="mw:ExtLink" href="ftp://www.google.com">www.гоогле.цом/фтп://дир</a>
+<a rel="mw:ExtLink" href="//www.google.com">www.гоогле.цом</a></p>
 !! end
 
 !! test
@@ -24954,11 +25019,25 @@ Don't block XML namespace declaration
 
 # -----------------------------------------------------------------
 # The following section of tests are primarily to spec requirements
-# around serialization of new/edited content.
+# around Parsoid's serialization (old, new, edited content)
 #
 # All these tests are marked Parsoid html2wt and html2html only
 # ----------------------------------------------------------------
 
+!! test
+Ignore rel attribute in a-tags during serialization to url-links
+!! options
+parsoid=html2wt
+!! html/parsoid
+<a href='http://en.wikipedia.org/wiki/Foobar'>http://en.wikipedia.org/wiki/Foobar</a>
+<a href='http://en.wikipedia.org/wiki/Foobar' rel='mw:ExtLink'>http://en.wikipedia.org/wiki/Foobar</a>
+<a href='http://en.wikipedia.org/wiki/Foobar' rel='mw:WikiLink'>http://en.wikipedia.org/wiki/Foobar</a>
+!! wikitext
+http://en.wikipedia.org/wiki/Foobar
+http://en.wikipedia.org/wiki/Foobar
+http://en.wikipedia.org/wiki/Foobar
+!! end
+
 # 'mi' is a localinterwiki prefix as well as a language
 !! test
 Serialize interwiki links pointing to the current wiki as plain wiki links (bug 65869)
@@ -24978,9 +25057,15 @@ parsoid=html2wt
 !! html/parsoid
 <a rel="mw:WikiLink" href="./Foo" title="Foo" data-parsoid='{}'>Foo</a>
 <a rel="mw:WikiLink" href="./Foo" title="Foo">Foo</a>
+<a href="//en.wikipedia.org/wiki/Foo">//en.wikipedia.org/wiki/Foo</a>
+<a href="http://en.wikipedia.org/wiki/Foo">http://en.wikipedia.org/wiki/Foo</a>
+<a href="//en.wikipedia.org/wiki/Foo_bar">//en.wikipedia.org/wiki/Foo bar</a>
 !! wikitext
 [[Foo]]
 [[Foo]]
+[[:en:Foo|//en.wikipedia.org/wiki/Foo]]
+http://en.wikipedia.org/wiki/Foo
+[[:en:Foo_bar|//en.wikipedia.org/wiki/Foo bar]]
 !! end
 
 !! test
@@ -25291,6 +25376,17 @@ parsoid=html2wt
 [[File:Foobar.jpg|link=]]
 !! end
 
+!! test
+Image: Invalid title as link
+!! wikitext
+[[File:Foobar.jpg|link=<]]
+!! html/php
+<p><a href="/wiki/File:Foobar.jpg" class="image" title="link=&lt;"><img alt="link=&lt;" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a>
+</p>
+!! html/parsoid
+<p><span class="mw-default-size" typeof="mw:Image" data-parsoid='{"optList":[{"ck":"link","ak":"link=&lt;"}]}' data-mw='{"caption":"link=&amp;lt;"}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"},"sa":{}}'><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"220","width":"1941"},"sa":{"resource":"File:Foobar.jpg"}}'/></a></span></p>
+!! end
+
 !! test
 Lists: Serialize correctly even when list content is wrapped in p-tags (like VE does)
 !! options
@@ -27140,3 +27236,12 @@ Thumbnail output
 </div>
 </div>
 !! end
+
+!! test
+unclosed internal link XSS (T137264)
+!! wikitext
+[[#%3Cscript%3Ealert(1)%3C/script%3E|
+!! html
+<p>[[#&lt;script&gt;alert(1)&lt;/script&gt;|
+</p>
+!! end
index ac8c43b..8c2b143 100644 (file)
@@ -324,6 +324,7 @@ class MediaWikiServicesTest extends MediaWikiTestCase {
                        '_MediaWikiTitleCodec' => [ '_MediaWikiTitleCodec', MediaWikiTitleCodec::class ],
                        'TitleFormatter' => [ 'TitleFormatter', TitleFormatter::class ],
                        'TitleParser' => [ 'TitleParser', TitleParser::class ],
+                       'VirtualRESTServiceClient' => [ 'VirtualRESTServiceClient', VirtualRESTServiceClient::class ]
                ];
        }
 
index b4ae765..b290f8f 100644 (file)
@@ -459,4 +459,30 @@ class TextContentTest extends MediaWikiLangTestCase {
                        $this->assertEquals( $expectedNative, $converted->getNativeData() );
                }
        }
+
+       /**
+        * @covers TextContent::normalizeLineEndings
+        * @dataProvider provideNormalizeLineEndings
+        */
+       public function testNormalizeLineEndings( $input, $expected ) {
+               $this->assertEquals( $expected, TextContent::normalizeLineEndings( $input ) );
+       }
+
+       public static function provideNormalizeLineEndings() {
+               return [
+                       [
+                               "Foo\r\nbar",
+                               "Foo\nbar"
+                       ],
+                       [
+                               "Foo\rbar",
+                               "Foo\nbar"
+                       ],
+                       [
+                               "Foobar\n  ",
+                               "Foobar"
+                       ]
+               ];
+       }
+
 }
index 7fe8055..a01cc6b 100644 (file)
@@ -5,6 +5,10 @@
  */
 class CachedBagOStuffTest extends PHPUnit_Framework_TestCase {
 
+       /**
+        * @covers CachedBagOStuff::__construct
+        * @covers CachedBagOStuff::doGet
+        */
        public function testGetFromBackend() {
                $backend = new HashBagOStuff;
                $cache = new CachedBagOStuff( $backend );
@@ -16,6 +20,10 @@ class CachedBagOStuffTest extends PHPUnit_Framework_TestCase {
                $this->assertEquals( 'bar', $cache->get( 'foo' ), 'cached' );
        }
 
+       /**
+        * @covers CachedBagOStuff::set
+        * @covers CachedBagOStuff::delete
+        */
        public function testSetAndDelete() {
                $backend = new HashBagOStuff;
                $cache = new CachedBagOStuff( $backend );
@@ -30,6 +38,10 @@ class CachedBagOStuffTest extends PHPUnit_Framework_TestCase {
                }
        }
 
+       /**
+        * @covers CachedBagOStuff::set
+        * @covers CachedBagOStuff::delete
+        */
        public function testWriteCacheOnly() {
                $backend = new HashBagOStuff;
                $cache = new CachedBagOStuff( $backend );
@@ -50,6 +62,9 @@ class CachedBagOStuffTest extends PHPUnit_Framework_TestCase {
                $this->assertEquals( 'old', $cache->get( 'foo' ) ); // Reloaded from backend
        }
 
+       /**
+        * @covers CachedBagOStuff::doGet
+        */
        public function testCacheBackendMisses() {
                $backend = new HashBagOStuff;
                $cache = new CachedBagOStuff( $backend );
index fce09ae..c4db0cf 100644 (file)
@@ -5,6 +5,9 @@
  */
 class HashBagOStuffTest extends PHPUnit_Framework_TestCase {
 
+       /**
+        * @covers HashBagOStuff::delete
+        */
        public function testDelete() {
                $cache = new HashBagOStuff();
                for ( $i = 0; $i < 10; $i++ ) {
@@ -15,6 +18,9 @@ class HashBagOStuffTest extends PHPUnit_Framework_TestCase {
                }
        }
 
+       /**
+        * @covers HashBagOStuff::clear
+        */
        public function testClear() {
                $cache = new HashBagOStuff();
                for ( $i = 0; $i < 10; $i++ ) {
@@ -27,6 +33,10 @@ class HashBagOStuffTest extends PHPUnit_Framework_TestCase {
                }
        }
 
+       /**
+        * @covers HashBagOStuff::doGet
+        * @covers HashBagOStuff::expire
+        */
        public function testExpire() {
                $cache = new HashBagOStuff();
                $cacheInternal = TestingAccessWrapper::newFromObject( $cache );
@@ -45,6 +55,9 @@ class HashBagOStuffTest extends PHPUnit_Framework_TestCase {
 
        /**
         * Ensure maxKeys eviction prefers keeping new keys.
+        *
+        * @covers HashBagOStuff::__construct
+        * @covers HashBagOStuff::set
         */
        public function testEvictionAdd() {
                $cache = new HashBagOStuff( [ 'maxKeys' => 10 ] );
@@ -62,6 +75,9 @@ class HashBagOStuffTest extends PHPUnit_Framework_TestCase {
        /**
         * Ensure maxKeys eviction prefers recently set keys
         * even if the keys pre-exist.
+        *
+        * @covers HashBagOStuff::__construct
+        * @covers HashBagOStuff::set
         */
        public function testEvictionSet() {
                $cache = new HashBagOStuff( [ 'maxKeys' => 3 ] );
@@ -85,6 +101,10 @@ class HashBagOStuffTest extends PHPUnit_Framework_TestCase {
 
        /**
         * Ensure maxKeys eviction prefers recently retrieved keys (LRU).
+        *
+        * @covers HashBagOStuff::__construct
+        * @covers HashBagOStuff::doGet
+        * @covers HashBagOStuff::hasKey
         */
        public function testEvictionGet() {
                $cache = new HashBagOStuff( [ 'maxKeys' => 3 ] );
index 6df74d6..38d63e3 100644 (file)
@@ -23,6 +23,10 @@ class MultiWriteBagOStuffTest extends MediaWikiTestCase {
                ] );
        }
 
+       /**
+        * @covers MultiWriteBagOStuff::set
+        * @covers MultiWriteBagOStuff::doWrite
+        */
        public function testSetImmediate() {
                $key = wfRandomString();
                $value = wfRandomString();
@@ -34,6 +38,9 @@ class MultiWriteBagOStuffTest extends MediaWikiTestCase {
                $this->assertEquals( $value, $this->cache2->get( $key ), 'Written to tier 2' );
        }
 
+       /**
+        * @covers MultiWriteBagOStuff
+        */
        public function testSyncMerge() {
                $key = wfRandomString();
                $value = wfRandomString();
@@ -69,6 +76,9 @@ class MultiWriteBagOStuffTest extends MediaWikiTestCase {
                $dbw->commit();
        }
 
+       /**
+        * @covers MultiWriteBagOStuff::set
+        */
        public function testSetDelayed() {
                $key = wfRandomString();
                $value = wfRandomString();
index bd076ba..985554b 100644 (file)
@@ -92,6 +92,57 @@ class UserTest extends MediaWikiTestCase {
                $this->assertNotContains( 'nukeworld', $rights );
        }
 
+       /**
+        * @covers User::getRights
+        */
+       public function testUserGetRightsHooks() {
+               $user = new User;
+               $user->addGroup( 'unittesters' );
+               $user->addGroup( 'testwriters' );
+               $userWrapper = TestingAccessWrapper::newFromObject( $user );
+
+               $rights = $user->getRights();
+               $this->assertContains( 'test', $rights, 'sanity check' );
+               $this->assertContains( 'runtest', $rights, 'sanity check' );
+               $this->assertContains( 'writetest', $rights, 'sanity check' );
+               $this->assertNotContains( 'nukeworld', $rights, 'sanity check' );
+
+               // Add a hook manipluating the rights
+               $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'UserGetRights' => [ function ( $user, &$rights ) {
+                       $rights[] = 'nukeworld';
+                       $rights = array_diff( $rights, [ 'writetest' ] );
+               } ] ] );
+
+               $userWrapper->mRights = null;
+               $rights = $user->getRights();
+               $this->assertContains( 'test', $rights );
+               $this->assertContains( 'runtest', $rights );
+               $this->assertNotContains( 'writetest', $rights );
+               $this->assertContains( 'nukeworld', $rights );
+
+               // Add a Session that limits rights
+               $mock = $this->getMockBuilder( stdclass::class )
+                       ->setMethods( [ 'getAllowedUserRights', 'deregisterSession', 'getSessionId' ] )
+                       ->getMock();
+               $mock->method( 'getAllowedUserRights' )->willReturn( [ 'test', 'writetest' ] );
+               $mock->method( 'getSessionId' )->willReturn(
+                       new MediaWiki\Session\SessionId( str_repeat( 'X', 32 ) )
+               );
+               $session = MediaWiki\Session\TestUtils::getDummySession( $mock );
+               $mockRequest = $this->getMockBuilder( FauxRequest::class )
+                       ->setMethods( [ 'getSession' ] )
+                       ->getMock();
+               $mockRequest->method( 'getSession' )->willReturn( $session );
+               $userWrapper->mRequest = $mockRequest;
+
+               $userWrapper->mRights = null;
+               $rights = $user->getRights();
+               $this->assertContains( 'test', $rights );
+               $this->assertNotContains( 'runtest', $rights );
+               $this->assertNotContains( 'writetest', $rights );
+               $this->assertNotContains( 'nukeworld', $rights );
+       }
+
        /**
         * @dataProvider provideGetGroupsWithPermission
         * @covers User::getGroupsWithPermission
index 275c0d1..711eab6 100644 (file)
@@ -74,11 +74,9 @@ class ExtensionJsonValidationTest extends PHPUnit_Framework_TestCase {
                        $version <= ExtensionRegistry::MANIFEST_VERSION,
                        "$path is using a non-supported schema version"
                );
-               $retriever = new JsonSchema\Uri\UriRetriever();
-               $schema = $retriever->retrieve( 'file://' . $schemaPath );
 
-               $validator = new JsonSchema\Validator();
-               $validator->check( $data, $schema );
+               $validator = new JsonSchema\Validator;
+               $validator->check( $data, (object) [ '$ref' => 'file://' . $schemaPath ] );
                if ( $validator->isValid() ) {
                        // All good.
                        $this->assertTrue( true );