Merge "mw-ui-radio, mw-ui-checkbox: Don't reset every font style"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Thu, 28 Feb 2019 12:31:31 +0000 (12:31 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Thu, 28 Feb 2019 12:31:31 +0000 (12:31 +0000)
59 files changed:
RELEASE-NOTES-1.33
autoload.php
includes/Block.php
includes/DefaultSettings.php
includes/OutputPage.php
includes/actions/Action.php
includes/api/ApiBase.php
includes/api/ApiMain.php
includes/api/i18n/ar.json
includes/api/i18n/fr.json
includes/debug/logger/LogCapturingSpi.php
includes/installer/i18n/ar.json
includes/libs/objectcache/WANObjectCache.php
includes/libs/rdbms/database/DatabasePostgres.php
includes/libs/rdbms/loadbalancer/LoadBalancer.php
includes/libs/rdbms/loadbalancer/LoadBalancerSingle.php
includes/libs/redis/RedisConnRef.php
includes/media/BitmapHandler.php
includes/media/BitmapHandler_ClientOnly.php
includes/media/BmpHandler.php
includes/media/DjVuHandler.php
includes/media/ExifBitmapHandler.php
includes/media/GIFHandler.php
includes/media/ImageHandler.php
includes/media/JpegHandler.php
includes/media/MediaHandler.php
includes/media/MediaTransformOutput.php
includes/media/PNGHandler.php
includes/media/SvgHandler.php
includes/media/TiffHandler.php
includes/media/TransformationalImageHandler.php
includes/media/XCFHandler.php
includes/resourceloader/ResourceLoader.php
includes/resourceloader/ResourceLoaderImageModule.php
languages/i18n/aeb-arab.json
languages/i18n/ar.json
languages/i18n/arq.json
languages/i18n/be-tarask.json
languages/i18n/diq.json
languages/i18n/exif/diq.json
languages/i18n/pt-br.json
languages/i18n/sh.json
languages/i18n/tcy.json
languages/i18n/ur.json
languages/i18n/zh-hant.json
maintenance/populateExternallinksIndex60.php
maintenance/resetUserEmail.php
resources/src/mediawiki.base/mediawiki.base.js
resources/src/mediawiki.debug/debug.js
resources/src/mediawiki.ui/components/forms.less
tests/phpunit/HamcrestPHPUnitIntegration.php
tests/phpunit/MediaWikiLoggerPHPUnitTestListener.php
tests/phpunit/includes/TitlePermissionTest.php
tests/phpunit/includes/libs/objectcache/WANObjectCacheTest.php
tests/phpunit/includes/user/UserTest.php
tests/phpunit/mocks/media/MockDjVuHandler.php
tests/qunit/data/mediawiki.loader.getScript.example.js [new file with mode: 0644]
tests/qunit/suites/resources/mediawiki/mediawiki.loader.test.js
tests/selenium/wdio.conf.js

index 4a923c5..5154077 100644 (file)
@@ -122,6 +122,8 @@ production.
   passed a bad code.
 * ApiBase::checkTitleUserPermissions() now takes an options array as its third
   parameter. Passing a User object or null is deprecated.
+* The api-feature-usage log channel now has log context. The text message is
+  deprecated and will be removed in the future.
 
 === Languages updated in 1.33 ===
 MediaWiki supports over 350 languages. Many localisations are updated regularly.
@@ -321,6 +323,8 @@ because of Phabricator reports.
   Block::isCreateAccountBlocked and Block::isUsertalkEditAllowed to get and set
   block properties; use Block::appliesToRight and Block::appliesToUsertalk to
   check block behaviour.
+* The api-feature-usage log channel now has log context. The text message is
+  deprecated and will be removed in the future.
 
 === Other changes in 1.33 ===
 * (T201747) Html::openElement() warns if given an element name with a space
index 8eaecaa..aaf6f51 100644 (file)
@@ -889,6 +889,7 @@ $wgAutoloadLocalClasses = [
        'MediaWiki\\Logger\\ConsoleSpi' => __DIR__ . '/includes/debug/logger/ConsoleSpi.php',
        'MediaWiki\\Logger\\LegacyLogger' => __DIR__ . '/includes/debug/logger/LegacyLogger.php',
        'MediaWiki\\Logger\\LegacySpi' => __DIR__ . '/includes/debug/logger/LegacySpi.php',
+       'MediaWiki\\Logger\\LogCapturingSpi' => __DIR__ . '/includes/debug/logger/LogCapturingSpi.php',
        'MediaWiki\\Logger\\LoggerFactory' => __DIR__ . '/includes/debug/logger/LoggerFactory.php',
        'MediaWiki\\Logger\\MonologSpi' => __DIR__ . '/includes/debug/logger/MonologSpi.php',
        'MediaWiki\\Logger\\Monolog\\AvroFormatter' => __DIR__ . '/includes/debug/logger/monolog/AvroFormatter.php',
@@ -960,7 +961,6 @@ $wgAutoloadLocalClasses = [
        'MediaWiki\\Widget\\TitlesMultiselectWidget' => __DIR__ . '/includes/widget/TitlesMultiselectWidget.php',
        'MediaWiki\\Widget\\UserInputWidget' => __DIR__ . '/includes/widget/UserInputWidget.php',
        'MediaWiki\\Widget\\UsersMultiselectWidget' => __DIR__ . '/includes/widget/UsersMultiselectWidget.php',
-       'Mediawiki\\Logger\\LogCapturingSpi' => __DIR__ . '/includes/debug/logger/LogCapturingSpi.php',
        'MemcLockManager' => __DIR__ . '/includes/libs/lockmanager/MemcLockManager.php',
        'MemcachedBagOStuff' => __DIR__ . '/includes/libs/objectcache/MemcachedBagOStuff.php',
        'MemcachedClient' => __DIR__ . '/includes/libs/objectcache/MemcachedClient.php',
index 1a54394..09f6d9f 100644 (file)
@@ -1912,18 +1912,18 @@ class Block {
         * they can never edit it. (Ideally the flag would be stored as
         * null in these cases, but the database field isn't nullable.)
         *
+        * This method does not validate that the passed in talk page belongs to the
+        * block target since the target (an IP) might not be the same as the user's
+        * talk page (if they are logged in).
+        *
         * @since 1.33
         * @param Title|null $usertalk The user's user talk page. If null,
         *  and if the target is a User, the target's userpage is used
         * @return bool The user can edit their talk page
         */
        public function appliesToUsertalk( Title $usertalk = null ) {
-               $target = $this->target;
-               $targetIsUser = $target instanceof User;
-               $targetName = $targetIsUser ? $target->getName() : $target;
-
                if ( !$usertalk ) {
-                       if ( $targetIsUser ) {
+                       if ( $this->target instanceof User ) {
                                $usertalk = $this->target->getTalkPage();
                        } else {
                                throw new InvalidArgumentException(
@@ -1938,28 +1938,6 @@ class Block {
                        );
                }
 
-               switch ( $this->type ) {
-                       case self::TYPE_USER:
-                       case self::TYPE_IP:
-                               if ( $usertalk->getText() !== $targetName ) {
-                                       throw new InvalidArgumentException(
-                                               '$usertalk must be a talk page for the block target'
-                                       );
-                               }
-                               break;
-                       case self::TYPE_RANGE:
-                               if ( !IP::isInRange( $usertalk->getText(), $target ) ) {
-                                       throw new InvalidArgumentException(
-                                               '$usertalk must be a talk page for an IP within the block target range'
-                                       );
-                               }
-                               break;
-                       default:
-                               throw new LogicException(
-                                       'Cannot determine validity of $usertalk for this type of block'
-                               );
-               }
-
                if ( !$this->isSitewide() ) {
                        if ( $this->appliesToPage( $usertalk->getArticleID() ) ) {
                                return true;
index 5ede118..93113df 100644 (file)
@@ -9037,6 +9037,24 @@ $wgEnableRollbackConfirmationPrompt = true;
  */
 $wgEnableBlockNoticeStats = false;
 
+/**
+ * Origin Trials tokens.
+ *
+ * @since 1.34
+ * @var array
+ */
+$wgOriginTrials = [];
+
+/**
+ * Enable client-side Priority Hints.
+ *
+ * @warning EXPERIMENTAL!
+ *
+ * @since 1.34
+ * @var bool
+ */
+$wgPriorityHints = false;
+
 /**
  * For really cool vim folding this needs to be at the end:
  * vim: foldmarker=@{,@} foldmethod=marker
index 9b766bb..461df94 100644 (file)
@@ -2522,6 +2522,18 @@ class OutputPage extends ContextSource {
                return false;
        }
 
+       /**
+        * Get the Origin-Trial header values. This is used to enable Chrome Origin
+        * Trials: https://github.com/GoogleChrome/OriginTrials
+        *
+        * @return array
+        */
+       private function getOriginTrials() {
+               $config = $this->getConfig();
+
+               return $config->get( 'OriginTrials' );
+       }
+
        /**
         * Send cache control HTTP headers
         */
@@ -2688,6 +2700,11 @@ class OutputPage extends ContextSource {
                        $response->header( "X-Frame-Options: $frameOptions" );
                }
 
+               $originTrials = $this->getOriginTrials();
+               foreach ( $originTrials as $originTrial ) {
+                       $response->header( "Origin-Trial: $originTrial", false );
+               }
+
                ContentSecurityPolicy::sendHeaders( $this );
 
                if ( $this->mArticleBodyOnly ) {
index f288a5c..f892c5e 100644 (file)
@@ -101,8 +101,7 @@ abstract class Action implements MessageLocalizer {
                        if ( !class_exists( $classOrCallable ) ) {
                                return false;
                        }
-                       $obj = new $classOrCallable( $page, $context );
-                       return $obj;
+                       return new $classOrCallable( $page, $context );
                }
 
                if ( is_callable( $classOrCallable ) ) {
@@ -142,7 +141,7 @@ abstract class Action implements MessageLocalizer {
                        } else {
                                $actionName = 'view';
                        }
-               } elseif ( $actionName == 'editredlink' ) {
+               } elseif ( $actionName === 'editredlink' ) {
                        $actionName = 'edit';
                }
 
@@ -359,7 +358,7 @@ abstract class Action implements MessageLocalizer {
         */
        protected function setHeaders() {
                $out = $this->getOutput();
-               $out->setRobotPolicy( "noindex,nofollow" );
+               $out->setRobotPolicy( 'noindex,nofollow' );
                $out->setPageTitle( $this->getPageTitle() );
                $out->setSubtitle( $this->getDescription() );
                $out->setArticleRelated( true );
index 4898385..53c0a0b 100644 (file)
@@ -2223,12 +2223,23 @@ abstract class ApiBase extends ContextSource {
         */
        public function logFeatureUsage( $feature ) {
                $request = $this->getRequest();
-               $s = '"' . addslashes( $feature ) . '"' .
-                       ' "' . wfUrlencode( str_replace( ' ', '_', $this->getUser()->getName() ) ) . '"' .
-                       ' "' . $request->getIP() . '"' .
-                       ' "' . addslashes( $request->getHeader( 'Referer' ) ) . '"' .
-                       ' "' . addslashes( $this->getMain()->getUserAgent() ) . '"';
-               wfDebugLog( 'api-feature-usage', $s, 'private' );
+               $ctx = [
+                       'feature' => $feature,
+                       // Spaces to underscores in 'username' for historical reasons.
+                       'username' => str_replace( ' ', '_', $this->getUser()->getName() ),
+                       'ip' => $request->getIP(),
+                       'referer' => (string)$request->getHeader( 'Referer' ),
+                       'agent' => $this->getMain()->getUserAgent(),
+               ];
+
+               // Text string is deprecated. Remove (or replace with just $feature) in MW 1.34.
+               $s = '"' . addslashes( $ctx['feature'] ) . '"' .
+                       ' "' . wfUrlencode( $ctx['username'] ) . '"' .
+                       ' "' . $ctx['ip'] . '"' .
+                       ' "' . addslashes( $ctx['referer'] ) . '"' .
+                       ' "' . addslashes( $ctx['agent'] ) . '"';
+
+               wfDebugLog( 'api-feature-usage', $s, 'private', $ctx );
        }
 
        /**@}*/
index 634ebd1..8255269 100644 (file)
@@ -1632,7 +1632,7 @@ class ApiMain extends ApiBase {
         */
        protected function logRequest( $time, $e = null ) {
                $request = $this->getRequest();
-               $logCtx = [
+               $legacyLogCtx = [
                        'ts' => time(),
                        'ip' => $request->getIP(),
                        'userAgent' => $this->getUserAgent(),
@@ -1643,17 +1643,43 @@ class ApiMain extends ApiBase {
                        'params' => [],
                ];
 
+               $logCtx = [
+                       '$schema' => '/mediawiki/api/request/0.0.1',
+                       'meta' => [
+                               'id' => UIDGenerator::newUUIDv1(),
+                               'dt' => gmdate( 'c' ),
+                               'domain' => $this->getConfig()->get( 'ServerName' ),
+                               'stream' => 'mediawiki.api-request'
+                       ],
+                       'http' => [
+                               'method' => $request->getMethod(),
+                               'client_ip' => $request->getIP(),
+                               'request_headers' => [
+                                       'user-agent' => $request->getHeader( 'User-agent' ),
+                                       'api-user-agent' => $request->getHeader( 'Api-user-agent' )
+                               ],
+                       ],
+                       'database' => wfWikiID(),
+                       'backend_time_ms' => (int)round( $time * 1000 ),
+                       'params' => []
+               ];
+
+               $logCtx['meta']['request_id'] =
+                       $logCtx['http']['request_headers']['x-request-id'] = WebRequest::getRequestId();
+
                if ( $e ) {
+                       $logCtx['api_error_codes'] = [];
                        foreach ( $this->errorMessagesFromException( $e ) as $msg ) {
-                               $logCtx['errorCodes'][] = $msg->getApiCode();
+                               $legacyLogCtx['errorCodes'][] = $msg->getApiCode();
+                               $logCtx['api_error_codes'][] = $msg->getApiCode();
                        }
                }
 
                // Construct space separated message for 'api' log channel
                $msg = "API {$request->getMethod()} " .
                        wfUrlencode( str_replace( ' ', '_', $this->getUser()->getName() ) ) .
-                       " {$logCtx['ip']} " .
-                       "T={$logCtx['timeSpentBackend']}ms";
+                       " {$legacyLogCtx['ip']} " .
+                       "T={$legacyLogCtx['timeSpentBackend']}ms";
 
                $sensitive = array_flip( $this->getSensitiveParams() );
                foreach ( $this->getParamsUsed() as $name ) {
@@ -1672,13 +1698,17 @@ class ApiMain extends ApiBase {
                                $encValue = $this->encodeRequestLogValue( $value );
                        }
 
+                       $legacyLogCtx['params'][$name] = $value;
                        $logCtx['params'][$name] = $value;
                        $msg .= " {$name}={$encValue}";
                }
 
                wfDebugLog( 'api', $msg, 'private' );
-               // ApiAction channel is for structured data consumers
-               wfDebugLog( 'ApiAction', '', 'private', $logCtx );
+               // ApiAction channel is for structured data consumers.
+               // The ApiAction was using logging channel is deprecated and is replaced
+               // by the api-request channel.
+               wfDebugLog( 'ApiAction', '', 'private', $legacyLogCtx );
+               wfDebugLog( 'api-request', '', 'private', $logCtx );
        }
 
        /**
index d3b75e9..0912dc0 100644 (file)
        "apihelp-query+blocks-paramvalue-prop-range": "يضيف نطاق عناوين الآيبي المتأثرة بالمنع.",
        "apihelp-query+blocks-paramvalue-prop-flags": "يوسم المنع بـ(المنع التلقائي والمجهولون فقط وما إلى ذلك).",
        "apihelp-query+blocks-paramvalue-prop-restrictions": "يضيف قيود المنع الجزئي إذا لم يكن المنع على مستوى الموقع.",
-       "apihelp-query+blocks-param-show": "إظÙ\87ار Ø§Ù\84عÙ\86اصر Ø§Ù\84تÙ\8a ØªØ³ØªÙ\88Ù\81Ù\8a Ù\87Ø°Ù\87 Ø§Ù\84Ù\85عاÙ\8aÙ\8aر Ù\81Ù\82Ø·Ø\8c\nعÙ\84Ù\89 Ø³Ø¨Ù\8aÙ\84 Ø§Ù\84Ù\85ثاÙ\84Ø\8c Ù\84Ù\85شاÙ\87دة Ø¹Ù\85Ù\84Ù\8aات Ø§Ù\84Ù\85Ù\86ع ØºÙ\8aر Ø§Ù\84Ù\85حددة Ù\81Ù\82Ø· Ø¹Ù\84Ù\89 Ø¹Ù\86اÙ\88Ù\8aÙ\86 Ø¢يبي; اضبط <kbd>$1show=ip|!temp</kbd>.",
+       "apihelp-query+blocks-param-show": "إظÙ\87ار Ø§Ù\84عÙ\86اصر Ø§Ù\84تÙ\8a ØªØ³ØªÙ\88Ù\81Ù\8a Ù\87Ø°Ù\87 Ø§Ù\84Ù\85عاÙ\8aÙ\8aر Ù\81Ù\82Ø·Ø\8c\nعÙ\84Ù\89 Ø³Ø¨Ù\8aÙ\84 Ø§Ù\84Ù\85ثاÙ\84Ø\8c Ù\84Ù\85شاÙ\87دة Ø¹Ù\85Ù\84Ù\8aات Ø§Ù\84Ù\85Ù\86ع ØºÙ\8aر Ø§Ù\84Ù\85حددة Ù\81Ù\82Ø· Ø¹Ù\84Ù\89 Ø¹Ù\86اÙ\88Ù\8aÙ\86 Ø£يبي; اضبط <kbd>$1show=ip|!temp</kbd>.",
        "apihelp-query+blocks-example-simple": "قائمة المنع.",
        "apihelp-query+blocks-example-users": "إدراج عمليات منع المستخدمين <kbd>Alice</kbd> و<kbd>Bob</kbd>.",
        "apihelp-query+categories-summary": "أدرج جميع التصنيفات التي تنتمي إليها الصفحات.",
        "apihelp-query+recentchanges-param-excludeuser": "لا تسرد التغييرات بواسطة هذا المستخدم.",
        "apihelp-query+recentchanges-param-tag": "إدراج التغييرات الموسومة بهذ الوسم فقط.",
        "apihelp-query+recentchanges-param-prop": "تضمين أجزاء إضافية من المعلومات:",
-       "apihelp-query+recentchanges-paramvalue-prop-user": "Ù\8aضÙ\8aÙ\81 Ø§Ù\84Ù\85ستخدÙ\85 Ø§Ù\84Ù\85سؤÙ\88Ù\84 Ø¹Ù\86 Ø§Ù\84تحرÙ\8aر Ù\88اÙ\84Ù\88سÙ\88Ù\85 Ø¥Ø°Ø§ Ù\83اÙ\86 Ù\8aÙ\88جد Ø¢يبي.",
+       "apihelp-query+recentchanges-paramvalue-prop-user": "Ù\8aضÙ\8aÙ\81 Ø§Ù\84Ù\85ستخدÙ\85 Ø§Ù\84Ù\85سؤÙ\88Ù\84 Ø¹Ù\86 Ø§Ù\84تحرÙ\8aر Ù\88اÙ\84Ù\88سÙ\88Ù\85 Ø¥Ø°Ø§ Ù\83اÙ\86 Ù\8aÙ\88جد Ø£يبي.",
        "apihelp-query+recentchanges-paramvalue-prop-userid": "يضيف المستخدم المسؤول عن التعديل.",
        "apihelp-query+recentchanges-paramvalue-prop-comment": "يضيف التعليق للتحرير.",
        "apihelp-query+recentchanges-paramvalue-prop-parsedcomment": "يضيف التعليق المحلل للتحرير.",
        "apihelp-tokens-example-emailmove": "استرداد رمز بريد إلكتروني ورمز نقل.",
        "apihelp-unblock-summary": "إلغاء منع المستخدم.",
        "apihelp-unblock-param-id": "معرف المنع لرفع المنع (تم الحصول عليه من خلال <kbd>list=blocks</kbd>)، لا يمكن استخدامه مع <var>$1user</var> أو <var>$1userid</var>.",
-       "apihelp-unblock-param-user": "اسÙ\85 Ø§Ù\84Ù\85ستخدÙ\85Ø\8c Ø£Ù\88 Ø¹Ù\86Ù\88اÙ\86 Ø¢Ù\8aبÙ\8a Ø£Ù\88 Ù\86طاÙ\82 Ø¹Ù\86Ù\88اÙ\86 Ø¢يبي لمنعه، لا يمكن أن يُستخدَم جنبا إلى جنب مع <var>$1id</var> أو <var>$1userid</var>.",
+       "apihelp-unblock-param-user": "اسÙ\85 Ø§Ù\84Ù\85ستخدÙ\85Ø\8c Ø£Ù\88 Ø¹Ù\86Ù\88اÙ\86 Ø£Ù\8aبÙ\8a Ø£Ù\88 Ù\86طاÙ\82 Ø¹Ù\86Ù\88اÙ\86 Ø£يبي لمنعه، لا يمكن أن يُستخدَم جنبا إلى جنب مع <var>$1id</var> أو <var>$1userid</var>.",
        "apihelp-unblock-param-userid": "معرف الآيبي لرفع منعه، لا يمكن أن يُستخدَم جنبا إلى جنب مع <var>$1id</var> أو <var>$1user</var>.",
        "apihelp-unblock-param-reason": "سبب لرفع للمنع.",
        "apihelp-unblock-param-tags": "تغيير الوسوم للتطبيق على الإدخال في سجل المنع.",
index fad2389..73eb145 100644 (file)
@@ -37,7 +37,7 @@
        "apihelp-main-extended-description": "<div class=\"hlist plainlinks api-main-links\">\n* [[mw:Special:MyLanguage/API:Main_page|Documentation]]\n* [[mw:Special:MyLanguage/API:FAQ|FAQ]]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api Liste de diffusion]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce Annonces de l’API]\n* [https://phabricator.wikimedia.org/maniphest/query/GebfyV4uCaLd/#R Bogues et demandes]\n</div>\n<strong>État :</strong> L’API MédiaWiki est une interface stable et mature qui est supportée et améliorée de façon active. Bien que nous essayions de l’éviter, nous pouvons avoir parfois besoin de faire des modifications impactantes ; inscrivez-vous à [https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ la liste de diffusion mediawiki-api-announce] pour être informé des mises à jour.\n\n<strong>Requêtes erronées :</strong> Si des requêtes erronées sont envoyées à l’API, un entête HTTP sera renvoyé avec la clé « MediaWiki-API-Error ». La valeur de cet entête et le code d’erreur renvoyé prendront la même valeur. Pour plus d’information, voyez [[mw:Special:MyLanguage/API:Errors_and_warnings|API: Errors and warnings]].\n\n<p class=\"mw-apisandbox-link\"><strong>Test :</strong> Pour faciliter le test des requêtes de l’API, voyez [[Special:ApiSandbox]].</p>",
        "apihelp-main-param-action": "Quelle action effectuer.",
        "apihelp-main-param-format": "Le format de sortie.",
-       "apihelp-main-param-maxlag": "La latence maximale peut être utilisée quand MédiaWiki est installé sur un cluster de base de données répliqué. Pour éviter des actions provoquant un supplément de latence de réplication de site, ce paramètre peut faire attendre le client jusqu’à ce que la latence de réplication soit inférieure à une valeur spécifiée. En cas de latence excessive, le code d’erreur <samp>maxlag</samp> est renvoyé avec un message tel que <samp>Attente de $host : $lag secondes de délai</samp>.<br />Voyez [[mw:Special:MyLanguage/Manual:Maxlag_parameter|Manuel: Maxlag parameter]] pour plus d’information.",
+       "apihelp-main-param-maxlag": "La latence maximale peut être utilisée quand MédiaWiki est installé sur un cluster de base de données répliqué. Pour éviter des actions provoquant un supplément de latence de réplication de site, ce paramètre peut faire attendre le client jusqu’à ce que la latence de réplication soit inférieure à une valeur spécifiée. En cas de latence excessive, le code d’erreur <samp>maxlag</samp> est renvoyé avec un message tel que <samp>Attente de $host : $lag secondes de délai</samp>.<br />Voyez [[mw:Special:MyLanguage/Manual:Maxlag_parameter|Manuel: paramètre Maxlag]] pour plus d’information.",
        "apihelp-main-param-smaxage": "Fixer l’entête HTTP de contrôle de cache <code>s-maxage</code> à ce nombre de secondes. Les erreurs ne sont jamais mises en cache.",
        "apihelp-main-param-maxage": "Fixer l’entête HTTP de contrôle de cache <code>max-age</code> à ce nombre de secondes. Les erreurs ne sont jamais mises en cache.",
        "apihelp-main-param-assert": "Vérifier si l’utilisateur est connecté si la valeur est <kbd>user</kbd>, ou s’il a le droit d’un utilisateur robot si la valeur est <kbd>bot</kbd>.",
        "apihelp-query+deletedrevs-param-excludeuser": "Ne pas lister les révisions par cet utilisateur.",
        "apihelp-query+deletedrevs-param-namespace": "Lister uniquement les pages dans cet espace de noms.",
        "apihelp-query+deletedrevs-param-limit": "Le nombre maximal de révisions à lister.",
-       "apihelp-query+deletedrevs-param-prop": "Quelles propriétés obtenir :\n;revid : Ajoute l’ID de la révision supprimée.\n;parentid : Ajoute l’ID de la révision précédente de la page.\n;user : Ajoute l’utilisateur ayant fait la révision.\n;userid : Ajoute l’ID de l’utilisateur qui a fait la révision.\n;comment : Ajoute le commentaire de la révision.\n;parsedcomment : Ajoute le commentaire analysé de la révision.\n;minor : Marque si la révision est mineure.\n;len : Ajoute la longueur (en octets) de la révision.\n;sha1 : Ajoute le SHA-1 (base 16) de la révision.\n;content : Ajoute le contenu de la révision.\n;token : <span class=\"apihelp-deprecated\">Désuet.</span> Fournit le jeton de modification.\n;tags : Balises pour la révision.",
+       "apihelp-query+deletedrevs-param-prop": "Quelles propriétés obtenir :\n;revid : ajoute l’ID de la révision supprimée.\n;parentid : ajoute l’ID de la révision précédente de la page.\n;user : ajoute l’utilisateur ayant fait la révision.\n;userid : ajoute l’ID de l’utilisateur qui a fait la révision.\n;comment : ajoute le commentaire de la révision.\n;parsedcomment : ajoute le commentaire analysé de la révision.\n;minor : marque si la révision est mineure.\n;len : ajoute la longueur (en octets) de la révision.\n;sha1 : ajoute le SHA-1 (base 16) de la révision.\n;content : ajoute le contenu de la révision.\n;token : <span class=\"apihelp-deprecated\">désuet.</span> Fournit le jeton de modification.\n;tags : balises pour la révision.",
        "apihelp-query+deletedrevs-example-mode1": "Lister les dernières révisions supprimées des pages <kbd>Main Page</kbd> et <kbd>Talk:Main Page</kbd>, avec le contenu (mode 1).",
        "apihelp-query+deletedrevs-example-mode2": "Lister les 50 dernières contributions de <kbd>Bob</kbd> supprimées (mode 2).",
        "apihelp-query+deletedrevs-example-mode3-main": "Lister les 50 premières révisions supprimées dans l’espace de noms principal (mode 3)",
        "apihelp-unlinkaccount-summary": "Supprimer un compte tiers lié de l’utilisateur actuel.",
        "apihelp-unlinkaccount-example-simple": "Essayer de supprimer le lien de l’utilisateur actuel pour le fournisseur associé avec <kbd>FooAuthenticationRequest</kbd>.",
        "apihelp-upload-summary": "Téléverser un fichier, ou obtenir l’état des téléversements en cours.",
-       "apihelp-upload-extended-description": "Plusieurs méthodes sont disponibles :\n* Téléverser directement le contenu du fichier, en utilisant le paramètre <var>$1file</var>.\n* Téléverser le fichier par morceaux, en utilisant les paramètres <var>$1filesize</var>, <var>$1chunk</var>, and <var>$1offset</var>.\n* Pour que le serveur MédiaWiki cherche un fichier depuis une URL, utilisez le paramètre <var>$1url</var>.\n* Terminer un téléversement précédent qui a échoué à cause d’avertissements, en utilisant le paramètre <var>$1filekey</var>.\nNoter que le POST HTTP doit être fait comme un téléversement de fichier (par ex. en utilisant <code>multipart/form-data</code>) en envoyant le <code>multipart/form-data</code>.",
+       "apihelp-upload-extended-description": "Plusieurs méthodes sont disponibles :\n* Téléverser directement le contenu du fichier, en utilisant le paramètre <var>$1file</var>.\n* Téléverser le fichier par morceaux, en utilisant les paramètres <var>$1filesize</var>, <var>$1chunk</var>, and <var>$1offset</var>.\n* Pour que le serveur MédiaWiki cherche un fichier depuis une URL, utilisez le paramètre <var>$1url</var>.\n* Terminer un téléversement précédent qui a échoué à cause d’avertissements, en utilisant le paramètre <var>$1filekey</var>.\nNoter que le POST HTTP doit être fait comme un téléversement de fichier (par exemple en utilisant <code>multipart/form-data</code>) en envoyant le <var>$1file</var>.",
        "apihelp-upload-param-filename": "Nom de fichier cible.",
        "apihelp-upload-param-comment": "Téléverser le commentaire. Utilisé aussi comme texte de la page initiale pour les nouveaux fichiers si <var>$1text</var> n’est pas spécifié.",
        "apihelp-upload-param-tags": "Modifier les balises à appliquer à l’entrée du journal de téléversement et à la révision de la page du fichier.",
index 64d5563..bd3a620 100644 (file)
@@ -1,6 +1,6 @@
 <?php
 
-namespace Mediawiki\Logger;
+namespace MediaWiki\Logger;
 
 use Psr\Log\AbstractLogger;
 use Psr\Log\LoggerInterface;
index a3f918d..73a2525 100644 (file)
        "config-memcached-servers": "خوادم Memcached:",
        "config-memcached-help": "قائمة عناوين الآيبي لاستخدامها في Memcached،\nيجب تحديد واحد لكل سطر وتحديد المنفذ المراد استخدامه. على سبيل المثال:\n 127.0.0.1:11211\n 192.168.1.25:1234",
        "config-memcache-needservers": "لقد حددت Memcached كنوع ذاكرة تخزين مؤقت ولكن لم تحدد أية خوادم.",
-       "config-memcache-badip": "Ù\84Ù\82د Ø£Ø¯Ø®Ù\84ت Ø¹Ù\86Ù\88اÙ\86 Ø¢يبي غير صالح لـMemcached: $1.",
+       "config-memcache-badip": "Ù\84Ù\82د Ø£Ø¯Ø®Ù\84ت Ø¹Ù\86Ù\88اÙ\86 Ø£يبي غير صالح لـMemcached: $1.",
        "config-memcache-noport": "لم تحدد منفذا لاستخدامه في خادم Memcached: $1،\nإذا كنت لا تعرف المنفذ، يكون الافتراضي هو 11211.",
        "config-memcache-badport": "يجب أن تتراوح أرقام منافذ الذاكرة بين $1 و$2.",
        "config-extensions": "امتدادات",
index 40030c3..976c176 100644 (file)
@@ -202,12 +202,9 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
        const FLD_VALUE = 1; // key to the cached value
        const FLD_TTL = 2; // key to the original TTL
        const FLD_TIME = 3; // key to the cache time
-       const FLD_FLAGS = 4; // key to the flags bitfield
+       const FLD_FLAGS = 4; // key to the flags bitfield (reserved number)
        const FLD_HOLDOFF = 5; // key to any hold-off TTL
 
-       /** @var int Treat this value as expired-on-arrival */
-       const FLG_STALE = 1;
-
        const ERR_NONE = 0; // no error
        const ERR_NO_RESPONSE = 1; // no response
        const ERR_UNREACHABLE = 2; // can't connect
@@ -525,41 +522,67 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
 
                // Do not cache potentially uncommitted data as it might get rolled back
                if ( !empty( $opts['pending'] ) ) {
-                       $this->logger->info( 'Rejected set() for {cachekey} due to pending writes.',
-                               [ 'cachekey' => $key ] );
+                       $this->logger->info(
+                               'Rejected set() for {cachekey} due to pending writes.',
+                               [ 'cachekey' => $key ]
+                       );
 
                        return true; // no-op the write for being unsafe
                }
 
-               $wrapExtra = []; // additional wrapped value fields
+               $logicalTTL = null; // logical TTL override
                // Check if there's a risk of writing stale data after the purge tombstone expired
                if ( $lag === false || ( $lag + $age ) > self::MAX_READ_LAG ) {
-                       // Case A: read lag with "lockTSE"; save but record value as stale
-                       if ( $lockTSE >= 0 ) {
-                               $ttl = max( 1, (int)$lockTSE ); // set() expects seconds
-                               $wrapExtra[self::FLD_FLAGS] = self::FLG_STALE; // mark as stale
-                       // Case B: any long-running transaction; ignore this set()
-                       } elseif ( $age > self::MAX_READ_LAG ) {
-                               $this->logger->info( 'Rejected set() for {cachekey} due to snapshot lag.',
-                                       [ 'cachekey' => $key, 'lag' => $lag, 'age' => $age ] );
-
-                               return true; // no-op the write for being unsafe
-                       // Case C: high replication lag; lower TTL instead of ignoring all set()s
+                       // Case A: any long-running transaction
+                       if ( $age > self::MAX_READ_LAG ) {
+                               if ( $lockTSE >= 0 ) {
+                                       // Store value as *almost* stale to avoid cache and mutex stampedes
+                                       $logicalTTL = self::TTL_SECOND;
+                                       $this->logger->info(
+                                               'Lowered set() TTL for {cachekey} due to snapshot lag.',
+                                               [ 'cachekey' => $key, 'lag' => $lag, 'age' => $age ]
+                                       );
+                               } else {
+                                       $this->logger->info(
+                                               'Rejected set() for {cachekey} due to snapshot lag.',
+                                               [ 'cachekey' => $key, 'lag' => $lag, 'age' => $age ]
+                                       );
+
+                                       return true; // no-op the write for being unsafe
+                               }
+                       // Case B: high replication lag; lower TTL instead of ignoring all set()s
                        } elseif ( $lag === false || $lag > self::MAX_READ_LAG ) {
-                               $ttl = $ttl ? min( $ttl, self::TTL_LAGGED ) : self::TTL_LAGGED;
-                               $this->logger->warning( 'Lowered set() TTL for {cachekey} due to replication lag.',
-                                       [ 'cachekey' => $key, 'lag' => $lag, 'age' => $age ] );
-                       // Case D: medium length request with medium replication lag; ignore this set()
+                               if ( $lockTSE >= 0 ) {
+                                       $logicalTTL = min( $ttl ?: INF, self::TTL_LAGGED );
+                               } else {
+                                       $ttl = min( $ttl ?: INF, self::TTL_LAGGED );
+                               }
+                               $this->logger->warning(
+                                       'Lowered set() TTL for {cachekey} due to replication lag.',
+                                       [ 'cachekey' => $key, 'lag' => $lag, 'age' => $age ]
+                               );
+                       // Case C: medium length request with medium replication lag
                        } else {
-                               $this->logger->info( 'Rejected set() for {cachekey} due to high read lag.',
-                                       [ 'cachekey' => $key, 'lag' => $lag, 'age' => $age ] );
+                               if ( $lockTSE >= 0 ) {
+                                       // Store value as *almost* stale to avoid cache and mutex stampedes
+                                       $logicalTTL = self::TTL_SECOND;
+                                       $this->logger->info(
+                                               'Lowered set() TTL for {cachekey} due to high read lag.',
+                                               [ 'cachekey' => $key, 'lag' => $lag, 'age' => $age ]
+                                       );
+                               } else {
+                                       $this->logger->info(
+                                               'Rejected set() for {cachekey} due to high read lag.',
+                                               [ 'cachekey' => $key, 'lag' => $lag, 'age' => $age ]
+                                       );
 
-                               return true; // no-op the write for being unsafe
+                                       return true; // no-op the write for being unsafe
+                               }
                        }
                }
 
                // Wrap that value with time/TTL/version metadata
-               $wrapped = $this->wrap( $value, $ttl, $now ) + $wrapExtra;
+               $wrapped = $this->wrap( $value, $logicalTTL ?: $ttl, $now );
 
                $func = function ( $cache, $key, $cWrapped ) use ( $wrapped ) {
                        return ( is_string( $cWrapped ) )
@@ -1222,19 +1245,18 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
                $minTime = $opts['minAsOf'] ?? self::MIN_TIMESTAMP_NONE;
                $versioned = isset( $opts['version'] );
                $touchedCallback = $opts['touchedCallback'] ?? null;
+               $initialTime = $this->getCurrentTime();
 
                // Get a collection name to describe this class of key
                $kClass = $this->determineKeyClass( $key );
 
-               // Get the current key value
+               // Get the current key value and populate $curTTL and $asOf accordingly
                $curTTL = null;
                $cValue = $this->get( $key, $curTTL, $checkKeys, $asOf ); // current value
                $value = $cValue; // return value
-
                // Apply additional dynamic expiration logic if supplied
                $curTTL = $this->applyTouchedCallback( $value, $asOf, $curTTL, $touchedCallback );
 
-               $preCallbackTime = $this->getCurrentTime();
                // Determine if a cached value regeneration is needed or desired
                if (
                        $this->isValid( $value, $versioned, $asOf, $minTime ) &&
@@ -1242,7 +1264,7 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
                ) {
                        $preemptiveRefresh = (
                                $this->worthRefreshExpiring( $curTTL, $lowTTL ) ||
-                               $this->worthRefreshPopular( $asOf, $ageNew, $popWindow, $preCallbackTime )
+                               $this->worthRefreshPopular( $asOf, $ageNew, $popWindow, $initialTime )
                        );
 
                        if ( !$preemptiveRefresh ) {
@@ -1267,7 +1289,7 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
                // Decide if only one thread should handle regeneration at a time
                $useMutex =
                        // Note that since tombstones no-op set(), $lockTSE and $curTTL cannot be used to
-                       // deduce the key hotness because $curTTL will always keep increasing until the
+                       // deduce the key hotness because |$curTTL| will always keep increasing until the
                        // tombstone expires or is overwritten by a new tombstone. Also, even if $lockTSE
                        // is not set, constant regeneration of a key for the tombstone lifetime might be
                        // very expensive. Assume tombstoned keys are possibly hot in order to reduce
@@ -1317,6 +1339,7 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
                        throw new InvalidArgumentException( "Invalid cache miss callback provided." );
                }
 
+               $preCallbackTime = $this->getCurrentTime();
                // Generate the new value from the callback...
                $setOpts = [];
                ++$this->callbackDepth;
@@ -1328,7 +1351,9 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
                $valueIsCacheable = ( $value !== false && $ttl >= 0 );
 
                if ( $valueIsCacheable ) {
-                       $ago = max( $this->getCurrentTime() - $preCallbackTime, 0.0 );
+                       $ago = max( $this->getCurrentTime() - $initialTime, 0.0 );
+                       $this->stats->timing( "wanobjectcache.$kClass.regen_set_delay", 1000 * $ago );
+
                        if ( $isKeyTombstoned ) {
                                if ( $this->checkAndSetCooloff( $key, $kClass, $ago, $lockTSE, $hasLock ) ) {
                                        // When delete() is called, writes are write-holed by the tombstone,
@@ -1354,7 +1379,7 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
 
                if ( $hasLock ) {
                        // Avoid using delete() to avoid pointless mcrouter broadcasting
-                       $this->cache->changeTTL( self::MUTEX_KEY_PREFIX . $key, (int)$preCallbackTime - 60 );
+                       $this->cache->changeTTL( self::MUTEX_KEY_PREFIX . $key, (int)$initialTime - 60 );
                }
 
                $miss = is_infinite( $minTime ) ? 'renew' : 'miss';
@@ -2146,12 +2171,7 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
                        return [ false, null ];
                }
 
-               $flags = $wrapped[self::FLD_FLAGS] ?? 0;
-               if ( ( $flags & self::FLG_STALE ) == self::FLG_STALE ) {
-                       // Treat as expired, with the cache time as the expiration
-                       $age = $now - $wrapped[self::FLD_TIME];
-                       $curTTL = min( -$age, self::TINY_NEGATIVE );
-               } elseif ( $wrapped[self::FLD_TTL] > 0 ) {
+               if ( $wrapped[self::FLD_TTL] > 0 ) {
                        // Get the approximate time left on the key
                        $age = $now - $wrapped[self::FLD_TIME];
                        $curTTL = max( $wrapped[self::FLD_TTL] - $age, 0.0 );
index 0935d5a..381b1fd 100644 (file)
@@ -653,6 +653,18 @@ __INDEXATTR__;
                return true;
        }
 
+       protected function makeUpdateOptionsArray( $options ) {
+               if ( !is_array( $options ) ) {
+                       $options = [ $options ];
+               }
+
+               // PostgreSQL doesn't support anything like "ignore" for
+               // UPDATE.
+               $options = array_diff( $options, [ 'IGNORE' ] );
+
+               return parent::makeUpdateOptionsArray( $options );
+       }
+
        /**
         * INSERT SELECT wrapper
         * $varMap must be an associative array of the form [ 'dest1' => 'source1', ... ]
index f2e4e3d..9e4b15f 100644 (file)
@@ -1842,12 +1842,6 @@ class LoadBalancer implements ILoadBalancer {
                }
        }
 
-       /**
-        * @param IDatabase $conn
-        * @param DBMasterPos|bool $pos
-        * @param int|null $timeout
-        * @return bool
-        */
        public function safeWaitForMasterPos( IDatabase $conn, $pos = false, $timeout = null ) {
                $timeout = max( 1, $timeout ?: $this->waitTimeout );
 
@@ -1862,6 +1856,12 @@ class LoadBalancer implements ILoadBalancer {
                                $pos = $masterConn->getMasterPos();
                        } else {
                                $masterConn = $this->openConnection( $this->getWriterIndex(), self::DOMAIN_ANY );
+                               if ( !$masterConn ) {
+                                       throw new DBReplicationWaitError(
+                                               null,
+                                               "Could not obtain a master database connection to get the position"
+                                       );
+                               }
                                $pos = $masterConn->getMasterPos();
                                $this->closeConnection( $masterConn );
                        }
index 2161b66..fcddfcf 100644 (file)
@@ -26,7 +26,12 @@ namespace Wikimedia\Rdbms;
 use InvalidArgumentException;
 
 /**
- * Trivial LoadBalancer that always returns an injected connection handle
+ * Trivial LoadBalancer that always returns an injected connection handle.
+ *
+ * Note that, while this LoadBalancer does not open any connections itself,
+ * it still closes the injected connection at times, including during destruction.
+ * It is therefore unsuitable for use in tests unless you have a Database instance
+ * separate from the main test database (which is expected to stay open).
  */
 class LoadBalancerSingle extends LoadBalancer {
        /** @var IDatabase */
@@ -55,7 +60,8 @@ class LoadBalancerSingle extends LoadBalancer {
                        'trxProfiler' => $params['trxProfiler'] ?? null,
                        'srvCache' => $params['srvCache'] ?? null,
                        'wanCache' => $params['wanCache'] ?? null,
-                       'localDomain' => $params['localDomain'] ?? $this->db->getDomainID()
+                       'localDomain' => $params['localDomain'] ?? $this->db->getDomainID(),
+                       'readOnlyReason' => $params['readOnlyReason'] ?? false,
                ] );
 
                if ( isset( $params['readOnlyReason'] ) ) {
index d09620b..db29760 100644 (file)
@@ -33,9 +33,10 @@ class RedisConnRef implements LoggerAwareInterface {
        protected $pool;
        /** @var Redis */
        protected $conn;
-
-       protected $server; // string
-       protected $lastError; // string
+       /** @var string */
+       protected $server;
+       /** @var string|null */
+       protected $lastError;
 
        /**
         * @var LoggerInterface
index e2d32cf..b8b706d 100644 (file)
@@ -91,7 +91,7 @@ class BitmapHandler extends TransformationalImageHandler {
         * @param array &$params
         * @return bool
         */
-       function normaliseParams( $image, &$params ) {
+       public function normaliseParams( $image, &$params ) {
                global $wgMaxInterlacingAreas;
                if ( !parent::normaliseParams( $image, $params ) ) {
                        return false;
index fa5b0a6..8e7998e 100644 (file)
@@ -37,7 +37,7 @@ class BitmapHandler_ClientOnly extends BitmapHandler {
         * @param array &$params
         * @return bool
         */
-       function normaliseParams( $image, &$params ) {
+       public function normaliseParams( $image, &$params ) {
                return ImageHandler::normaliseParams( $image, $params );
        }
 
index ed6e76f..09cbdac 100644 (file)
@@ -44,7 +44,7 @@ class BmpHandler extends BitmapHandler {
         * @param array|null $params
         * @return array
         */
-       function getThumbType( $text, $mime, $params = null ) {
+       public function getThumbType( $text, $mime, $params = null ) {
                return [ 'png', 'image/png' ];
        }
 
index 00dfb72..ed62ba1 100644 (file)
@@ -32,7 +32,7 @@ class DjVuHandler extends ImageHandler {
        /**
         * @return bool
         */
-       function isEnabled() {
+       public function isEnabled() {
                global $wgDjvuRenderer, $wgDjvuDump, $wgDjvuToXML;
                if ( !$wgDjvuRenderer || ( !$wgDjvuDump && !$wgDjvuToXML ) ) {
                        wfDebug( "DjVu is disabled, please set \$wgDjvuRenderer and \$wgDjvuDump\n" );
@@ -130,7 +130,7 @@ class DjVuHandler extends ImageHandler {
         * @param array $params
         * @return array
         */
-       function getScriptParams( $params ) {
+       protected function getScriptParams( $params ) {
                return [
                        'width' => $params['width'],
                        'page' => $params['page'],
@@ -353,7 +353,7 @@ class DjVuHandler extends ImageHandler {
                return $this->getDjVuImage( $image, $path )->getImageSize();
        }
 
-       function getThumbType( $ext, $mime, $params = null ) {
+       public function getThumbType( $ext, $mime, $params = null ) {
                global $wgDjvuOutputExtension;
                static $mime;
                if ( !isset( $mime ) ) {
@@ -364,7 +364,7 @@ class DjVuHandler extends ImageHandler {
                return [ $wgDjvuOutputExtension, $mime ];
        }
 
-       function getMetadata( $image, $path ) {
+       public function getMetadata( $image, $path ) {
                wfDebug( "Getting DjVu metadata for $path\n" );
 
                $xml = $this->getDjVuImage( $image, $path )->retrieveMetaData();
@@ -380,17 +380,17 @@ class DjVuHandler extends ImageHandler {
                return 'djvuxml';
        }
 
-       function isMetadataValid( $image, $metadata ) {
+       public function isMetadataValid( $image, $metadata ) {
                return !empty( $metadata ) && $metadata != serialize( [] );
        }
 
-       function pageCount( File $image ) {
+       public function pageCount( File $image ) {
                $info = $this->getDimensionInfo( $image );
 
                return $info ? $info['pageCount'] : false;
        }
 
-       function getPageDimensions( File $image, $page ) {
+       public function getPageDimensions( File $image, $page ) {
                $index = $page - 1; // MW starts pages at 1
 
                $info = $this->getDimensionInfo( $image );
index 4267210..1760eb8 100644 (file)
@@ -83,7 +83,7 @@ class ExifBitmapHandler extends BitmapHandler {
         * @param array $metadata
         * @return bool|int
         */
-       function isMetadataValid( $image, $metadata ) {
+       public function isMetadataValid( $image, $metadata ) {
                global $wgShowEXIF;
                if ( !$wgShowEXIF ) {
                        # Metadata disabled and so an empty field is expected
@@ -127,7 +127,7 @@ class ExifBitmapHandler extends BitmapHandler {
         * @param bool|IContextSource $context Context to use (optional)
         * @return array|bool
         */
-       function formatMetadata( $image, $context = false ) {
+       public function formatMetadata( $image, $context = false ) {
                $meta = $this->getCommonMetaArray( $image );
                if ( count( $meta ) === 0 ) {
                        return false;
index d65f872..556e83c 100644 (file)
@@ -29,7 +29,7 @@
 class GIFHandler extends BitmapHandler {
        const BROKEN_FILE = '0'; // value to store in img_metadata if error extracting metadata.
 
-       function getMetadata( $image, $filename ) {
+       public function getMetadata( $image, $filename ) {
                try {
                        $parsedGIFMetadata = BitmapMetadataHandler::GIF( $filename );
                } catch ( Exception $e ) {
@@ -47,7 +47,7 @@ class GIFHandler extends BitmapHandler {
         * @param bool|IContextSource $context Context to use (optional)
         * @return array|bool
         */
-       function formatMetadata( $image, $context = false ) {
+       public function formatMetadata( $image, $context = false ) {
                $meta = $this->getCommonMetaArray( $image );
                if ( count( $meta ) === 0 ) {
                        return false;
@@ -125,7 +125,7 @@ class GIFHandler extends BitmapHandler {
                return 'parsed-gif';
        }
 
-       function isMetadataValid( $image, $metadata ) {
+       public function isMetadataValid( $image, $metadata ) {
                if ( $metadata === self::BROKEN_FILE ) {
                        // Do not repetitivly regenerate metadata on broken file.
                        return self::METADATA_GOOD;
@@ -156,7 +156,7 @@ class GIFHandler extends BitmapHandler {
         * @param File $image
         * @return string
         */
-       function getLongDesc( $image ) {
+       public function getLongDesc( $image ) {
                global $wgLang;
 
                $original = parent::getLongDesc( $image );
index e88c1b0..302e743 100644 (file)
@@ -74,7 +74,7 @@ abstract class ImageHandler extends MediaHandler {
                }
        }
 
-       function getScriptParams( $params ) {
+       protected function getScriptParams( $params ) {
                return [ 'width' => $params['width'] ];
        }
 
@@ -238,7 +238,7 @@ abstract class ImageHandler extends MediaHandler {
         * @param File $file
         * @return string
         */
-       function getLongDesc( $file ) {
+       public function getLongDesc( $file ) {
                global $wgLang;
                $pages = $file->pageCount();
                $size = htmlspecialchars( $wgLang->formatSize( $file->getSize() ) );
index f7de5f5..4bcb53d 100644 (file)
@@ -34,7 +34,7 @@ class JpegHandler extends ExifBitmapHandler {
        const SRGB_EXIF_COLOR_SPACE = 'sRGB';
        const SRGB_ICC_PROFILE_DESCRIPTION = 'sRGB IEC61966-2.1';
 
-       function normaliseParams( $image, &$params ) {
+       public function normaliseParams( $image, &$params ) {
                if ( !parent::normaliseParams( $image, $params ) ) {
                        return false;
                }
@@ -90,7 +90,7 @@ class JpegHandler extends ExifBitmapHandler {
                return $res;
        }
 
-       function getScriptParams( $params ) {
+       protected function getScriptParams( $params ) {
                $res = parent::getScriptParams( $params );
                if ( isset( $params['quality'] ) ) {
                        $res['quality'] = $params['quality'];
@@ -98,7 +98,7 @@ class JpegHandler extends ExifBitmapHandler {
                return $res;
        }
 
-       function getMetadata( $image, $filename ) {
+       public function getMetadata( $image, $filename ) {
                try {
                        $meta = BitmapMetadataHandler::Jpeg( $filename );
                        if ( !is_array( $meta ) ) {
index 66a4291..a54da7d 100644 (file)
@@ -87,7 +87,7 @@ abstract class MediaHandler {
         * @param File $image
         * @param array &$params
         */
-       abstract function normaliseParams( $image, &$params );
+       abstract public function normaliseParams( $image, &$params );
 
        /**
         * Get an image size array like that returned by getimagesize(), or false if it
@@ -119,7 +119,7 @@ abstract class MediaHandler {
         * @param string $path The filename
         * @return string A string of metadata in php serialized form (Run through serialize())
         */
-       function getMetadata( $image, $path ) {
+       public function getMetadata( $image, $path ) {
                return '';
        }
 
@@ -195,7 +195,7 @@ abstract class MediaHandler {
         * @param string $metadata The metadata in serialized form
         * @return bool
         */
-       function isMetadataValid( $image, $metadata ) {
+       public function isMetadataValid( $image, $metadata ) {
                return self::METADATA_GOOD;
        }
 
@@ -287,7 +287,7 @@ abstract class MediaHandler {
         * @param array|null $params Handler specific rendering parameters
         * @return array Thumbnail extension and MIME type
         */
-       function getThumbType( $ext, $mime, $params = null ) {
+       public function getThumbType( $ext, $mime, $params = null ) {
                $magic = MediaWiki\MediaWikiServices::getInstance()->getMimeAnalyzer();
                if ( !$ext || $magic->isMatchingExtension( $ext, $mime ) === false ) {
                        // The extension is not valid for this MIME type and we do
@@ -340,7 +340,7 @@ abstract class MediaHandler {
         * @param File $file
         * @return bool
         */
-       function pageCount( File $file ) {
+       public function pageCount( File $file ) {
                return false;
        }
 
@@ -381,7 +381,7 @@ abstract class MediaHandler {
         * False if the handler is disabled for all files
         * @return bool
         */
-       function isEnabled() {
+       public function isEnabled() {
                return true;
        }
 
@@ -401,7 +401,7 @@ abstract class MediaHandler {
         * @param int $page What page to get dimensions of
         * @return array|bool
         */
-       function getPageDimensions( File $image, $page ) {
+       public function getPageDimensions( File $image, $page ) {
                $gis = $this->getImageSize( $image, $image->getLocalRefPath() );
                if ( $gis ) {
                        return [
@@ -477,7 +477,7 @@ abstract class MediaHandler {
         * @param bool|IContextSource $context Context to use (optional)
         * @return array|bool
         */
-       function formatMetadata( $image, $context = false ) {
+       public function formatMetadata( $image, $context = false ) {
                return false;
        }
 
@@ -582,7 +582,7 @@ abstract class MediaHandler {
         * @param File $file
         * @return string
         */
-       function getLongDesc( $file ) {
+       public function getLongDesc( $file ) {
                return self::getGeneralLongDesc( $file );
        }
 
@@ -660,7 +660,7 @@ abstract class MediaHandler {
         * @param string $fileName The local path to the file.
         * @return Status
         */
-       function verifyUpload( $fileName ) {
+       public function verifyUpload( $fileName ) {
                return Status::newGood();
        }
 
index 12048a9..34f7e8c 100644 (file)
@@ -275,6 +275,8 @@ abstract class MediaTransformOutput {
  * @ingroup Media
  */
 class ThumbnailImage extends MediaTransformOutput {
+       private static $firstNonIconImageRendered = false;
+
        /**
         * Get a thumbnail object from a file and parameters.
         * If $path is set to null, the output file is treated as a source copy.
@@ -356,6 +358,8 @@ class ThumbnailImage extends MediaTransformOutput {
         * @return string
         */
        function toHtml( $options = [] ) {
+               global $wgPriorityHints;
+
                if ( count( func_get_args() ) == 2 ) {
                        throw new MWException( __METHOD__ . ' called in the old style' );
                }
@@ -370,6 +374,14 @@ class ThumbnailImage extends MediaTransformOutput {
                        'decoding' => 'async',
                ];
 
+               if ( $wgPriorityHints
+                       && !self::$firstNonIconImageRendered
+                       && $this->width * $this->height > 100 * 100 ) {
+                       self::$firstBigImageRendered = true;
+
+                       $attribs['importance'] = 'high';
+               }
+
                if ( !empty( $options['custom-url-link'] ) ) {
                        $linkAttribs = [ 'href' => $options['custom-url-link'] ];
                        if ( !empty( $options['title'] ) ) {
index 6748b26..a4b4dc5 100644 (file)
@@ -34,7 +34,7 @@ class PNGHandler extends BitmapHandler {
         * @param string $filename
         * @return string
         */
-       function getMetadata( $image, $filename ) {
+       public function getMetadata( $image, $filename ) {
                try {
                        $metadata = BitmapMetadataHandler::PNG( $filename );
                } catch ( Exception $e ) {
@@ -52,7 +52,7 @@ class PNGHandler extends BitmapHandler {
         * @param bool|IContextSource $context Context to use (optional)
         * @return array|bool
         */
-       function formatMetadata( $image, $context = false ) {
+       public function formatMetadata( $image, $context = false ) {
                $meta = $this->getCommonMetaArray( $image );
                if ( count( $meta ) === 0 ) {
                        return false;
@@ -111,7 +111,7 @@ class PNGHandler extends BitmapHandler {
                return 'parsed-png';
        }
 
-       function isMetadataValid( $image, $metadata ) {
+       public function isMetadataValid( $image, $metadata ) {
                if ( $metadata === self::BROKEN_FILE ) {
                        // Do not repetitivly regenerate metadata on broken file.
                        return self::METADATA_GOOD;
@@ -142,7 +142,7 @@ class PNGHandler extends BitmapHandler {
         * @param File $image
         * @return string
         */
-       function getLongDesc( $image ) {
+       public function getLongDesc( $image ) {
                global $wgLang;
                $original = parent::getLongDesc( $image );
 
index e3057f4..b7745df 100644 (file)
@@ -41,7 +41,7 @@ class SvgHandler extends ImageHandler {
                'title' => 'ObjectName',
        ];
 
-       function isEnabled() {
+       public function isEnabled() {
                global $wgSVGConverters, $wgSVGConverter;
                if ( !isset( $wgSVGConverters[$wgSVGConverter] ) ) {
                        wfDebug( "\$wgSVGConverter is invalid, disabling SVG rendering.\n" );
@@ -399,7 +399,7 @@ class SvgHandler extends ImageHandler {
                }
        }
 
-       function getThumbType( $ext, $mime, $params = null ) {
+       public function getThumbType( $ext, $mime, $params = null ) {
                return [ 'png', 'image/png' ];
        }
 
@@ -412,7 +412,7 @@ class SvgHandler extends ImageHandler {
         * @param File $file
         * @return string
         */
-       function getLongDesc( $file ) {
+       public function getLongDesc( $file ) {
                global $wgLang;
 
                $metadata = $this->unpackMetadata( $file->getMetadata() );
@@ -438,7 +438,7 @@ class SvgHandler extends ImageHandler {
         * @param string $filename
         * @return string Serialised metadata
         */
-       function getMetadata( $file, $filename ) {
+       public function getMetadata( $file, $filename ) {
                $metadata = [ 'version' => self::SVG_METADATA_VERSION ];
                try {
                        $metadata += SVGMetadataExtractor::getMetadata( $filename );
@@ -469,7 +469,7 @@ class SvgHandler extends ImageHandler {
                return 'parsed-svg';
        }
 
-       function isMetadataValid( $image, $metadata ) {
+       public function isMetadataValid( $image, $metadata ) {
                $meta = $this->unpackMetadata( $metadata );
                if ( $meta === false ) {
                        return self::METADATA_BAD;
@@ -493,7 +493,7 @@ class SvgHandler extends ImageHandler {
         * @param bool|IContextSource $context Context to use (optional)
         * @return array|bool
         */
-       function formatMetadata( $file, $context = false ) {
+       public function formatMetadata( $file, $context = false ) {
                $result = [
                        'visible' => [],
                        'collapsed' => []
@@ -594,7 +594,7 @@ class SvgHandler extends ImageHandler {
         * @param array $params
         * @return array
         */
-       function getScriptParams( $params ) {
+       protected function getScriptParams( $params ) {
                $scriptParams = [ 'width' => $params['width'] ];
                if ( isset( $params['lang'] ) ) {
                        $scriptParams['lang'] = $params['lang'];
index 441513e..15c4dbf 100644 (file)
@@ -64,7 +64,7 @@ class TiffHandler extends ExifBitmapHandler {
         * @param array|null $params
         * @return bool
         */
-       function getThumbType( $ext, $mime, $params = null ) {
+       public function getThumbType( $ext, $mime, $params = null ) {
                global $wgTiffThumbnailType;
 
                return $wgTiffThumbnailType;
@@ -76,7 +76,7 @@ class TiffHandler extends ExifBitmapHandler {
         * @throws MWException
         * @return string
         */
-       function getMetadata( $image, $filename ) {
+       public function getMetadata( $image, $filename ) {
                global $wgShowEXIF;
 
                if ( $wgShowEXIF ) {
index ea0d61b..38dc390 100644 (file)
@@ -41,7 +41,7 @@ abstract class TransformationalImageHandler extends ImageHandler {
         * 'physicalWidth' and 'physicalHeight' indicate the thumbnail dimensions.
         * @return bool
         */
-       function normaliseParams( $image, &$params ) {
+       public function normaliseParams( $image, &$params ) {
                if ( !parent::normaliseParams( $image, $params ) ) {
                        return false;
                }
index 0cb618f..33f33bd 100644 (file)
@@ -49,7 +49,7 @@ class XCFHandler extends BitmapHandler {
         * @param array|null $params
         * @return array
         */
-       function getThumbType( $ext, $mime, $params = null ) {
+       public function getThumbType( $ext, $mime, $params = null ) {
                return [ 'png', 'image/png' ];
        }
 
index 4158082..3d13350 100644 (file)
@@ -424,7 +424,7 @@ class ResourceLoader implements LoggerAwareInterface {
                // Add the QUnit testrunner as implicit dependency to extension test suites.
                foreach ( $testModules['qunit'] as &$module ) {
                        // Shuck any single-module dependency as an array
-                       if ( is_string( $module['dependencies'] ) ) {
+                       if ( isset( $module['dependencies'] ) && is_string( $module['dependencies'] ) ) {
                                $module['dependencies'] = [ $module['dependencies'] ];
                        }
 
index b711cbd..c555eb8 100644 (file)
@@ -28,6 +28,7 @@
  */
 class ResourceLoaderImageModule extends ResourceLoaderModule {
 
+       /** @var array|null */
        protected $definition = null;
 
        /**
@@ -38,10 +39,18 @@ class ResourceLoaderImageModule extends ResourceLoaderModule {
 
        protected $origin = self::ORIGIN_CORE_SITEWIDE;
 
+       /** @var ResourceLoaderImage[]|null */
+       protected $imageObjects = null;
+       /** @var array */
        protected $images = [];
+       /** @var string|null */
        protected $defaultColor = null;
        protected $useDataURI = true;
+       /** @var array|null */
+       protected $globalVariants = null;
+       /** @var array */
        protected $variants = [];
+       /** @var string|null */
        protected $prefix = null;
        protected $selectorWithoutVariant = '.{prefix}-{name}';
        protected $selectorWithVariant = '.{prefix}-{name}-{variant}';
@@ -240,7 +249,7 @@ class ResourceLoaderImageModule extends ResourceLoaderModule {
         */
        public function getImages( ResourceLoaderContext $context ) {
                $skin = $context->getSkin();
-               if ( !isset( $this->imageObjects ) ) {
+               if ( $this->imageObjects === null ) {
                        $this->loadFromDefinition();
                        $this->imageObjects = [];
                }
@@ -288,7 +297,7 @@ class ResourceLoaderImageModule extends ResourceLoaderModule {
         */
        public function getGlobalVariants( ResourceLoaderContext $context ) {
                $skin = $context->getSkin();
-               if ( !isset( $this->globalVariants ) ) {
+               if ( $this->globalVariants === null ) {
                        $this->loadFromDefinition();
                        $this->globalVariants = [];
                }
index b36855f..7d2a59b 100644 (file)
        "revdelete-hide-image": "أخف محتوى الملف",
        "revdelete-hide-name": "أخف الفعل والهدف",
        "revdelete-hide-comment": "أخف تعليق التعديل",
-       "revdelete-hide-user": "أخÙ\81 Ø§Ø³Ù\85/Ø¢يبي المستخدم",
+       "revdelete-hide-user": "اسÙ\85/Ø£يبي المستخدم",
        "revdelete-hide-restricted": "أخف البيانات عن الإداريين إضافة إلى الآخرين",
        "revdelete-radio-same": "(لا تغير)",
        "revdelete-radio-set": "نعم",
index eed3fc1..7507ac6 100644 (file)
        "block": "منع المستخدم",
        "unblock": "إلغاء منع مستخدم",
        "blockip": "منع {{GENDER:$1|المستخدم|المستخدمة}}",
-       "blockiptext": "استخدÙ\85 Ø§Ù\84Ù\86Ù\85Ù\88ذج Ø§Ù\84تاÙ\84Ù\8a Ù\84Ù\85Ù\86ع Ù\85ستخدÙ\85Ø\8c Ø£Ù\88 Ø¹Ù\86Ù\88اÙ\86 Ø¢Ù\8aبÙ\8aØ\8c Ù\85عÙ\8aÙ\86 Ù\85Ù\86 Ø§Ù\84تعدÙ\8aÙ\84 Ø£Ù\88 Ø¥Ù\86شاء Ø­Ø³Ø§Ø¨Ø§Øª Ø¬Ø¯Ù\8aدة. ØªÙ\8fستخدÙ\85 Ù\87Ø°Ù\87 Ø§Ù\84عÙ\85Ù\84Ù\8aØ© Ù\84Ù\85Ù\86ع Ø§Ù\84تخرÙ\8aب Ù\81Ù\82Ø·Ø\8c Ù\88Ù\8aجب Ø£Ù\86 ØªØªÙ\85اشÙ\89 Ù\85ع [[{{MediaWiki:Policy-url}}|سÙ\8aاسة Ø§Ù\84Ù\85Ù\86ع]]. Ø£Ø¯Ø®Ù\84 ØªØ¹Ù\84Ù\8aÙ\84اÙ\8b Ù\88اضحÙ\8bا Ù\84سبب Ø§Ù\84Ù\85Ù\86ع Ù\81Ù\8a Ø§Ù\84خاÙ\86Ø© Ø§Ù\84Ù\85خصصة Ù\84Ø°Ù\84Ù\83 (Ù\85Ø«Ù\84اÙ\8b: Ø°Ù\83ر ØµÙ\81حات Ù\85حددة ØªÙ\85Ù\91 ØªØ®Ø±Ù\8aبÙ\87ا Ù\85Ù\86 Ù\82بÙ\84 Ø§Ù\84Ù\85ستخدÙ\85).\nÙ\8aÙ\85Ù\83Ù\86Ù\83 Ù\85Ù\86ع Ù\86طاÙ\82ات Ø¹Ù\86اÙ\88Ù\8aÙ\86 IP Ø¨Ø§Ø³ØªØ®Ø¯Ø§Ù\85 [https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing CIDR] Ù\82Ù\88اعد; Ø£Ù\83بر Ù\86طاÙ\82 Ù\85سÙ\85Ù\88Ø­ Ø¨Ù\87 Ù\87Ù\88 /$1 Ø¥Ù\84Ù\89 IPv4 Ù\88 /$2 Ø¥Ù\84Ù\89 IPv6.",
+       "blockiptext": "استخدÙ\85 Ø§Ù\84Ù\86Ù\85Ù\88ذج Ø§Ù\84تاÙ\84Ù\8a Ù\84Ù\85Ù\86ع Ù\85ستخدÙ\85Ø\8c Ø£Ù\88 Ø¹Ù\86Ù\88اÙ\86 Ø£Ù\8aبÙ\8aØ\8c Ù\85عÙ\8aÙ\86 Ù\85Ù\86 Ø§Ù\84تعدÙ\8aÙ\84 Ø£Ù\88 Ø¥Ù\86شاء Ø­Ø³Ø§Ø¨Ø§Øª Ø¬Ø¯Ù\8aدة.\nتÙ\8fستخدÙ\85 Ù\87Ø°Ù\87 Ø§Ù\84عÙ\85Ù\84Ù\8aØ© Ù\84Ù\85Ù\86ع Ø§Ù\84تخرÙ\8aب Ù\81Ù\82Ø·Ø\8c Ù\88Ù\8aجب Ø£Ù\86 ØªØªÙ\85اشÙ\89 Ù\85ع [[{{MediaWiki:Policy-url}}|سÙ\8aاسة Ø§Ù\84Ù\85Ù\86ع]].\nأدخÙ\84 ØªØ¹Ù\84Ù\8aÙ\84اÙ\8b Ù\88اضحÙ\8bا Ù\84سبب Ø§Ù\84Ù\85Ù\86ع Ù\81Ù\8a Ø§Ù\84خاÙ\86Ø© Ø§Ù\84Ù\85خصصة Ù\84Ø°Ù\84Ù\83 (Ù\85Ø«Ù\84اÙ\8b: Ø°Ù\83ر ØµÙ\81حات Ù\85حددة ØªÙ\85Ù\91 ØªØ®Ø±Ù\8aبÙ\87ا Ù\85Ù\86 Ù\82بÙ\84 Ø§Ù\84Ù\85ستخدÙ\85).\nÙ\8aÙ\85Ù\83Ù\86Ù\83 Ù\85Ù\86ع Ù\86طاÙ\82ات Ø¹Ù\86اÙ\88Ù\8aÙ\86 IP Ø¨Ø§Ø³ØªØ®Ø¯Ø§Ù\85 [https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing CIDR] Ù\82Ù\88اعد; Ø£Ù\83بر Ù\86طاÙ\82 Ù\85سÙ\85Ù\88Ø­ Ø¨Ù\87 Ù\87Ù\88 /$1 Ù\84IPv4 Ù\88 /$2 Ù\84IPv6.",
        "ipaddressorusername": "عنوان الأيبي أو اسم المستخدم:",
        "ipbreason": "السبب:",
        "ipbreason-dropdown": "*أسباب المنع الشائعة\n** كتابة معلومات زائفة\n** إزالة المحتوى من الصفحات\n** سبام وصلات لمواقع خارجية\n** كتابة كلام لا معنى له في الصفحات\n** سلوك عدواني\n** إساءة استخدام حسابات متعددة\n** اسم مستخدم غير مقبول",
        "ipblocklist-legend": "إيجاد مستخدم ممنوع",
        "blocklist-userblocks": "أخفِ منع الحسابات",
        "blocklist-tempblocks": "أخفِ المنع المؤقت",
-       "blocklist-addressblocks": "أخÙ\81Ù\90 Ù\85Ù\86ع Ø¹Ù\86Ù\88اÙ\86 Ø¢يبي واحد",
+       "blocklist-addressblocks": "أخÙ\81Ù\90 Ù\85Ù\86ع Ø¹Ù\86Ù\88اÙ\86 Ø£يبي واحد",
        "blocklist-rangeblocks": "أخفِ منع النطاقات",
        "blocklist-timestamp": "الزمن",
        "blocklist-target": "الهدف",
index 4700ede..23536d4 100644 (file)
        "showpreview": "بين معاينة",
        "showdiff": "عرض التبديلات",
        "blankarticle": "<strong>ردّ البال:</strong> الپاجة الّي كريّيتها راهي خاوية.\nيلا تعاود تكليكي على $1\"، الپاجة غادي تنخلق بلا ما يكون فيها حتا محتاوا.",
-       "anoneditwarning": "'''تÙ\88Ù\84Ù\8aÙ\87Ø©:''' Ø±Ø§Ù\83 Ù\85ا Ø¯Ø®Ù\84تش Ø¨Ù\84 Ø­Ø³Ø§Ø¨ ØªØ§Ø¹Ù\83.\nÙ\8aÙ\84ا ØªØ¯Ù\8aر Ø´Ù\8a ØªØ¨Ø¯Ø§Ù\84Ø\8c ØºØ§Ø¯Ù\8a ØªØªØ³Ø¬Ù\91Ù\84 Ù\84ادرÙ\8aسة Ø¢يبي تاعك فل متراخ تاع هاد الصفحة و تكون باينة ل كلّ واحد. يلا [$1 تتكونيكتا]</strong> ولا <strong>[$2 تخلق حساب]</strong>، التبدالات تاعك غادي يبانو تحت السميّة تاع المستعملي تاعك، و كاين تاني مزيّات وحدخرين.",
+       "anoneditwarning": "'''تÙ\88Ù\84Ù\8aÙ\87Ø©:''' Ø±Ø§Ù\83 Ù\85ا Ø¯Ø®Ù\84تش Ø¨Ù\84 Ø­Ø³Ø§Ø¨ ØªØ§Ø¹Ù\83.\nÙ\8aÙ\84ا ØªØ¯Ù\8aر Ø´Ù\8a ØªØ¨Ø¯Ø§Ù\84Ø\8c ØºØ§Ø¯Ù\8a ØªØªØ³Ø¬Ù\91Ù\84 Ù\84ادرÙ\8aسة Ø£يبي تاعك فل متراخ تاع هاد الصفحة و تكون باينة ل كلّ واحد. يلا [$1 تتكونيكتا]</strong> ولا <strong>[$2 تخلق حساب]</strong>، التبدالات تاعك غادي يبانو تحت السميّة تاع المستعملي تاعك، و كاين تاني مزيّات وحدخرين.",
        "anonpreviewwarning": "<em>ما راكش مسجّل داخل. لوكان تحفّظ التبدالات ضركا غادي تتسجّل لادريسة إيپي تاعك فل تاريخ تاع هاد الپاجة.</em>",
        "missingsummary": "<strong>تفكار:</strong> راك ما مدّيتش تلخيص على التبدال تاعك.\nيلا تكليكي على \"$1\" مجّديد، التبدال تاعك غادي يتسجّل بلاش.",
        "selfredirect": "<strong>ردّ البال:</strong> راك توجّه هاد الپاجة على روحها.\nبالاك راك غلطت فل ختيّار تاع التقيان تاع الپاجة، ولا تاني ما راكش فل پاجة الّي راك حاب تإيديتيها.\nيلا تكليكي على \"$1\" مجّديد، هاد التوجاه غادي ينخلق كيما هاك.",
index 0b07256..f0ad576 100644 (file)
        "wantedfiles": "Запатрабаваныя файлы",
        "wantedfiletext-cat": "Наступныя файлы выкарыстоўваюцца, але іх няма. Файлы з вонкавых сховішчаў могуць знаходзіцца ў сьпісе без уліку іх існаваньня. Любыя такія няслушныя ўваходжаньні будуць <del>выкрасьленыя</del>. Дадаткова, старонкі, якія ўбудоўваюць няісныя файлы, прыведзеныя на [[:$1]].",
        "wantedfiletext-cat-noforeign": "Наступныя файлы ўжваюцца, але не існуюць. Дадаткова, старонкі, у якія ўключаныя няісныя файлы, прыведзеныя ў [[:$1]].",
-       "wantedfiletext-nocat": "Наступныя файлы выкарыстоўваюцца, але іх няма. Файлы са зьнешніх сховішчаў могуць знаходзіцца ў сьпісе без уліку іх існаваньня. Любыя такія няслушныя ўваходжаньні будуць <del>выкрасьленыя</del>.",
+       "wantedfiletext-nocat": "Наступныя файлы выкарыстоўваюцца, але іх няма. Файлы з вонкавых сховішчаў могуць знаходзіцца ў сьпісе без уліку іх існаваньня. Любыя такія няслушныя ўваходжаньні будуць <del>выкрасьленыя</del>.",
        "wantedfiletext-nocat-noforeign": "Наступныя файлы выкарыстоўваюцца, але іх няма.",
        "wantedtemplates": "Запатрабаваныя шаблёны",
        "mostlinked": "Старонкі, на якія найчасьцей спасылаюцца",
index 58c8ff4..a6ac049 100644 (file)
        "invalidtitle": "Sernuşteyo nêravêrde",
        "invalidtitle-knownnamespace": "Canemey \"$2\" u metnê \"$3\" xırabo",
        "invalidtitle-unknownnamespace": "Sernameye nêşınasiya yana amraiya canameyo  $1 u metno \"$2\" xırab",
-       "exception-nologin": "Şıma cıkewtış nêvıraşto",
+       "exception-nologin": "Nêkewt cı",
        "exception-nologin-text": "Na pele ya zi nê karkerdışi rê nê wiki de cıkewtış icab keno.",
        "exception-nologin-text-manual": "Na pele resayışi re $1 bıgire.",
        "virus-badscanner": "Eyaro şaş: no virus-cıgerayox nêzanyeno: ''$1''",
        "nav-login-createaccount": "Dekew de / hesab vıraze",
        "logout": "Bıveciye",
        "userlogout": "Bıveciye",
-       "notloggedin": "Şıma cıkewtış nêvıraşto",
+       "notloggedin": "Nêkewt cı",
        "userlogin-noaccount": "Hesabê şıma çıniyo?",
        "userlogin-joinproject": "Cıkewe {{SITENAME}}",
        "createaccount": "Hesab vıraze",
        "uploadbtn": "Dosya bar ke",
        "reuploaddesc": "Barkerdışi iptal ke u peyser şo formê barkerdışi",
        "upload-tryagain": "Deskripyonê dosyayî ke vurîya ey qeyd bike",
-       "uploadnologin": "Şıma cıkewtış nêvıraşto",
+       "uploadnologin": "Nêkewt cı",
        "uploadnologintext": "Ti şeni $1 dosya bar bikere.",
        "upload_directory_missing": "Direktorê dosyayê ($1)î biyo vînî u webserver de nieşkeno viraziye.",
        "upload_directory_read_only": "Direktorê dosyayê ($1)î webserver de nieşkeno binuse.",
        "watchlistfor2": "Qandê $1 ($2)",
        "nowatchlist": "listeya temaşa kerdıişê şıma de yew madde zi çina.",
        "watchlistanontext": "qey vurnayişê maddeya listeya temaşakerdiş ronıştış akerê",
-       "watchnologin": "Şıma cıkewtış nêvıraşto",
+       "watchnologin": "Nêkewt cı",
        "addwatch": "Lista seyrkerdışi ke",
        "addedwatchtext": "\"[[:$1]]\" u perra cıya werênayışi [[Special:Watchlist|lista şımaya ewniyayışi]] rê ameyo cıkerdış.",
        "addedwatchtext-short": "Pera $1`i çebyê listeya seyran de şıma",
        "anonuser": "karberê anonim o {{SITENAME}}i $1",
        "lastmodifiedatby": "Ena per tewr peyên roca $2, $1 de terefê $3 ra vurneya ya.",
        "othercontribs": "xebatê $1 ıney geriyayo diqqeti/geriyayo nezer.",
-       "others": "bini",
+       "others": "ê bini",
        "siteusers": "{{SITENAME}} {{PLURAL:$2|karber|karberan}} $1",
        "anonusers": "{{SITENAME}} {{PLURAL:$2|karberê eyê|karberanê eyê}} anonimi $1",
        "creditspage": "şınasnameyê peli",
        "version-editors": "Vurnayoği",
        "version-antispam": "Spam vındarnayış",
        "version-api": "API",
-       "version-other": "Bin",
+       "version-other": "Sewbi",
        "version-mediahandlers": "Kulbê medyayî",
        "version-hooks": "Çengelî",
        "version-parser-extensiontags": "Etiketê ekstensiyon ê parserî",
        "htmlform-required": "Ena deger lazim o",
        "htmlform-submit": "Bişirav",
        "htmlform-reset": "Vurnayişî reyna biyar",
-       "htmlform-selectorother-other": "Bin",
+       "htmlform-selectorother-other": "Sewbi",
        "htmlform-no": "Nê",
        "htmlform-yes": "Eya",
        "htmlform-chosen-placeholder": "Opsiyon weçine",
index 1b1134e..c6c1906 100644 (file)
        "exif-meteringmode-4": "zaf noqtayın",
        "exif-meteringmode-5": "Qalıb",
        "exif-meteringmode-6": "qısmî",
-       "exif-meteringmode-255": "Bin",
+       "exif-meteringmode-255": "Sewbi",
        "exif-lightsource-0": "Nêzanaye",
        "exif-lightsource-1": "Roşnê Tici",
        "exif-lightsource-2": "Florasant",
index 3bf2a7a..f6e49f1 100644 (file)
        "filedelete-maintenance": "Eliminação e restauro de arquivos estão temporariamente desativados durante manutenção.",
        "filedelete-maintenance-title": "Não é possível excluir o arquivo",
        "mimesearch": "Pesquisa MIME",
-       "mimesearch-summary": "Esta página possibilita que os arquivos sejam filtrados a partir de seu [[w:pt:Tipo de mídia da Internet|tipo MIME]]. Sintaxe de busca: \"tipo/subtipo\" ou \"tipo/*\"(por exemplo, <code>image/jpeg</code>, <code>image/png</code>, <code>application/*</code>).",
+       "mimesearch-summary": "Esta página permite a filtragem de arquivos pelo seu tipo MIME.\nSintaxe: \"tipo de conteúdo/subtipo\" ou \"tipo de conteúdo/*\". Exemplos: <code>image/jpeg</code>, <code>image/png</code>, <code>application/*</code>.",
        "mimetype": "tipo MIME:",
        "download": "download",
        "unwatchedpages": "Páginas não vigiadas",
        "sp-contributions-newbies-sub": "Para contas novas",
        "sp-contributions-newbies-title": "Contribuições de contas novas",
        "sp-contributions-blocklog": "registro de bloqueios",
-       "sp-contributions-suppresslog": "Contribuições de {{GENDER:$1|usuário}} suprimidas",
+       "sp-contributions-suppresslog": "contribuições suprimidas {{GENDER:$1|do usuário|da usuária}}",
        "sp-contributions-deleted": "{{GENDER:$1|contribuições}} eliminadas",
        "sp-contributions-uploads": "envios",
        "sp-contributions-logs": "registros",
index db5c414..0324e2d 100644 (file)
        "right-purge": "Osvježavanje keša za stranice bez potvrde",
        "right-autoconfirmed": "Izbjegavanje ograničenja stopa temeljenih na IP-u",
        "right-bot": "Postavljen kao automatski proces",
-       "right-nominornewtalk": "Male izmjene na stranici za razgovor ne uzrokuju prikazivanje oznake ''nova poruka'' na stranici za razgovor",
+       "right-nominornewtalk": "Male izmjene na stranicama za razgovor ne uzrokuju obavještenje o novim porukama",
        "right-apihighlimits": "Korištenje viših ograničenja u API upitima",
        "right-writeapi": "Korištenje opcije ''write API''",
        "right-delete": "Brisanje stranica",
        "right-editusercss": "Uređivanje CSS datoteka drugih korisnika",
        "right-edituserjson": "Uređivanje JSON datoteka drugih korisnika",
        "right-edituserjs": "Uređivanje JavaScript datoteka drugih korisnika",
+       "right-editsitecss": "Uređivanje CSS za cijelo wiki",
+       "right-editsitejson": "Uređivanje JSON-a za cijelo wiki",
+       "right-editsitejs": "Uređivanje JavaScripta za cijelo wiki",
        "right-editmyusercss": "Uredite svoje vlastite CSS datoteke",
        "right-editmyuserjs": "Uredite vlastite korisničke JavaScript datoteke",
        "right-viewmywatchlist": "Pregled vlastitog popisa praćenih stranica",
        "listgrouprights-namespaceprotection-header": "Ograničenja imenskog prostora",
        "listgrouprights-namespaceprotection-namespace": "Imenski prostor",
        "listgrouprights-namespaceprotection-restrictedto": "Prava kojima se dozvoljava korisniku da uređuje",
+       "listgrants-summary": "Ovo je popis dozvola, svaka sa svojim pravima. Korisnici mogu autorizirati prilozi koji će koristiti račun, ali uz ograničena prava u zavisnosti od tog koju dozvolu im korisnik omogući. Međutim, prilog koji djeluje u ime korisnika ograničen je na prava samog korisnika. Moguće je da postoje [[{{MediaWiki:Listgrouprights-helppage}}|dodatne informacije]] o pojedinim pravima.",
        "trackingcategories": "Praćenje kategorija",
        "trackingcategories-summary": "Ova stranica prikazuje prateće kategorije koje MediaWiki softver automatski popunjava. Njihovi nazivi se mogu promijeniti izmjenom odgovarajućih sistemskih poruka u imenskom prostoru {{ns:8}}.",
        "trackingcategories-msg": "Praćenje kategorije",
index e3c64b6..9675e6e 100644 (file)
        "mergehistory-submit": "ಪಡಿಪಾಟೊಲೆನ್ ಸಮ್ಮಿಲಾಲೆ.",
        "mergehistory-empty": "ಒವ್ವೆ ಪಡಿಪಾಟೊಲು ಸಮ್ಮಿಲಾಪುಜಾ.",
        "mergehistory-done": "$1 ಟು {{PLURAL:$3|ಇತ್ತಿನ|ಉಪ್ಪುನ}} $3 {{PLURAL:$3|ಪಡಿಪಾಟ|ಪಡಿಪಾಟೊಲು}}    [[:$2]] ಡು ಸಮ್ಮಿಲಾಂಡ್.",
+       "mergehistory-fail": "ಚರಿತ್ರೆ ಸಮ್ಮಿಲ ಮಲ್ಪರೆ ಆತಿಜಿ, ದಯಮಲ್ತ್  ಪುಟ ಬೊಕ ಕಾಲ ಪರಿಮಾನೊಲೆನ್ ಪಿರಪರಿಶೀಲಿಸಾಲೆ.",
+       "mergehistory-fail-bad-timestamp": "ಕಾಲಮೊಹರ್ ಅಮಾನ್ಯ ಆತ್ಂಡ್.",
+       "mergehistory-fail-invalid-source": "ಮೂಲಪುಟ ಅಮಾನ್ಯ ಆತ್ಂಡ್.",
+       "mergehistory-fail-invalid-dest": "ಗಮ್ಯತಾನದ ಪುಟ ಅಮಾನ್ಯ ಆದುಂಡು.",
+       "mergehistory-fail-no-change": "ಚರಿತ್ರೆ ಸಮ್ಮಿಲ ಒವ್ವೆ ಪಡಿಪಾಟೊಲೆನ್ ಸಮ್ಮಿಲ ಮಲ್ತಿಜಿ. ದಯಮಲ್ತ್ ಪುಟ ಬೊಕ ಕಾಲ ಪರಿಮಾನೊಲೆನ್ ಪಿರ ಪರಿಶೀಲಿಸಾಲೆ.",
+       "mergehistory-fail-permission": "ಚರಿತ್ರೆ ಸಮ್ಮಿಲಾವರೆ ಬೋಡಾಯಿನಾತ್ ಅನುಮತಿಲಿಜ್ಜಿ.",
+       "mergehistory-fail-self-merge": "ಮೂಲ ಬೊಕ ಗಮ್ಯತಾನ ಪುಟೊಲು ಒಂಜೇ ಆದುಂಡು.",
+       "mergehistory-fail-timestamps-overlap": "ಮೂಲ ಪಡಿಪಾಟೊಲು ಅತಿವ್ಯಾಪಿ ಆತಾ ಇಜಿಂಡ ಗಮ್ಯತಾನ ಪಡಿಪಾಟೊಲೆನ ನಂತರ ಬರ್ಪಾ.",
+       "mergehistory-fail-toobig": "ಚಲನೆ ಆಪಿನ {{PLURAL:$1|ಪಡಿಪಾಟ|ಪಡಿಪಾಟೊಲು}} $1 ಮಿತಿ ಮೀರಿನ ಕಾರಣ ಚರಿತ್ರೆ ಸಮ್ಮಿಲ ಮಲ್ಪರಾತಿಜಿ.",
+       "mergehistory-no-source": "ಮೂಲ ಪುಟ $1 ಇಜ್ಜಿ.",
+       "mergehistory-no-destination": "ಗಮ್ಯತಾನ ಪುಟ $1 ಇಜ್ಜಿ.",
+       "mergehistory-invalid-source": "ಮೂಲ ಪುಟ  ಒಂಜಿ ಮಾನ್ಯ ತರೆಬರವು ಆದಿಪ್ಪೊಡು.",
+       "mergehistory-invalid-destination": "ಗಮ್ಯತಾನ ಪುಟ ಒಂಜಿ ಮಾನ್ಯ ತೆರಬರವು ಆದಿಪ್ಪೊಡು.",
+       "mergehistory-autocomment": " [[:$1]]ನೆನ್ [[:$2]] ಗ್ ಸಮ್ಮಿಲಾಂಡ್",
+       "mergehistory-comment": " [[:$1]] ಎನ್ [[:$2]] ಗ್ ಸಮ್ಮಿಲಾಂಡ್: $3",
+       "mergehistory-same-destination": "ಮೂಲ ಬೊಕ ಗಮ್ಯತಾನ ಪುಟೊಲು ಒಂಜೇ ಆವರೆ ಬಲ್ಲಿ.",
        "mergehistory-reason": "ಕಾರಣ:",
        "mergelog": "ಸೇರ್ಗೆದ ದಾಕಲೆ",
        "revertmerge": "ಅನ್-ಮರ್ಜ್ ಮಲ್ಪುಲೆ",
+       "mergelogpagetext": "ತಿರ್ತ್'ದ ಪಟ್ಟಿಡ್  ಅತೀಇಂಚೊಗು  ಒಂಜಿ ಪುಟ ಚರಿತ್ರೆ  ಬೊಕೊಂಜೆಕ್  ಸಮ್ಮಿಲಾತಿನವು ಉಂಡು.",
        "history-title": "\"$1\" ಪುಟೊತ ಆವೃತ್ತಿ ಇತಿಹಾಸೊ",
        "difference-title": "\"$1\" ಆವೃತ್ತಿಲೆನ ನಡುತ ವ್ಯತ್ಯಾಸೊ",
+       "difference-title-multipage": "ಪುಟ \"$1\" ಬೊಕ \"$2\"ನಡುತ ವ್ಯತ್ಯಾಸ",
+       "difference-multipage": "(ಪುಟೊಲೆನ ನಡುಟು ಉಪ್ಪುನ ವ್ಯತ್ಯಾಸ)",
        "lineno": "$1ನೇ ಸಾಲ್:",
        "compareselectedversions": "ಆಯ್ಕೆ ಮಲ್ತಿನ ಆವೃತ್ತಿಲೆನ್ ಹೊಂದಾಣಿಕೆ ಮಲ್ತ್ ತೂಲೆ",
+       "showhideselectedversions": "ಅಜತಿನ ಪಡಿಪಾಟೊಲು ತೋಜುನೆನ್ ಬದಲಾಲೆ",
        "editundo": "ದುಂಬುದಲೆಕೊ",
        "diff-empty": "(ದಾಲ ವ್ಯತ್ಯಾಸೊ ಇಜ್ಜಿ)",
        "diff-multi-sameuser": "(ಒಂಜೇ ಸದಸ್ಯೆರೆ {{PLURAL:$1|ನಡುತ್ತ ಬದಲಾವಣೆನ್|$1 ನಡುತ್ತ ಬದಲಾವಣೆಲೆನ್}} ತೋಜಾದಿಜಿ)",
        "diff-multi-otherusers": "({{PLURAL:$2|ಕುಡೊರಿ ಸದಸ್ಯೆರ್‌ನ|$2 ಸದಸ್ಯೆರ್ಲೆನ}}  {{PLURAL:$1|ಒಂಜಿ ನಡುತ್ತ ಬದಲಾವಣೆನ್|$1 ನಡುತ್ತ ಬದಲಾವಣೆಲೆನ್}} ತೋಜಾದಿಜಿ)",
+       "diff-multi-manyusers": "({{PLURAL:$1|ಒಂಜಿ ನಡುತ ಪಡಿಪಾಟ|$1 ನಡುತ ಪಡಿಪಾಟೊಲು}}  $2 ಡುದು ಎಚ್ಚದ {{PLURAL:$2|ಬಳಕೆದಾರೆ|ಬಳಕೆದಾರೆರ್}} ತೋಜಾದಿಜಿ)",
+       "diff-paragraph-moved-tonew": "ವಾಕ್ಯಪಂಕ್ತಿ ಚಲನೆ ಆತ್ಂಡ್. ಪೊಸ ಜಾಗೊಗು ನೆಗೆಪರೆ ಒತ್ತುಲೆ.",
+       "diff-paragraph-moved-toold": "ವಾಕ್ಯಪಂಕ್ತಿ ಚಲನೆ ಆತ್ಂಡ್. ದುಂಬುಇತ್ತಿನ ಜಾಗೊಗು ನೆಗೆಪರೆ ಒತ್ತುಲೆ.",
        "searchresults": "ನಾಡ್‍ಪತ್ತ್‌ನೆತ ಪಲಿತಾಂಸೊಲು",
        "searchresults-title": "\"$1\"ಕ್ ನಾಡ್‍ಪತ್ತ್‌ನೆತ ಪಲಿತಾಂಸೊಲು",
        "notextmatches": "ವಾ ಪುಟೊತ ಪಠ್ಯೊಡುಲಾ ಹೋಲಿಕೆ ಇಜ್ಜಿ",
index 34d6d38..a1a109a 100644 (file)
@@ -39,7 +39,8 @@
                        "Abdulq",
                        "Fitoschido",
                        "Dcljr",
-                       "Bukhari"
+                       "Bukhari",
+                       "Sajidkhan"
                ]
        },
        "tog-underline": "ربط کی خط کشیدگی:",
        "pageinfo-display-title": "عنوان",
        "pageinfo-default-sort": "کلید برائے ابتدائی ترتیب",
        "pageinfo-length": "صفحہ کا حجم (بائٹ میں)",
-       "pageinfo-namespace": "نام فضا",
+       "pageinfo-namespace": "نیم سپیس",
        "pageinfo-article-id": "صفحہ کی شناخت",
        "pageinfo-language": "زبان",
        "pageinfo-language-change": "تبدیلی",
index 62646c3..e3a7300 100644 (file)
        "deletepage": "刪除頁面",
        "confirm": "確認",
        "excontent": "內容為:「$1」",
-       "excontentauthor": "內容為:\"$1\",且僅有一位貢獻者 \"[[Special:Contributions/$2|$2]]\" ([[User talk:$2|對話]])",
+       "excontentauthor": "內容為:「$1」,且僅有一位貢獻者「[[Special:Contributions/$2|$2]]」([[User talk:$2|對話]])",
        "exbeforeblank": "被清空前的內容為:\"$1\"",
        "delete-confirm": "刪除 \"$1\"",
        "delete-legend": "刪除",
index 9b029fe..695773e 100644 (file)
@@ -73,7 +73,7 @@ class PopulateExternallinksIndex60 extends LoggedUpdateMaintenance {
                                        ],
                                        [
                                                'el_id' => $row->el_id,
-                                       ], __METHOD__, [ 'IGNORE' ]
+                                       ], __METHOD__
                                );
                        }
                        wfWaitForSlaves();
index 771d19b..3ba5c6b 100644 (file)
@@ -32,10 +32,10 @@ require_once __DIR__ . '/Maintenance.php';
 class ResetUserEmail extends Maintenance {
        public function __construct() {
                $this->addDescription( "Resets a user's email" );
-               $this->addArg( 'user', 'Username or user ID, if starts with #', true );
+               $this->addArg( 'user', 'Username or user ID, if starts with #' );
                $this->addArg( 'email', 'Email to assign' );
 
-               $this->addOption( 'no-reset-password', 'Don\'t reset the user\'s password', false, false );
+               $this->addOption( 'no-reset-password', 'Don\'t reset the user\'s password' );
 
                parent::__construct();
        }
index 107ab33..9016c7c 100644 (file)
                var deferred = $.Deferred();
 
                // Allow calling with a single dependency as a string
-               if ( typeof dependencies === 'string' ) {
+               if ( !Array.isArray( dependencies ) ) {
                        dependencies = [ dependencies ];
                }
 
                        return deferred.reject( e ).promise();
                }
 
-               mw.loader.enqueue( dependencies, function () {
-                       deferred.resolve( mw.loader.require );
-               }, deferred.reject );
+               mw.loader.enqueue(
+                       dependencies,
+                       function () { deferred.resolve( mw.loader.require ); },
+                       deferred.reject
+               );
 
                return deferred.promise();
        };
 
+       /**
+        * Load a script by URL.
+        *
+        * Example:
+        *
+        *     mw.loader.getScript(
+        *         'https://example.org/x-1.0.0.js'
+        *     )
+        *         .then( function () {
+        *             // Script succeeded. You can use X now.
+        *         }, function ( e ) {
+        *             // Script failed. X is not avaiable
+        *             mw.log.error( e.message ); // => "Failed to load script"
+        *         } );
+        *     } );
+        *
+        * @member mw.loader
+        * @param {string} url Script URL
+        * @return {jQuery.Promise} Resolved when the script is loaded
+        */
+       mw.loader.getScript = function ( url ) {
+               return $.ajax( url, { dataType: 'script', cache: true } )
+                       .catch( function () {
+                               throw new Error( 'Failed to load script' );
+                       } );
+       };
+
        // Alias $j to jQuery for backwards compatibility
        // @deprecated since 1.23 Use $ or jQuery instead
        mw.log.deprecate( window, '$j', $, 'Use $ or jQuery instead.' );
index 8da7a06..86bc44a 100644 (file)
                        $table = $( '<table>' ).attr( 'id', 'mw-debug-querylist' );
 
                        $( '<tr>' )
-                               .append( $( '<th>' ).text( '#' ).css( 'width', '4em' ) )
-                               .append( $( '<th>' ).text( 'SQL' ) )
-                               .append( $( '<th>' ).text( 'Time' ).css( 'width', '8em' ) )
-                               .append( $( '<th>' ).text( 'Call' ).css( 'width', '18em' ) )
+                               .append( $( '<th>' ).attr( 'scope', 'col' ).text( '#' ).css( 'width', '4em' ) )
+                               .append( $( '<th>' ).attr( 'scope', 'col' ).text( 'SQL' ) )
+                               .append( $( '<th>' ).attr( 'scope', 'col' ).text( 'Time' ).css( 'width', '8em' ) )
+                               .append( $( '<th>' ).attr( 'scope', 'col' ).text( 'Call' ).css( 'width', '18em' ) )
                                .appendTo( $table );
 
                        for ( i = 0, length = this.data.queries.length; i < length; i += 1 ) {
                                $table = $( '<table>' ).appendTo( $unit );
 
                                $( '<tr>' )
-                                       .html( '<th>Key</th><th>Value</th>' )
+                                       .html( '<th scope="col">Key</th><th scope="col">Value</th>' )
                                        .appendTo( $table );
 
                                for ( key in data ) {
                                        $( '<tr>' )
-                                               .append( $( '<th>' ).text( key ) )
+                                               .append( $( '<th>' ).attr( 'scope', 'row' ).text( key ) )
                                                .append( $( '<td>' ).text( data[ key ] ) )
                                                .appendTo( $table );
                                }
index 09bf9ca..5fa8e5a 100644 (file)
        // This specifies styling for individual field validation error messages.
        // Show them below the fields to prevent line break glitches, and leave
        // some space between the field and the error message box.
-       .mw-ui-vform-field .error {
-               display: block;
-               margin-top: 5px;
+       .mw-ui-vform-field {
+               .error,
+               .warning {
+                       display: block;
+                       margin-top: 5px;
+               }
        }
 }
 
index def08ff..aa383e2 100644 (file)
@@ -27,8 +27,8 @@ trait HamcrestPHPUnitIntegration {
         * Wrapper around Hamcrest's assertThat, which marks the assertion
         * for PHPUnit so the test is not marked as risky
         */
-       public function assertThatHamcrest( /* ... */ ) {
-               call_user_func_array( 'assertThat', func_get_args() );
+       public function assertThatHamcrest( ...$args ) {
+               assertThat( ...$args );
                $this->addToAssertionCount( 1 );
        }
 }
index 502685d..adb0196 100644 (file)
@@ -1,8 +1,8 @@
 <?php
 
-use Mediawiki\Logger\LoggerFactory;
-use Mediawiki\Logger\Spi;
-use Mediawiki\Logger\LogCapturingSpi;
+use MediaWiki\Logger\LoggerFactory;
+use MediaWiki\Logger\Spi;
+use MediaWiki\Logger\LogCapturingSpi;
 
 /**
  * Replaces the logging SPI on each test run. This allows
index 1157331..3d8c643 100644 (file)
@@ -1037,4 +1037,54 @@ class TitlePermissionTest extends MediaWikiLangTestCase {
                $this->assertEquals( [],
                        $this->title->getUserPermissionsErrors( 'edit', $this->user ) );
        }
+
+       /**
+        * @covers Title::checkUserBlock
+        *
+        * Tests to determine that the passed in permission does not get mixed up with
+        * an action of the same name.
+        */
+       public function testUserBlockAction() {
+               global $wgLang;
+
+               $tester = $this->getMockBuilder( Action::class )
+                       ->disableOriginalConstructor()
+                       ->getMock();
+               $tester->method( 'getName' )
+                       ->willReturn( 'tester' );
+               $tester->method( 'getRestriction' )
+                       ->willReturn( 'test' );
+               $tester->method( 'requiresUnblock' )
+                       ->willReturn( false );
+
+               $this->setMwGlobals( [
+                       'wgActions' => [
+                               'tester' => $tester,
+                       ],
+                       'wgGroupPermissions' => [
+                               '*' => [
+                                       'tester' => true,
+                               ],
+                       ],
+               ] );
+
+               $now = time();
+               $this->user->mBlockedby = $this->user->getName();
+               $this->user->mBlock = new Block( [
+                       'address' => '127.0.8.1',
+                       'by' => $this->user->getId(),
+                       'reason' => 'no reason given',
+                       'timestamp' => $now,
+                       'auto' => false,
+                       'expiry' => 'infinity',
+               ] );
+
+               $errors = [ [ 'blockedtext',
+                               '[[User:Useruser|Useruser]]', 'no reason given', '127.0.0.1',
+                               'Useruser', null, 'infinite', '127.0.8.1',
+                               $wgLang->timeanddate( wfTimestamp( TS_MW, $now ), true ) ] ];
+
+               $this->assertEquals( $errors,
+                       $this->title->getUserPermissionsErrors( 'tester', $this->user ) );
+       }
 }
index d9b7e18..a044372 100644 (file)
@@ -911,30 +911,65 @@ class WANObjectCacheTest extends PHPUnit\Framework\TestCase {
        public function testLockTSESlow() {
                $cache = $this->cache;
                $key = wfRandomString();
+               $key2 = wfRandomString();
                $value = wfRandomString();
 
+               $mockWallClock = 1549343530.2053;
+               $priorTime = $mockWallClock;
+               $cache->setMockTime( $mockWallClock );
+
                $calls = 0;
-               $func = function ( $oldValue, &$ttl, &$setOpts ) use ( &$calls, $value, $cache, $key ) {
+               $func = function ( $oldValue, &$ttl, &$setOpts ) use ( &$calls, $value, $priorTime ) {
                        ++$calls;
-                       $setOpts['since'] = microtime( true ) - 10;
-                       // Immediately kill any mutex rather than waiting a second
-                       $cache->delete( $cache::MUTEX_KEY_PREFIX . $key );
+                       $setOpts['since'] = $priorTime - 10;
                        return $value;
                };
 
-               // Value should be marked as stale due to snapshot lag
+               // Value should be given a low logical TTL due to snapshot lag
                $curTTL = null;
-               $ret = $cache->getWithSetCallback( $key, 30, $func, [ 'lockTSE' => 5 ] );
+               $ret = $cache->getWithSetCallback( $key, 300, $func, [ 'lockTSE' => 5 ] );
                $this->assertEquals( $value, $ret );
                $this->assertEquals( $value, $cache->get( $key, $curTTL ), 'Value was populated' );
-               $this->assertLessThan( 0, $curTTL, 'Value has negative curTTL' );
+               $this->assertEquals( 1, $curTTL, 'Value has reduced logical TTL', 0.01 );
                $this->assertEquals( 1, $calls, 'Value was generated' );
 
+               $mockWallClock += 2;
+
+               $ret = $cache->getWithSetCallback( $key, 300, $func, [ 'lockTSE' => 5 ] );
+               $this->assertEquals( $value, $ret );
+               $this->assertEquals( 2, $calls, 'Callback used (mutex acquired)' );
+
                // Acquire a lock to verify that getWithSetCallback uses lockTSE properly
                $this->internalCache->add( $cache::MUTEX_KEY_PREFIX . $key, 1, 0 );
-               $ret = $cache->getWithSetCallback( $key, 30, $func, [ 'lockTSE' => 5 ] );
+
+               $ret = $cache->getWithSetCallback( $key, 300, $func, [ 'lockTSE' => 5 ] );
                $this->assertEquals( $value, $ret );
-               $this->assertEquals( 1, $calls, 'Callback was not used' );
+               $this->assertEquals( 3, $calls, 'Callback was not used (mutex not acquired)' );
+
+               $calls = 0;
+               $func2 = function ( $oldValue, &$ttl, &$setOpts ) use ( &$calls, $value, $priorTime ) {
+                       ++$calls;
+                       $setOpts['lag'] = 15;
+                       return $value;
+               };
+
+               // Value should be given a low logical TTL due to replication lag
+               $curTTL = null;
+               $ret = $cache->getWithSetCallback( $key2, 300, $func2, [ 'lockTSE' => 5 ] );
+               $this->assertEquals( $value, $ret );
+               $this->assertEquals( $value, $cache->get( $key2, $curTTL ), 'Value was populated' );
+               $this->assertEquals( 30, $curTTL, 'Value has reduced logical TTL', 0.01 );
+               $this->assertEquals( 1, $calls, 'Value was generated' );
+
+               $ret = $cache->getWithSetCallback( $key2, 300, $func2, [ 'lockTSE' => 5 ] );
+               $this->assertEquals( $value, $ret );
+               $this->assertEquals( 1, $calls, 'Callback was used (not expired)' );
+
+               $mockWallClock += 31;
+
+               $ret = $cache->getWithSetCallback( $key2, 300, $func2, [ 'lockTSE' => 5 ] );
+               $this->assertEquals( $value, $ret );
+               $this->assertEquals( 2, $calls, 'Callback was used (mutex acquired)' );
        }
 
        /**
@@ -950,8 +985,6 @@ class WANObjectCacheTest extends PHPUnit\Framework\TestCase {
                $calls = 0;
                $func = function () use ( &$calls, $value, $cache, $key ) {
                        ++$calls;
-                       // Immediately kill any mutex rather than waiting a second
-                       $cache->delete( $cache::MUTEX_KEY_PREFIX . $key );
                        return $value;
                };
 
index 863ff50..64bdf31 100644 (file)
@@ -599,7 +599,6 @@ class UserTest extends MediaWikiTestCase {
                ] );
 
                // 1. Log in a test user, and block them.
-               $userBlocker = $this->getTestSysop()->getUser();
                $user1tmp = $this->getTestUser()->getUser();
                $request1 = new FauxRequest();
                $request1->getSession()->setUser( $user1tmp );
@@ -610,7 +609,6 @@ class UserTest extends MediaWikiTestCase {
                ] );
                $block->setBlocker( $this->getTestSysop()->getUser() );
                $block->setTarget( $user1tmp );
-               $block->setBlocker( $userBlocker );
                $res = $block->insert();
                $this->assertTrue( (bool)$res['id'], 'Failed to insert block' );
                $user1 = User::newFromSession( $request1 );
@@ -682,14 +680,12 @@ class UserTest extends MediaWikiTestCase {
                ] );
 
                // 1. Log in a test user, and block them.
-               $userBlocker = $this->getTestSysop()->getUser();
                $testUser = $this->getTestUser()->getUser();
                $request1 = new FauxRequest();
                $request1->getSession()->setUser( $testUser );
                $block = new Block( [ 'enableAutoblock' => true ] );
                $block->setBlocker( $this->getTestSysop()->getUser() );
                $block->setTarget( $testUser );
-               $block->setBlocker( $userBlocker );
                $res = $block->insert();
                $this->assertTrue( (bool)$res['id'], 'Failed to insert block' );
                $user = User::newFromSession( $request1 );
@@ -729,14 +725,12 @@ class UserTest extends MediaWikiTestCase {
                ] );
 
                // 1. Log in a test user, and block them indefinitely.
-               $userBlocker = $this->getTestSysop()->getUser();
                $user1Tmp = $this->getTestUser()->getUser();
                $request1 = new FauxRequest();
                $request1->getSession()->setUser( $user1Tmp );
                $block = new Block( [ 'enableAutoblock' => true, 'expiry' => 'infinity' ] );
                $block->setBlocker( $this->getTestSysop()->getUser() );
                $block->setTarget( $user1Tmp );
-               $block->setBlocker( $userBlocker );
                $res = $block->insert();
                $this->assertTrue( (bool)$res['id'], 'Failed to insert block' );
                $user1 = User::newFromSession( $request1 );
@@ -829,14 +823,12 @@ class UserTest extends MediaWikiTestCase {
                ] );
 
                // 1. Log in a blocked test user.
-               $userBlocker = $this->getTestSysop()->getUser();
                $user1tmp = $this->getTestUser()->getUser();
                $request1 = new FauxRequest();
                $request1->getSession()->setUser( $user1tmp );
                $block = new Block( [ 'enableAutoblock' => true ] );
                $block->setBlocker( $this->getTestSysop()->getUser() );
                $block->setTarget( $user1tmp );
-               $block->setBlocker( $userBlocker );
                $res = $block->insert();
                $this->assertTrue( (bool)$res['id'], 'Failed to insert block' );
                $user1 = User::newFromSession( $request1 );
@@ -876,14 +868,12 @@ class UserTest extends MediaWikiTestCase {
                ] );
 
                // 1. Log in a blocked test user.
-               $userBlocker = $this->getTestSysop()->getUser();
                $user1tmp = $this->getTestUser()->getUser();
                $request1 = new FauxRequest();
                $request1->getSession()->setUser( $user1tmp );
                $block = new Block( [ 'enableAutoblock' => true ] );
                $block->setBlocker( $this->getTestSysop()->getUser() );
                $block->setTarget( $user1tmp );
-               $block->setBlocker( $userBlocker );
                $res = $block->insert();
                $this->assertTrue( (bool)$res['id'], 'Failed to insert block' );
                $user1 = User::newFromSession( $request1 );
index 0e0b943..29cc6b3 100644 (file)
@@ -22,7 +22,7 @@
  */
 
 class MockDjVuHandler extends DjVuHandler {
-       function isEnabled() {
+       public function isEnabled() {
                return true;
        }
 
diff --git a/tests/qunit/data/mediawiki.loader.getScript.example.js b/tests/qunit/data/mediawiki.loader.getScript.example.js
new file mode 100644 (file)
index 0000000..e5e4759
--- /dev/null
@@ -0,0 +1 @@
+mw.getScriptExampleScriptLoaded = true;
index cb028a9..8b06bd6 100644 (file)
@@ -24,6 +24,7 @@
                        // exposed for cross-file mocks.
                        delete mw.loader.testCallback;
                        delete mw.loader.testFail;
+                       delete mw.getScriptExampleScriptLoaded;
                }
        } ) );
 
                } );
        } );
 
+       QUnit.test( '.getScript() - success', function ( assert ) {
+               var scriptUrl = QUnit.fixurl(
+                       mw.config.get( 'wgScriptPath' ) + '/tests/qunit/data/mediawiki.loader.getScript.example.js'
+               );
+
+               return mw.loader.getScript( scriptUrl ).then(
+                       function () {
+                               assert.strictEqual( mw.getScriptExampleScriptLoaded, true, 'Data attached to a global object is available' );
+                       }
+               );
+       } );
+
+       QUnit.test( '.getScript() - failure', function ( assert ) {
+               assert.rejects(
+                       mw.loader.getScript( 'https://example.test/not-found' ),
+                       /Failed to load script/,
+                       'Descriptive error message'
+               );
+       } );
+
 }() );
index 11be135..bdabdbf 100644 (file)
@@ -154,6 +154,7 @@ exports.config = {
        */
        beforeTest: function ( test ) {
                if ( process.env.DISPLAY && process.env.DISPLAY.startsWith( ':' ) ) {
+                       var logBuffer;
                        let videoPath = filePath( test, logPath, 'mp4' );
                        const { spawn } = require( 'child_process' );
                        ffmpeg = spawn( 'ffmpeg', [
@@ -166,17 +167,29 @@ exports.config = {
                                videoPath // output file
                        ] );
 
+                       logBuffer = function ( buffer, prefix ) {
+                               let lines = buffer.toString().trim().split( '\n' );
+                               lines.forEach( function ( line ) {
+                                       console.log( prefix + line );
+                               } );
+                       };
+
                        ffmpeg.stdout.on( 'data', ( data ) => {
-                               console.log( `ffmpeg stdout: ${data}` );
+                               logBuffer( data, 'ffmpeg stdout: ' );
                        } );
 
                        ffmpeg.stderr.on( 'data', ( data ) => {
-                               console.log( `ffmpeg stderr: ${data}` );
+                               logBuffer( data, 'ffmpeg stderr: ' );
                        } );
 
-                       ffmpeg.on( 'close', ( code ) => {
+                       ffmpeg.on( 'close', ( code, signal ) => {
                                console.log( '\n\tVideo location:', videoPath, '\n' );
-                               console.log( `ffmpeg exited with code ${code}` );
+                               if ( code !== null ) {
+                                       console.log( `\tffmpeg exited with code ${code} ${videoPath}` );
+                               }
+                               if ( signal !== null ) {
+                                       console.log( `\tffmpeg received signal ${signal} ${videoPath}` );
+                               }
                        } );
                }
        },