Merge "Prepare for REL1_32 cut, labelling master as 1.33-alpha"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Wed, 17 Oct 2018 01:52:30 +0000 (01:52 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Wed, 17 Oct 2018 01:52:30 +0000 (01:52 +0000)
65 files changed:
RELEASE-NOTES-1.32
autoload.php
includes/DefaultSettings.php
includes/GlobalFunctions.php
includes/Title.php
includes/api/ApiBase.php
includes/api/ApiErrorFormatter.php
includes/api/ApiFeedWatchlist.php
includes/api/ApiMain.php
includes/api/ApiPageSet.php
includes/api/ApiQueryBase.php
includes/api/ApiQueryImageInfo.php
includes/api/ApiQueryInfo.php
includes/api/ApiQueryLogEvents.php
includes/api/ApiUsageException.php
includes/api/UsageException.php [deleted file]
includes/api/i18n/ar.json
includes/api/i18n/de.json
includes/api/i18n/en.json
includes/api/i18n/fr.json
includes/api/i18n/he.json
includes/api/i18n/it.json
includes/api/i18n/ko.json
includes/api/i18n/pt-br.json
includes/api/i18n/pt.json
includes/api/i18n/qqq.json
includes/api/i18n/ru.json
includes/api/i18n/uk.json
includes/api/i18n/zh-hans.json
includes/cache/MessageCache.php
includes/cache/localisation/LocalisationCache.php
includes/changetags/ChangeTags.php
includes/import/ImportableUploadRevisionImporter.php
includes/installer/MysqlUpdater.php
includes/jobqueue/JobRunner.php
includes/jobqueue/jobs/RefreshLinksJob.php
includes/libs/rdbms/database/DatabaseDomain.php
includes/libs/rdbms/loadbalancer/LoadBalancer.php
includes/parser/ParserOutput.php
includes/specials/SpecialApiHelp.php
includes/user/User.php
includes/widget/ExpiryInputWidget.php
languages/i18n/bg.json
languages/i18n/ckb.json
languages/i18n/et.json
languages/i18n/io.json
languages/i18n/lv.json
languages/i18n/rue.json
languages/i18n/ur.json
languages/i18n/yi.json
languages/messages/MessagesKo.php
maintenance/updateExtensionJsonSchema.php
resources/src/mediawiki.legacy/shared.css
resources/src/mediawiki.less/mediawiki.mixins.less
resources/src/mediawiki.widgets/mw.widgets.ExpiryInputWidget.js
tests/phpunit/includes/api/ApiErrorFormatterTest.php
tests/phpunit/includes/api/ApiMainTest.php
tests/phpunit/includes/api/ApiQueryInfoTest.php [new file with mode: 0644]
tests/phpunit/includes/cache/MessageCacheTest.php
tests/phpunit/includes/libs/rdbms/database/DatabaseDomainTest.php
tests/phpunit/includes/session/SessionBackendTest.php
tests/phpunit/includes/session/SessionManagerTest.php
tests/phpunit/includes/user/UserTest.php
tests/selenium/README.md
tests/selenium/wdio.conf.js

index a63a16d..9e71285 100644 (file)
@@ -180,6 +180,10 @@ production.
 * (T198214) The 'disabletidy' parameter to action=parse has been
   deprecated; untidy output will not be supported by future wikitext
   parsers.
+* Added intestactionsdetail to action=query&prop=info to allow retrieving the
+  reasons an action is not allowed.
+* Deprecated action=query&prop=info inprop=readable in favor of
+  intestactions=read.
 
 === Action API internal changes in 1.32 ===
 * Added 'ApiParseMakeOutputPage' hook.
@@ -191,6 +195,26 @@ production.
   * ApiFeedContributions::feedItemAuthor()
   * ApiFeedContributions::feedItemDesc()
   * ApiQueryRevisionsBase::extractRevisionInfo()
+* The following deprecated methods have been removed:
+  * ApiBase::profileIn() (deprecated in 1.25)
+  * ApiBase::profileOut() (deprecated in 1.25)
+  * ApiBase::safeProfileOut() (deprecated in 1.25)
+  * ApiBase::profileDBIn() (deprecated in 1.25)
+  * ApiBase::profileDBOut() (deprecated in 1.25)
+  * ApiBase::dieUsage() (deprecated in 1.29)
+  * ApiBase::dieUsageMsg() (deprecated in 1.29)
+  * ApiBase::dieUsageMsgOrDebug() (deprecated in 1.29)
+  * ApiBase::getErrorFromStatus() (deprecated in 1.29)
+  * ApiBase::parseMsg() (deprecated in 1.29)
+  * ApiBase::setWarning() (deprecated in 1.29)
+  * ApiPageSet::getInvalidTitles() (deprecated in 1.26)
+  * ApiQueryLogEvents::addLogParams() (deprecated in 1.25)
+  * ApiUsageException::getCodeString() (deprecated in 1.29)
+  * ApiUsageException::getMessageArray() (deprecated in 1.29)
+* Class UsageException, deprecated in 1.29, has been removed.
+* ApiErrorFormatter: Added getFormat() and newWithFormat(). In particular, you
+  can now easily test $formatter->getFormat() === 'bc', and then call
+  $formatter->newWithFormat( 'plaintext' ) to get a non-BC formatter.
 
 === Languages updated in 1.32 ===
 MediaWiki supports over 350 languages. Many localisations are updated regularly.
@@ -358,6 +382,23 @@ because of Phabricator reports.
 * The hook 'UnknownAction', deprecated since 1.19, has now been removed.
 * The hook 'ParserLimitReport', deprecated since 1.22, has been removed. Use
   the hooks 'ParserLimitReportPrepare' and 'ParserLimitReportFormat' instead.
+* The following deprecated API methods have been removed:
+  * ApiBase::profileIn() (deprecated in 1.25)
+  * ApiBase::profileOut() (deprecated in 1.25)
+  * ApiBase::safeProfileOut() (deprecated in 1.25)
+  * ApiBase::profileDBIn() (deprecated in 1.25)
+  * ApiBase::profileDBOut() (deprecated in 1.25)
+  * ApiBase::dieUsage() (deprecated in 1.29)
+  * ApiBase::dieUsageMsg() (deprecated in 1.29)
+  * ApiBase::dieUsageMsgOrDebug() (deprecated in 1.29)
+  * ApiBase::getErrorFromStatus() (deprecated in 1.29)
+  * ApiBase::parseMsg() (deprecated in 1.29)
+  * ApiBase::setWarning() (deprecated in 1.29)
+  * ApiPageSet::getInvalidTitles() (deprecated in 1.26)
+  * ApiQueryLogEvents::addLogParams() (deprecated in 1.25)
+  * ApiUsageException::getCodeString() (deprecated in 1.29)
+  * ApiUsageException::getMessageArray() (deprecated in 1.29)
+* Class UsageException, deprecated in 1.29, has been removed.
 
 === Deprecations in 1.32 ===
 * HTMLForm::setSubmitProgressive() is deprecated. No need to call it. Submit
@@ -534,6 +575,8 @@ because of Phabricator reports.
 * The $wgUseKeyHeader configuration option and the
   OutputPage::getKeyHeader() method have been deprecated; the relevant
   draft IETF spec expired without becoming a standard.
+* Deprecated API action=query&prop=info inprop=readable in favor of
+  intestactions=read.
 
 === Other changes in 1.32 ===
 * (T198811) The following tables have had their UNIQUE indexes turned into
index 0f92ccb..3e6b4a2 100644 (file)
@@ -1536,7 +1536,6 @@ $wgAutoloadLocalClasses = [
        'UploadStashWrongOwnerException' => __DIR__ . '/includes/upload/UploadStash.php',
        'UploadStashZeroLengthFileException' => __DIR__ . '/includes/upload/UploadStash.php',
        'UppercaseCollation' => __DIR__ . '/includes/collation/UppercaseCollation.php',
-       'UsageException' => __DIR__ . '/includes/api/UsageException.php',
        'User' => __DIR__ . '/includes/user/User.php',
        'UserArray' => __DIR__ . '/includes/user/UserArray.php',
        'UserArrayFromResult' => __DIR__ . '/includes/user/UserArrayFromResult.php',
index 191589e..15d2627 100644 (file)
@@ -9001,15 +9001,6 @@ $wgMultiContentRevisionSchemaMigrationStage = SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_
  */
 $wgActorTableSchemaMigrationStage = SCHEMA_COMPAT_OLD;
 
-/**
- * Temporary option to disable the date picker from the Expiry Widget.
- *
- * @since 1.32
- * @deprecated 1.32
- * @var bool
- */
-$wgExpiryWidgetNoDatePicker = false;
-
 /**
  * change_tag table schema migration stage.
  *
index b536e69..4531b54 100644 (file)
@@ -2847,15 +2847,19 @@ function wfGetNull() {
 function wfWaitForSlaves(
        $ifWritesSince = null, $wiki = false, $cluster = false, $timeout = null
 ) {
+       $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
+
        if ( $cluster === '*' ) {
                $cluster = false;
-               $wiki = false;
+               $domain = false;
        } elseif ( $wiki === false ) {
-               $wiki = wfWikiID();
+               $domain = $lbFactory->getLocalDomainID();
+       } else {
+               $domain = $wiki;
        }
 
        $opts = [
-               'wiki' => $wiki,
+               'domain' => $domain,
                'cluster' => $cluster,
                // B/C: first argument used to be "max seconds of lag"; ignore such values
                'ifWritesSince' => ( $ifWritesSince > 1e9 ) ? $ifWritesSince : null
@@ -2864,7 +2868,6 @@ function wfWaitForSlaves(
                $opts['timeout'] = $timeout;
        }
 
-       $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
        return $lbFactory->waitForReplication( $opts );
 }
 
index de551b4..51d8b13 100644 (file)
@@ -4846,8 +4846,9 @@ class Title implements LinkTarget {
 
                $dbw = wfGetDB( DB_MASTER );
                $dbw->onTransactionPreCommitOrIdle(
-                       function () {
-                               ResourceLoaderWikiModule::invalidateModuleCache( $this, null, null, wfWikiID() );
+                       function () use ( $dbw ) {
+                               ResourceLoaderWikiModule::invalidateModuleCache(
+                                       $this, null, null, $dbw->getDomainId() );
                        },
                        __METHOD__
                );
index c2e37e0..bb86536 100644 (file)
@@ -1872,6 +1872,16 @@ abstract class ApiBase extends ContextSource {
                return $status;
        }
 
+       /**
+        * Call wfTransactionalTimeLimit() if this request was POSTed
+        * @since 1.26
+        */
+       protected function useTransactionalTimeLimit() {
+               if ( $this->getRequest()->wasPosted() ) {
+                       wfTransactionalTimeLimit();
+               }
+       }
+
        /**@}*/
 
        /************************************************************************//**
@@ -2671,352 +2681,6 @@ abstract class ApiBase extends ContextSource {
                return false;
        }
 
-       /**
-        * @deprecated since 1.25
-        */
-       public function profileIn() {
-               wfDeprecated( __METHOD__, '1.25' );
-       }
-
-       /**
-        * @deprecated since 1.25
-        */
-       public function profileOut() {
-               wfDeprecated( __METHOD__, '1.25' );
-       }
-
-       /**
-        * @deprecated since 1.25
-        */
-       public function safeProfileOut() {
-               wfDeprecated( __METHOD__, '1.25' );
-       }
-
-       /**
-        * @deprecated since 1.25
-        */
-       public function profileDBIn() {
-               wfDeprecated( __METHOD__, '1.25' );
-       }
-
-       /**
-        * @deprecated since 1.25
-        */
-       public function profileDBOut() {
-               wfDeprecated( __METHOD__, '1.25' );
-       }
-
-       /**
-        * Call wfTransactionalTimeLimit() if this request was POSTed
-        * @since 1.26
-        */
-       protected function useTransactionalTimeLimit() {
-               if ( $this->getRequest()->wasPosted() ) {
-                       wfTransactionalTimeLimit();
-               }
-       }
-
-       /**
-        * @deprecated since 1.29, use ApiBase::addWarning() instead
-        * @param string $warning Warning message
-        */
-       public function setWarning( $warning ) {
-               wfDeprecated( __METHOD__, '1.29' );
-               $msg = new ApiRawMessage( $warning, 'warning' );
-               $this->getErrorFormatter()->addWarning( $this->getModulePath(), $msg );
-       }
-
-       /**
-        * Throw an ApiUsageException, which will (if uncaught) call the main module's
-        * error handler and die with an error message.
-        *
-        * @deprecated since 1.29, use self::dieWithError() instead
-        * @param string $description One-line human-readable description of the
-        *   error condition, e.g., "The API requires a valid action parameter"
-        * @param string $errorCode Brief, arbitrary, stable string to allow easy
-        *   automated identification of the error, e.g., 'unknown_action'
-        * @param int $httpRespCode HTTP response code
-        * @param array|null $extradata Data to add to the "<error>" element; array in ApiResult format
-        * @throws ApiUsageException always
-        */
-       public function dieUsage( $description, $errorCode, $httpRespCode = 0, $extradata = null ) {
-               wfDeprecated( __METHOD__, '1.29' );
-               $this->dieWithError(
-                       new RawMessage( '$1', [ $description ] ),
-                       $errorCode,
-                       $extradata,
-                       $httpRespCode
-               );
-       }
-
-       /**
-        * Get error (as code, string) from a Status object.
-        *
-        * @since 1.23
-        * @deprecated since 1.29, use ApiErrorFormatter::arrayFromStatus instead
-        * @param Status $status
-        * @param array|null &$extraData Set if extra data from IApiMessage is available (since 1.27)
-        * @return array Array of code and error string
-        * @throws MWException
-        */
-       public function getErrorFromStatus( $status, &$extraData = null ) {
-               wfDeprecated( __METHOD__, '1.29' );
-               if ( $status->isGood() ) {
-                       throw new MWException( 'Successful status passed to ApiBase::dieStatus' );
-               }
-
-               $errors = $status->getErrorsByType( 'error' );
-               if ( !$errors ) {
-                       // No errors? Assume the warnings should be treated as errors
-                       $errors = $status->getErrorsByType( 'warning' );
-               }
-               if ( !$errors ) {
-                       // Still no errors? Punt
-                       $errors = [ [ 'message' => 'unknownerror-nocode', 'params' => [] ] ];
-               }
-
-               if ( $errors[0]['message'] instanceof MessageSpecifier ) {
-                       $msg = $errors[0]['message'];
-               } else {
-                       $msg = new Message( $errors[0]['message'], $errors[0]['params'] );
-               }
-               if ( !$msg instanceof IApiMessage ) {
-                       $key = $msg->getKey();
-                       $params = $msg->getParams();
-                       array_unshift( $params, self::$messageMap[$key] ?? $key );
-                       $msg = ApiMessage::create( $params );
-               }
-
-               return [
-                       $msg->getApiCode(),
-                       ApiErrorFormatter::stripMarkup( $msg->inLanguage( 'en' )->useDatabase( false )->text() )
-               ];
-       }
-
-       /**
-        * @deprecated since 1.29. Prior to 1.29, this was a public mapping from
-        *  arbitrary strings (often message keys used elsewhere in MediaWiki) to
-        *  API codes and message texts, and a few interfaces required poking
-        *  something in here. Now we're repurposing it to map those same strings
-        *  to i18n messages, and declaring that any interface that requires poking
-        *  at this is broken and needs replacing ASAP.
-        */
-       private static $messageMap = [
-               'unknownerror' => 'apierror-unknownerror',
-               'unknownerror-nocode' => 'apierror-unknownerror-nocode',
-               'ns-specialprotected' => 'ns-specialprotected',
-               'protectedinterface' => 'protectedinterface',
-               'namespaceprotected' => 'namespaceprotected',
-               'customcssprotected' => 'customcssprotected',
-               'customjsprotected' => 'customjsprotected',
-               'cascadeprotected' => 'cascadeprotected',
-               'protectedpagetext' => 'protectedpagetext',
-               'protect-cantedit' => 'protect-cantedit',
-               'deleteprotected' => 'deleteprotected',
-               'badaccess-group0' => 'badaccess-group0',
-               'badaccess-groups' => 'badaccess-groups',
-               'titleprotected' => 'titleprotected',
-               'nocreate-loggedin' => 'nocreate-loggedin',
-               'nocreatetext' => 'nocreatetext',
-               'movenologintext' => 'movenologintext',
-               'movenotallowed' => 'movenotallowed',
-               'confirmedittext' => 'confirmedittext',
-               'blockedtext' => 'apierror-blocked',
-               'autoblockedtext' => 'apierror-autoblocked',
-               'systemblockedtext' => 'apierror-systemblocked',
-               'actionthrottledtext' => 'apierror-ratelimited',
-               'alreadyrolled' => 'alreadyrolled',
-               'cantrollback' => 'cantrollback',
-               'readonlytext' => 'readonlytext',
-               'sessionfailure' => 'sessionfailure',
-               'cannotdelete' => 'cannotdelete',
-               'notanarticle' => 'apierror-missingtitle',
-               'selfmove' => 'selfmove',
-               'immobile_namespace' => 'apierror-immobilenamespace',
-               'articleexists' => 'articleexists',
-               'hookaborted' => 'hookaborted',
-               'cantmove-titleprotected' => 'cantmove-titleprotected',
-               'imagenocrossnamespace' => 'imagenocrossnamespace',
-               'imagetypemismatch' => 'imagetypemismatch',
-               'ip_range_invalid' => 'ip_range_invalid',
-               'range_block_disabled' => 'range_block_disabled',
-               'nosuchusershort' => 'nosuchusershort',
-               'badipaddress' => 'badipaddress',
-               'ipb_expiry_invalid' => 'ipb_expiry_invalid',
-               'ipb_already_blocked' => 'ipb_already_blocked',
-               'ipb_blocked_as_range' => 'ipb_blocked_as_range',
-               'ipb_cant_unblock' => 'ipb_cant_unblock',
-               'mailnologin' => 'apierror-cantsend',
-               'ipbblocked' => 'ipbblocked',
-               'ipbnounblockself' => 'ipbnounblockself',
-               'usermaildisabled' => 'usermaildisabled',
-               'blockedemailuser' => 'apierror-blockedfrommail',
-               'notarget' => 'apierror-notarget',
-               'noemail' => 'noemail',
-               'rcpatroldisabled' => 'rcpatroldisabled',
-               'markedaspatrollederror-noautopatrol' => 'markedaspatrollederror-noautopatrol',
-               'delete-toobig' => 'delete-toobig',
-               'movenotallowedfile' => 'movenotallowedfile',
-               'userrights-no-interwiki' => 'userrights-no-interwiki',
-               'userrights-nodatabase' => 'userrights-nodatabase',
-               'nouserspecified' => 'nouserspecified',
-               'noname' => 'noname',
-               'summaryrequired' => 'apierror-summaryrequired',
-               'import-rootpage-invalid' => 'import-rootpage-invalid',
-               'import-rootpage-nosubpage' => 'import-rootpage-nosubpage',
-               'readrequired' => 'apierror-readapidenied',
-               'writedisabled' => 'apierror-noapiwrite',
-               'writerequired' => 'apierror-writeapidenied',
-               'missingparam' => 'apierror-missingparam',
-               'invalidtitle' => 'apierror-invalidtitle',
-               'nosuchpageid' => 'apierror-nosuchpageid',
-               'nosuchrevid' => 'apierror-nosuchrevid',
-               'nosuchuser' => 'nosuchusershort',
-               'invaliduser' => 'apierror-invaliduser',
-               'invalidexpiry' => 'apierror-invalidexpiry',
-               'pastexpiry' => 'apierror-pastexpiry',
-               'create-titleexists' => 'apierror-create-titleexists',
-               'missingtitle-createonly' => 'apierror-missingtitle-createonly',
-               'cantblock' => 'apierror-cantblock',
-               'canthide' => 'apierror-canthide',
-               'cantblock-email' => 'apierror-cantblock-email',
-               'cantunblock' => 'apierror-permissiondenied-generic',
-               'cannotundelete' => 'cannotundelete',
-               'permdenied-undelete' => 'apierror-permissiondenied-generic',
-               'createonly-exists' => 'apierror-articleexists',
-               'nocreate-missing' => 'apierror-missingtitle',
-               'cantchangecontentmodel' => 'apierror-cantchangecontentmodel',
-               'nosuchrcid' => 'apierror-nosuchrcid',
-               'nosuchlogid' => 'apierror-nosuchlogid',
-               'protect-invalidaction' => 'apierror-protect-invalidaction',
-               'protect-invalidlevel' => 'apierror-protect-invalidlevel',
-               'toofewexpiries' => 'apierror-toofewexpiries',
-               'cantimport' => 'apierror-cantimport',
-               'cantimport-upload' => 'apierror-cantimport-upload',
-               'importnofile' => 'importnofile',
-               'importuploaderrorsize' => 'importuploaderrorsize',
-               'importuploaderrorpartial' => 'importuploaderrorpartial',
-               'importuploaderrortemp' => 'importuploaderrortemp',
-               'importcantopen' => 'importcantopen',
-               'import-noarticle' => 'import-noarticle',
-               'importbadinterwiki' => 'importbadinterwiki',
-               'import-unknownerror' => 'apierror-import-unknownerror',
-               'cantoverwrite-sharedfile' => 'apierror-cantoverwrite-sharedfile',
-               'sharedfile-exists' => 'apierror-fileexists-sharedrepo-perm',
-               'mustbeposted' => 'apierror-mustbeposted',
-               'show' => 'apierror-show',
-               'specialpage-cantexecute' => 'apierror-specialpage-cantexecute',
-               'invalidoldimage' => 'apierror-invalidoldimage',
-               'nodeleteablefile' => 'apierror-nodeleteablefile',
-               'fileexists-forbidden' => 'fileexists-forbidden',
-               'fileexists-shared-forbidden' => 'fileexists-shared-forbidden',
-               'filerevert-badversion' => 'filerevert-badversion',
-               'noimageredirect-anon' => 'apierror-noimageredirect-anon',
-               'noimageredirect-logged' => 'apierror-noimageredirect',
-               'spamdetected' => 'apierror-spamdetected',
-               'contenttoobig' => 'apierror-contenttoobig',
-               'noedit-anon' => 'apierror-noedit-anon',
-               'noedit' => 'apierror-noedit',
-               'wasdeleted' => 'apierror-pagedeleted',
-               'blankpage' => 'apierror-emptypage',
-               'editconflict' => 'editconflict',
-               'hashcheckfailed' => 'apierror-badmd5',
-               'missingtext' => 'apierror-notext',
-               'emptynewsection' => 'apierror-emptynewsection',
-               'revwrongpage' => 'apierror-revwrongpage',
-               'undo-failure' => 'undo-failure',
-               'content-not-allowed-here' => 'content-not-allowed-here',
-               'edit-hook-aborted' => 'edit-hook-aborted',
-               'edit-gone-missing' => 'edit-gone-missing',
-               'edit-conflict' => 'edit-conflict',
-               'edit-already-exists' => 'edit-already-exists',
-               'invalid-file-key' => 'apierror-invalid-file-key',
-               'nouploadmodule' => 'apierror-nouploadmodule',
-               'uploaddisabled' => 'uploaddisabled',
-               'copyuploaddisabled' => 'copyuploaddisabled',
-               'copyuploadbaddomain' => 'apierror-copyuploadbaddomain',
-               'copyuploadbadurl' => 'apierror-copyuploadbadurl',
-               'filename-tooshort' => 'filename-tooshort',
-               'filename-toolong' => 'filename-toolong',
-               'illegal-filename' => 'illegal-filename',
-               'filetype-missing' => 'filetype-missing',
-               'mustbeloggedin' => 'apierror-mustbeloggedin',
-       ];
-
-       /**
-        * @deprecated do not use
-        * @param array|string|MessageSpecifier $error Element of a getUserPermissionsErrors()-style array
-        * @return ApiMessage
-        */
-       private function parseMsgInternal( $error ) {
-               $msg = Message::newFromSpecifier( $error );
-               if ( !$msg instanceof IApiMessage ) {
-                       $key = $msg->getKey();
-                       if ( isset( self::$messageMap[$key] ) ) {
-                               $params = $msg->getParams();
-                               array_unshift( $params, self::$messageMap[$key] );
-                       } else {
-                               $params = [ 'apierror-unknownerror', wfEscapeWikiText( $key ) ];
-                       }
-                       $msg = ApiMessage::create( $params );
-               }
-               return $msg;
-       }
-
-       /**
-        * Return the error message related to a certain array
-        * @deprecated since 1.29
-        * @param array|string|MessageSpecifier $error Element of a getUserPermissionsErrors()-style array
-        * @return array [ 'code' => code, 'info' => info ]
-        */
-       public function parseMsg( $error ) {
-               wfDeprecated( __METHOD__, '1.29' );
-               // Check whether someone passed the whole array, instead of one element as
-               // documented. This breaks if it's actually an array of fallback keys, but
-               // that's long-standing misbehavior introduced in r87627 to incorrectly
-               // fix T30797.
-               if ( is_array( $error ) ) {
-                       $first = reset( $error );
-                       if ( is_array( $first ) ) {
-                               wfDebug( __METHOD__ . ' was passed an array of arrays. ' . wfGetAllCallers( 5 ) );
-                               $error = $first;
-                       }
-               }
-
-               $msg = $this->parseMsgInternal( $error );
-               return [
-                       'code' => $msg->getApiCode(),
-                       'info' => ApiErrorFormatter::stripMarkup(
-                               $msg->inLanguage( 'en' )->useDatabase( false )->text()
-                       ),
-                       'data' => $msg->getApiData()
-               ];
-       }
-
-       /**
-        * Output the error message related to a certain array
-        * @deprecated since 1.29, use ApiBase::dieWithError() instead
-        * @param array|string|MessageSpecifier $error Element of a getUserPermissionsErrors()-style array
-        * @throws ApiUsageException always
-        */
-       public function dieUsageMsg( $error ) {
-               wfDeprecated( __METHOD__, '1.29' );
-               $this->dieWithError( $this->parseMsgInternal( $error ) );
-       }
-
-       /**
-        * Will only set a warning instead of failing if the global $wgDebugAPI
-        * is set to true. Otherwise behaves exactly as dieUsageMsg().
-        * @deprecated since 1.29, use ApiBase::dieWithErrorOrDebug() instead
-        * @param array|string|MessageSpecifier $error Element of a getUserPermissionsErrors()-style array
-        * @throws ApiUsageException
-        * @since 1.21
-        */
-       public function dieUsageMsgOrDebug( $error ) {
-               wfDeprecated( __METHOD__, '1.29' );
-               $this->dieWithErrorOrDebug( $this->parseMsgInternal( $error ) );
-       }
-
        /**
         * Return the description message.
         *
index af23f95..847afd8 100644 (file)
@@ -58,6 +58,26 @@ class ApiErrorFormatter {
                $this->format = $format;
        }
 
+       /**
+        * Return a formatter like this one but with a different format
+        *
+        * @since 1.32
+        * @param string $format New format.
+        * @return ApiErrorFormatter
+        */
+       public function newWithFormat( $format ) {
+               return new self( $this->result, $this->lang, $format, $this->useDB );
+       }
+
+       /**
+        * Fetch the format for this formatter
+        * @since 1.32
+        * @return string
+        */
+       public function getFormat() {
+               return $this->format;
+       }
+
        /**
         * Fetch the Language for this formatter
         * @since 1.29
@@ -164,16 +184,6 @@ class ApiErrorFormatter {
                        $msg = Message::newFromSpecifier( $exception );
                        $params = [];
                } else {
-                       // Extract code and data from the exception, if applicable
-                       if ( $exception instanceof UsageException ) {
-                               $data = $exception->getMessageArray();
-                               if ( !$options['code'] ) {
-                                       $options['code'] = $data['code'];
-                               }
-                               unset( $data['code'], $data['info'] );
-                               $options['data'] = array_merge( $data, $options['data'] );
-                       }
-
                        if ( isset( $options['wrap'] ) ) {
                                $msg = $options['wrap'];
                        } else {
@@ -371,6 +381,10 @@ class ApiErrorFormatter_BackCompat extends ApiErrorFormatter {
                parent::__construct( $result, Language::factory( 'en' ), 'none', false );
        }
 
+       public function getFormat() {
+               return 'bc';
+       }
+
        public function arrayFromStatus( StatusValue $status, $type = 'error', $format = null ) {
                if ( $status->isGood() || !$status->getErrors() ) {
                        return [];
index c21ac12..37ec3cf 100644 (file)
@@ -157,12 +157,8 @@ class ApiFeedWatchlist extends ApiBase {
                                        $feedItems[] = new FeedItem( $errorTitle, $errorText, '', '', '' );
                                }
                        } else {
-                               if ( $e instanceof UsageException ) {
-                                       $errorCode = $e->getCodeString();
-                               } else {
-                                       // Something is seriously wrong
-                                       $errorCode = 'internal_api_error';
-                               }
+                               // Something is seriously wrong
+                               $errorCode = 'internal_api_error';
                                $errorTitle = $this->msg( 'api-feed-error-title', $errorCode );
                                $errorText = $e->getMessage();
                                $feedItems[] = new FeedItem( $errorTitle, $errorText, '', '', '' );
index 3b305f9..d2a7db2 100644 (file)
@@ -566,8 +566,8 @@ class ApiMain extends ApiBase {
         */
        protected function handleException( $e ) {
                // T65145: Rollback any open database transactions
-               if ( !( $e instanceof ApiUsageException || $e instanceof UsageException ) ) {
-                       // UsageExceptions are intentional, so don't rollback if that's the case
+               if ( !$e instanceof ApiUsageException ) {
+                       // ApiUsageExceptions are intentional, so don't rollback if that's the case
                        MWExceptionHandler::rollbackMasterChangesAndLog( $e );
                }
 
@@ -612,13 +612,6 @@ class ApiMain extends ApiBase {
                                        $this->addWarning( $error );
                                }
                        }
-               } catch ( UsageException $ex ) {
-                       // The error printer itself is failing. Try suppressing its request
-                       // parameters and redo.
-                       $failed = true;
-                       $this->addWarning(
-                               [ 'apiwarn-errorprinterfailed-ex', $ex->getMessage() ], 'errorprinterfailed'
-                       );
                }
                if ( $failed ) {
                        $this->mPrinter = null;
@@ -1012,9 +1005,6 @@ class ApiMain extends ApiBase {
         * If an ApiUsageException, errors/warnings will be extracted from the
         * embedded StatusValue.
         *
-        * If a base UsageException, the getMessageArray() method will be used to
-        * extract the code and English message for a single error (no warnings).
-        *
         * Any other exception will be returned with a generic code and wrapper
         * text around the exception's (presumably English) message as a single
         * error (no warnings).
@@ -1032,13 +1022,6 @@ class ApiMain extends ApiBase {
                        }
                } elseif ( $type !== 'error' ) {
                        // None of the rest have any messages for non-error types
-               } elseif ( $e instanceof UsageException ) {
-                       // User entered incorrect parameters - generate error response
-                       $data = Wikimedia\quietCall( [ $e, 'getMessageArray' ] );
-                       $code = $data['code'];
-                       $info = $data['info'];
-                       unset( $data['code'], $data['info'] );
-                       $messages[] = new ApiRawMessage( [ '$1', $info ], $code, $data );
                } else {
                        // Something is seriously wrong
                        $config = $this->getConfig();
@@ -1108,7 +1091,7 @@ class ApiMain extends ApiBase {
                } else {
                        $path = null;
                }
-               if ( $e instanceof ApiUsageException || $e instanceof UsageException ) {
+               if ( $e instanceof ApiUsageException ) {
                        $link = wfExpandUrl( wfScript( 'api' ) );
                        $result->addContentValue(
                                $path,
index 26846f4..194a511 100644 (file)
@@ -418,19 +418,6 @@ class ApiPageSet extends ApiBase {
                return $this->mGoodTitles + $this->mMissingTitles;
        }
 
-       /**
-        * Titles that were deemed invalid by Title::newFromText()
-        * The array's index will be unique and negative for each item
-        * @deprecated since 1.26, use self::getInvalidTitlesAndReasons()
-        * @return string[] Array of strings (not Title objects)
-        */
-       public function getInvalidTitles() {
-               wfDeprecated( __METHOD__, '1.26' );
-               return array_map( function ( $t ) {
-                       return $t['title'];
-               }, $this->mInvalidTitles );
-       }
-
        /**
         * Titles that were deemed invalid by Title::newFromText()
         * The array's index will be unique and negative for each item
index b9ed9f2..8630561 100644 (file)
@@ -436,9 +436,14 @@ abstract class ApiQueryBase extends ApiBase {
         * @return void
         */
        public function showHiddenUsersAddBlockInfo( $showBlockInfo ) {
+               $db = $this->getDB();
+
                $this->addTables( 'ipblocks' );
                $this->addJoinConds( [
-                       'ipblocks' => [ 'LEFT JOIN', 'ipb_user=user_id' ],
+                       'ipblocks' => [ 'LEFT JOIN', [
+                               'ipb_user=user_id',
+                               'ipb_expiry > ' . $db->addQuotes( $db->timestamp() ),
+                       ] ],
                ] );
 
                $this->addFields( 'ipb_deleted' );
index 18d2804..37a7ff3 100644 (file)
@@ -59,7 +59,8 @@ class ApiQueryImageInfo extends ApiQueryBase {
                if ( isset( $params['badfilecontexttitle'] ) ) {
                        $badFileContextTitle = Title::newFromText( $params['badfilecontexttitle'] );
                        if ( !$badFileContextTitle ) {
-                               $this->dieUsage( 'Invalid title in badfilecontexttitle parameter', 'invalid-title' );
+                               $p = $this->getModulePrefix();
+                               $this->dieWithError( [ 'apierror-bad-badfilecontexttitle', $p ], 'invalid-title' );
                        }
                } else {
                        $badFileContextTitle = false;
@@ -301,7 +302,7 @@ class ApiQueryImageInfo extends ApiQueryBase {
 
                $paramList = $h->parseParamString( $otherParams );
                if ( !$paramList ) {
-                       // Just set a warning (instead of dieUsage), as in many cases
+                       // Just set a warning (instead of dieWithError), as in many cases
                        // we could still render the image using width and height parameters,
                        // and this type of thing could happen between different versions of
                        // handlers.
index 3b7b00d..2ab3c56 100644 (file)
@@ -527,11 +527,27 @@ class ApiQueryInfo extends ApiQueryBase {
                                return null; // force a continuation
                        }
 
+                       $detailLevel = $this->params['testactionsdetail'];
+                       $rigor = $detailLevel === 'quick' ? 'quick' : 'secure';
+                       $errorFormatter = $this->getErrorFormatter();
+                       if ( $errorFormatter->getFormat() === 'bc' ) {
+                               // Eew, no. Use a more modern format here.
+                               $errorFormatter = $errorFormatter->newWithFormat( 'plaintext' );
+                       }
+
                        $user = $this->getUser();
                        $pageInfo['actions'] = [];
                        foreach ( $this->params['testactions'] as $action ) {
                                $this->countTestedActions++;
-                               $pageInfo['actions'][$action] = $title->userCan( $action, $user );
+
+                               if ( $detailLevel === 'boolean' ) {
+                                       $pageInfo['actions'][$action] = $title->userCan( $action, $user );
+                               } else {
+                                       $pageInfo['actions'][$action] = $errorFormatter->arrayFromStatus( $this->errorArrayToStatus(
+                                               $title->getUserPermissionsErrors( $action, $user, $rigor ),
+                                               $user
+                                       ) );
+                               }
                        }
                }
 
@@ -955,11 +971,19 @@ class ApiQueryInfo extends ApiQueryBase {
                                        // need to be added to getCacheMode()
                                ],
                                ApiBase::PARAM_HELP_MSG_PER_VALUE => [],
+                               ApiBase::PARAM_DEPRECATED_VALUES => [
+                                       'readable' => true, // Since 1.32
+                               ],
                        ],
                        'testactions' => [
                                ApiBase::PARAM_TYPE => 'string',
                                ApiBase::PARAM_ISMULTI => true,
                        ],
+                       'testactionsdetail' => [
+                               ApiBase::PARAM_TYPE => [ 'boolean', 'full', 'quick' ],
+                               ApiBase::PARAM_DFLT => 'boolean',
+                               ApiBase::PARAM_HELP_MSG_PER_VALUE => [],
+                       ],
                        'token' => [
                                ApiBase::PARAM_DEPRECATED => true,
                                ApiBase::PARAM_ISMULTI => true,
index 39be2c1..2d95cd3 100644 (file)
@@ -263,32 +263,6 @@ class ApiQueryLogEvents extends ApiQueryBase {
                $result->addIndexedTagName( [ 'query', $this->getModuleName() ], 'item' );
        }
 
-       /**
-        * @deprecated since 1.25 Use LogFormatter::formatParametersForApi instead
-        * @param ApiResult $result
-        * @param array &$vals
-        * @param string $params
-        * @param string $type
-        * @param string $action
-        * @param string $ts
-        * @param bool $legacy
-        * @return array
-        */
-       public static function addLogParams( $result, &$vals, $params, $type,
-               $action, $ts, $legacy = false
-       ) {
-               wfDeprecated( __METHOD__, '1.25' );
-
-               $entry = new ManualLogEntry( $type, $action );
-               $entry->setParameters( $params );
-               $entry->setTimestamp( $ts );
-               $entry->setLegacy( $legacy );
-               $formatter = LogFormatter::newFromEntry( $entry );
-               $vals['params'] = $formatter->formatParametersForApi();
-
-               return $vals;
-       }
-
        private function extractRowInfo( $row ) {
                $logEntry = DatabaseLogEntry::newFromRow( $row );
                $vals = [
index 7f8a26b..f930452 100644 (file)
  * If possible, use ApiBase::dieWithError() instead of throwing this directly.
  *
  * @ingroup API
- * @note This currently extends UsageException for backwards compatibility, so
- *  all the existing code that catches UsageException won't break when stuff
- *  starts throwing ApiUsageException. Eventually UsageException will go away
- *  and this will (probably) extend MWException directly.
  */
-class ApiUsageException extends UsageException implements ILocalizedException {
+class ApiUsageException extends MWException implements ILocalizedException {
 
        protected $modulePath;
        protected $status;
@@ -53,12 +49,7 @@ class ApiUsageException extends UsageException implements ILocalizedException {
                // customized by the local wiki.
                $enMsg = clone $this->getApiMessage();
                $enMsg->inLanguage( 'en' )->useDatabase( false );
-               parent::__construct(
-                       ApiErrorFormatter::stripMarkup( $enMsg->text() ),
-                       $enMsg->getApiCode(),
-                       $httpCode,
-                       $enMsg->getApiData()
-               );
+               parent::__construct( ApiErrorFormatter::stripMarkup( $enMsg->text() ), $httpCode );
        }
 
        /**
@@ -111,32 +102,6 @@ class ApiUsageException extends UsageException implements ILocalizedException {
                return $this->status;
        }
 
-       /**
-        * @deprecated Do not use. This only exists here because UsageException is in
-        *  the inheritance chain for backwards compatibility.
-        * @inheritDoc
-        */
-       public function getCodeString() {
-               wfDeprecated( __METHOD__, '1.29' );
-               return $this->getApiMessage()->getApiCode();
-       }
-
-       /**
-        * @deprecated Do not use. This only exists here because UsageException is in
-        *  the inheritance chain for backwards compatibility.
-        * @inheritDoc
-        */
-       public function getMessageArray() {
-               wfDeprecated( __METHOD__, '1.29' );
-               $enMsg = clone $this->getApiMessage();
-               $enMsg->inLanguage( 'en' )->useDatabase( false );
-
-               return [
-                       'code' => $enMsg->getApiCode(),
-                       'info' => ApiErrorFormatter::stripMarkup( $enMsg->text() ),
-               ] + $enMsg->getApiData();
-       }
-
        /**
         * @inheritDoc
         */
diff --git a/includes/api/UsageException.php b/includes/api/UsageException.php
deleted file mode 100644 (file)
index 426ce91..0000000
+++ /dev/null
@@ -1,90 +0,0 @@
-<?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
- */
-
-/**
- * This exception will be thrown when dieUsage is called to stop module execution.
- *
- * @ingroup API
- * @deprecated since 1.29, use ApiUsageException instead
- */
-class UsageException extends MWException {
-
-       private $mCodestr;
-
-       /**
-        * @var null|array
-        */
-       private $mExtraData;
-
-       /**
-        * @param string $message
-        * @param string $codestr
-        * @param int $code
-        * @param array|null $extradata
-        */
-       public function __construct( $message, $codestr, $code = 0, $extradata = null ) {
-               parent::__construct( $message, $code );
-               $this->mCodestr = $codestr;
-               $this->mExtraData = $extradata;
-
-               if ( !$this instanceof ApiUsageException ) {
-                       wfDeprecated( __METHOD__, '1.29' );
-               }
-
-               // This should never happen, so throw an exception about it that will
-               // hopefully get logged with a backtrace (T138585)
-               if ( !is_string( $codestr ) || $codestr === '' ) {
-                       throw new InvalidArgumentException( 'Invalid $codestr, was ' .
-                               ( $codestr === '' ? 'empty string' : gettype( $codestr ) )
-                       );
-               }
-       }
-
-       /**
-        * @return string
-        */
-       public function getCodeString() {
-               wfDeprecated( __METHOD__, '1.29' );
-               return $this->mCodestr;
-       }
-
-       /**
-        * @return array
-        */
-       public function getMessageArray() {
-               wfDeprecated( __METHOD__, '1.29' );
-               $result = [
-                       'code' => $this->mCodestr,
-                       'info' => $this->getMessage()
-               ];
-               if ( is_array( $this->mExtraData ) ) {
-                       $result = array_merge( $result, $this->mExtraData );
-               }
-
-               return $result;
-       }
-
-       /**
-        * @return string
-        */
-       public function __toString() {
-               return "{$this->getCodeString()}: {$this->getMessage()}";
-       }
-}
index d6af2f1..7559a53 100644 (file)
        "apierror-assertnameduserfailed": "تأكيد أن المستخدم \"$1\" فشل.",
        "apierror-assertuserfailed": "التأكيد على فشل تسجيل الدخول للمستخدم.",
        "apierror-autoblocked": "عنوان الآيبي الخاص بك تم منعه تلقائيا; لأنه تم استخدامه بواسطة مستخدم ممنوع.",
+       "apierror-bad-badfilecontexttitle": "عنوان غير صالح في الوسيط <var>$1badfilecontexttitle</var>.",
        "apierror-badconfig-resulttoosmall": "إن قيمة <code>$wgAPIMaxResultSize</code> في هذا الويكي صغيرة جدا لا تحتوي على معلومات النتائج الأساسية.",
        "apierror-badcontinue": "متابعة غير صحيحة; يجب عليك تمرير القيمة الأصلية التي تم إرجاعها بواسطة الاستعلام السابق.",
        "apierror-baddiff": "لا يمكن استرجاع الفرقك; إحدى أو كلا المراجعتين غير موجودة أو ليست لديك صلاحية لعرضها.",
        "apiwarn-deprecation-withreplacement": "تم إيقاف <kbd>$1</kbd>; الرجاء استخدام <kbd>$2</kbd> بدلا من ذلك.",
        "apiwarn-difftohidden": "لا يمكنك إجراء مقارنة مع r$1: المحتوى مخفي.",
        "apiwarn-errorprinterfailed": "فشل خطأ الطباعة; سوف يعيد دون وسائط.",
-       "apiwarn-errorprinterfailed-ex": "فشل خطأ الطباعة (سوف يعيد دون وسائط): $1.",
        "apiwarn-ignoring-invalid-templated-value": "تجاهل القيمة <kbd>$2</kbd> في <var>$1</var> عند معالجة وسائط القوالب.",
        "apiwarn-invalidcategory": "\"$1\" ليس تصنيفا.",
        "apiwarn-invalidtitle": "\"$1\" ليس عنوانا صالحا.",
index c1fba86..913e129 100644 (file)
@@ -75,6 +75,7 @@
        "apihelp-compare-paramvalue-prop-diff": "Das Unterschieds-HTML.",
        "apihelp-compare-paramvalue-prop-diffsize": "Die Größe des Unterschieds-HTML in Bytes.",
        "apihelp-compare-paramvalue-prop-title": "Die Seitentitel der Versionen „Von“ und „Nach“.",
+       "apihelp-compare-paramvalue-prop-size": "Die Größe der Versionen „from“ und „to“.",
        "apihelp-compare-example-1": "Unterschied zwischen Version 1 und 2 abrufen",
        "apihelp-createaccount-summary": "Erstellt ein neues Benutzerkonto.",
        "apihelp-createaccount-param-preservestate": "Falls <kbd>[[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]]</kbd> für <samp>hasprimarypreservedstate</samp> wahr ausgegeben hat, sollten Anfragen, die als <samp>primary-required</samp> markiert wurden, ausgelassen werden. Falls ein nicht-leerer Wert für <samp>preservedusername</samp> zurückgegeben wurde, muss dieser Benutzername für den Parameter <var>username</var> verwendet werden.",
        "apihelp-query+allrevisions-param-generatetitles": "Wenn als Generator verwendet, werden eher Titel als Bearbeitungs-IDs erzeugt.",
        "apihelp-query+allrevisions-example-user": "Liste die letzten 50 Beiträge, sortiert nach Benutzer <kbd>Beispiel</kbd> auf.",
        "apihelp-query+allrevisions-example-ns-main": "Liste die ersten 50 Bearbeitungen im Hauptnamensraum auf.",
+       "apihelp-query+mystashedfiles-summary": "Ruft eine Liste der Dateien im aktuellen Benutzeruploadspeicher ab.",
        "apihelp-query+mystashedfiles-param-prop": "Welche Eigenschaften für die Dateien abgerufen werden sollen.",
        "apihelp-query+mystashedfiles-paramvalue-prop-size": "Ruft die Dateigröße und Bildabmessungen ab.",
        "apihelp-query+mystashedfiles-paramvalue-prop-type": "Ruft den MIME- und Medientyp der Datei ab.",
        "apihelp-query+filearchive-paramvalue-prop-archivename": "Fügt den Dateinamen der Archivversion für die nicht-neuesten Versionen hinzu.",
        "apihelp-query+filearchive-example-simple": "Eine Liste aller gelöschten Dateien auflisten",
        "apihelp-query+filerepoinfo-summary": "Gebe Metainformationen über Bild-Repositorien zurück, die im Wiki eingerichtet sind.",
+       "apihelp-query+filerepoinfo-paramvalue-prop-displayname": "Der menschenlesbare Name des Repositoriumwikis.",
        "apihelp-query+filerepoinfo-paramvalue-prop-local": "Ob dieses Repositorium das lokale ist oder nicht.",
        "apihelp-query+filerepoinfo-paramvalue-prop-rootUrl": "Wurzel-URL-Pfad für Bildpfade.",
        "apihelp-query+filerepoinfo-paramvalue-prop-scriptDirUrl": "Wurzel-URL-Pfad für die MediaWiki-Installation des Repositoriumwikis.",
        "apihelp-query+info-paramvalue-prop-notificationtimestamp": "Der Beobachtungslisten-Benachrichtigungs-Zeitstempel jeder Seite.",
        "apihelp-query+info-paramvalue-prop-subjectid": "Die Seitenkennung der Elternseite jeder Diskussionsseite.",
        "apihelp-query+info-paramvalue-prop-readable": "Ob der Benutzer diese Seite betrachten darf.",
+       "apihelp-query+info-paramvalue-prop-preload": "Gibt den Text aus, der von EditFormPreloadText zurückgegeben wurde.",
        "apihelp-query+info-paramvalue-prop-displaytitle": "Gibt die Art und Weise an, in der der Seitentitel tatsächlich angezeigt wird.",
        "apihelp-query+info-paramvalue-prop-varianttitles": "Gibt den Anzeigetitel in allen Varianten der Sprache des Websiteinhalts aus.",
        "apihelp-query+info-param-testactions": "Überprüft, ob der aktuelle Benutzer gewisse Aktionen auf der Seite ausführen kann.",
        "apihelp-query+iwlinks-param-prefix": "Gibt nur Interwiki-Links mit diesem Präfix zurück.",
        "apihelp-query+iwlinks-param-dir": "Die Auflistungsrichtung.",
        "apihelp-query+iwlinks-example-simple": "Ruft die Interwikilinks von der Seite <kbd>Hauptseite</kbd> ab.",
+       "apihelp-query+langbacklinks-summary": "Findet alle Seiten, die auf den angegebenen Sprachlink verlinken.",
        "apihelp-query+langbacklinks-param-lang": "Sprache für den Sprachlink.",
        "apihelp-query+langbacklinks-param-limit": "Wie viele Gesamtseiten zurückgegeben werden sollen.",
        "apihelp-query+langbacklinks-param-prop": "Zurückzugebende Eigenschaften:",
+       "apihelp-query+langbacklinks-paramvalue-prop-lllang": "Ergänzt den Sprachcode des Sprachlinks.",
        "apihelp-query+langbacklinks-paramvalue-prop-lltitle": "Ergänzt den Titel des Sprachlinks.",
        "apihelp-query+langbacklinks-param-dir": "Die Auflistungsrichtung.",
        "apihelp-query+langbacklinks-example-simple": "Ruft Seiten ab, die auf [[:fr:Test]] verlinken.",
+       "apihelp-query+langlinks-summary": "Gibt alle Interlanguagelinks von den angegebenen Seiten zurück.",
        "apihelp-query+langlinks-param-limit": "Wie viele Sprachlinks zurückgegeben werden sollen.",
        "apihelp-query+langlinks-param-prop": "Zusätzlich zurückzugebende Eigenschaften jedes Interlanguage-Links:",
        "apihelp-query+langlinks-paramvalue-prop-url": "Ergänzt die vollständige URL.",
        "apihelp-query+linkshere-paramvalue-prop-pageid": "Die Seitenkennung jeder Seite.",
        "apihelp-query+linkshere-paramvalue-prop-title": "Titel jeder Seite.",
        "apihelp-query+linkshere-paramvalue-prop-redirect": "Markieren, falls die Seite eine Weiterleitung ist.",
+       "apihelp-query+linkshere-param-namespace": "Nur Seiten in diesen Namensräumen einschließen.",
        "apihelp-query+linkshere-param-limit": "Wie viel zurückgegeben werden soll.",
        "apihelp-query+linkshere-example-simple": "Holt eine Liste von Seiten, die auf [[Main Page]] verlinken.",
        "apihelp-query+logevents-summary": "Ruft Ereignisse von Logbüchern ab.",
        "apihelp-query+logevents-paramvalue-prop-user": "Ergänzt den verantwortlichen Benutzer für das Logbuchereignis.",
        "apihelp-query+logevents-paramvalue-prop-timestamp": "Ergänzt den Zeitstempel des Logbucheintrags.",
        "apihelp-query+logevents-paramvalue-prop-comment": "Ergänzt den Kommentar des Logbuchereignisses.",
+       "apihelp-query+logevents-paramvalue-prop-parsedcomment": "Ergänzt den geparsten Kommentar des Logbuchereignisses.",
        "apihelp-query+logevents-paramvalue-prop-details": "Listet zusätzliche Einzelheiten über das Logbuchereignis auf.",
        "apihelp-query+logevents-paramvalue-prop-tags": "Listet Markierungen für das Logbuchereignis auf.",
        "apihelp-query+logevents-param-type": "Filtert nur Logbucheinträge mit diesem Typ heraus.",
        "api-help-right-apihighlimits": "Höhere Beschränkungen in API-Anfragen verwenden (langsame Anfragen: $1; schnelle Anfragen: $2). Die Beschränkungen für langsame Anfragen werden auch auf Mehrwertparameter angewandt.",
        "api-help-open-in-apisandbox": "<small>[in Spielwiese öffnen]</small>",
        "api-help-authmanagerhelper-messageformat": "Zu verwendendes Format zur Rückgabe von Nachrichten.",
+       "apierror-bad-badfilecontexttitle": "Ungültiger Titel im Parameter <var>$1badfilecontexttitle</var>.",
        "apierror-badgenerator-unknown": "<kbd>generator=$1</kbd> unbekannt.",
        "apierror-badip": "Der IP-Parameter ist nicht gültig.",
        "apierror-badmd5": "Die angegebene MD5-Prüfsumme war falsch.",
index 253380c..25bf3f7 100644 (file)
        "apihelp-query+info-paramvalue-prop-notificationtimestamp": "The watchlist notification timestamp of each page.",
        "apihelp-query+info-paramvalue-prop-subjectid": "The page ID of the parent page for each talk page.",
        "apihelp-query+info-paramvalue-prop-url": "Gives a full URL, an edit URL, and the canonical URL for each page.",
-       "apihelp-query+info-paramvalue-prop-readable": "Whether the user can read this page.",
+       "apihelp-query+info-paramvalue-prop-readable": "Whether the user can read this page. Use <kbd>intestactions=read</kbd> instead.",
        "apihelp-query+info-paramvalue-prop-preload": "Gives the text returned by EditFormPreloadText.",
        "apihelp-query+info-paramvalue-prop-displaytitle": "Gives the manner in which the page title is actually displayed.",
        "apihelp-query+info-paramvalue-prop-varianttitles": "Gives the display title in all variants of the site content language.",
        "apihelp-query+info-param-testactions": "Test whether the current user can perform certain actions on the page.",
+       "apihelp-query+info-param-testactionsdetail": "Detail level for <var>$1testactions</var>. Use the [[Special:ApiHelp/main|main module]]'s <var>errorformat</var> and <var>errorlang</var> parameters to control the format of the messages returned.",
+       "apihelp-query+info-paramvalue-testactionsdetail-boolean": "Return a boolean value for each action.",
+       "apihelp-query+info-paramvalue-testactionsdetail-full": "Return messages describing why the action is disallowed, or an empty array if it is allowed.",
+       "apihelp-query+info-paramvalue-testactionsdetail-quick": "Like <kbd>full</kbd> but skipping expensive checks.",
        "apihelp-query+info-param-token": "Use [[Special:ApiHelp/query+tokens|action=query&meta=tokens]] instead.",
        "apihelp-query+info-example-simple": "Get information about the page <kbd>Main Page</kbd>.",
        "apihelp-query+info-example-protection": "Get general and protection information about the page <kbd>Main Page</kbd>.",
        "apierror-assertnameduserfailed": "Assertion that the user is \"$1\" failed.",
        "apierror-assertuserfailed": "Assertion that the user is logged in failed.",
        "apierror-autoblocked": "Your IP address has been blocked automatically, because it was used by a blocked user.",
+       "apierror-bad-badfilecontexttitle": "Invalid title in <var>$1badfilecontexttitle</var> parameter.",
        "apierror-badconfig-resulttoosmall": "The value of <code>$wgAPIMaxResultSize</code> on this wiki is too small to hold basic result information.",
        "apierror-badcontinue": "Invalid continue param. You should pass the original value returned by the previous query.",
        "apierror-baddiff": "The diff cannot be retrieved. One or both revisions do not exist or you do not have permission to view them.",
        "apiwarn-deprecation-withreplacement": "<kbd>$1</kbd> has been deprecated. Please use <kbd>$2</kbd> instead.",
        "apiwarn-difftohidden": "Couldn't diff to r$1: content is hidden.",
        "apiwarn-errorprinterfailed": "Error printer failed. Will retry without params.",
-       "apiwarn-errorprinterfailed-ex": "Error printer failed (will retry without params): $1",
        "apiwarn-ignoring-invalid-templated-value": "Ignoring value <kbd>$2</kbd> in <var>$1</var> when processing templated parameters.",
        "apiwarn-invalidcategory": "\"$1\" is not a category.",
        "apiwarn-invalidtitle": "\"$1\" is not a valid title.",
index 1c0bb0e..33c05e0 100644 (file)
        "apierror-assertnameduserfailed": "La vérification que l’utilisateur est « $1 » a échoué.",
        "apierror-assertuserfailed": "La vérification que l’utilisateur est connecté a échoué.",
        "apierror-autoblocked": "Votre adresse IP a été bloquée automatiquement, parce qu’elle a été utilisée par un utilisateur bloqué.",
+       "apierror-bad-badfilecontexttitle": "Titre invalide dans le paramètre <var>$1badfilecontexttitle</var> .",
        "apierror-badconfig-resulttoosmall": "La valeur de <code>$wgAPIMaxResultSize</code> sur ce wiki est trop petite pour contenir des informations de résultat basiques.",
        "apierror-badcontinue": "Paramètre de continuation non valide. Vous devez passer la valeur d’origine renvoyée par la requête précédente.",
        "apierror-baddiff": "La différence ne peut être récupérée. Une ou les deux révisions n’existent pas ou vous n’avez pas le droit de les voir.",
        "apiwarn-deprecation-withreplacement": "<kbd>$1</kbd> est désuet. Veuillez utiliser <kbd>$2</kbd> à la place.",
        "apiwarn-difftohidden": "Impossible de faire un diff avec r$1 : le contenu est masqué.",
        "apiwarn-errorprinterfailed": "Erreur échec imprimante. Nouvel essai sans paramètres.",
-       "apiwarn-errorprinterfailed-ex": "Erreur d’échec de l’impression (réessayera sans paramètres) : $1",
        "apiwarn-ignoring-invalid-templated-value": "Ignorer la valeur <kbd>$2</kbd> dans <var>$1</var> en traitant les paramètres de modèle.",
        "apiwarn-invalidcategory": "« $1 » n'est pas une catégorie.",
        "apiwarn-invalidtitle": "« $1 » n’est pas un titre valide.",
index 4bfb522..3370d73 100644 (file)
        "apiwarn-deprecation-withreplacement": "<kbd>$1</kbd> מיושן. יש להשתמש ב־<kbd>$2</kbd> במקום זה.",
        "apiwarn-difftohidden": "לא היה אפשר לעשות השוואה עם גרסה $1: התוכן מוסתר.",
        "apiwarn-errorprinterfailed": "מדפיס השגיאות לא עבד. ינסה שוב ללא פרמטרים.",
-       "apiwarn-errorprinterfailed-ex": "מדפיס השגיאות לא עבד (ינסה שוב ללא פרמטרים): $1",
        "apiwarn-ignoring-invalid-templated-value": "לא ייעשה שימוש בערך <kbd>$2</kbd> שבפרמטר <var>$1</var> בעת עיבוד הפרמטרים בתבנית.",
        "apiwarn-invalidcategory": "\"$1\" אינה קטגוריה.",
        "apiwarn-invalidtitle": "\"$1\" אינה כותרת תקינה.",
index 5a76e4c..e71f0f3 100644 (file)
        "api-help-authmanagerhelper-returnurl": "URL di ritorno per i flussi di autenticazione di terze parti, deve essere assoluto. E' necessario fornirlo, oppure va fornito <var>$1continue</var>.\n\nAlla ricezione di una risposta <samp>REDIRECT</samp>, in genere si apre un browser o una vista web all'URL specificato <samp>redirecttarget</samp> per un flusso di autenticazione di terze parti. Quando questo è completato, la terza parte invierà il browser o la vista web a questo URL. Dovresti estrarre qualsiasi parametro POST o della richiesta dall'URL e passarli come un request <var>$1continue</var> a questo modulo API.",
        "api-help-authmanagerhelper-continue": "Questa richiesta è una continuazione dopo una precedente risposta <samp>UI</samp> o <samp>REDIRECT</samp>. È necessario fornirlo, oppure fornire <var>$1returnurl</var>.",
        "api-help-authmanagerhelper-additional-params": "Questo modulo accetta parametri aggiuntivi a seconda delle richieste di autenticazione disponibili. Utilizza <kbd>[[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]]</kbd> con <kbd>amirequestsfor=$1</kbd> (o una precedente risposta da questo modulo, se applicabile) per determinare le richieste disponibili e i campi usati da queste.",
+       "apierror-bad-badfilecontexttitle": "Titolo non valido nel parametro <var>$1badfilecontexttitle</var>.",
        "apierror-compare-notext": "Il parametro <var>$1</var> non può essere usato senza <var>$2</var>.",
        "apierror-invalidoldimage": "Il parametro <var>oldimage</var> ha un formato non valido.",
        "apierror-invaliduserid": "L'ID utente <var>$1</var> non è valido.",
index 631e681..a31655d 100644 (file)
        "apierror-assertnameduserfailed": "사용자의 \"$1\" 지정 표명이 실패했습니다.",
        "apierror-assertuserfailed": "사용자의 로그인 실패 표명이 발생했습니다.",
        "apierror-autoblocked": "사용자의 IP 주소는 차단된 사용자에 의해 사용되었으므로 자동으로 차단된 상태입니다.",
+       "apierror-bad-badfilecontexttitle": "<var>$1badfilecontexttitle</var> 변수에 유효하지 않은 제목이 있습니다.",
        "apierror-badgenerator-unknown": "알 수 없는 <kbd>generator=$1</kbd>.",
        "apierror-badip": "IP 변수가 유효하지 않습니다.",
        "apierror-badmd5": "제공된 MD5 해시가 잘못되었습니다.",
index 9c3f11b..db3e013 100644 (file)
        "apierror-assertnameduserfailed": "Afirmação de que o usuário é \"$1\" falhou.",
        "apierror-assertuserfailed": "Afirmação de que o usuário está logado falhou.",
        "apierror-autoblocked": "O seu endereço de IP foi bloqueado automaticamente, porque ele foi usado por um usuário bloqueado.",
+       "apierror-bad-badfilecontexttitle": "Título inválido no parâmetro <var>$1badfilecontexttitle</var>.",
        "apierror-badconfig-resulttoosmall": "O valor de <code>$wgAPIMaxResultSize</code> nesta wiki é muito pequeno para manter a informação básica de resultados.",
        "apierror-badcontinue": "Parâmetro continue inválido. Você deve passar o valor original retornado pela consulta anterior.",
        "apierror-baddiff": "O diff não pode ser recuperado. Uma ou ambas as revisões não existem ou você não tem permissão para visualizá-las.",
        "apiwarn-deprecation-withreplacement": "<kbd>$1</kbd> está obsoleto. Por favor, use <kbd>$2</kbd> em vez.",
        "apiwarn-difftohidden": "Não foi possível diferenciar r$1: o conteúdo está oculto.",
        "apiwarn-errorprinterfailed": "Falha na impressora de erro. Repetirá sem parâmetros.",
-       "apiwarn-errorprinterfailed-ex": "Falha na impressora de erro (repetirá sem parâmetros): $1",
        "apiwarn-ignoring-invalid-templated-value": "Ignorando o valor <kbd>$2</kbd> em <var>$1</var> ao processar parâmetros de predefinição.",
        "apiwarn-invalidcategory": "\"$1\" não é uma categoria.",
        "apiwarn-invalidtitle": "\"$1\" não é um título válido.",
index b5da399..38cdaf2 100644 (file)
        "apiwarn-deprecation-withreplacement": "<kbd>$1</kbd> foi descontinuado. Em substituição, use <kbd>$2</kbd>, por favor.",
        "apiwarn-difftohidden": "Não foi possível criar uma lista das diferenças em relação à r$1: o conteúdo está ocultado.",
        "apiwarn-errorprinterfailed": "A impressora de erros falhou. Será feita nova tentativa sem parâmetros.",
-       "apiwarn-errorprinterfailed-ex": "A impressora de erros falhou (será feita nova tentativa sem parâmetros): $1",
        "apiwarn-ignoring-invalid-templated-value": "A ignorar o valor <kbd>$2</kbd> em <var>$1</var> ao processar parâmetros modelados.",
        "apiwarn-invalidcategory": "\"$1\" não é uma categoria.",
        "apiwarn-invalidtitle": "\"$1\" não é um título válido.",
index eb3fdef..d279330 100644 (file)
        "apihelp-query+info-paramvalue-prop-displaytitle": "{{doc-apihelp-paramvalue|query+info|prop|displaytitle}}",
        "apihelp-query+info-paramvalue-prop-varianttitles": "{{doc-apihelp-paramvalue|query+info|prop|varianttitles}}",
        "apihelp-query+info-param-testactions": "{{doc-apihelp-param|query+info|testactions}}",
+       "apihelp-query+info-param-testactionsdetail": "{{doc-apihelp-param|query+info|testactionsdetail}}",
+       "apihelp-query+info-paramvalue-testactionsdetail-boolean": "{{doc-apihelp-paramvalue|query+info|testactionsdetail|boolean}}",
+       "apihelp-query+info-paramvalue-testactionsdetail-full": "{{doc-apihelp-paramvalue|query+info|testactionsdetail|full}}",
+       "apihelp-query+info-paramvalue-testactionsdetail-quick": "{{doc-apihelp-paramvalue|query+info|testactionsdetail|quick}}",
        "apihelp-query+info-param-token": "{{doc-apihelp-param|query+info|token}}",
        "apihelp-query+info-example-simple": "{{doc-apihelp-example|query+info}}",
        "apihelp-query+info-example-protection": "{{doc-apihelp-example|query+info}}",
        "apierror-assertnameduserfailed": "{{doc-apierror}}\n\nParameters:\n* $1 - User name passed in.",
        "apierror-assertuserfailed": "{{doc-apierror}}",
        "apierror-autoblocked": "{{doc-apierror}}",
+       "apierror-bad-badfilecontexttitle": "{{doc-apierror}}\n\nParameters:\n* $1 - Module parameter prefix, e.g. \"bl\".",
        "apierror-badconfig-resulttoosmall": "{{doc-apierror}}",
        "apierror-badcontinue": "{{doc-apierror}}",
        "apierror-baddiff": "{{doc-apierror}}",
        "apiwarn-deprecation-withreplacement": "{{doc-apierror}}\n\nParameters:\n* $1 - Query string fragment that is deprecated, e.g. \"action=tokens\".\n* $2 - Query string fragment to use instead, e.g. \"action=tokens\".",
        "apiwarn-difftohidden": "{{doc-apierror}}\n\nParameters:\n* $1 - Revision ID number.\n\n\"r\" is short for \"revision\". You may translate it.",
        "apiwarn-errorprinterfailed": "{{doc-apierror}}",
-       "apiwarn-errorprinterfailed-ex": "{{doc-apierror}}\n\nParameters:\n* $1 - Exception message, which may already end in punctuation. Probably in English.",
        "apiwarn-ignoring-invalid-templated-value": "{{doc-apierror}}\n\nParameters:\n* $1 - Target parameter having a bad value.\n* $2 - The bad value being ignored.",
        "apiwarn-invalidcategory": "{{doc-apierror}}\n\nParameters:\n* $1 - Supplied category name.",
        "apiwarn-invalidtitle": "{{doc-apierror}}\n\nParameters:\n* $1 - Supplied title.",
index 4450b6c..6a239f6 100644 (file)
        "apierror-assertnameduserfailed": "Проверка того, что участник — «$1», провалилась.",
        "apierror-assertuserfailed": "Проверка того, что участник авторизован, провалилась.",
        "apierror-autoblocked": "Ваш IP-адрес был автоматически заблокирован, потому что он был использован заблокированным участником.",
+       "apierror-bad-badfilecontexttitle": "Неверное название в параметре <var>$1badfilecontexttitle</var>.",
        "apierror-badconfig-resulttoosmall": "Значение <code>$wgAPIMaxResultSize</code> этой вики слишком мало, чтобы вместить базовую информацию о результате.",
        "apierror-badcontinue": "Некорректный параметр continue. Вы должны передать значение, возвращённое предыдущим запросом.",
        "apierror-baddiff": "Сравнение версий не может быть проведено. Одна или обе версии не существуют или у вас не достаточно прав чтобы просматривать их.",
        "apiwarn-deprecation-withreplacement": "<kbd>$1</kbd> не поддерживается. Пожалуйста, используйте <kbd>$2</kbd>.",
        "apiwarn-difftohidden": "Невозможно сравнить с r$1: содержимое скрыто.",
        "apiwarn-errorprinterfailed": "Сборщик ошибок упал. Будет совершена повторная попытка без параметров.",
-       "apiwarn-errorprinterfailed-ex": "Сборщик ошибок упал (будет совершена повторная попытка без параметров): $1",
        "apiwarn-ignoring-invalid-templated-value": "При обработке шаблонных параметров значение <kbd>$2</kbd> параметра <var>$1</var> проигнорировано.",
        "apiwarn-invalidcategory": "«$1» не является категорией.",
        "apiwarn-invalidtitle": "«$1» не является некорректным заголовком.",
index 4bff7ef..eb5e4fb 100644 (file)
        "apiwarn-deprecation-withreplacement": "<kbd>$1</kbd> є застарілим. Будь ласка, використовуйте натомість <kbd>$2</kbd>.",
        "apiwarn-difftohidden": "Не вдалося відкрити версію r$1: вміст приховано.",
        "apiwarn-errorprinterfailed": "Невдача через помилку принтера. Буде здійснено повторну спробу без параметрів.",
-       "apiwarn-errorprinterfailed-ex": "Невдача через помилку принтера (буде здійснено повторну спробу без параметрів): $1",
        "apiwarn-invalidcategory": "«$1» не є категорією.",
        "apiwarn-invalidtitle": "«$1» не є коректною назвою.",
        "apiwarn-invalidxmlstylesheetext": "Таблиця стилів повинна мати розширення <code>.xsl</code>.",
index 60cf575..cee1b7a 100644 (file)
        "apiwarn-deprecation-withreplacement": "<kbd>$1</kbd>已弃用。请改用<kbd>$2</kbd>。",
        "apiwarn-difftohidden": "不能与r$1做差异比较:内容被隐藏。",
        "apiwarn-errorprinterfailed": "错误打印失败。将在没有参数的前提下重试。",
-       "apiwarn-errorprinterfailed-ex": "错误打印失败(将在没有参数的前提下重试):$1",
        "apiwarn-ignoring-invalid-templated-value": "当处理模板参数时,忽略<var>$1</var>中的值<kbd>$2</kbd>。",
        "apiwarn-invalidcategory": "“$1”不是一个分类。",
        "apiwarn-invalidtitle": "“$1”不是一个有效的标题。",
index 4e0d0a7..869f768 100644 (file)
@@ -52,6 +52,13 @@ class MessageCache {
         */
        protected $cache;
 
+       /**
+        * Map of (lowercase message key => index) for all software defined messages
+        *
+        * @var array
+        */
+       protected $overridable;
+
        /**
         * @var bool[] Map of (language code => boolean)
         */
@@ -258,6 +265,8 @@ class MessageCache {
                        return true;
                }
 
+               $this->overridable = array_flip( Language::getMessageKeysFor( $code ) );
+
                # 8 lines of code just to say (once) that message cache is disabled
                if ( $this->mDisable ) {
                        static $shownDisabled = false;
@@ -1029,14 +1038,18 @@ class MessageCache {
                                $this->cache->getField( $code, 'HASH' )
                        );
                } else {
-                       // Message page does not exist or does not override a software message.
-                       // Load the message page, utilizing the individual message cache.
-                       $entry = $this->loadCachedMessagePageEntry(
-                               $title,
-                               $code,
-                               $this->cache->getField( $code, 'HASH' )
-                       );
-                       if ( substr( $entry, 0, 1 ) !== ' ' ) {
+                       // Message page either does not exist or does not override a software message
+                       if ( !isset( $this->overridable[$this->contLang->lcfirst( $title )] ) ) {
+                               // Message page does not override any software-defined message. A custom
+                               // message might be defined to have content or settings specific to the wiki.
+                               // Load the message page, utilizing the individual message cache as needed.
+                               $entry = $this->loadCachedMessagePageEntry(
+                                       $title,
+                                       $code,
+                                       $this->cache->getField( $code, 'HASH' )
+                               );
+                       }
+                       if ( $entry === null || substr( $entry, 0, 1 ) !== ' ' ) {
                                // Message does not have a MediaWiki page definition; try hook handlers
                                $message = false;
                                Hooks::run( 'MessagesPreLoad', [ $title, &$message, $code ] );
index d0381cf..f20637a 100644 (file)
@@ -525,15 +525,20 @@ class LocalisationCache {
                ini_set( 'apc.cache_by_default', $_apcEnabled );
                Wikimedia\restoreWarnings();
 
+               $data = [];
                if ( $_fileType == 'core' || $_fileType == 'extension' ) {
-
-                       // Lnguage files aren't required to contain all the possible variables, so suppress warnings
-                       // when variables don't exist in tests
-                       Wikimedia\suppressWarnings();
-                       $data = compact( self::$allKeys );
-                       Wikimedia\restoreWarnings();
+                       foreach ( self::$allKeys as $key ) {
+                               // Not all keys are set in language files, so
+                               // check they exist first
+                               if ( isset( $$key ) ) {
+                                       $data[$key] = $$key;
+                               }
+                       }
                } elseif ( $_fileType == 'aliases' ) {
-                       $data = compact( 'aliases' );
+                       if ( isset( $aliases ) ) {
+                               /** @suppress PhanUndeclaredVariable */
+                               $data['aliases'] = $aliases;
+                       }
                } else {
                        throw new MWException( __METHOD__ . ": Invalid file type: $_fileType" );
                }
index 8dc63e5..d9ca8d7 100644 (file)
@@ -836,7 +836,10 @@ class ChangeTags {
                                                break;
                                        };
                                }
-                               $conds['ct_tag_id'] = $filterTagIds;
+
+                               if ( $filterTagIds !== [] ) {
+                                       $conds['ct_tag_id'] = $filterTagIds;
+                               }
                        } else {
                                $conds['ct_tag'] = $filter_tag;
                        }
index 95a171b..4fbddb5 100644 (file)
@@ -100,7 +100,8 @@ class ImportableUploadRevisionImporter implements UploadRevisionImporter {
                        return $this->newNotOkStatus();
                }
 
-               $user = $importableRevision->getUserObj() ?: User::newFromName( $importableRevision->getUser() );
+               $user = $importableRevision->getUserObj()
+                       ?: User::newFromName( $importableRevision->getUser(), false );
 
                # Do the actual upload
                if ( $archiveName ) {
index a9ca286..82cf7f4 100644 (file)
@@ -931,7 +931,9 @@ class MysqlUpdater extends DatabaseUpdater {
                                if ( $count == 0 ) {
                                        $lbFactory = $services->getDBLoadBalancerFactory();
                                        $lbFactory->waitForReplication( [
-                                               'wiki' => wfWikiID(), 'timeout' => self::REPLICATION_WAIT_TIMEOUT ] );
+                                               'domain' => $lbFactory->getLocalDomainID(),
+                                               'timeout' => self::REPLICATION_WAIT_TIMEOUT
+                                       ] );
                                }
                                $this->db->insert( 'templatelinks',
                                        [
index 1e83167..39b5b3b 100644 (file)
@@ -131,7 +131,7 @@ class JobRunner implements LoggerAwareInterface {
                }
                // Bail out if there is too much DB lag.
                // This check should not block as we want to try other wiki queues.
-               list( , $maxLag ) = $lbFactory->getMainLB( wfWikiID() )->getMaxLag();
+               list( , $maxLag ) = $lbFactory->getMainLB()->getMaxLag();
                if ( $maxLag >= self::MAX_ALLOWED_LAG ) {
                        $response['reached'] = 'replica-lag-limit';
                        return $response;
@@ -536,7 +536,7 @@ class JobRunner implements LoggerAwareInterface {
                $syncThreshold = $this->config->get( 'JobSerialCommitThreshold' );
 
                $time = false;
-               $lb = $lbFactory->getMainLB( wfWikiID() );
+               $lb = $lbFactory->getMainLB();
                if ( $syncThreshold !== false && $lb->getServerCount() > 1 ) {
                        // Generally, there is one master connection to the local DB
                        $dbwSerial = $lb->getAnyOpenConnection( $lb->getWriterIndex() );
index 3488eb6..3f922a3 100644 (file)
@@ -91,7 +91,7 @@ class RefreshLinksJob extends Job {
                        if ( !isset( $this->params['range'] ) ) {
                                $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
                                if ( !$lbFactory->waitForReplication( [
-                                               'wiki'    => wfWikiID(),
+                                               'domain'  => $lbFactory->getLocalDomainID(),
                                                'timeout' => self::LAG_WAIT_TIMEOUT
                                ] ) ) { // only try so hard
                                        $stats = MediaWikiServices::getInstance()->getStatsdDataFactory();
index 6c9c54e..7559b06 100644 (file)
@@ -84,6 +84,10 @@ class DatabaseDomain {
                        $database = null;
                }
 
+               if ( $schema === '' ) {
+                       $schema = null;
+               }
+
                return new self( $database, $schema, $prefix );
        }
 
@@ -96,10 +100,10 @@ class DatabaseDomain {
 
        /**
         * @param DatabaseDomain|string $other
-        * @return bool
+        * @return bool Whether the domain instances are the same by value
         */
        public function equals( $other ) {
-               if ( $other instanceof DatabaseDomain ) {
+               if ( $other instanceof self ) {
                        return (
                                $this->database === $other->database &&
                                $this->schema === $other->schema &&
@@ -110,6 +114,44 @@ class DatabaseDomain {
                return ( $this->getId() === $other );
        }
 
+       /**
+        * Check whether the domain $other meets the specifications of this domain
+        *
+        * If this instance has a null database specifier, then $other can have any database
+        * specified, including the null, and likewise if the schema specifier is null. This
+        * is not transitive like equals() since a domain that explicitly wants a certain
+        * database or schema cannot be satisfied by one of another (nor null). If the prefix
+        * is empty and the DB and schema are both null, then the entire domain is considered
+        * unspecified, and any prefix of $other is considered compatible.
+        *
+        * @param DatabaseDomain|string $other
+        * @return bool
+        * @since 1.32
+        */
+       public function isCompatible( $other ) {
+               if ( $this->isUnspecified() ) {
+                       return true; // even the prefix doesn't matter
+               }
+
+               $other = ( $other instanceof self ) ? $other : self::newFromId( $other );
+
+               return (
+                       ( $this->database === $other->database || $this->database === null ) &&
+                       ( $this->schema === $other->schema || $this->schema === null ) &&
+                       $this->prefix === $other->prefix
+               );
+       }
+
+       /**
+        * @return bool
+        * @since 1.32
+        */
+       public function isUnspecified() {
+               return (
+                       $this->database === null && $this->schema === null && $this->prefix === ''
+               );
+       }
+
        /**
         * @return string|null Database name
         */
@@ -150,7 +192,12 @@ class DatabaseDomain {
                if ( $this->schema !== null ) {
                        $parts[] = $this->schema;
                }
-               if ( $this->prefix != '' ) {
+               if ( $this->prefix != '' || $this->schema !== null ) {
+                       // If there is a schema, then we need the prefix to disambiguate.
+                       // For engines like Postgres that use schemas, this awkwardness is hopefully
+                       // avoided since it is easy to have one DB per server (to avoid having many users)
+                       // and use schema/prefix to have wiki farms. For example, a domain schemes could be
+                       // wiki-<project>-<language>, e.g. "wiki-fitness-es"/"wiki-sports-fr"/"wiki-news-en".
                        $parts[] = $this->prefix;
                }
 
index d84ba65..1f3fe4c 100644 (file)
@@ -28,6 +28,7 @@ use BagOStuff;
 use EmptyBagOStuff;
 use WANObjectCache;
 use ArrayUtils;
+use UnexpectedValueException;
 use InvalidArgumentException;
 use RuntimeException;
 use Exception;
@@ -952,6 +953,16 @@ class LoadBalancer implements ILoadBalancer {
                        }
                }
 
+               // Final sanity check to make sure the right domain is selected
+               if (
+                       $conn instanceof IDatabase &&
+                       !$this->localDomain->isCompatible( $conn->getDomainID() )
+               ) {
+                       throw new UnexpectedValueException(
+                               "Got connection to '{$conn->getDomainID()}', " .
+                               "but expected local domain ('{$this->localDomain}')." );
+               }
+
                return $conn;
        }
 
@@ -1038,8 +1049,13 @@ class LoadBalancer implements ILoadBalancer {
                        }
                }
 
-               // Increment reference count
                if ( $conn instanceof IDatabase ) {
+                       // Final sanity check to make sure the right domain is selected
+                       if ( !$domainInstance->isCompatible( $conn->getDomainID() ) ) {
+                               throw new UnexpectedValueException(
+                                       "Got connection to '{$conn->getDomainID()}', but expected '$domain'." );
+                       }
+                       // Increment reference count
                        $refCount = $conn->getLBInfo( 'foreignPoolRefCount' );
                        $conn->setLBInfo( 'foreignPoolRefCount', $refCount + 1 );
                }
index 6d238ca..847214a 100644 (file)
@@ -325,7 +325,6 @@ class ParserOutput extends CacheTime {
                        $text = preg_replace_callback(
                                self::EDITSECTION_REGEX,
                                function ( $m ) {
-                                       global $wgOut, $wgLang;
                                        $editsectionPage = Title::newFromText( htmlspecialchars_decode( $m[1] ) );
                                        $editsectionSection = htmlspecialchars_decode( $m[2] );
                                        $editsectionContent = isset( $m[4] ) ? Sanitizer::decodeCharReferences( $m[3] ) : null;
@@ -334,11 +333,12 @@ class ParserOutput extends CacheTime {
                                                throw new MWException( "Bad parser output text." );
                                        }
 
-                                       $skin = $wgOut->getSkin();
-                                       return $skin->doEditSectionLink( $editsectionPage,
+                                       $context = RequestContext::getMain();
+                                       return $context->getSkin()->doEditSectionLink(
+                                               $editsectionPage,
                                                $editsectionSection,
                                                $editsectionContent,
-                                               $wgLang
+                                               $context->getLanguage()
                                        );
                                },
                                $text
index 5448013..c725d10 100644 (file)
@@ -82,11 +82,6 @@ class SpecialApiHelp extends UnlistedSpecialPage {
                                $this->msg( 'apihelp-no-such-module', $moduleName )->inContentLanguage()->parse()
                        ) );
                        return;
-               } catch ( UsageException $ex ) {
-                       $this->getOutput()->addHTML( Html::rawElement( 'span', [ 'class' => 'error' ],
-                               $this->msg( 'apihelp-no-such-module', $moduleName )->inContentLanguage()->parse()
-                       ) );
-                       return;
                }
 
                ApiHelp::getHelp( $this->getContext(), $module, $options );
index fe9a5c9..5e5ca1b 100644 (file)
@@ -492,7 +492,7 @@ class User implements IDBAccessObject, UserIdentity {
         * @param int $userId
         */
        public static function purge( $wikiId, $userId ) {
-               $cache = ObjectCache::getMainWANInstance();
+               $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
                $key = $cache->makeGlobalKey( 'user', 'id', $wikiId, $userId );
                $cache->delete( $key );
        }
@@ -503,7 +503,10 @@ class User implements IDBAccessObject, UserIdentity {
         * @return string
         */
        protected function getCacheKey( WANObjectCache $cache ) {
-               return $cache->makeGlobalKey( 'user', 'id', wfWikiID(), $this->mId );
+               $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
+               $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
+
+               return $cache->makeGlobalKey( 'user', 'id', $lbFactory->getLocalDomainID(), $this->mId );
        }
 
        /**
index f9985eb..289cc85 100644 (file)
@@ -20,11 +20,6 @@ class ExpiryInputWidget extends Widget {
         */
        protected $relativeInput;
 
-       /**
-        * @var bool
-        */
-       protected $noDatePicker;
-
        /**
         * @var bool
         */
@@ -37,11 +32,8 @@ class ExpiryInputWidget extends Widget {
        public function __construct( Widget $relativeInput, array $options = [] ) {
                $config = \RequestContext::getMain()->getConfig();
 
-               $options['noDatePicker'] = $config->get( 'ExpiryWidgetNoDatePicker' );
-
                parent::__construct( $options );
 
-               $this->noDatePicker = $options['noDatePicker'];
                $this->required = $options['required'] ?? false;
 
                // Properties
@@ -49,14 +41,11 @@ class ExpiryInputWidget extends Widget {
                $this->relativeInput->addClasses( [ 'mw-widget-ExpiryWidget-relative' ] );
 
                // Initialization
-               $classes = [
-                       'mw-widget-ExpiryWidget',
-               ];
-               if ( $options['noDatePicker'] === false ) {
-                       $classes[] = 'mw-widget-ExpiryWidget-hasDatePicker';
-               }
                $this
-                       ->addClasses( $classes )
+                       ->addClasses( [
+                               'mw-widget-ExpiryWidget',
+                               'mw-widget-ExpiryWidget-hasDatePicker'
+                       ] )
                        ->appendContent( $this->relativeInput );
        }
 
@@ -68,7 +57,6 @@ class ExpiryInputWidget extends Widget {
         * {@inheritdoc}
         */
        public function getConfig( &$config ) {
-               $config['noDatePicker'] = $this->noDatePicker;
                $config['required'] = $this->required;
                $config['relativeInput'] = [];
                $this->relativeInput->getConfig( $config['relativeInput'] );
index 39ac9dc..12ffcf9 100644 (file)
        "right-editusercss": "Редактиране на CSS файловете на други потребители",
        "right-edituserjson": "Редактиране на JSON файловете на други потребители",
        "right-edituserjs": "Редактиране на JavaScript файловете на други потребители",
+       "right-editsitecss": "Редактиране на CSS за цялото уики",
+       "right-editsitejson": "Редактиране на JSON за цялото уики",
+       "right-editsitejs": "Редактиране на JavaScript за цялото уики",
        "right-editmyusercss": "Редактиране на собствените потребителски CSS файлове",
        "right-editmyuserjson": "Редактиране на собствените потребителски JSON файлове",
        "right-editmyuserjs": "Редактиране на собствените потребителски JavaScript файлове",
index 75940d2..5d4e5b7 100644 (file)
@@ -24,7 +24,7 @@
                        "Fitoschido"
                ]
        },
-       "tog-underline": "ھێڵ ھێنان بەژێر بەستەرەکان:",
+       "tog-underline": "ھێڵھێنان بەژێر بەستەرەکان:",
        "tog-hideminor": "دەستکارییە بچووکەکان لە دوایین گۆڕانکارییەکاندا بشارەوە",
        "tog-hidepatrolled": "لە دوایین گۆڕانکارییەکاندا دەستکارییە پاس دراوەکان بشارەوە",
        "tog-newpageshidepatrolled": "لە پێرستی پەڕە نوێکاندا پەڕە پاس دراوەکان بشارەوە",
        "yourpasswordagain": "دیسان تێپەڕوشەکە بنووسەوە:",
        "createacct-yourpasswordagain": "تێپەروشە پشتڕاست بکەرەوە",
        "createacct-yourpasswordagain-ph": "تێپەروشە دیسان بنووسەوە",
-       "userlogin-remembermypassword": "چوونەژوورەوەکەم ڕابگرە",
+       "userlogin-remembermypassword": "لەژوورەوە بمھێڵەرەوە",
        "userlogin-signwithsecure": "پەیوەندیی دڵنیا بەکاربھێنە",
        "cannotlogin-title": "ناتوانیت بچیتە ژوورەوە",
        "cannotlogin-text": "توانای چوونەژوورەوەت نییە",
        "wrongpasswordempty": "تێپەڕەوشەی لێدراو بەتاڵبوو.\nتکایە هەوڵ بدەوە.",
        "passwordtooshort": "تێپەڕوشەکەت لانی کەم دەبێ {{PLURAL:$1|١ پیت|$1 پیت}} بێت.",
        "passwordtoolong": "تێپەڕ وشەکان ناتوانرێت لە {{PLURAL:$1|١ کارەکتەر|$1 کارەکتەر}} درێژتر بێت.",
-       "passwordtoopopular": "تێپەڕ وشە باوەکان ناتواندرێت دابنرێن. تکایە تێپەڕ وشەیەکی دەگمەنتر ھەڵبژێرە.",
+       "passwordtoopopular": "تێپەڕەوشە باوەکان ناکرێت دابنرێن. تکایە تێپەڕەوشەیەکی دەگمەنتر ھەڵبژێرە.",
        "password-name-match": "تێپەڕوشەکەت ئەبێ جیاواز بێت لە ناوی بەکارهێنەریت.",
        "password-login-forbidden": "بەکارهێنانی ئەم ناوی بەکارهێنەر و تێپەڕەووشەیە قەدەغەکراوە.",
        "mailmypassword": "تێپەڕوشەکە ڕێک بخەوە",
        "resetpass-temp-emailed": "تۆ بە تێپەڕوشەیەکی کاتیی ھاتوویتە ژوورەوە. بۆ تەواوکردنی چوونە ژوورەوە تێپەڕوشەیەکی نوێ لێرە دابنێ.",
        "resetpass-temp-password": "تێپەڕوشەی کاتی:",
        "resetpass-expired": "تێپەڕ وشەکەت بەسەر چووە، تکایە تێپەڕ وشەیەکی نوێ دابنێ بۆ چوونە ژوورەوە",
-       "resetpass-validity-soft": "تێپەڕ وشەکەت دروست نییە: $1",
+       "resetpass-validity-soft": "تێپەڕ وشەکەت دروست نییە: $1\n\nتکایە تێپەڕەوشەیەکی نوێ ھەڵبژێرە ئێستا، یان کرتە بکە لە \"{{int:authprovider-resetpass-skip-label}}\" بۆ گۆڕینی لە دواییدا.",
        "passwordreset": "ڕێکخستنەوەی تێپەڕوشە",
        "passwordreset-text-one": "ئەم فۆرمە تەواو بکە بۆ بەدەستھێنانی تێپەڕ وشەیەکی کاتیی بە ئیمەیڵ",
        "passwordreset-text-many": "{{PLURAL:$1|یەکێک لەم بۆشاییانە بڕ بکەرەوە بۆ بەدەستھێنانی تێپەڕ وشەیەکی کاتیی بە ئیمەیڵ}}",
        "anonpreviewwarning": "«نەڕۆشتوویتە ژوورەوە. پاشەکەوتکردن، ئەدرەسی IPەکەت لە مێژووی دەستکاریی ئەم پەڕە تۆمار دەکات.»",
        "missingsummary": "'''وە بیر خستنەوە:''' پوختەیەکت نەنووسیوە بۆ چۆنیەتی گۆڕانکارییەکەت.\nئەگەر جارێکی تر پاشکەوت کردن لێبدەی، بێ پوختە تۆمار دەکرێ.",
        "selfredirect": "<strong>ئاگاداری:</strong> تۆ خەریکی گواستنەوەی ئەم پەڕەیەیت بۆ سەر خۆی. لەوانەیە خەریکی گواستنەوەی پەڕەیەکی ھەڵە بیت یان ھەوڵی گواستنەوە دەدەیت بۆ ئامانجێکی ھەڵە. \nئەگەر دووبارە کرتە لەسەر «$1» بکەیتەوە، ڕەوانەکەرەکە دروست دەکرێت بەھەرحاڵ.",
-       "missingcommenttext": "تکاÛ\8cÛ\95 Ù\84Û\95 Ø®Ù\88ارÛ\95Ù\88Û\95 Ø´Ø±Û\86Ú¤Û\95Û\8cÛ\95ک بنووسە.",
+       "missingcommenttext": "تکاÛ\8cÛ\95 Ù\84Û\8eدÙ\88اÙ\86Û\8eک بنووسە.",
        "missingcommentheader": "'''بیرهێنانەوە:''' بۆ ئەم بۆچوونەت سەردێڕ\\بابەت ڕاچاو نەکردووە.\nئەگەر دیسان «$1» لێبدەی، دەستکاریەکەت بێ سەردێڕ یان بابەت پاشەکەوت دەبێ.",
        "summary-preview": "پێشبینینی کورتەی دەستکاری:",
        "subject-preview": "پێشبینینی بابەت:",
        "revdelete-legend": "ڕێکخستنی سنووردارکردنی دیاریکردن",
        "revdelete-hide-text": "دەقی پێداچوونەوە",
        "revdelete-hide-image": "ناوەڕۆکی پەڕگە بشارەوە",
-       "revdelete-hide-name": "داشاردÙ\86Û\8c Ù\85Û\95بÛ\95ست Ù\88 Ú©Ø±Ø¯Û\95Ù\88Û\95",
+       "revdelete-hide-name": "شاردÙ\86Û\95Ù\88Û\95Û\8c Ø¦Ø§Ù\85اÙ\86ج Ù\88 Ú\95اگÛ\95Û\8cÛ\8eÙ\86Û\95کاÙ\86",
        "revdelete-hide-comment": "کورتەی دەستکاری",
        "revdelete-hide-user": "ناوی بەکارھێنەر/ناونیشانی ئایپی دەستکاریکەر",
        "revdelete-hide-restricted": "بەرگری دراوە لە بەڕێوبەران هەر وەک ئەوانی دیکە",
        "recentchangesdays": "ژمارە ڕۆژە نیشاندراوەکان لە دوایین گۆڕانکارییەکان:",
        "recentchangesdays-max": "(ئەوپەڕی $1 {{PLURAL:$1|ڕۆژە|ڕۆژە}})",
        "recentchangescount": "ژمارەی گۆڕانکارییەکان کە نیشان ئەدرێن لە حاڵەتی دیفاڵت:",
-       "prefs-help-recentchangescount": "ئÛ\95Ù\85Û\95 Ø¯Ù\88اÛ\8cÛ\8cÙ\86 Ú¯Û\86Ú\95اÙ\86کارÛ\8cÛ\8cÛ\95کاÙ\86Ø\8c Ù\85Û\8eÚ\98Ù\88Ù\88Û\8c Ù¾Û\95Ú\95Û\95کاÙ\86 Ù\88 Ù\84Û\86Ú¯Û\95کاÙ\86Û\8cØ´ Ù\84Û\95بÛ\95ردÛ\95گرÛ\8eت.",
+       "prefs-help-recentchangescount": "زÛ\86رترÛ\8cÙ\86 Ú\98Ù\85ارÛ\95: Ù¡Ù Ù Ù ",
        "prefs-help-watchlist-token2": "ئەمە کلیلێکی تایبەتیی پێڕستی چاودێرییەکەتە. ھەرکەسێک بیزانێت دەتوانێت پێڕستی چاودێرییەکەت ببینێت، بۆیە لای خۆت بیپارێزە.\nئەگەر پێویستی کرد، [[Special:ResetTokens|دەتوانیت بیگۆڕیت]].",
        "savedprefs": "ھەڵبژاردەکانت پاشەکەوت کران",
        "timezonelegend": "ناوچەی کاتی:",
        "prefs-files": "پەڕگەکان",
        "prefs-custom-css": "CSSی دڵخواز",
        "prefs-custom-js": "جاڤاسکریپتی دڵخواز",
-       "prefs-common-config": "سی‌ئێس‌ئێس/جاڤاسکریپتی ھاوبەش بۆ گشت پێستەکان:",
+       "prefs-common-config": "سی‌ئێس‌ئێس/جەیسن/جاڤاسکریپتی ھاوبەش بۆ گشت پێستەکان:",
        "prefs-reset-intro": "دەتوانی لەم لاپەڕە بۆ گەڕانەوەی هەڵبژاردەکانت بۆ بنچینەیی ماڵپەر کەڵک وەرگریت.\nگەر ئەوە بکەی ئیتر گۆڕانەکەت ناگەڕێتەوە.",
        "prefs-emailconfirm-label": "پشتڕاستکردنەوەی ئیمەیل:",
        "youremail": "ئیمەیل:",
        "badsig": "کۆدی ئیمزای ھەڵە.\nبە تاگە HTMLکاندا بچۆرەوە.",
        "badsiglength": "واژووەکەت زۆر درێژە.\nواژوو نابێ لە $1 {{PLURAL:$1|نووسە}} درێژتر بێت.",
        "yourgender": "پێت خۆشە چۆن وەسف بکرێیت؟",
-       "gender-unknown": "Ù¾Û\8eÙ\85 Ø®Û\86Ø´Û\95 Ø¨Ø§Ø³Û\8c Ù\86Û\95Ú©Û\95Ù\85",
+       "gender-unknown": "Ù\84Û\95کاتÛ\8c Ø¦Ø§Ù\85اÚ\98Û\95داÙ\86 Ø¨Û\95تÛ\86Ø\8c Ù\86Û\95رÙ\85اÙ\85Û\8eرÛ\95Ú©Û\95 Ù\88Ø´Û\95Û\8c Ú\95Û\95Ú¯Û\95زھاÙ\88تا Ø¨Û\95کار Ø¯Û\95بات Ø¨Û\95Ù¾Û\8eÛ\8c ØªÙ\88اÙ\86ا",
        "gender-male": "نێر",
        "gender-female": "مێ",
        "prefs-help-gender": "ئەم ھەڵبژاردەیە دڵخوازانەیە.\nبۆ بانگکردن و ئاماژەپێکردن بە شێوەیەکی دروست لەلایەن نەرمامێرەوە بەکاردێت.\nئەم زانیارییە گشتی دەبێت.",
        "group-sysop": "بەڕێوەبەران",
        "group-interface-admin": "بەڕێوەبەرانی ڕووکار",
        "group-bureaucrat": "بیوروکراتەکان",
-       "group-suppress": "چاودێرەکان",
+       "group-suppress": "سەرکووتکەرەکان",
        "group-all": "(ھەموو)",
        "group-user-member": "{{GENDER:$1|بەکارھێنەر}}",
        "group-autoconfirmed-member": "{{GENDER:$1|بەکارھێنەرانی پەسەندکراوی خۆگەڕ}}",
index a6a7ea6..c2439de 100644 (file)
        "badarticleerror": "Seda toimingut ei saa sellel leheküljel sooritada.",
        "cannotdelete": "Lehekülge või faili \"$1\" ei saa kustutada.\nVõimalik, et keegi on selle juba kustutanud.",
        "cannotdelete-title": "Lehekülge \"$1\" ei saa kustutada",
+       "delete-scheduled": "Lehekülje \"$1\" kustutamine ootab järge.\nPalun ole kannatlik.",
        "delete-hook-aborted": "Haak katkestas kustutamise.\nSeletust pole toodud.",
        "no-null-revision": "Lehekülje \"$1\" nullredaktsiooni ei õnnestunud teha.",
        "badtitle": "Vigane pealkiri",
        "movepage-moved": "<strong>\"$1\" on teisaldatud pealkirja \"$2\" alla.</strong>",
        "movepage-moved-redirect": "Ümbersuunamisleht loodud.",
        "movepage-moved-noredirect": "Ümbersuunamist ei loodud.",
+       "movepage-delete-first": "Sihtlehekülge ei saa teisaldamise käigus kustutada, kuna sellel on liiga palju redaktsioone. Palun kustuta lehekülg kõigepealt käsitsi ja proovi seejärel uuesti.",
        "articleexists": "Sellise pealkirjaga lehekülg on juba olemas või pole valitud pealkiri lubatav.\nPalun vali teistsugune pealkiri.",
        "cantmove-titleprotected": "Lehte ei saa sinna teisaldada, sest uus pealkiri on artikli loomise eest kaitstud",
        "movetalk": "Teisalda seonduv arutelulehekülg",
        "pageinfo-category-files": "Faile",
        "pageinfo-user-id": "Kasutaja identifikaator",
        "pageinfo-file-hash": "Räsiväärtus",
+       "pageinfo-view-protect-log": "Vaata selle lehekülje kaitsmislogi.",
        "markaspatrolleddiff": "Märgi kontrollituks",
        "markaspatrolledtext": "Märgi see leht kontrollituks",
        "markaspatrolledtext-file": "Märgi see failiversioon kontrollituks",
index 97d8350..5184f3c 100644 (file)
        "rcfilters-view-tags": "Redakturi etiketizata",
        "rcfilters-view-namespaces-tooltip": "Filtrar rezulti segun nomo",
        "rcfilters-liveupdates-button": "Quika aktualigi",
+       "rcfilters-watchlist-markseen-button": "Indikar \"vidita\" en omna modifikuri",
        "rcnotefrom": "Infre {{PLURAL:$5|esas la chanjo|esas la chanji}} de <strong>$3, $4</strong> (montrata til <strong>$1</strong>).",
        "rclistfrom": "Montrar nova chanji startante de $3 $2",
        "rcshowhideminor": "$1 mikra redakturi",
        "sp-contributions-hideminor": "Celar kurta redaktaji",
        "sp-contributions-submit": "Serchez",
        "whatlinkshere": "Quo ligesas adhike",
-       "whatlinkshere-title": "Pagini qui ligas ad \"$1\"",
+       "whatlinkshere-title": "Pagini qui ligesas a(d) \"$1\"",
        "whatlinkshere-page": "Pagino:",
        "linkshere": "Ca pagini esas ligilizita a(d) <strong>$2</strong>:",
        "nolinkshere": "Nula pagino ligas ad <strong>$2</strong>.",
        "tooltip-n-recentchanges": "Listo di recenta chanji en la wiki.",
        "tooltip-n-randompage": "Vizitez hazarda pagino",
        "tooltip-n-help": "La loko por trovar ulo.",
-       "tooltip-t-whatlinkshere": "Montrez omna wiki pagini qui ligas ad hike",
+       "tooltip-t-whatlinkshere": "Listo di omna Wikipagini qui ligesas adhike",
        "tooltip-t-recentchangeslinked": "Recenta chanji di pagini ligita de ca pagino",
        "tooltip-feed-rss": "RSS provizero por ica pagino",
        "tooltip-feed-atom": "Atom provizero por ica pagino",
index 58ec8d2..04c8c24 100644 (file)
        "filehist-comment": "Komentārs",
        "imagelinks": "Faila lietojums",
        "linkstoimage": "Šo failu izmanto {{PLURAL:$1|šajās $1 lapās|šajā $1 lapā|šajās $1 lapās}}:",
-       "linkstoimage-more": "Uz šo failu ir saites vairāk nekā $1 {{PLURAL:$1|lapās|lapā|lapās}}.\nŠajā sarakstā ir tikai {{PLURAL:$1|pirmās $1 saistītās lapas|pirmā $1 saistītā lapa|pirmās $1 saistītās lapas}} uz šo failu.\nPieejams arī [[Special:WhatLinksHere/$2|pilns saraksts]].",
+       "linkstoimage-more": "Šo failu izmanto vairāk nekā $1 {{PLURAL:$1|lapās|lapā|lapās}}.\nŠajā sarakstā ir tikai {{PLURAL:$1|pirmās $1 lapas|pirmā lapa|pirmās $1 lapas}}, kas izmanto šo failu.\nPieejams arī [[Special:WhatLinksHere/$2|pilns saraksts]].",
        "nolinkstoimage": "Šo failu neizmanto nevienā lapā.",
        "morelinkstoimage": "Skatīt [[Special:WhatLinksHere/$1|vairāk saites]] uz šo failu.",
        "linkstoimage-redirect": "$1 (faila pāradresācija) $2",
index 9ff949e..97fc212 100644 (file)
        "right-nominornewtalk": "Невыписованя новых повідомлїнь по малых управах діскузной сторінкы",
        "right-apihighlimits": "Хоснованя высшых лімітів в  API запытах",
        "right-writeapi": "Хосновати API про писаня",
-       "right-delete": "Ð\97мазаня сторінок",
+       "right-delete": "Ð\9cазаня сторінок",
        "right-bigdelete": "Мазаня сторінок з довгов історіёв",
        "right-deletelogentry": "Мазаня тай обновлїня окремых записів лоґів\n,",
        "right-deleterevision": "Мазаня і обновованя конкретных ревізій сторінок",
index 8ed22c4..a5bc856 100644 (file)
        "group-autoconfirmed": "خود توثیق شدہ صارفین",
        "group-bot": "روبہ جات",
        "group-sysop": "منتظمین",
+       "group-interface-admin": "انٹرفیس منتظمین",
        "group-bureaucrat": "مامورین اداری",
        "group-suppress": "Suppressors",
        "group-all": "(تمام)",
        "group-autoconfirmed-member": "خودتوثیق شدہ صارف",
        "group-bot-member": "خودکار صارف",
        "group-sysop-member": "{{GENDER:$1|منتظم}}",
+       "group-interface-admin-member": "{{GENDER:$1|انٹرفیس منتظم}}",
        "group-bureaucrat-member": "{{GENDER:$1|مامور اداری}}",
        "group-suppress-member": "{{GENDER:$1|suppressor}}",
        "grouppage-user": "{{ns:project}}:صارفین",
        "grouppage-autoconfirmed": "{{ns:project}}:خود توثیق شدہ صارف",
        "grouppage-bot": "{{ns:project}}:روبہ جات",
        "grouppage-sysop": "{{ns:project}}:منتظمین",
+       "grouppage-interface-admin": "{{ns:project}}:انٹرفیس منتظمین",
        "grouppage-bureaucrat": "{{ns:project}}:مامورین اداری",
        "grouppage-suppress": "{{ns:project}}:پوشیدگی",
        "right-read": "مطالعہ صفحات",
index 5227669..4d21f32 100644 (file)
        "badarticleerror": "מען קען נישט טאן די אקציע וואס איר ווילט אויף דעם בלאט.",
        "cannotdelete": "נישט געווען מעגלעך אויסמעקן דעם בלאט אדער די טעקע \"$1\".\nקען זיין  אז דאס איז שוין געווארן אויסגעמעקט דורך אן אנדערן.",
        "cannotdelete-title": "מען קען נישט אויסמעקן בלאט \"$1\"",
+       "delete-scheduled": "דעם בלאט \"$1\" פלאנירט מען אויסצומעקן.\nהאטס געדולד.",
        "delete-hook-aborted": "אויסמעקונג אנולירט דורך hook.\nנישט געגעבן קיין דערקלערונג.",
        "no-null-revision": "נישט מעגלעך צו שאפן א נול־ווערסיע פונעם בלאט \"$1\".",
        "badtitle": "שלעכט קעפל",
        "botpasswords-existing": "עקזיסטירנדע באט פאסווערטער",
        "botpasswords-createnew": "שאפֿן א ניי באט פאסווארט",
        "botpasswords-editexisting": "רעדאקטירן אן עקזיסטירנדיק באט פאסווארט",
+       "botpasswords-label-needsreset": "(פאסווארט דארף ווערן צוריקגעשטעלט)",
        "botpasswords-label-appid": "באט נאמען:",
        "botpasswords-label-create": "שאַפֿן",
        "botpasswords-label-update": "דערהײַנטיקן",
        "prefs-watchlist-edits": "מאַקסימום צאָל ענדערונגען צו ווייַזן אין אויפֿפאַסונג ליסטע:",
        "prefs-watchlist-edits-max": "מאַקסימום נומער: 1000",
        "prefs-watchlist-token": "אויפֿפאַסונג ליסטע סימן:",
+       "prefs-watchlist-managetokens": "פֿארוואלטן סימנים",
        "prefs-misc": "פֿאַרשידנס",
        "prefs-resetpass": "טוישן פאַסווארט",
        "prefs-changeemail": "ענדערן אדער אראפנעמען ע-פּאָסט אַדרעס",
        "group-autoconfirmed": "באַשטעטיקטע באַניצער",
        "group-bot": "באטס",
        "group-sysop": "סיסאפן",
+       "group-interface-admin": "אייבערפֿלאך־אדמיניסטראטארן",
        "group-bureaucrat": "ביוראקראטן",
        "group-suppress": "אונטערדריקער",
        "group-all": "(אלע)",
        "group-autoconfirmed-member": "{{GENDER:$1|באַשטעטיקטער באַניצער|באַשטעטיקטע באַניצערין}}",
        "group-bot-member": "{{GENDER:$1|באט}}",
        "group-sysop-member": "{{GENDER:$1|סיסאפ}}",
+       "group-interface-admin-member": "{{GENDER:$1|אייבערפֿלאך־אדמיניסטראטאר}}",
        "group-bureaucrat-member": "{{GENDER:$1|ביוראקראַט}}",
        "group-suppress-member": "{{GENDER:$1|אונטעדריקער}}",
        "grouppage-user": "{{ns:project}}:אײַנגעשריבענער באניצער",
        "action-editcontentmodel": "רעדאקטירן אינהאלט־מאדעל פון א בלאט",
        "action-managechangetags": "שאפן און (אומ)אקטיווירן טאגן פון דער דאטנבאזע",
        "action-applychangetags": "אנווענדן טאגן צוזאמען מיט אייערע ענדערונגען",
+       "action-deletechangetags": "אויסמעקן טאגן פון דער דאטנבאזע",
        "action-purge": "אויסרייניגן דעם דאזיגן בלאט",
        "nchanges": "{{PLURAL:$1|ענדערונג|$1 ענדערונגען}}",
        "enhancedrc-since-last-visit": "$1 {{PLURAL:$1|זײַט לעצטן וויזיט}}",
        "rcfilters-savedqueries-new-name-label": "נאָמען",
        "rcfilters-savedqueries-apply-label": "שאפן פילטער",
        "rcfilters-savedqueries-cancel-label": "אַנולירן",
+       "rcfilters-clear-all-filters": "אפראמען אלע פֿילטערן",
        "rcfilters-show-new-changes": "ווייזן די נייעסטע ענדערונגען",
        "rcfilters-search-placeholder": "פֿילטערן ענדערונגען (ניצט מעניו אדער זוכט פֿילטער־נאמען)",
        "rcfilters-invalid-filter": "אומגילטיגער פֿילטער",
        "rcfilters-filter-minor-description": "רעדאקטירונגען וואס דער שרייבער האט מארקירט פֿארמינערט.",
        "rcfilters-filter-watchlist-watched-label": "אויף דער אויפֿפאַסונג ליסטע",
        "rcfilters-filter-watchlist-notwatched-label": "נישט אויף דער אויפֿפאַסונג ליסטע",
+       "rcfilters-filter-watchlistactivity-seen-label": "געזעענע ענדערונגען",
        "rcfilters-filtergroup-changetype": "טיפ ענדערונג",
        "rcfilters-filter-pageedits-label": "בלאט רעדאקטירונגען",
        "rcfilters-filter-newpages-label": "בלאַט־שאַפֿונגען",
        "filehist-filesize": "טעקע גרייס",
        "filehist-comment": "באמערקונג",
        "imagelinks": "טעקע באַניץ",
-       "linkstoimage": "{{PLURAL:$1|×\93ער ×¤×\90×\9c×\92× ×\93ער ×\91×\9c×\90×\98 × ×\99צ×\98\93×\99 ×¤×\90×\9c×\92× ×\93×¢ ×\91×\9c×¢×\98ער × ×\99צ×\9f}} ×\93×\90ס ×\93×\90×\96×\99×\92×¢ ×\91×\99×\9c×\93:",
+       "linkstoimage": "{{PLURAL:$1|×\93ער ×¤×\90×\9c×\92× ×\93ער ×\91×\9c×\90×\98 × ×\99צ×\98\93×\99 ×¤×\90×\9c×\92× ×\93×¢ ×\91×\9c×¢×\98ער × ×\99צ×\9f}} ×\93×\99 ×\93×\90×\96×\99×\92×¢ ×\98עקע:",
        "linkstoimage-more": "מער ווי $1 {{PLURAL:$1|בלאַט ניצט|בלעטער ניצן}} די דאזיגע טעקע.\nדי פֿאלגנדע ליסטע ווײַזט נאר {{PLURAL:$1|דעם ערשטן בלאַט וואס ניצט|די ערשטע $1 בלעטער וואס ניצן}} די טעקע.\nס'איז פֿאַראַן א [[Special:WhatLinksHere/$2|פֿולע רשימה]].",
-       "nolinkstoimage": "× ×\99ש×\98×\90 ×§×\99×\99×\9f ×\91×\9c×¢×\98ער ×\95×\95×\90ס ×¤×\90ר×\91×\99× ×\93×\9f ×¦×\95 די טעקע.",
+       "nolinkstoimage": "× ×\99ש×\98×\90 ×§×\99×\99×\9f ×\91×\9c×¢×\98ער ×\95×\95×\90ס × ×\99צ×\9f די טעקע.",
        "morelinkstoimage": "באַקוקן  [[Special:WhatLinksHere/$1|מער לינקען]] צו דער טעקע.",
        "linkstoimage-redirect": "$1 (טעקע ווײַטערפֿירונג) $2",
        "duplicatesoffile": "די פֿאלגנדע {{PLURAL:$1|טעקע דופליקירט|$1 טעקעס דופליקירן}} די דאזיגע טעקע ([[Special:FileDuplicateSearch/$2|נאך פרטים]]):",
        "cachedspecial-refresh-now": "באקוקן די לעצטע.",
        "categories": "קאַטעגאָריעס",
        "categories-submit": "ווייזן",
-       "categoriespagetext": "×\93×\99 ×¤Ö¿×\90×\9c×\92×¢× ×\93×¢ {{PLURAL:$1| ×§×\90Ö·×\98×¢×\92×\90ָר×\99×¢ ×\90Ö·× ×\98×\94×\90Ö·×\9c×\98|ק×\90Ö·×\98×¢×\92×\90ָר×\99עס ×\90Ö·× ×\98×\94×\90Ö·×\9c×\98×\9f}} ×\91×\9c×¢×\98ער ×\90×\93ער ×\9e×¢×\93×\99×¢.\n[[Special:UnusedCategories|×\90×\95×\9e×\91×\90Ö·× ×\99צ×\98×¢ ×§×\90Ö·×\98×¢×\92×\90ר×\99עס]] ×\96×¢× ×¢×\9f × ×\99ש×\98 ×\92×¢×\95×\95×\99×\96×\9f ×\93×\90.\nזעט אויך [[Special:WantedCategories|געזוכטע קאַטעגאריעס]].",
+       "categoriespagetext": "×\93×\99 ×¤Ö¿×\90×\9c×\92×¢× ×\93×¢ {{PLURAL:$1| ×§×\90Ö·×\98×¢×\92×\90ָר×\99×¢ ×¢×§×\96×\99ס×\98×\99ר×\98|ק×\90Ö·×\98×¢×\92×\90ָר×\99עס ×¢×§×\96×\99ס×\98×\99ר×\9f}} ×\90×\95×\99×£ ×\93ער ×\95×\95×\99ק×\99, ×\90×\95×\9f ×\90×\99×\96 ×\90פשר ×\90×\99×\9f ×\91×\90× ×\99×¥ ×\90פשר × ×\99ש×\98.\n\nזעט אויך [[Special:WantedCategories|געזוכטע קאַטעגאריעס]].",
        "categoriesfrom": "ווײַזן קאַטעגאריעס אָנהייבנדיג פֿון:",
        "deletedcontributions": "אויסגעמעקטע באַניצער בײַשטײַערונגען",
        "deletedcontributions-title": "אויסגעמעקטע באַניצער בײַשטײַערונגען",
        "move-page-legend": "באַוועגן בלאַט",
        "movepagetext": "זיך באניצן מיט דעם פֿארעם וועט פֿארענדערן דעם נאמען פֿון דעם בלאט, און וועט אריבערפֿירן זיין געשיכטע צום נייעם נאמען.\nדאס אלטע קעפל וועט ווערן א ווייטערפֿירונג בלאט צום נייעם קעפל.\n\nאיר קענט דערהיינטיגן ווייטערפֿירונגען צום אלטן נאמען אויטאמאטיש.\n\nטאמער נישט, טוט פֿארזיכערן אז עס איז נישטא קיין [[Special:DoubleRedirects|געטאפלטע]] אדער [[Special:BrokenRedirects|צעבראכענע ווייטערפֿירונגען]].\n\nאיר זענט פֿאראנטווארטלעך זיכער מאכן אז אלע פֿארבינדונגען ווערן געריכטעט צום געהעריגן ציל.\n\nדער בלאט וועט <strong> נישט</strong> ווערן אריבערגעפֿירט אויב עס איז שוין דא א בלאט אונטער דעם נייעם נאמען, אחוץ ווען ער איז א ווייטערפֿירונג בלאט, און ער האט נישט קיין געשיכטע פון ענדערונגען.\nפשט דערפֿון, אז איר קענט איבערקערן א ווייטערפֿירונג וואס איר האט אט געמאכט בטעות, און איר קענט נישט אריבערשרייבן אן עקסיסטירנדן בלאט.\n\n<strong>הערה:</strong>\n אזא ענדערונג קען זיין דראסטיש און נישט געווינטשען פאר א פאפולערן בלאט;\nביטע פֿארזיכערט אז איר פֿארשטייט די ווייטגרייכנדע קאנסקווענסן צו דער אקציע בעפֿאר איר פֿירט דאס אויס.",
        "movepagetext-noredirectfixer": "זיך באניצן מיט דעם פֿארעם אונטן וועט פֿארענדערן דעם נאמען פֿון דעם בלאט, און וועט אריבערפֿירן זיין געשיכטע צום נייעם נאמען.\n\nדאס אלטע קעפל וועט ווערן א ווייטערפֿירן בלאט צום נײַעם נאמען.\n\nטוט פֿארזיכערן אז עס בלײַבן נישט קיין [[Special:DoubleRedirects|געטאפלטע]] אדער [[Special:BrokenRedirects|צעבראכענע]] ווייטערפֿירונגען.\n\nאיר זענט פֿאראנטווארטלעך זיכער מאכן אז אלע פֿארבינדונגען ווערן געריכטעט צו דער געהעריגער ריכטונג.\n\nאַכטונג: דער בלאַט וועט <strong>נישט</strong> ווערן אַריבערגעפֿירט אויב עס איז שוין דאָ א בלאט אונטער דעם נײַעם נאמען, אחוץ ווען ער איז א ווײַטערפֿירונג בלאט, און ער האט נישט קיין געשיכטע פון ענדערונגען.\nפשט דערפֿון, אז איר קענט איבערקערן א ווייטערפֿירונג וואס איר האט אט געמאכט בטעות, און איר קענט נישט אריבערשרײַבן אַן עקסיסטירנדן בלאט.\n\n<strong>הערה:</strong> אזא ענדערונג קען זיין דראַסטיש און נישט געוואונטשן פֿאַר א פאפולערן בלאַט; ביטע פֿאַרזיכערט אז איר פֿאַרשטייט די ווײַטגרייכנדע קאנסעקווענסן צו דער אַקציע בעפֿאַר איר גייט ווײַטער.",
-       "movepagetalktext": "×\98×\90×\9eער ×¦×\99×\99×\9bנס איר דאס קעסטל, וועט דער אסאסיציאירטער רעדן בלאט ווערן באַוועגט אויטאמאֵטיש צום נײַעם קעפל, אחוץ ווען ס'איז שוין דא א נישט-ליידיגער רעדן־בלאט.\n\nאין דעם פֿאל, וועט איר דארפֿן באַוועגן אדער צונויפֿגיסן דעם בלאט האַנטלעך, ווען איר ווילט.",
+       "movepagetalktext": "×\98×\90×\9eער ×¦×\99×\99×\9b× ×\98 איר דאס קעסטל, וועט דער אסאסיציאירטער רעדן בלאט ווערן באַוועגט אויטאמאֵטיש צום נײַעם קעפל, אחוץ ווען ס'איז שוין דא א נישט-ליידיגער רעדן־בלאט.\n\nאין דעם פֿאל, וועט איר דארפֿן באַוועגן אדער צונויפֿגיסן דעם בלאט האַנטלעך, ווען איר ווילט.",
        "moveuserpage-warning": "'''ווארענונג:''' איר האלט ביי באוועגן א באניצער בלאט. ביטע באמערקט אז נאר דער בלאט ווערט באוועגט אבער דער באניצער נאמען ווערט ''נישט'' געענדערט.",
        "movecategorypage-warning": "<strong>ווארענונג:</strong> איר האלט ביי באוועגן א קאטעגאריע בלאט. גיט אכט אז נאר דער בלאט וועט ווערן באוועגט, אבער די בלעטער אין דער אלטער קאטעגאריע וועט מען <em>נישט</em> ארײַנשטעלן אין דער נייער קאטעגאריע.",
        "movenologintext": "איר דארפֿט זיך אײַנשרײַבן און זײַן  [[Special:UserLogin|אַרײַנלאגירט]] צו באַוועגן א בלאַט.",
        "previousdiff": "פריערדיקער אונטערשייד →",
        "nextdiff": "קומענדיקע ווערסיע ←",
        "mediawarning": "'''ווארענונג''': דער טעקע טיפ קען אנטהאלטן בייזוויליקן קאד.\nדורכפירן דעם קאד קען שעדיקן אייער סיסטעם.",
-       "imagemaxsize": "מאקסימאלע בילד גרייס :<br />''(פאר טעקע באשרייבונג בלעטער)''",
+       "imagemaxsize": "מאקסימאלע בילד גרייס אויף טעקע באשרייבונג בלעטער:",
        "thumbsize": "קליינבילד גרייס:",
        "widthheight": "$1 × $2",
        "widthheightpage": "$1 × $2, {{PLURAL:$3|איין בלאט|$3 בלעטער}}",
index a3b3a71..de0a559 100644 (file)
@@ -365,7 +365,9 @@ $magicWords = [
 ];
 
 $bookstoreList = [
-       'Aladin.co.kr' => 'http://www.aladin.co.kr/catalog/book.asp?ISBN=$1',
+       'Aladin.co.kr' => 'https://www.aladin.co.kr/catalog/book.asp?ISBN=$1',
+       'National Library of Korea' => 'http://www.nl.go.kr/search/web_search/search/list.php?search_field1=all&tmode=1&value1=$1',
+       'Naver' => 'https://book.naver.com/search/search.nhn?query=$1',
        'inherit' => true,
 ];
 
index 6233d5b..a27c8a5 100644 (file)
@@ -59,6 +59,13 @@ class UpdateExtensionJsonSchema extends Maintenance {
                                                $json['config'][$name]['merge_strategy'] = $value[ExtensionRegistry::MERGE_STRATEGY];
                                                unset( $value[ExtensionRegistry::MERGE_STRATEGY] );
                                        }
+                                       if ( isset( $config["@$name"] ) ) {
+                                               // Put 'description' first for better human-legibility.
+                                               $json['config'][$name] = array_merge(
+                                                       [ 'description' => $config["@$name"] ],
+                                                       $json['config'][$name]
+                                               );
+                                       }
                                }
                        }
                }
index 83e0d8a..cdf47d0 100644 (file)
@@ -210,9 +210,7 @@ td.mw-submit {
        display: inline;
        margin: 0;
        padding: 0;
-       list-style: none;
-       list-style-type: none;
-       list-style-image: none;
+       list-style: none none;
        vertical-align: middle !ie;
 }
 
index 55be237..7e7821e 100644 (file)
@@ -1,4 +1,4 @@
-// Common Less mixin library for MediaWiki
+// Common LESS mixin library for MediaWiki
 //
 // By default the folder containing this file is included in the LESS import paths,
 // which makes this file importable by all less files via `@import 'mediawiki.mixins';`.
index 26f347a..333b8f9 100644 (file)
                // Parent constructor
                mw.widgets.ExpiryWidget.parent.call( this, config );
 
-               // If the wiki does not want the date picker, then initialize the relative
-               // field and exit.
-               if ( config.noDatePicker ) {
-                       this.relativeField.on( 'change', function ( event ) {
-                               // Emit a change event for this widget.
-                               this.emit( 'change', event );
-                       }.bind( this ) );
-
-                       // Initialization
-                       this.$element
-                               .addClass( 'mw-widget-ExpiryWidget' )
-                               .append(
-                                       this.relativeField.$element
-                               );
-
-                       return;
-               }
-
                // Properties
                this.inputSwitch = new OO.ui.ButtonSelectWidget( {
                        tabIndex: -1,
index f36faf7..d11e314 100644 (file)
@@ -14,6 +14,7 @@ class ApiErrorFormatterTest extends MediaWikiLangTestCase {
                $result = new ApiResult( 8388608 );
                $formatter = new ApiErrorFormatter( $result, Language::factory( 'de' ), 'wikitext', false );
                $this->assertSame( 'de', $formatter->getLanguage()->getCode() );
+               $this->assertSame( 'wikitext', $formatter->getFormat() );
 
                $formatter->addMessagesFromStatus( null, Status::newGood() );
                $this->assertSame(
@@ -31,6 +32,25 @@ class ApiErrorFormatterTest extends MediaWikiLangTestCase {
                );
        }
 
+       /**
+        * @covers ApiErrorFormatter
+        * @covers ApiErrorFormatter_BackCompat
+        */
+       public function testNewWithFormat() {
+               $result = new ApiResult( 8388608 );
+               $formatter = new ApiErrorFormatter( $result, Language::factory( 'de' ), 'wikitext', false );
+               $formatter2 = $formatter->newWithFormat( 'html' );
+
+               $this->assertSame( $formatter->getLanguage(), $formatter2->getLanguage() );
+               $this->assertSame( 'html', $formatter2->getFormat() );
+
+               $formatter3 = new ApiErrorFormatter_BackCompat( $result );
+               $formatter4 = $formatter3->newWithFormat( 'html' );
+               $this->assertNotInstanceOf( ApiErrorFormatter_BackCompat::class, $formatter4 );
+               $this->assertSame( $formatter3->getLanguage(), $formatter4->getLanguage() );
+               $this->assertSame( 'html', $formatter4->getFormat() );
+       }
+
        /**
         * @covers ApiErrorFormatter
         * @dataProvider provideErrorFormatter
@@ -351,6 +371,7 @@ class ApiErrorFormatterTest extends MediaWikiLangTestCase {
                $formatter = new ApiErrorFormatter_BackCompat( $result );
 
                $this->assertSame( 'en', $formatter->getLanguage()->getCode() );
+               $this->assertSame( 'bc', $formatter->getFormat() );
 
                $this->assertSame( [], $formatter->arrayFromStatus( Status::newGood() ) );
 
@@ -526,10 +547,6 @@ class ApiErrorFormatterTest extends MediaWikiLangTestCase {
         * @param array $expect
         */
        public function testGetMessageFromException( $exception, $options, $expect ) {
-               if ( $exception instanceof UsageException ) {
-                       $this->hideDeprecated( 'UsageException::getMessageArray' );
-               }
-
                $result = new ApiResult( 8388608 );
                $formatter = new ApiErrorFormatter( $result, Language::factory( 'en' ), 'html', false );
 
@@ -555,10 +572,6 @@ class ApiErrorFormatterTest extends MediaWikiLangTestCase {
         * @param array $expect
         */
        public function testGetMessageFromException_BC( $exception, $options, $expect ) {
-               if ( $exception instanceof UsageException ) {
-                       $this->hideDeprecated( 'UsageException::getMessageArray' );
-               }
-
                $result = new ApiResult( 8388608 );
                $formatter = new ApiErrorFormatter_BackCompat( $result );
 
@@ -579,12 +592,6 @@ class ApiErrorFormatterTest extends MediaWikiLangTestCase {
        }
 
        public static function provideGetMessageFromException() {
-               Wikimedia\suppressWarnings();
-               $usageException = new UsageException(
-                       '<b>Something broke!</b>', 'ue-code', 0, [ 'xxx' => 'yyy', 'baz' => 23 ]
-               );
-               Wikimedia\restoreWarnings();
-
                return [
                        'Normal exception' => [
                                new RuntimeException( '<b>Something broke!</b>' ),
@@ -604,24 +611,6 @@ class ApiErrorFormatterTest extends MediaWikiLangTestCase {
                                        'data' => [ 'foo' => 'bar', 'baz' => 42 ],
                                ]
                        ],
-                       'UsageException' => [
-                               $usageException,
-                               [],
-                               [
-                                       'text' => '&#60;b&#62;Something broke!&#60;/b&#62;',
-                                       'code' => 'ue-code',
-                                       'data' => [ 'xxx' => 'yyy', 'baz' => 23 ],
-                               ]
-                       ],
-                       'UsageException, wrapped' => [
-                               $usageException,
-                               [ 'wrap' => 'parentheses', 'code' => 'some-code', 'data' => [ 'foo' => 'bar', 'baz' => 42 ] ],
-                               [
-                                       'text' => '(&#60;b&#62;Something broke!&#60;/b&#62;)',
-                                       'code' => 'some-code',
-                                       'data' => [ 'xxx' => 'yyy', 'baz' => 42, 'foo' => 'bar' ],
-                               ]
-                       ],
                        'LocalizedException' => [
                                new LocalizedException( [ 'returnto', '<b>FooBar</b>' ] ),
                                [],
index 20d758e..a6083cd 100644 (file)
@@ -1010,10 +1010,6 @@ class ApiMainTest extends ApiTestCase {
                        MWExceptionHandler::getRedactedTraceAsString( $dbex )
                )->inLanguage( 'en' )->useDatabase( false )->text();
 
-               Wikimedia\suppressWarnings();
-               $usageEx = new UsageException( 'Usage exception!', 'ue', 0, [ 'foo' => 'bar' ] );
-               Wikimedia\restoreWarnings();
-
                $apiEx1 = new ApiUsageException( null,
                        StatusValue::newFatal( new ApiRawMessage( 'An error', 'sv-error1' ) ) );
                TestingAccessWrapper::newFromObject( $apiEx1 )->modulePath = 'foo+bar';
@@ -1059,23 +1055,6 @@ class ApiMainTest extends ApiTestCase {
                                        'servedby' => wfHostname(),
                                ]
                        ],
-                       [
-                               $usageEx,
-                               [ 'existing-error', 'ue' ],
-                               [
-                                       'warnings' => [
-                                               [ 'code' => 'existing-warning', 'text' => 'existing warning', 'module' => 'main' ],
-                                       ],
-                                       'errors' => [
-                                               [ 'code' => 'existing-error', 'text' => 'existing error', 'module' => 'main' ],
-                                               [ 'code' => 'ue', 'text' => "Usage exception!", 'data' => [ 'foo' => 'bar' ] ]
-                                       ],
-                                       'docref' => "See $doclink for API usage. Subscribe to the mediawiki-api-announce mailing " .
-                                               "list at &lt;https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce&gt; " .
-                                               "for notice of API deprecations and breaking changes.",
-                                       'servedby' => wfHostname(),
-                               ]
-                       ],
                        [
                                $apiEx1,
                                [ 'existing-error', 'sv-error1', 'sv-error2' ],
diff --git a/tests/phpunit/includes/api/ApiQueryInfoTest.php b/tests/phpunit/includes/api/ApiQueryInfoTest.php
new file mode 100644 (file)
index 0000000..80043da
--- /dev/null
@@ -0,0 +1,146 @@
+<?php
+
+/**
+ * @group API
+ * @group medium
+ * @group Database
+ *
+ * @coversDefaultClass ApiQueryInfo
+ */
+class ApiQueryInfoTest extends ApiTestCase {
+
+       /**
+        * @covers ::execute
+        * @covers ::extractPageInfo
+        */
+       public function testExecute() {
+               $page = $this->getExistingTestPage( 'Pluto' );
+               $title = $page->getTitle();
+
+               list( $data ) = $this->doApiRequest( [
+                               'action' => 'query',
+                               'prop' => 'info',
+                               'titles' => $title->getText(),
+               ] );
+
+               $this->assertArrayHasKey( 'query', $data );
+               $this->assertArrayHasKey( 'pages', $data['query'] );
+               $this->assertArrayHasKey( $page->getId(), $data['query']['pages'] );
+
+               $info = $data['query']['pages'][$page->getId()];
+               $this->assertSame( $page->getId(), $info['pageid'] );
+               $this->assertSame( $title->getNamespace(), $info['ns'] );
+               $this->assertSame( $title->getText(), $info['title'] );
+               $this->assertSame( $title->getContentModel(), $info['contentmodel'] );
+               $this->assertSame( $title->getPageLanguage()->getCode(), $info['pagelanguage'] );
+               $this->assertSame( $title->getPageLanguage()->getHtmlCode(), $info['pagelanguagehtmlcode'] );
+               $this->assertSame( $title->getPageLanguage()->getDir(), $info['pagelanguagedir'] );
+               $this->assertSame( wfTimestamp( TS_ISO_8601, $title->getTouched() ), $info['touched'] );
+               $this->assertSame( $title->getLatestRevID(), $info['lastrevid'] );
+               $this->assertSame( $title->getLength(), $info['length'] );
+               $this->assertSame( $title->isNewPage(), $info['new'] );
+               $this->assertArrayNotHasKey( 'actions', $info );
+       }
+
+       /**
+        * @covers ::execute
+        * @covers ::extractPageInfo
+        */
+       public function testExecuteEditActions() {
+               $page = $this->getExistingTestPage( 'Pluto' );
+               $title = $page->getTitle();
+
+               list( $data ) = $this->doApiRequest( [
+                               'action' => 'query',
+                               'prop' => 'info',
+                               'titles' => $title->getText(),
+                               'intestactions' => 'edit'
+               ] );
+
+               $this->assertArrayHasKey( 'query', $data );
+               $this->assertArrayHasKey( 'pages', $data['query'] );
+               $this->assertArrayHasKey( $page->getId(), $data['query']['pages'] );
+
+               $info = $data['query']['pages'][$page->getId()];
+               $this->assertArrayHasKey( 'actions', $info );
+               $this->assertArrayHasKey( 'edit', $info['actions'] );
+               $this->assertTrue( $info['actions']['edit'] );
+       }
+
+       /**
+        * @covers ::execute
+        * @covers ::extractPageInfo
+        */
+       public function testExecuteEditActionsFull() {
+               $page = $this->getExistingTestPage( 'Pluto' );
+               $title = $page->getTitle();
+
+               list( $data ) = $this->doApiRequest( [
+                               'action' => 'query',
+                               'prop' => 'info',
+                               'titles' => $title->getText(),
+                               'intestactions' => 'edit',
+                               'intestactionsdetail' => 'full',
+               ] );
+
+               $this->assertArrayHasKey( 'query', $data );
+               $this->assertArrayHasKey( 'pages', $data['query'] );
+               $this->assertArrayHasKey( $page->getId(), $data['query']['pages'] );
+
+               $info = $data['query']['pages'][$page->getId()];
+               $this->assertArrayHasKey( 'actions', $info );
+               $this->assertArrayHasKey( 'edit', $info['actions'] );
+               $this->assertInternalType( 'array', $info['actions']['edit'] );
+               $this->assertSame( [], $info['actions']['edit'] );
+       }
+
+       /**
+        * @covers ::execute
+        * @covers ::extractPageInfo
+        */
+       public function testExecuteEditActionsFullBlock() {
+               $badActor = $this->getTestUser()->getUser();
+               $sysop = $this->getTestSysop()->getUser();
+
+               $block = new \Block( [
+                       'address' => $badActor->getName(),
+                       'user' => $badActor->getId(),
+                       'by' => $sysop->getId(),
+                       'expiry' => 'infinity',
+                       'sitewide' => 0,
+                       'enableAutoblock' => true,
+               ] );
+
+               $block->insert();
+
+               $page = $this->getExistingTestPage( 'Pluto' );
+               $title = $page->getTitle();
+
+               list( $data ) = $this->doApiRequest( [
+                               'action' => 'query',
+                               'prop' => 'info',
+                               'titles' => $title->getText(),
+                               'intestactions' => 'edit',
+                               'intestactionsdetail' => 'full',
+               ], null, false, $badActor );
+
+               $block->delete();
+
+               $this->assertArrayHasKey( 'query', $data );
+               $this->assertArrayHasKey( 'pages', $data['query'] );
+               $this->assertArrayHasKey( $page->getId(), $data['query']['pages'] );
+
+               $info = $data['query']['pages'][$page->getId()];
+               $this->assertArrayHasKey( 'actions', $info );
+               $this->assertArrayHasKey( 'edit', $info['actions'] );
+               $this->assertInternalType( 'array', $info['actions']['edit'] );
+               $this->assertArrayHasKey( 0, $info['actions']['edit'] );
+               $this->assertArrayHasKey( 'code', $info['actions']['edit'][0] );
+               $this->assertSame( 'blocked', $info['actions']['edit'][0]['code'] );
+               $this->assertArrayHasKey( 'data', $info['actions']['edit'][0] );
+               $this->assertArrayHasKey( 'blockinfo', $info['actions']['edit'][0]['data'] );
+               $this->assertArrayHasKey( 'blockid', $info['actions']['edit'][0]['data']['blockinfo'] );
+               $this->assertSame( $block->getId(), $info['actions']['edit'][0]['data']['blockinfo']['blockid'] );
+       }
+
+}
index 661f325..c340c08 100644 (file)
@@ -196,4 +196,21 @@ class MessageCacheTest extends MediaWikiLangTestCase {
                        [ 'ćaB', 'ćaB' ],
                ];
        }
+
+       public function testNoDBAccess() {
+               global $wgContLanguageCode;
+
+               $dbr = wfGetDB( DB_REPLICA );
+
+               MessageCache::singleton()->getMsgFromNamespace( 'allpages', $wgContLanguageCode );
+
+               $this->assertEquals( 0, $dbr->trxLevel() );
+               $dbr->setFlag( DBO_TRX, $dbr::REMEMBER_PRIOR ); // make queries trigger TRX
+
+               MessageCache::singleton()->getMsgFromNamespace( 'go', $wgContLanguageCode );
+
+               $dbr->restoreFlags();
+
+               $this->assertEquals( 0, $dbr->trxLevel(), "No DB read queries" );
+       }
 }
index b2e7155..b36fe11 100644 (file)
@@ -130,4 +130,87 @@ class DatabaseDomainTest extends PHPUnit\Framework\TestCase {
                $this->assertSame( null, $domain->getSchema() );
                $this->assertSame( '', $domain->getTablePrefix() );
        }
+
+       public static function provideIsCompatible() {
+               return [
+                       'Basic' =>
+                               [ 'foo', 'foo', null, '', true ],
+                       'db+prefix' =>
+                               [ 'foo-bar', 'foo', null, 'bar', true ],
+                       'db+schema+prefix' =>
+                               [ 'foo-bar-baz', 'foo', 'bar', 'baz', true ],
+                       'db+dontcare_schema+prefix' =>
+                               [ 'foo-bar-baz', 'foo', null, 'baz', false ],
+                       '?h -> -' =>
+                               [ 'foo?hbar-baz-baa', 'foo-bar', 'baz', 'baa', true ],
+                       '?? -> ?' =>
+                               [ 'foo??bar-baz-baa', 'foo?bar', 'baz', 'baa', true ],
+                       'Nothing' =>
+                               [ '', null, null, '', true ],
+                       'dontcaredb+dontcaredbschema+prefix' =>
+                               [ 'mywiki-mediawiki-prefix', null, null, 'prefix', false ],
+                       'dontcaredb+schema+prefix' =>
+                               [ 'mywiki-schema-prefix', null, 'schema', 'prefix', false ],
+                       'db+dontcareschema+prefix' =>
+                               [ 'mywiki-schema-prefix', 'mywiki', null, 'prefix', false ],
+                       'postgres-db-jobqueue' =>
+                               [ 'postgres-mediawiki-', 'postgres', null, '', false ]
+               ];
+       }
+
+       /**
+        * @dataProvider provideIsCompatible
+        * @covers Wikimedia\Rdbms\DatabaseDomain::isCompatible
+        */
+       public function testIsCompatible( $id, $db, $schema, $prefix, $transitive ) {
+               $compareIdObj = DatabaseDomain::newFromId( $id );
+               $this->assertInstanceOf( DatabaseDomain::class, $compareIdObj );
+
+               $fromId = new DatabaseDomain( $db, $schema, $prefix );
+
+               $this->assertTrue( $fromId->isCompatible( $id ), 'constructed equals string' );
+               $this->assertTrue( $fromId->isCompatible( $compareIdObj ), 'fromId equals string' );
+
+               $this->assertEquals( $transitive, $compareIdObj->isCompatible( $fromId ),
+                       'test transitivity of nulls components' );
+       }
+
+       public static function provideIsCompatible2() {
+               return [
+                       'db+schema+prefix' =>
+                               [ 'mywiki-schema-prefix', 'thatwiki', 'schema', 'prefix' ],
+                       'dontcaredb+dontcaredbschema+prefix' =>
+                               [ 'thatwiki-mediawiki-otherprefix', null, null, 'prefix' ],
+                       'dontcaredb+schema+prefix' =>
+                               [ 'mywiki-otherschema-prefix', null, 'schema', 'prefix' ],
+                       'db+dontcareschema+prefix' =>
+                               [ 'notmywiki-schema-prefix', 'mywiki', null, 'prefix' ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideIsCompatible2
+        * @covers Wikimedia\Rdbms\DatabaseDomain::isCompatible
+        */
+       public function testIsCompatible2( $id, $db, $schema, $prefix ) {
+               $compareIdObj = DatabaseDomain::newFromId( $id );
+               $this->assertInstanceOf( DatabaseDomain::class, $compareIdObj );
+
+               $fromId = new DatabaseDomain( $db, $schema, $prefix );
+
+               $this->assertFalse( $fromId->isCompatible( $id ), 'constructed equals string' );
+               $this->assertFalse( $fromId->isCompatible( $compareIdObj ), 'fromId equals string' );
+       }
+
+       /**
+        * @covers Wikimedia\Rdbms\DatabaseDomain::isUnspecified
+        */
+       public function testIsUnspecified() {
+               $domain = new DatabaseDomain( null, null, '' );
+               $this->assertTrue( $domain->isUnspecified() );
+               $domain = new DatabaseDomain( 'mywiki', null, '' );
+               $this->assertFalse( $domain->isUnspecified() );
+               $domain = new DatabaseDomain( 'mywiki', null, '' );
+               $this->assertFalse( $domain->isUnspecified() );
+       }
 }
index ae19278..48c3d17 100644 (file)
@@ -941,6 +941,7 @@ class SessionBackendTest extends MediaWikiTestCase {
                \Wikimedia\quietCall( 'session_start' );
                $backend->unpersist();
                $this->assertSame( self::SESSIONID . 'x', session_id() );
+               session_write_close();
 
                session_id( self::SESSIONID );
                $wrap->persist = true;
index e042f76..b33cd24 100644 (file)
@@ -82,6 +82,7 @@ class SessionManagerTest extends MediaWikiTestCase {
                $context->setRequest( $request );
                $id = $request->getSession()->getId();
 
+               session_write_close();
                session_id( '' );
                $session = SessionManager::getGlobalSession();
                $this->assertSame( $id, $session->getId() );
index cee15e8..1e1f739 100644 (file)
@@ -1015,6 +1015,7 @@ class UserTest extends MediaWikiTestCase {
        }
 
        public function testActorId() {
+               $domain = MediaWikiServices::getInstance()->getDBLoadBalancer()->getLocalDomainID();
                $this->hideDeprecated( 'User::selectFields' );
 
                // Newly-created user has an actor ID
@@ -1042,7 +1043,7 @@ class UserTest extends MediaWikiTestCase {
                        'Actor ID can be retrieved for user loaded with User::selectFields()' );
 
                $this->db->delete( 'actor', [ 'actor_user' => $id ], __METHOD__ );
-               User::purge( wfWikiId(), $id );
+               User::purge( $domain, $id );
                // Because WANObjectCache->delete() stupidly doesn't delete from the process cache.
                ObjectCache::getMainWANInstance()->clearProcessCache();
 
index a7c9aa6..038b757 100644 (file)
@@ -20,8 +20,26 @@ If using MediaWiki-Vagrant:
 
     npm run selenium
 
-By default, Chrome will run in headless mode. If you want to see Chrome, set DISPLAY
-environment variable to any value:
+There are three supported modes of running the tests:
+
+- Headless. It's the default. You will not see the browser while tests are
+  running because it's running in a headless mode. This mode should run fine
+  on all supported platforms.
+- Headless recording. Set DISPLAY environment variable to a value that starts
+  with colon (`:`) and video of each test will be recorded. Browser will run
+  headless. Recording videos works only on Linux.
+- Visible. If you want to see the browser, set DISPLAY environment variable to
+  any value that does not start with colon. This mode will not work in a
+  headless environment like MediaWiki-Vagrant.
+
+Example recording session:
+
+    sudo apt-get install chromedriver ffmpeg xvfb
+    export DISPLAY=:94
+    Xvfb "$DISPLAY" -screen 0 1280x1024x24 &
+    npm run selenium
+
+Example visible session:
 
     DISPLAY=1 npm run selenium
 
index 8b47dff..916ee74 100644 (file)
@@ -1,8 +1,20 @@
 const fs = require( 'fs' ),
        path = require( 'path' ),
-       saveScreenshot = require( 'wdio-mediawiki' ).saveScreenshot,
-       logPath = process.env.LOG_DIR || __dirname + '/log';
+       logPath = process.env.LOG_DIR || path.join( __dirname, '/log' );
 
+let ffmpeg;
+
+// get current test title and clean it, to use it as file name
+function fileName( title ) {
+       return encodeURIComponent( title.replace( /\s+/g, '-' ) );
+}
+
+// build file path
+function filePath( test, screenshotPath, extension ) {
+       return path.join( screenshotPath, `${fileName( test.parent )}-${fileName( test.title )}.${extension}` );
+}
+
+// relative path
 function relPath( foo ) {
        return path.resolve( __dirname, '../..', foo );
 }
@@ -135,16 +147,57 @@ exports.config = {
        // =====
        // See also: http://webdriver.io/guide/testrunner/configurationfile.html
 
+       /**
+       * Function to be executed before a test (in Mocha/Jasmine) or a step (in Cucumber) starts.
+       * @param {Object} test test details
+       */
+       beforeTest: function ( test ) {
+               if ( process.env.DISPLAY && process.env.DISPLAY.startsWith( ':' ) ) {
+                       let videoPath = filePath( test, this.screenshotPath, 'mp4' );
+                       const { spawn } = require( 'child_process' );
+                       ffmpeg = spawn( 'ffmpeg', [
+                               '-f', 'x11grab', //  grab the X11 display
+                               '-video_size', '1280x1024', // video size
+                               '-i', process.env.DISPLAY, // input file url
+                               '-loglevel', 'error', // log only errors
+                               '-y', // overwrite output files without asking
+                               '-pix_fmt', 'yuv420p', // QuickTime Player support, "Use -pix_fmt yuv420p for compatibility with outdated media players"
+                               videoPath // output file
+                       ] );
+
+                       ffmpeg.stdout.on( 'data', ( data ) => {
+                               console.log( `ffmpeg stdout: ${data}` );
+                       } );
+
+                       ffmpeg.stderr.on( 'data', ( data ) => {
+                               console.log( `ffmpeg stderr: ${data}` );
+                       } );
+
+                       ffmpeg.on( 'close', ( code ) => {
+                               console.log( '\n\tVideo location:', videoPath, '\n' );
+                               console.log( `ffmpeg exited with code ${code}` );
+                       } );
+               }
+       },
+
        /**
         * Save a screenshot when test fails.
         *
         * @param {Object} test Mocha Test object
         */
        afterTest: function ( test ) {
-               var filePath;
-               if ( !test.passed ) {
-                       filePath = saveScreenshot( test.title );
-                       console.log( '\n\tScreenshot: ' + filePath + '\n' );
+               if ( ffmpeg ) {
+                       // stop video recording
+                       ffmpeg.kill( 'SIGINT' );
+               }
+
+               // if test passed, ignore, else take and save screenshot
+               if ( test.passed ) {
+                       return;
                }
+               // save screenshot
+               let screenshotPath = filePath( test, this.screenshotPath, 'png' );
+               browser.saveScreenshot( screenshotPath );
+               console.log( '\n\tScreenshot location:', screenshotPath, '\n' );
        }
 };