Merge "Delete unused variable"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Mon, 3 Dec 2018 17:25:22 +0000 (17:25 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Mon, 3 Dec 2018 17:25:22 +0000 (17:25 +0000)
75 files changed:
RELEASE-NOTES-1.33
includes/DefaultSettings.php
includes/MWNamespace.php
includes/MediaWikiServices.php
includes/MovePage.php
includes/Revision/FallbackSlotRoleHandler.php [new file with mode: 0644]
includes/Revision/MainSlotRoleHandler.php [new file with mode: 0644]
includes/Revision/RenderedRevision.php
includes/Revision/RevisionRenderer.php
includes/Revision/RevisionStore.php
includes/Revision/RevisionStoreFactory.php
includes/Revision/SlotRoleHandler.php [new file with mode: 0644]
includes/Revision/SlotRoleRegistry.php [new file with mode: 0644]
includes/ServiceWiring.php
includes/Storage/DerivedPageDataUpdater.php
includes/Storage/PageUpdater.php
includes/Title.php
includes/api/ApiComparePages.php
includes/api/ApiFeedContributions.php
includes/api/ApiQueryRevisionsBase.php
includes/api/i18n/hu.json
includes/block/BlockRestriction.php
includes/block/Restriction/Restriction.php
includes/content/Content.php
includes/content/ContentHandler.php
includes/debug/logger/monolog/SyslogHandler.php
includes/diff/DifferenceEngine.php
includes/export/DumpOutput.php
includes/htmlform/fields/HTMLExpiryField.php
includes/htmlform/fields/HTMLMultiSelectField.php
includes/media/MediaTransformOutput.php
includes/page/WikiPage.php
includes/upload/UploadFromChunks.php
languages/Language.php
languages/i18n/ace.json
languages/i18n/azb.json
languages/i18n/be-tarask.json
languages/i18n/be.json
languages/i18n/cs.json
languages/i18n/da.json
languages/i18n/el.json
languages/i18n/en.json
languages/i18n/eo.json
languages/i18n/fi.json
languages/i18n/hu.json
languages/i18n/mk.json
languages/i18n/nl.json
languages/i18n/nn.json
languages/i18n/qqq.json
languages/i18n/ru.json
languages/i18n/sl.json
languages/i18n/sr-ec.json
languages/i18n/sv.json
languages/i18n/uk.json
languages/i18n/ur.json
languages/i18n/yue.json
languages/i18n/zh-hans.json
resources/Resources.php
resources/src/mediawiki.util.js
tests/phpunit/includes/Revision/FallbackSlotRoleHandlerTest.php [new file with mode: 0644]
tests/phpunit/includes/Revision/MainSlotRoleHandlerTest.php [new file with mode: 0644]
tests/phpunit/includes/Revision/RevisionRendererTest.php
tests/phpunit/includes/Revision/RevisionStoreDbTestBase.php
tests/phpunit/includes/Revision/RevisionStoreFactoryTest.php
tests/phpunit/includes/Revision/RevisionStoreTest.php
tests/phpunit/includes/Revision/SlotRoleHandlerTest.php [new file with mode: 0644]
tests/phpunit/includes/Revision/SlotRoleRegistryTest.php [new file with mode: 0644]
tests/phpunit/includes/RevisionDbTestBase.php
tests/phpunit/includes/RevisionTest.php
tests/phpunit/includes/Storage/DerivedPageDataUpdaterTest.php
tests/phpunit/includes/Storage/PageUpdaterTest.php
tests/phpunit/includes/TitlePermissionTest.php
tests/phpunit/includes/jobqueue/jobs/RefreshLinksJobTest.php
tests/phpunit/includes/page/WikiPageDbTestBase.php
tests/phpunit/includes/page/WikiPageMcrReadNewDbTest.php

index 677b1c1..628227e 100644 (file)
@@ -152,6 +152,7 @@ because of Phabricator reports.
   * filterIntval()
   * filterTimezoneInput()
   * getTimeZoneList()
+* mw.util.jsMessage(), deprecated in 1.20, was removed. Use mw.notify instead.
 
 === Deprecations in 1.33 ===
 * The configuration option $wgUseESI has been deprecated, and is expected
index 358b466..c3a716f 100644 (file)
@@ -8618,6 +8618,9 @@ $wgUploadMaintenance = false;
  * defined for a given namespace, pages in that namespace will use the CONTENT_MODEL_WIKITEXT
  * (except for the special case of JS and CS pages).
  *
+ * @note To determine the default model for a new page's main slot, or any slot in general,
+ * use SlotRoleHandler::getDefaultModel() together with SlotRoleRegistry::getRoleHandler().
+ *
  * @since 1.21
  */
 $wgNamespaceContentModels = [];
index e03a29b..98e70bf 100644 (file)
@@ -19,6 +19,7 @@
  *
  * @file
  */
+use MediaWiki\MediaWikiServices;
 
 /**
  * This is a utility class with only static functions
@@ -462,13 +463,17 @@ class MWNamespace {
         * Get the default content model for a namespace
         * This does not mean that all pages in that namespace have the model
         *
+        * @note To determine the default model for a new page's main slot, or any slot in general,
+        * use SlotRoleHandler::getDefaultModel() together with SlotRoleRegistry::getRoleHandler().
+        *
         * @since 1.21
         * @param int $index Index to check
         * @return null|string Default model name for the given namespace, if set
         */
        public static function getNamespaceContentModel( $index ) {
-               global $wgNamespaceContentModels;
-               return $wgNamespaceContentModels[$index] ?? null;
+               $config = MediaWikiServices::getInstance()->getMainConfig();
+               $models = $config->get( 'NamespaceContentModels' );
+               return $models[$index] ?? null;
        }
 
        /**
index f3ca7d4..0e36b22 100644 (file)
@@ -17,6 +17,7 @@ use MediaWiki\Http\HttpRequestFactory;
 use MediaWiki\Preferences\PreferencesFactory;
 use MediaWiki\Shell\CommandFactory;
 use MediaWiki\Revision\RevisionRenderer;
+use MediaWiki\Revision\SlotRoleRegistry;
 use MediaWiki\Special\SpecialPageFactory;
 use MediaWiki\Storage\BlobStore;
 use MediaWiki\Storage\BlobStoreFactory;
@@ -840,6 +841,14 @@ class MediaWikiServices extends ServiceContainer {
                return $this->getService( 'SkinFactory' );
        }
 
+       /**
+        * @since 1.33
+        * @return SlotRoleRegistry
+        */
+       public function getSlotRoleRegistry() {
+               return $this->getService( 'SlotRoleRegistry' );
+       }
+
        /**
         * @since 1.31
         * @return NameTableStore
index 0fd697b..bb76395 100644 (file)
@@ -20,6 +20,7 @@
  */
 
 use MediaWiki\MediaWikiServices;
+use MediaWiki\Revision\SlotRecord;
 
 /**
  * Handles the backend logic of moving a page from one title
@@ -137,7 +138,8 @@ class MovePage {
                        $status->fatal(
                                'content-not-allowed-here',
                                ContentHandler::getLocalizedName( $this->oldTitle->getContentModel() ),
-                               $this->newTitle->getPrefixedText()
+                               $this->newTitle->getPrefixedText(),
+                               SlotRecord::MAIN
                        );
                }
 
diff --git a/includes/Revision/FallbackSlotRoleHandler.php b/includes/Revision/FallbackSlotRoleHandler.php
new file mode 100644 (file)
index 0000000..78dfd39
--- /dev/null
@@ -0,0 +1,71 @@
+<?php
+/**
+ * This file is part of MediaWiki.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+namespace MediaWiki\Revision;
+
+use MediaWiki\Linker\LinkTarget;
+
+/**
+ * A SlotRoleHandler for providing basic functionality for undefined slot roles.
+ *
+ * This class is intended to be used when encountering slots with a role that used to be defined
+ * by an extension, but no longer is backed by hany specific handler, since the extension in
+ * question has been uninstalled. It may also be used for pages imported from another wiki.
+ *
+ * @since 1.33
+ */
+class FallbackSlotRoleHandler extends SlotRoleHandler {
+
+       public function __construct( $role ) {
+               // treat unknown content as plain text
+               parent::__construct( $role, CONTENT_MODEL_TEXT );
+       }
+
+       /**
+        * @param LinkTarget $page
+        *
+        * @return bool Always false, to prevent undefined slots from being used in new revisions.
+        */
+       public function isAllowedOn( LinkTarget $page ) {
+               return false;
+       }
+
+       /**
+        * @param string $model
+        * @param LinkTarget $page
+        *
+        * @return bool Always false, to prevent undefined slots from being used for
+        *         arbitrary content.
+        */
+       public function isAllowedModel( $model, LinkTarget $page ) {
+               return false;
+       }
+
+       public function getOutputLayoutHints() {
+               // TODO: should be return [ 'display' => 'none'] here, causing undefined slots
+               // to be hidden? We'd still need some place to surface the content of such
+               // slots, see T209923.
+
+               return parent::getOutputLayoutHints(); // TODO: Change the autogenerated stub
+       }
+
+}
diff --git a/includes/Revision/MainSlotRoleHandler.php b/includes/Revision/MainSlotRoleHandler.php
new file mode 100644 (file)
index 0000000..6c6fdd6
--- /dev/null
@@ -0,0 +1,132 @@
+<?php
+/**
+ * This file is part of MediaWiki.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+namespace MediaWiki\Revision;
+
+use ContentHandler;
+use Hooks;
+use MediaWiki\Linker\LinkTarget;
+use Title;
+
+/**
+ * A SlotRoleHandler for the main slot. While most slot roles serve a specific purpose and
+ * thus typically exhibit the same behaviour on all pages, the main slot is used for different
+ * things in different pages, typically depending on the namespace, a "file extension" in
+ * the page name, or the content model of the slot's content.
+ *
+ * MainSlotRoleHandler implements some of the per-namespace and per-model behavior that was
+ * supported prior to MediaWiki Version 1.33.
+ *
+ * @since 1.33
+ */
+class MainSlotRoleHandler extends SlotRoleHandler {
+
+       /**
+        * @var string[] A mapping of namespaces to content models.
+        * @see $wgNamespaceContentModels
+        */
+       private $namespaceContentModels;
+
+       /**
+        * @param string[] $namespaceContentModels A mapping of namespaces to content models,
+        *        typically from $wgNamespaceContentModels.
+        */
+       public function __construct( array $namespaceContentModels ) {
+               parent::__construct( 'main', CONTENT_MODEL_WIKITEXT );
+               $this->namespaceContentModels = $namespaceContentModels;
+       }
+
+       public function supportsArticleCount() {
+               return true;
+       }
+
+       /**
+        * @param string $model
+        * @param LinkTarget $page
+        *
+        * @return bool
+        */
+       public function isAllowedModel( $model, LinkTarget $page ) {
+               $title = Title::newFromLinkTarget( $page );
+               $handler = ContentHandler::getForModelID( $model );
+               return $handler->canBeUsedOn( $title );
+       }
+
+       /**
+        * @param LinkTarget $page
+        *
+        * @return string
+        */
+       public function getDefaultModel( LinkTarget $page ) {
+               // NOTE: this method must not rely on $title->getContentModel() directly or indirectly,
+               //       because it is used to initialize the mContentModel member.
+
+               $ext = '';
+               $ns = $page->getNamespace();
+               $model = $this->namespaceContentModels[$ns] ?? null;
+
+               // Hook can determine default model
+               $title = Title::newFromLinkTarget( $page );
+               if ( !Hooks::run( 'ContentHandlerDefaultModelFor', [ $title, &$model ] ) ) {
+                       if ( !is_null( $model ) ) {
+                               return $model;
+                       }
+               }
+
+               // Could this page contain code based on the title?
+               $isCodePage = $ns === NS_MEDIAWIKI && preg_match( '!\.(css|js|json)$!u', $title->getText(), $m );
+               if ( $isCodePage ) {
+                       $ext = $m[1];
+               }
+
+               // Is this a user subpage containing code?
+               $isCodeSubpage = $ns === NS_USER
+                       && !$isCodePage
+                       && preg_match( "/\\/.*\\.(js|css|json)$/", $title->getText(), $m );
+
+               if ( $isCodeSubpage ) {
+                       $ext = $m[1];
+               }
+
+               // Is this wikitext, according to $wgNamespaceContentModels or the DefaultModelFor hook?
+               $isWikitext = is_null( $model ) || $model == CONTENT_MODEL_WIKITEXT;
+               $isWikitext = $isWikitext && !$isCodePage && !$isCodeSubpage;
+
+               if ( !$isWikitext ) {
+                       switch ( $ext ) {
+                               case 'js':
+                                       return CONTENT_MODEL_JAVASCRIPT;
+                               case 'css':
+                                       return CONTENT_MODEL_CSS;
+                               case 'json':
+                                       return CONTENT_MODEL_JSON;
+                               default:
+                                       return is_null( $model ) ? CONTENT_MODEL_TEXT : $model;
+                       }
+               }
+
+               // We established that it must be wikitext
+
+               return CONTENT_MODEL_WIKITEXT;
+       }
+
+}
index c8f56e9..094105a 100644 (file)
@@ -230,6 +230,7 @@ class RenderedRevision implements SlotRenderingProvider {
                                        'Access to the content has been suppressed for this audience'
                                );
                        } else {
+                               // XXX: allow SlotRoleHandler to control the ParserOutput?
                                $output = $this->getSlotParserOutputUncached( $content, $withHtml );
 
                                if ( $withHtml && !$output->hasText() ) {
index 265ad13..f97390a 100644 (file)
@@ -50,15 +50,24 @@ class RevisionRenderer {
        /** @var ILoadBalancer */
        private $loadBalancer;
 
+       /** @var SlotRoleRegistry */
+       private $roleRegistery;
+
        /** @var string|bool */
        private $wikiId;
 
        /**
         * @param ILoadBalancer $loadBalancer
+        * @param SlotRoleRegistry $roleRegistry
         * @param bool|string $wikiId
         */
-       public function __construct( ILoadBalancer $loadBalancer, $wikiId = false ) {
+       public function __construct(
+               ILoadBalancer $loadBalancer,
+               SlotRoleRegistry $roleRegistry,
+               $wikiId = false
+       ) {
                $this->loadBalancer = $loadBalancer;
+               $this->roleRegistery = $roleRegistry;
                $this->wikiId = $wikiId;
 
                $this->saveParseLogger = new NullLogger();
@@ -184,8 +193,6 @@ class RevisionRenderer {
                        return $rrev->getSlotParserOutput( SlotRecord::MAIN );
                }
 
-               // TODO: put fancy layout logic here, see T200915.
-
                // move main slot to front
                if ( isset( $slots[SlotRecord::MAIN] ) ) {
                        $slots = [ SlotRecord::MAIN => $slots[SlotRecord::MAIN] ] + $slots;
@@ -201,6 +208,7 @@ class RevisionRenderer {
                        $out = $rrev->getSlotParserOutput( $role, $hints );
                        $slotOutput[$role] = $out;
 
+                       // XXX: should the SlotRoleHandler be able to intervene here?
                        $combinedOutput->mergeInternalMetaDataFrom( $out, $role );
                        $combinedOutput->mergeTrackingMetaDataFrom( $out );
                }
@@ -210,6 +218,16 @@ class RevisionRenderer {
                        $first = true;
                        /** @var ParserOutput $out */
                        foreach ( $slotOutput as $role => $out ) {
+                               $roleHandler = $this->roleRegistery->getRoleHandler( $role );
+
+                               // TODO: put more fancy layout logic here, see T200915.
+                               $layout = $roleHandler->getOutputLayoutHints();
+                               $display = $layout['display'] ?? 'section';
+
+                               if ( $display === 'none' ) {
+                                       continue;
+                               }
+
                                if ( $first ) {
                                        // skip header for the first slot
                                        $first = false;
@@ -219,6 +237,8 @@ class RevisionRenderer {
                                        $html .= Html::rawElement( 'h1', [ 'class' => 'mw-slot-header' ], $headText );
                                }
 
+                               // XXX: do we want to put a wrapper div around the output?
+                               // Do we want to let $roleHandler do that?
                                $html .= $out->getRawText();
                                $combinedOutput->mergeHtmlMetaDataFrom( $out );
                        }
index fc1f6df..ab73b2c 100644 (file)
@@ -132,6 +132,9 @@ class RevisionStore
        /** @var int An appropriate combination of SCHEMA_COMPAT_XXX flags. */
        private $mcrMigrationStage;
 
+       /** @var SlotRoleRegistry */
+       private $slotRoleRegistry;
+
        /**
         * @todo $blobStore should be allowed to be any BlobStore!
         *
@@ -146,11 +149,11 @@ class RevisionStore
         * @param CommentStore $commentStore
         * @param NameTableStore $contentModelStore
         * @param NameTableStore $slotRoleStore
+        * @param SlotRoleRegistry $slotRoleRegistry
         * @param int $mcrMigrationStage An appropriate combination of SCHEMA_COMPAT_XXX flags
         * @param ActorMigration $actorMigration
         * @param bool|string $wikiId
         *
-        * @throws MWException if $mcrMigrationStage or $wikiId is invalid.
         */
        public function __construct(
                ILoadBalancer $loadBalancer,
@@ -159,6 +162,7 @@ class RevisionStore
                CommentStore $commentStore,
                NameTableStore $contentModelStore,
                NameTableStore $slotRoleStore,
+               SlotRoleRegistry $slotRoleRegistry,
                $mcrMigrationStage,
                ActorMigration $actorMigration,
                $wikiId = false
@@ -199,6 +203,7 @@ class RevisionStore
                $this->commentStore = $commentStore;
                $this->contentModelStore = $contentModelStore;
                $this->slotRoleStore = $slotRoleStore;
+               $this->slotRoleRegistry = $slotRoleRegistry;
                $this->mcrMigrationStage = $mcrMigrationStage;
                $this->actorMigration = $actorMigration;
                $this->wikiId = $wikiId;
@@ -923,7 +928,7 @@ class RevisionStore
                $format = $content->getDefaultFormat();
                $model = $content->getModel();
 
-               $this->checkContent( $content, $title );
+               $this->checkContent( $content, $title, $slot->getRole() );
 
                return $this->blobStore->storeBlob(
                        $content->serialize( $format ),
@@ -982,11 +987,12 @@ class RevisionStore
         *
         * @param Content $content
         * @param Title $title
+        * @param string $role
         *
         * @throws MWException
         * @throws MWUnknownContentModelException
         */
-       private function checkContent( Content $content, Title $title ) {
+       private function checkContent( Content $content, Title $title, $role ) {
                // Note: may return null for revisions that have not yet been inserted
 
                $model = $content->getModel();
@@ -1005,7 +1011,8 @@ class RevisionStore
 
                        $this->assertCrossWikiContentLoadingIsSafe();
 
-                       $defaultModel = ContentHandler::getDefaultModelFor( $title );
+                       $roleHandler = $this->slotRoleRegistry->getRoleHandler( $role );
+                       $defaultModel = $roleHandler->getDefaultModel( $title );
                        $defaultHandler = ContentHandler::getForModelID( $defaultModel );
                        $defaultFormat = $defaultHandler->getDefaultFormat();
 
@@ -1350,9 +1357,8 @@ class RevisionStore
                        $mainSlotRow->model_name = function ( SlotRecord $slot ) use ( $title ) {
                                $this->assertCrossWikiContentLoadingIsSafe();
 
-                               // TODO: MCR: consider slot role in getDefaultModelFor()! Use LinkTarget!
-                               // TODO: MCR: deprecate $title->getModel().
-                               return ContentHandler::getDefaultModelFor( $title );
+                               return $this->slotRoleRegistry->getRoleHandler( $slot->getRole() )
+                                       ->getDefaultModel( $title );
                        };
                }
 
index 30ffc99..6b3117f 100644 (file)
@@ -72,10 +72,14 @@ class RevisionStoreFactory {
        /** @var NameTableStoreFactory */
        private $nameTables;
 
+       /** @var SlotRoleRegistry */
+       private $slotRoleRegistry;
+
        /**
         * @param ILBFactory $dbLoadBalancerFactory
         * @param BlobStoreFactory $blobStoreFactory
         * @param NameTableStoreFactory $nameTables
+        * @param SlotRoleRegistry $slotRoleRegistry
         * @param WANObjectCache $cache
         * @param CommentStore $commentStore
         * @param ActorMigration $actorMigration
@@ -88,6 +92,7 @@ class RevisionStoreFactory {
                ILBFactory $dbLoadBalancerFactory,
                BlobStoreFactory $blobStoreFactory,
                NameTableStoreFactory $nameTables,
+               SlotRoleRegistry $slotRoleRegistry,
                WANObjectCache $cache,
                CommentStore $commentStore,
                ActorMigration $actorMigration,
@@ -98,6 +103,7 @@ class RevisionStoreFactory {
                Assert::parameterType( 'integer', $migrationStage, '$migrationStage' );
                $this->dbLoadBalancerFactory = $dbLoadBalancerFactory;
                $this->blobStoreFactory = $blobStoreFactory;
+               $this->slotRoleRegistry = $slotRoleRegistry;
                $this->nameTables = $nameTables;
                $this->cache = $cache;
                $this->commentStore = $commentStore;
@@ -124,6 +130,7 @@ class RevisionStoreFactory {
                        $this->commentStore,
                        $this->nameTables->getContentModels( $wikiId ),
                        $this->nameTables->getSlotRoles( $wikiId ),
+                       $this->slotRoleRegistry,
                        $this->mcrMigrationStage,
                        $this->actorMigration,
                        $wikiId
diff --git a/includes/Revision/SlotRoleHandler.php b/includes/Revision/SlotRoleHandler.php
new file mode 100644 (file)
index 0000000..85b4c5a
--- /dev/null
@@ -0,0 +1,159 @@
+<?php
+/**
+ * This file is part of MediaWiki.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+namespace MediaWiki\Revision;
+
+use MediaWiki\Linker\LinkTarget;
+
+/**
+ * SlotRoleHandler instances are used to declare the existence and behavior of slot roles.
+ * Most importantly, they control which content model can be used for the slot, and how it is
+ * represented in the rendered verswion of page content.
+ *
+ * @since 1.33
+ */
+class SlotRoleHandler {
+
+       /**
+        * @var string
+        */
+       private $role;
+
+       /**
+        * @var array
+        * @see getOutputLayoutHints
+        */
+       private $layout = [
+               'display' => 'section', // use 'none' to suppress
+               'region' => 'center',
+               'placement' => 'append'
+       ];
+
+       /**
+        * @var string
+        */
+       private $contentModel;
+
+       /**
+        * @param string $role The name of the slot role defined by this SlotRoleHandler. See
+        *        SlotRoleRegistry::defineRole for more information.
+        * @param string $contentModel The default content model for this slot. As per the default
+        *        implementation of isAllowedModel(), also the only content model allowed for the
+        *        slot. Subclasses may however handle default and allowed models differently.
+        * @param array $layout Layout hints, for use by RevisionRenderer. See getOutputLayoutHints.
+        */
+       public function __construct( $role, $contentModel, $layout = [] ) {
+               $this->role = $role;
+               $this->contentModel = $contentModel;
+               $this->layout = array_merge( $this->layout, $layout );
+       }
+
+       /**
+        * @return string The role this SlotRoleHandler applies to
+        */
+       public function getRole() {
+               return $this->role;
+       }
+
+       /**
+        * Layout hints for use while laying out the combined output of all slots, typically by
+        * RevisionRenderer. The layout hints are given as an associative array. Well-known keys
+        * to use:
+        *
+        * * "display": how the output of this slot should be represented. Supported values:
+        *   - "section": show as a top level section of the region.
+        *   - "none": do not show at all
+        *   Further values that may be supported in the future include "box" and "banner".
+        * * "region": in which region of the page the output should be placed. Supported values:
+        *   - "center": the central content area.
+        *   Further values that may be supported in the future include "top" and "bottom", "left"
+        *   and "right", "header" and "footer".
+        * * "placement": placement relative to other content of the same area.
+        *   - "append": place at the end, after any output processed previously.
+        *   Further values that may be supported in the future include "prepend". A "weight" key
+        *   may be introduced for more fine grained control.
+        *
+        * @return array an associative array of hints
+        */
+       public function getOutputLayoutHints() {
+               return $this->layout;
+       }
+
+       /**
+        * The message key for the translation of the slot name.
+        *
+        * @return string
+        */
+       public function getNameMessageKey() {
+               return 'slot-name-' . $this->role;
+       }
+
+       /**
+        * Determines the content model to use per default for this slot on the given page.
+        *
+        * The default implementation always returns the content model provided to the constructor.
+        * Subclasses may base the choice on default model on the page title or namespace.
+        * The choice should not depend on external state, such as the page content.
+        *
+        * @param LinkTarget $page
+        *
+        * @return string
+        */
+       public function getDefaultModel( LinkTarget $page ) {
+               return $this->contentModel;
+       }
+
+       /**
+        * Determines whether the given model can be used on this slot on the given page.
+        *
+        * The default implementation checks whether $model is the content model provided to the
+        * constructor. Subclasses may allow other models and may base the decision on the page title
+        * or namespace. The choice should not depend on external state, such as the page content.
+        *
+        * @note This should be checked when creating new revisions. Existing revisions
+        *       are not guaranteed to comply with the return value.
+        *
+        * @param string $model
+        * @param LinkTarget $page
+        *
+        * @return bool
+        */
+       public function isAllowedModel( $model, LinkTarget $page ) {
+               return ( $model === $this->contentModel );
+       }
+
+       /**
+        * Whether this slot should be considered when determining whether a page should be counted
+        * as an "article" in the site statistics.
+        *
+        * For a page to be considered countable, one of the page's slots must return true from this
+        * method, and Content::isCountable() must return true for the content of that slot.
+        *
+        * The default implementation always returns false.
+        *
+        * @return string
+        */
+       public function supportsArticleCount() {
+               return false;
+       }
+
+}
diff --git a/includes/Revision/SlotRoleRegistry.php b/includes/Revision/SlotRoleRegistry.php
new file mode 100644 (file)
index 0000000..b108b98
--- /dev/null
@@ -0,0 +1,236 @@
+<?php
+/**
+ * This file is part of MediaWiki.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+namespace MediaWiki\Revision;
+
+use InvalidArgumentException;
+use LogicException;
+use MediaWiki\Linker\LinkTarget;
+use MediaWiki\Storage\NameTableStore;
+use Wikimedia\Assert\Assert;
+
+/**
+ * A registry service for SlotRoleHandlers, used to define which slot roles are available on
+ * which page.
+ *
+ * Extensions may use the SlotRoleRegistry to register the slots they define.
+ *
+ * In the context of the SlotRoleRegistry, it is useful to distinguish between "defined" and "known"
+ * slot roles: A slot role is "defined" if defineRole() or defineRoleWithModel() was called for
+ * that role. A slot role is "known" if the NameTableStore provided to the constructor as the
+ * $roleNamesStore parameter has an ID associated with that role, which essentially means that
+ * the role at some point has been used on the wiki. Roles that are not "defined" but are
+ * "known" typically belong to extensions that used to be installed on the wiki, but no longer are.
+ * Such slots should be considered ok for display and administrative operations, but only "defined"
+ * slots should be supported for editing.
+ *
+ * @since 1.33
+ */
+class SlotRoleRegistry {
+
+       /**
+        * @var NameTableStore
+        */
+       private $roleNamesStore;
+
+       /**
+        * @var callable[]
+        */
+       private $instantiators = [];
+
+       /**
+        * @var SlotRoleHandler[]
+        */
+       private $handlers;
+
+       /**
+        * SlotRoleRegistry constructor.
+        *
+        * @param NameTableStore $roleNamesStore
+        */
+       public function __construct( NameTableStore $roleNamesStore ) {
+               $this->roleNamesStore = $roleNamesStore;
+       }
+
+       /**
+        * Defines a slot role.
+        *
+        * For use by extensions that wish to define roles beyond the main slot role.
+        *
+        * @see defineRoleWithModel()
+        *
+        * @param string $role The role name of the slot to define. This should follow the
+        *        same convention as message keys:
+        * @param callable $instantiator called with $role as a parameter;
+        *        Signature: function ( string $role ): SlotRoleHandler
+        */
+       public function defineRole( $role, callable $instantiator ) {
+               if ( $this->isDefinedRole( $role ) ) {
+                       throw new LogicException( "Role $role is already defined" );
+               }
+
+               $this->instantiators[$role] = $instantiator;
+       }
+
+       /**
+        * Defines a slot role that allows only the given content model, and has no special
+        * behavior.
+        *
+        * For use by extensions that wish to define roles beyond the main slot role, but have
+        * no need to implement any special behavior for that slot.
+        *
+        * @see defineRole()
+        *
+        * @param string $role The role name of the slot to define, see defineRole()
+        *        for more information.
+        * @param string $model A content model name, see ContentHandler
+        * @param array $layout See SlotRoleHandler getOutputLayoutHints
+        */
+       public function defineRoleWithModel( $role, $model, $layout = [] ) {
+               $this->defineRole(
+                       $role,
+                       function ( $role ) use ( $model, $layout ) {
+                               return new SlotRoleHandler( $role, $model, $layout );
+                       }
+               );
+       }
+
+       /**
+        * Gets the SlotRoleHandler that should be used when processing content of the given role.
+        *
+        * @param string $role
+        *
+        * @throws InvalidArgumentException If $role is not a known slot role.
+        * @return SlotRoleHandler The handler to be used for $role. This may be a
+        *         FallbackSlotRoleHandler if the slot is "known" but not "defined".
+        */
+       public function getRoleHandler( $role ) {
+               if ( !isset( $this->handlers[$role] ) ) {
+                       if ( !$this->isDefinedRole( $role ) ) {
+                               if ( $this->isKnownRole( $role ) ) {
+                                       // The role has no handler defined, but is represented in the database.
+                                       // This may happen e.g. when the extension that defined the role was uninstalled.
+                                       wfWarn( __METHOD__ . ": known but undefined slot role $role" );
+                                       $this->handlers[$role] = new FallbackSlotRoleHandler( $role );
+                               } else {
+                                       // The role doesn't have a handler defined, and is not represented in
+                                       // the database. Something must be quite wrong.
+                                       throw new InvalidArgumentException( "Unknown role $role" );
+                               }
+                       } else {
+                               $handler = call_user_func( $this->instantiators[$role], $role );
+
+                               Assert::postcondition(
+                                       $handler instanceof SlotRoleHandler,
+                                       "Instantiator for $role role must return a SlotRoleHandler"
+                               );
+
+                               $this->handlers[$role] = $handler;
+                       }
+               }
+
+               return $this->handlers[$role];
+       }
+
+       /**
+        * Returns the list of roles allowed when creating a new revision on the given page.
+        * The choice should not depend on external state, such as the page content.
+        * Note that existing revisions of that page are not guaranteed to comply with this list.
+        *
+        * All implementations of this method are required to return at least all "required" roles.
+        *
+        * @param LinkTarget $title
+        *
+        * @return string[]
+        */
+       public function getAllowedRoles( LinkTarget $title ) {
+               // TODO: allow this to be overwritten per namespace (or page type)
+               // TODO: decide how to control which slots are offered for editing per default (T209927)
+               return $this->getDefinedRoles();
+       }
+
+       /**
+        * Returns the list of roles required when creating a new revision on the given page.
+        * The should not depend on external state, such as the page content.
+        * Note that existing revisions of that page are not guaranteed to comply with this list.
+        *
+        * All required roles are implicitly considered "allowed", so any roles
+        * returned by this method will also be returned by getAllowedRoles().
+        *
+        * @param LinkTarget $title
+        *
+        * @return string[]
+        */
+       public function getRequiredRoles( LinkTarget $title ) {
+               // TODO: allow this to be overwritten per namespace (or page type)
+               return [ 'main' ];
+       }
+
+       /**
+        * Returns the list of roles defined by calling defineRole().
+        *
+        * This list should be used when enumerating slot roles that can be used for editing.
+        *
+        * @return string[]
+        */
+       public function getDefinedRoles() {
+               return array_keys( $this->instantiators );
+       }
+
+       /**
+        * Returns the list of known roles, including the ones returned by getDefinedRoles(),
+        * and roles that exist according to the NameTableStore provided to the constructor.
+        *
+        * This list should be used when enumerating slot roles that can be used in queries or
+        * for display.
+        *
+        * @return string[]
+        */
+       public function getKnownRoles() {
+               return array_unique( array_merge(
+                       $this->getDefinedRoles(),
+                       $this->roleNamesStore->getMap()
+               ) );
+       }
+
+       /**
+        * Whether the given role is defined, that is, it was defined by calling defineRole().
+        *
+        * @param string $role
+        * @return bool
+        */
+       public function isDefinedRole( $role ) {
+               return in_array( $role, $this->getDefinedRoles(), true );
+       }
+
+       /**
+        * Whether the given role is known, that is, it's either defined or exist according to
+        * the NameTableStore provided to the constructor.
+        *
+        * @param string $role
+        * @return bool
+        */
+       public function isKnownRole( $role ) {
+               return in_array( $role, $this->getKnownRoles(), true );
+       }
+
+}
index 33517a0..9a94389 100644 (file)
@@ -48,8 +48,10 @@ use MediaWiki\Logger\LoggerFactory;
 use MediaWiki\MediaWikiServices;
 use MediaWiki\Preferences\PreferencesFactory;
 use MediaWiki\Preferences\DefaultPreferencesFactory;
+use MediaWiki\Revision\MainSlotRoleHandler;
 use MediaWiki\Revision\RevisionFactory;
 use MediaWiki\Revision\RevisionLookup;
+use MediaWiki\Revision\SlotRoleRegistry;
 use MediaWiki\Revision\RevisionRenderer;
 use MediaWiki\Revision\RevisionStore;
 use MediaWiki\Revision\RevisionStoreFactory;
@@ -420,9 +422,12 @@ return [
        },
 
        'RevisionRenderer' => function ( MediaWikiServices $services ) : RevisionRenderer {
-               $renderer = new RevisionRenderer( $services->getDBLoadBalancer() );
-               $renderer->setLogger( LoggerFactory::getInstance( 'SaveParse' ) );
+               $renderer = new RevisionRenderer(
+                       $services->getDBLoadBalancer(),
+                       $services->getSlotRoleRegistry()
+               );
 
+               $renderer->setLogger( LoggerFactory::getInstance( 'SaveParse' ) );
                return $renderer;
        },
 
@@ -436,6 +441,7 @@ return [
                        $services->getDBLoadBalancerFactory(),
                        $services->getBlobStoreFactory(),
                        $services->getNameTableStoreFactory(),
+                       $services->getSlotRoleRegistry(),
                        $services->getMainWANObjectCache(),
                        $services->getCommentStore(),
                        $services->getActorMigration(),
@@ -519,6 +525,22 @@ return [
                return $factory;
        },
 
+       'SlotRoleRegistry' => function ( MediaWikiServices $services ) : SlotRoleRegistry {
+               $config = $services->getMainConfig();
+
+               $registry = new SlotRoleRegistry(
+                       $services->getNameTableStoreFactory()->getSlotRoles()
+               );
+
+               $registry->defineRole( 'main', function () use ( $config ) {
+                       return new MainSlotRoleHandler(
+                               $config->get( 'NamespaceContentModels' )
+                       );
+               } );
+
+               return $registry;
+       },
+
        'SpecialPageFactory' => function ( MediaWikiServices $services ) : SpecialPageFactory {
                return new SpecialPageFactory(
                        $services->getMainConfig(),
index 1fc41f0..c401d44 100644 (file)
@@ -44,6 +44,7 @@ use MediaWiki\Revision\RevisionRecord;
 use MediaWiki\Revision\RevisionRenderer;
 use MediaWiki\Revision\RevisionSlots;
 use MediaWiki\Revision\RevisionStore;
+use MediaWiki\Revision\SlotRoleRegistry;
 use MediaWiki\Revision\SlotRecord;
 use MediaWiki\User\UserIdentity;
 use MessageCache;
@@ -209,6 +210,9 @@ class DerivedPageDataUpdater implements IDBAccessObject {
         */
        private $revisionRenderer;
 
+       /** @var SlotRoleRegistry */
+       private $slotRoleRegistry;
+
        /**
         * A stage identifier for managing the life cycle of this instance.
         * Possible stages are 'new', 'knows-current', 'has-content', 'has-revision', and 'done'.
@@ -255,6 +259,7 @@ class DerivedPageDataUpdater implements IDBAccessObject {
         * @param WikiPage $wikiPage ,
         * @param RevisionStore $revisionStore
         * @param RevisionRenderer $revisionRenderer
+        * @param SlotRoleRegistry $slotRoleRegistry
         * @param ParserCache $parserCache
         * @param JobQueueGroup $jobQueueGroup
         * @param MessageCache $messageCache
@@ -265,6 +270,7 @@ class DerivedPageDataUpdater implements IDBAccessObject {
                WikiPage $wikiPage,
                RevisionStore $revisionStore,
                RevisionRenderer $revisionRenderer,
+               SlotRoleRegistry $slotRoleRegistry,
                ParserCache $parserCache,
                JobQueueGroup $jobQueueGroup,
                MessageCache $messageCache,
@@ -276,6 +282,7 @@ class DerivedPageDataUpdater implements IDBAccessObject {
                $this->parserCache = $parserCache;
                $this->revisionStore = $revisionStore;
                $this->revisionRenderer = $revisionRenderer;
+               $this->slotRoleRegistry = $slotRoleRegistry;
                $this->jobQueueGroup = $jobQueueGroup;
                $this->messageCache = $messageCache;
                $this->contLang = $contLang;
@@ -636,12 +643,26 @@ class DerivedPageDataUpdater implements IDBAccessObject {
                $hasLinks = null;
 
                if ( $this->articleCountMethod === 'link' ) {
+                       // NOTE: it would be more appropriate to determine for each slot separately
+                       // whether it has links, and use that information with that slot's
+                       // isCountable() method. However, that would break parity with
+                       // WikiPage::isCountable, which uses the pagelinks table to determine
+                       // whether the current revision has links.
                        $hasLinks = (bool)count( $this->getCanonicalParserOutput()->getLinks() );
                }
 
-               // TODO: MCR: ask all slots if they have links [SlotHandler/PageTypeHandler]
-               $mainContent = $this->getRawContent( SlotRecord::MAIN );
-               return $mainContent->isCountable( $hasLinks );
+               foreach ( $this->getModifiedSlotRoles() as $role ) {
+                       $roleHandler = $this->slotRoleRegistry->getRoleHandler( $role );
+                       if ( $roleHandler->supportsArticleCount() ) {
+                               $content = $this->getRawContent( $role );
+
+                               if ( $content->isCountable( $hasLinks ) ) {
+                                       return true;
+                               }
+                       }
+               }
+
+               return false;
        }
 
        /**
@@ -649,6 +670,7 @@ class DerivedPageDataUpdater implements IDBAccessObject {
         */
        public function isRedirect() {
                // NOTE: main slot determines redirect status
+               // TODO: MCR: this should be controlled by a PageTypeHandler
                $mainContent = $this->getRawContent( SlotRecord::MAIN );
 
                return $mainContent->isRedirect();
index 043e00e..6cbdcc6 100644 (file)
@@ -31,7 +31,6 @@ use Content;
 use ContentHandler;
 use DeferredUpdates;
 use Hooks;
-use InvalidArgumentException;
 use LogicException;
 use ManualLogEntry;
 use MediaWiki\Linker\LinkTarget;
@@ -39,6 +38,7 @@ use MediaWiki\Revision\MutableRevisionRecord;
 use MediaWiki\Revision\RevisionAccessException;
 use MediaWiki\Revision\RevisionRecord;
 use MediaWiki\Revision\RevisionStore;
+use MediaWiki\Revision\SlotRoleRegistry;
 use MediaWiki\Revision\SlotRecord;
 use MWException;
 use RecentChange;
@@ -96,6 +96,11 @@ class PageUpdater {
         */
        private $revisionStore;
 
+       /**
+        * @var SlotRoleRegistry
+        */
+       private $slotRoleRegistry;
+
        /**
         * @var boolean see $wgUseAutomaticEditSummaries
         * @see $wgUseAutomaticEditSummaries
@@ -148,13 +153,15 @@ class PageUpdater {
         * @param DerivedPageDataUpdater $derivedDataUpdater
         * @param LoadBalancer $loadBalancer
         * @param RevisionStore $revisionStore
+        * @param SlotRoleRegistry $slotRoleRegistry
         */
        public function __construct(
                User $user,
                WikiPage $wikiPage,
                DerivedPageDataUpdater $derivedDataUpdater,
                LoadBalancer $loadBalancer,
-               RevisionStore $revisionStore
+               RevisionStore $revisionStore,
+               SlotRoleRegistry $slotRoleRegistry
        ) {
                $this->user = $user;
                $this->wikiPage = $wikiPage;
@@ -162,6 +169,7 @@ class PageUpdater {
 
                $this->loadBalancer = $loadBalancer;
                $this->revisionStore = $revisionStore;
+               $this->slotRoleRegistry = $slotRoleRegistry;
 
                $this->slotsUpdate = new RevisionSlotsUpdate();
        }
@@ -317,14 +325,6 @@ class PageUpdater {
                return $this->derivedDataUpdater->grabCurrentRevision();
        }
 
-       /**
-        * @return string
-        */
-       private function getTimestampNow() {
-               // TODO: allow an override to be injected for testing
-               return wfTimestampNow();
-       }
-
        /**
         * Check flags and add EDIT_NEW or EDIT_UPDATE to them as needed.
         *
@@ -346,8 +346,7 @@ class PageUpdater {
         * @param Content $content
         */
        public function setContent( $role, Content $content ) {
-               // TODO: MCR: check the role and the content's model against the list of supported
-               // roles, see T194046.
+               $this->ensureRoleAllowed( $role );
 
                $this->slotsUpdate->modifyContent( $role, $content );
        }
@@ -358,6 +357,8 @@ class PageUpdater {
         * @param SlotRecord $slot
         */
        public function setSlot( SlotRecord $slot ) {
+               $this->ensureRoleAllowed( $slot->getRole() );
+
                $this->slotsUpdate->modifySlot( $slot );
        }
 
@@ -376,6 +377,7 @@ class PageUpdater {
         *        by the new revision.
         */
        public function inheritSlot( SlotRecord $originalSlot ) {
+               // NOTE: slots can be inherited even if the role is not "allowed" on the title.
                // NOTE: this slot is inherited from some other revision, but it's
                // a "modified" slot for the RevisionSlotsUpdate and DerivedPageDataUpdater,
                // since it's not implicitly inherited from the parent revision.
@@ -393,9 +395,7 @@ class PageUpdater {
         * @param string $role A slot role name (but not "main")
         */
        public function removeSlot( $role ) {
-               if ( $role === SlotRecord::MAIN ) {
-                       throw new InvalidArgumentException( 'Cannot remove the main slot!' );
-               }
+               $this->ensureRoleNotRequired( $role );
 
                $this->slotsUpdate->removeSlot( $role );
        }
@@ -635,20 +635,38 @@ class PageUpdater {
                        throw new RuntimeException( 'Something is trying to edit an article with an empty title' );
                }
 
-               // TODO: MCR: check the role and the content's model against the list of supported
-               // and required roles, see T194046.
+               // NOTE: slots can be inherited even if the role is not "allowed" on the title.
+               $status = Status::newGood();
+               $this->checkAllRolesAllowed(
+                       $this->slotsUpdate->getModifiedRoles(),
+                       $status
+               );
+               $this->checkNoRolesRequired(
+                       $this->slotsUpdate->getRemovedRoles(),
+                       $status
+               );
 
-               // Make sure the given content type is allowed for this page
-               // TODO: decide: Extend check to other slots? Consider the role in check? [PageType]
-               $mainContentHandler = $this->getContentHandler( SlotRecord::MAIN );
-               if ( !$mainContentHandler->canBeUsedOn( $this->getTitle() ) ) {
-                       $this->status = Status::newFatal( 'content-not-allowed-here',
-                               ContentHandler::getLocalizedName( $mainContentHandler->getModelID() ),
-                               $this->getTitle()->getPrefixedText()
-                       );
+               if ( !$status->isOK() ) {
                        return null;
                }
 
+               // Make sure the given content is allowed in the respective slots of this page
+               foreach ( $this->slotsUpdate->getModifiedRoles() as $role ) {
+                       $slot = $this->slotsUpdate->getModifiedSlot( $role );
+                       $roleHandler = $this->slotRoleRegistry->getRoleHandler( $role );
+
+                       if ( !$roleHandler->isAllowedModel( $slot->getModel(), $this->getTitle() ) ) {
+                               $contentHandler = ContentHandler::getForModelID( $slot->getModel() );
+                               $this->status = Status::newFatal( 'content-not-allowed-here',
+                                       ContentHandler::getLocalizedName( $contentHandler->getModelID() ),
+                                       $this->getTitle()->getPrefixedText(),
+                                       wfMessage( $roleHandler->getNameMessageKey() )
+                                       // TODO: defer message lookup to caller
+                               );
+                               return null;
+                       }
+               }
+
                // Load the data from the master database if needed. Needed to check flags.
                // NOTE: This grabs the parent revision as the CAS token, if grabParentRevision
                // wasn't called yet. If the page is modified by another process before we are done with
@@ -882,13 +900,19 @@ class PageUpdater {
                        $content = $slot->getContent();
 
                        // XXX: We may push this up to the "edit controller" level, see T192777.
-                       // TODO: change the signature of PrepareSave to not take a WikiPage!
+                       // XXX: prepareSave() and isValid() could live in SlotRoleHandler
+                       // XXX: PrepareSave should not take a WikiPage!
                        $prepStatus = $content->prepareSave( $wikiPage, $flags, $oldid, $user );
 
                        // TODO: MCR: record which problem arose in which slot.
                        $status->merge( $prepStatus );
                }
 
+               $this->checkAllRequiredRoles(
+                       $rev->getSlotRoles(),
+                       $status
+               );
+
                return $rev;
        }
 
@@ -1216,4 +1240,71 @@ class PageUpdater {
                );
        }
 
+       /**
+        * @return string[] Slots required for this page update, as a list of role names.
+        */
+       private function getRequiredSlotRoles() {
+               return $this->slotRoleRegistry->getRequiredRoles( $this->getTitle() );
+       }
+
+       /**
+        * @return string[] Slots allowed for this page update, as a list of role names.
+        */
+       private function getAllowedSlotRoles() {
+               return $this->slotRoleRegistry->getAllowedRoles( $this->getTitle() );
+       }
+
+       private function ensureRoleAllowed( $role ) {
+               $allowedRoles = $this->getAllowedSlotRoles();
+               if ( !in_array( $role, $allowedRoles ) ) {
+                       throw new PageUpdateException( "Slot role `$role` is not allowed." );
+               }
+       }
+
+       private function ensureRoleNotRequired( $role ) {
+               $requiredRoles = $this->getRequiredSlotRoles();
+               if ( in_array( $role, $requiredRoles ) ) {
+                       throw new PageUpdateException( "Slot role `$role` is required." );
+               }
+       }
+
+       private function checkAllRolesAllowed( array $roles, Status $status ) {
+               $allowedRoles = $this->getAllowedSlotRoles();
+
+               $forbidden = array_diff( $roles, $allowedRoles );
+               if ( !empty( $forbidden ) ) {
+                       $status->error(
+                               'edit-slots-cannot-add',
+                               count( $forbidden ),
+                               implode( ', ', $forbidden )
+                       );
+               }
+       }
+
+       private function checkNoRolesRequired( array $roles, Status $status ) {
+               $requiredRoles = $this->getRequiredSlotRoles();
+
+               $needed = array_diff( $roles, $requiredRoles );
+               if ( !empty( $needed ) ) {
+                       $status->error(
+                               'edit-slots-cannot-remove',
+                               count( $needed ),
+                               implode( ', ', $needed )
+                       );
+               }
+       }
+
+       private function checkAllRequiredRoles( array $roles, Status $status ) {
+               $requiredRoles = $this->getRequiredSlotRoles();
+
+               $missing = array_diff( $requiredRoles, $roles );
+               if ( !empty( $missing ) ) {
+                       $status->error(
+                               'edit-slots-missing',
+                               count( $missing ),
+                               implode( ', ', $missing )
+                       );
+               }
+       }
+
 }
index c151f4a..8b4075b 100644 (file)
@@ -978,6 +978,8 @@ class Title implements LinkTarget {
        /**
         * Get the page's content model id, see the CONTENT_MODEL_XXX constants.
         *
+        * @todo Deprecate this in favor of SlotRecord::getModel()
+        *
         * @param int $flags A bit field; may be Title::GAID_FOR_UPDATE to select for update
         * @return string Content model id
         */
@@ -2688,30 +2690,14 @@ class Title implements LinkTarget {
                }
 
                $useReplica = ( $rigor !== 'secure' );
-               $block = $user->getBlock( $useReplica );
-
-               // The block may explicitly allow an action (like "read" or "upload").
-               if ( $block && $block->prevents( $action ) === false ) {
-                       return $errors;
-               }
-
-               // Determine if the user is blocked from this action on this page.
-               try {
+               if ( ( $action == 'edit' || $action == 'create' )
+                       && !$user->isBlockedFrom( $this, $useReplica )
+               ) {
+                       // Don't block the user from editing their own talk page unless they've been
+                       // explicitly blocked from that too.
+               } elseif ( $user->isBlocked() && $user->getBlock()->prevents( $action ) !== false ) {
                        // @todo FIXME: Pass the relevant context into this function.
-                       $action = Action::factory( $action, WikiPage::factory( $this ), RequestContext::getMain() );
-               } catch ( Exception $e ) {
-                       $action = null;
-               }
-
-               // If no action object is returned, assume that the action requires unblock
-               // which is the default.
-               if ( !$action || $action->requiresUnblock() ) {
-                       if ( $user->isBlockedFrom( $this, $useReplica ) ) {
-                               // @todo FIXME: Pass the relevant context into this function.
-                               $errors[] = $block
-                                       ? $block->getPermissionsError( RequestContext::getMain() )
-                                       : [ 'badaccess-group0' ];
-                       }
+                       $errors[] = $user->getBlock()->getPermissionsError( RequestContext::getMain() );
                }
 
                return $errors;
index 76b7bce..393f435 100644 (file)
@@ -30,11 +30,15 @@ class ApiComparePages extends ApiBase {
        /** @var RevisionStore */
        private $revisionStore;
 
+       /** @var \MediaWiki\Revision\SlotRoleRegistry */
+       private $slotRoleRegistry;
+
        private $guessedTitle = false, $props;
 
        public function __construct( ApiMain $mainModule, $moduleName, $modulePrefix = '' ) {
                parent::__construct( $mainModule, $moduleName, $modulePrefix );
                $this->revisionStore = MediaWikiServices::getInstance()->getRevisionStore();
+               $this->slotRoleRegistry = MediaWikiServices::getInstance()->getSlotRoleRegistry();
        }
 
        public function execute() {
@@ -272,9 +276,8 @@ class ApiComparePages extends ApiBase {
                }
 
                $guessedTitle = $this->guessTitle();
-               if ( $guessedTitle && $role === SlotRecord::MAIN ) {
-                       // @todo: Use SlotRoleRegistry and do this for all slots
-                       return $guessedTitle->getContentModel();
+               if ( $guessedTitle ) {
+                       return $this->slotRoleRegistry->getRoleHandler( $role )->getDefaultModel( $guessedTitle );
                }
 
                if ( isset( $params["fromcontentmodel-$role"] ) ) {
@@ -582,10 +585,7 @@ class ApiComparePages extends ApiBase {
        }
 
        public function getAllowedParams() {
-               $slotRoles = MediaWikiServices::getInstance()->getSlotRoleStore()->getMap();
-               if ( !in_array( SlotRecord::MAIN, $slotRoles, true ) ) {
-                       $slotRoles[] = SlotRecord::MAIN;
-               }
+               $slotRoles = $this->slotRoleRegistry->getKnownRoles();
                sort( $slotRoles, SORT_STRING );
 
                // Parameters for the 'from' and 'to' content
index 5bf8da9..9edf929 100644 (file)
@@ -172,33 +172,29 @@ class ApiFeedContributions extends ApiBase {
         * @return string
         */
        protected function feedItemDesc( RevisionRecord $revision ) {
-               if ( $revision ) {
-                       $msg = wfMessage( 'colon-separator' )->inContentLanguage()->text();
-                       try {
-                               $content = $revision->getContent( SlotRecord::MAIN );
-                       } catch ( RevisionAccessException $e ) {
-                               $content = null;
-                       }
-
-                       if ( $content instanceof TextContent ) {
-                               // only textual content has a "source view".
-                               $html = nl2br( htmlspecialchars( $content->getNativeData() ) );
-                       } else {
-                               // XXX: we could get an HTML representation of the content via getParserOutput, but that may
-                               //     contain JS magic and generally may not be suitable for inclusion in a feed.
-                               //     Perhaps Content should have a getDescriptiveHtml method and/or a getSourceText method.
-                               // Compare also FeedUtils::formatDiffRow.
-                               $html = '';
-                       }
-
-                       $comment = $revision->getComment();
+               $msg = wfMessage( 'colon-separator' )->inContentLanguage()->text();
+               try {
+                       $content = $revision->getContent( SlotRecord::MAIN );
+               } catch ( RevisionAccessException $e ) {
+                       $content = null;
+               }
 
-                       return '<p>' . htmlspecialchars( $this->feedItemAuthor( $revision ) ) . $msg .
-                               htmlspecialchars( FeedItem::stripComment( $comment ? $comment->text : '' ) ) .
-                               "</p>\n<hr />\n<div>" . $html . '</div>';
+               if ( $content instanceof TextContent ) {
+                       // only textual content has a "source view".
+                       $html = nl2br( htmlspecialchars( $content->getNativeData() ) );
+               } else {
+                       // XXX: we could get an HTML representation of the content via getParserOutput, but that may
+                       //     contain JS magic and generally may not be suitable for inclusion in a feed.
+                       //     Perhaps Content should have a getDescriptiveHtml method and/or a getSourceText method.
+                       // Compare also FeedUtils::formatDiffRow.
+                       $html = '';
                }
 
-               return '';
+               $comment = $revision->getComment();
+
+               return '<p>' . htmlspecialchars( $this->feedItemAuthor( $revision ) ) . $msg .
+                       htmlspecialchars( FeedItem::stripComment( $comment ? $comment->text : '' ) ) .
+                       "</p>\n<hr />\n<div>" . $html . '</div>';
        }
 
        public function getAllowedParams() {
index c00010a..3d0a0fb 100644 (file)
@@ -616,10 +616,7 @@ abstract class ApiQueryRevisionsBase extends ApiQueryGeneratorBase {
        }
 
        public function getAllowedParams() {
-               $slotRoles = MediaWikiServices::getInstance()->getSlotRoleStore()->getMap();
-               if ( !in_array( SlotRecord::MAIN, $slotRoles, true ) ) {
-                       $slotRoles[] = SlotRecord::MAIN;
-               }
+               $slotRoles = MediaWikiServices::getInstance()->getSlotRoleRegistry()->getKnownRoles();
                sort( $slotRoles, SORT_STRING );
 
                return [
index 1211693..4258674 100644 (file)
@@ -38,6 +38,9 @@
        "apihelp-block-param-allowusertalk": "A felhasználó szerkeszthesse a saját vitalapját (a <var>[[mw:Special:MyLanguage/Manual:$wgBlockAllowsUTEdit|$wgBlockAllowsUTEdit]]</var> beállítástól függ).",
        "apihelp-block-param-reblock": "Jelenlegi blokk felülírása, ha a felhasználó már blokkolva van.",
        "apihelp-block-param-watchuser": "A szerkesztő vagy IP-cím szerkesztői- és vitalapjának figyelése.",
+       "apihelp-block-param-tags": "A blokknapló naplóbejegyzésére érvényesítendő változtatáscímkék.",
+       "apihelp-block-param-partial": "Teljes blokk helyett a felhasználó eltiltása bizonyos lapok vagy névterek szerkesztésétől.",
+       "apihelp-block-param-pagerestrictions": "A felhasználó számára blokkolandó címek listája. Csak akkor van hatása, ha a <var>partial</var> igaz.",
        "apihelp-block-example-ip-simple": "A <kbd>192.0.2.5</kbd> IP-cím blokkolása három napra <kbd>First strike</kbd> indoklással.",
        "apihelp-block-example-user-complex": "<kbd>Vandal</kbd> blokkolása határozatlan időre <kbd>Vandalism</kbd> indoklással, új fiók létrehozásának és e-mail küldésének megakadályozása.",
        "apihelp-checktoken-summary": "Egy <kbd>[[Special:ApiHelp/query+tokens|action=query&meta=tokens]]</kbd> kéréssel szerzett token érvényességének vizsgálata.",
index 43d70e6..5bf286d 100644 (file)
@@ -34,7 +34,6 @@ class BlockRestriction {
         *
         * @param int|array $blockId
         * @param IDatabase|null $db
-        * @param array $options Options to pass to the select query.
         * @return Restriction[]
         */
        public static function loadByBlockId( $blockId, IDatabase $db = null ) {
index f1cc1b0..5fefecc 100644 (file)
@@ -63,6 +63,7 @@ interface Restriction {
        /**
         * Creates a new Restriction from a database row.
         *
+        * @param \stdClass $row
         * @return self
         */
        public static function newFromRow( \stdClass $row );
index bb3fb10..1bb43f8 100644 (file)
@@ -241,6 +241,8 @@ interface Content {
         * that it's also in a countable location (e.g. a current revision in the
         * main namespace).
         *
+        * @see SlotRoleHandler::supportsArticleCount
+        *
         * @since 1.21
         *
         * @param bool|null $hasLinks If it is known whether this content contains
@@ -352,6 +354,8 @@ interface Content {
         * Returns whether this Content represents a redirect.
         * Shorthand for getRedirectTarget() !== null.
         *
+        * @see SlotRoleHandler::supportsRedirects
+        *
         * @since 1.21
         *
         * @return bool
index fab043a..5c18a33 100644 (file)
@@ -174,62 +174,17 @@ abstract class ContentHandler {
         * Note: this is used by, and may thus not use, Title::getContentModel()
         *
         * @since 1.21
+        * @deprecated since 1.33, use SlotRoleHandler::getDefaultModel() together with
+        * SlotRoleRegistry::getRoleHandler().
         *
         * @param Title $title
         *
         * @return string Default model name for the page given by $title
         */
        public static function getDefaultModelFor( Title $title ) {
-               // NOTE: this method must not rely on $title->getContentModel() directly or indirectly,
-               //       because it is used to initialize the mContentModel member.
-
-               $ns = $title->getNamespace();
-
-               $ext = false;
-               $m = null;
-               $model = MWNamespace::getNamespaceContentModel( $ns );
-
-               // Hook can determine default model
-               if ( !Hooks::run( 'ContentHandlerDefaultModelFor', [ $title, &$model ] ) ) {
-                       if ( !is_null( $model ) ) {
-                               return $model;
-                       }
-               }
-
-               // Could this page contain code based on the title?
-               $isCodePage = NS_MEDIAWIKI == $ns && preg_match( '!\.(css|js|json)$!u', $title->getText(), $m );
-               if ( $isCodePage ) {
-                       $ext = $m[1];
-               }
-
-               // Is this a user subpage containing code?
-               $isCodeSubpage = NS_USER == $ns
-                       && !$isCodePage
-                       && preg_match( "/\\/.*\\.(js|css|json)$/", $title->getText(), $m );
-               if ( $isCodeSubpage ) {
-                       $ext = $m[1];
-               }
-
-               // Is this wikitext, according to $wgNamespaceContentModels or the DefaultModelFor hook?
-               $isWikitext = is_null( $model ) || $model == CONTENT_MODEL_WIKITEXT;
-               $isWikitext = $isWikitext && !$isCodePage && !$isCodeSubpage;
-
-               if ( !$isWikitext ) {
-                       switch ( $ext ) {
-                               case 'js':
-                                       return CONTENT_MODEL_JAVASCRIPT;
-                               case 'css':
-                                       return CONTENT_MODEL_CSS;
-                               case 'json':
-                                       return CONTENT_MODEL_JSON;
-                               default:
-                                       return is_null( $model ) ? CONTENT_MODEL_TEXT : $model;
-                       }
-               }
-
-               // We established that it must be wikitext
-
-               return CONTENT_MODEL_WIKITEXT;
+               $slotRoleregistry = MediaWikiServices::getInstance()->getSlotRoleRegistry();
+               $mainSlotHandler = $slotRoleregistry->getRoleHandler( 'main' );
+               return $mainSlotHandler->getDefaultModel( $title );
        }
 
        /**
@@ -777,7 +732,7 @@ abstract class ContentHandler {
 
        /**
         * Determines whether the content type handled by this ContentHandler
-        * can be used on the given page.
+        * can be used for the main slot of the given page.
         *
         * This default implementation always returns true.
         * Subclasses may override this to restrict the use of this content model to specific locations,
@@ -787,6 +742,8 @@ abstract class ContentHandler {
         * @note this calls the ContentHandlerCanBeUsedOn hook which may be used to override which
         * content model can be used where.
         *
+        * @see SlotRoleHandler::isAllowedModel
+        *
         * @param Title $title The page's title.
         *
         * @return bool True if content of this kind can be used on the given page, false otherwise.
index 780ea94..ee3e9a1 100644 (file)
@@ -62,7 +62,7 @@ class SyslogHandler extends SyslogUdpHandler {
         * @param string $host Syslog host
         * @param int $port Syslog port
         * @param int $facility Syslog message facility
-        * @param string $level The minimum logging level at which this handler
+        * @param int $level The minimum logging level at which this handler
         *   will be triggered
         * @param bool $bubble Whether the messages that are handled can bubble up
         *   the stack or not
index 8d0971e..63cc2a8 100644 (file)
@@ -1055,7 +1055,7 @@ class DifferenceEngine extends ContextSource {
                        $slotDiff = $slotDiffRenderer->getDiff( $slotContents[$role]['old'],
                                $slotContents[$role]['new'] );
                        if ( $slotDiff && $role !== SlotRecord::MAIN ) {
-                               // TODO use human-readable role name at least
+                               // FIXME: ask SlotRoleHandler::getSlotNameMessage
                                $slotTitle = $role;
                                $difftext .= $this->getSlotHeader( $slotTitle );
                        }
index edd73fc..ab925b7 100644 (file)
@@ -76,7 +76,6 @@ class DumpOutput {
        /**
         * Override to write to a different stream type.
         * @param string $string
-        * @return bool
         */
        function write( $string ) {
                print $string;
index e7bdb44..fb0ca77 100644 (file)
@@ -15,6 +15,7 @@ class HTMLExpiryField extends HTMLFormField {
 
        /**
         * Relative Date Time Field.
+        * @param array $params
         */
        public function __construct( array $params = [] ) {
                parent::__construct( $params );
index 477cc4c..1c4a785 100644 (file)
@@ -121,7 +121,7 @@ class HTMLMultiSelectField extends HTMLFormField implements HTMLNestedFilterable
 
        /**
         * Get options and make them into arrays suitable for OOUI.
-        * @return array Options for inclusion in a select or whatever.
+        * @throws MWException
         */
        public function getOptionsOOUI() {
                // Sections make this difficult. See getInputOOUI().
index 010cf80..e3e7343 100644 (file)
@@ -217,7 +217,7 @@ abstract class MediaTransformOutput {
         * @return bool Success
         */
        public function streamFile( $headers = [] ) {
-               $this->streamFileWithStatus( $headers )->isOK();
+               return $this->streamFileWithStatus( $headers )->isOK();
        }
 
        /**
index 9f623ba..c7c7069 100644 (file)
@@ -26,6 +26,7 @@ use MediaWiki\MediaWikiServices;
 use MediaWiki\Revision\RevisionRecord;
 use MediaWiki\Revision\RevisionRenderer;
 use MediaWiki\Revision\RevisionStore;
+use MediaWiki\Revision\SlotRoleRegistry;
 use MediaWiki\Revision\SlotRecord;
 use MediaWiki\Storage\DerivedPageDataUpdater;
 use MediaWiki\Storage\PageUpdater;
@@ -232,6 +233,13 @@ class WikiPage implements Page, IDBAccessObject {
                return MediaWikiServices::getInstance()->getRevisionRenderer();
        }
 
+       /**
+        * @return SlotRoleRegistry
+        */
+       private function getSlotRoleRegistry() {
+               return MediaWikiServices::getInstance()->getSlotRoleRegistry();
+       }
+
        /**
         * @return ParserCache
         */
@@ -952,12 +960,17 @@ class WikiPage implements Page, IDBAccessObject {
                                // links.
                                $hasLinks = (bool)count( $editInfo->output->getLinks() );
                        } else {
-                               // NOTE: keep in sync with revisionRenderer::getLinkCount
+                               // NOTE: keep in sync with RevisionRenderer::getLinkCount
+                               // NOTE: keep in sync with DerivedPageDataUpdater::isCountable
                                $hasLinks = (bool)wfGetDB( DB_REPLICA )->selectField( 'pagelinks', 1,
                                        [ 'pl_from' => $this->getId() ], __METHOD__ );
                        }
                }
 
+               // TODO: MCR: determine $hasLinks for each slot, and use that info
+               // with that slot's Content's isCountable method. That requires per-
+               // slot ParserOutput in the ParserCache, or per-slot info in the
+               // pagelinks table.
                return $content->isCountable( $hasLinks );
        }
 
@@ -1665,6 +1678,7 @@ class WikiPage implements Page, IDBAccessObject {
                        $this, // NOTE: eventually, PageUpdater should not know about WikiPage
                        $this->getRevisionStore(),
                        $this->getRevisionRenderer(),
+                       $this->getSlotRoleRegistry(),
                        $this->getParserCache(),
                        JobQueueGroup::singleton(),
                        MessageCache::singleton(),
@@ -1769,7 +1783,8 @@ class WikiPage implements Page, IDBAccessObject {
                        $this, // NOTE: eventually, PageUpdater should not know about WikiPage
                        $this->getDerivedDataUpdater( $user, null, $forUpdate, true ),
                        $this->getDBLoadBalancer(),
-                       $this->getRevisionStore()
+                       $this->getRevisionStore(),
+                       $this->getSlotRoleRegistry()
                );
 
                $pageUpdater->setUsePageCreationLog( $wgPageCreationLog );
index ee6f250..58541b2 100644 (file)
@@ -54,11 +54,7 @@ class UploadFromChunks extends UploadFromFile {
                if ( $stash ) {
                        $this->stash = $stash;
                } else {
-                       if ( $user ) {
-                               wfDebug( __METHOD__ . " creating new UploadFromChunks instance for " . $user->getId() . "\n" );
-                       } else {
-                               wfDebug( __METHOD__ . " creating new UploadFromChunks instance with no user\n" );
-                       }
+                       wfDebug( __METHOD__ . " creating new UploadFromChunks instance for " . $user->getId() . "\n" );
                        $this->stash = new UploadStash( $this->repo, $this->user );
                }
        }
index aa287e9..0efade9 100644 (file)
@@ -1833,22 +1833,19 @@ class Language {
                while ( $hebrewMonth <= 12 ) {
                        # Calculate days in this month
                        if ( $isLeap && $hebrewMonth == 6 ) {
-                               # Adar in a leap year
-                               if ( $isLeap ) {
-                                       # Leap year - has Adar I, with 30 days, and Adar II, with 29 days
-                                       $days = 30;
+                               # Leap year - has Adar I, with 30 days, and Adar II, with 29 days
+                               $days = 30;
+                               if ( $hebrewDay <= $days ) {
+                                       # Day in Adar I
+                                       $hebrewMonth = 13;
+                               } else {
+                                       # Subtract the days of Adar I
+                                       $hebrewDay -= $days;
+                                       # Try Adar II
+                                       $days = 29;
                                        if ( $hebrewDay <= $days ) {
-                                               # Day in Adar I
-                                               $hebrewMonth = 13;
-                                       } else {
-                                               # Subtract the days of Adar I
-                                               $hebrewDay -= $days;
-                                               # Try Adar II
-                                               $days = 29;
-                                               if ( $hebrewDay <= $days ) {
-                                                       # Day in Adar II
-                                                       $hebrewMonth = 14;
-                                               }
+                                               # Day in Adar II
+                                               $hebrewMonth = 14;
                                        }
                                }
                        } elseif ( $hebrewMonth == 2 && $yearPattern == 2 ) {
index ef515eb..84d118e 100644 (file)
        "welcomecreation-msg": "Akun-neuh ka geupeugöt. \nDroeneuh jeuet neugantoe {{SITENAME}} [[Special:Preferences|peuatô]] meunyö neumeuh'eut.",
        "yourname": "Ureuëng ngui:",
        "userlogin-yourname": "Ureuëng ngui",
-       "userlogin-yourname-ph": "Peutamöng nan ureuëng ngui droëneuh",
+       "userlogin-yourname-ph": "Peutamöng nan ureueng ngui",
        "createacct-another-username-ph": "Pasoë nan ureuëng ngui droëneuh",
        "yourpassword": "Lageuem tamöng:",
        "userlogin-yourpassword": "Lageuem tamöng",
-       "userlogin-yourpassword-ph": "Pasoe lageuem tamöng droeneuh",
+       "userlogin-yourpassword-ph": "Pasoe lageuem tamöng",
        "createacct-yourpassword-ph": "Pasoe lageuem tamöng",
        "yourpasswordagain": "Pasoe lom lageuem tamöng:",
        "createacct-yourpasswordagain": "Peunyo lageuem tamöng",
        "sp-contributions-logs": "log",
        "sp-contributions-talk": "marit",
        "sp-contributions-search": "Mita soë nyang tuléh",
-       "sp-contributions-username": "Alamat IP atawa nan ureuëng ngui:",
+       "sp-contributions-username": "Alamat IP atawa nan ureueng ngui:",
        "sp-contributions-toponly": "Peuleumah geunantoe nyang baro mantong",
        "sp-contributions-newonly": "Peuleumah pumeugöt laman mantöng",
        "sp-contributions-hideminor": "Peusom peusaneut bacut",
index 9187e33..dd3d0e0 100644 (file)
        "rcfilters-filterlist-feedbacklink": "بیزه بو فیلترلره گؤره دوشوندوگونوزو بیلیندیرین!",
        "rcfilters-highlightbutton-title": "نتیجه‌لری هایلایتلا",
        "rcfilters-filtergroup-authorship": "دییشدیرن",
+       "rcfilters-filtergroup-userExpLevel": "ایشلدن آدیازدیرما و تجروبه‌سی",
+       "rcfilters-filter-user-experience-level-registered-label": "آدیازدیریلمیش",
+       "rcfilters-filter-user-experience-level-unregistered-label": "آدیازدیریلمامیش",
+       "rcfilters-filter-user-experience-level-learner-label": "اؤیرننلر",
+       "rcfilters-filter-user-experience-level-experienced-label": "تجروبه‌لی ایشلدنلر",
        "rcfilters-filtergroup-automated": "اوْتوماتیک دییشدیرمه‌لر",
        "rcfilters-filter-humans-label": "اینسان (غئیر روْبات)",
        "rcfilters-filter-humans-description": "اینسان اَلی ایله دییشدیرمه‌لر",
        "rcfilters-filtergroup-reviewstatus": "یوخلاما وضعیتی",
        "rcfilters-filter-minor-label": "کیچیک دَییشدیرمه‌لر",
+       "rcfilters-filter-major-label": "کیچیک اوْلمایان دییشدیرمه‌لر",
        "rcfilters-filtergroup-watchlist": "ایزلنمیش صفحه‌لر",
        "rcfilters-filter-watchlist-watched-label": "ایزلنمیش",
        "rcfilters-filtergroup-changetype": "دَییشیکلیک نوعو",
index dbec79d..124d23b 100644 (file)
        "localtime": "Мясцовы час:",
        "timezoneuseserverdefault": "Выкарыстоўваць стандартныя налады {{GRAMMAR:родны|{{SITENAME}}}} ($1)",
        "timezoneuseoffset": "Іншы (пазначце ніжэй розьніцу ў часе)",
+       "timezone-useoffset-placeholder": "Напрыклад: «-07:00» ці «01:00»",
        "servertime": "Час на сэрвэры:",
        "guesstimezone": "Запоўніць з браўзэра",
        "timezoneregion-africa": "Афрыка",
index f0c83c7..de62590 100644 (file)
        "view": "Паказ",
        "view-foreign": "Глядзець на $1",
        "edit": "Правіць",
-       "edit-local": "Правіць тутэйшае апісанне",
+       "edit-local": "Правіць лакальнае апісанне",
        "create": "Стварыць",
-       "create-local": "Дадаць тутэйшае апісанне",
+       "create-local": "Дадаць лакальнае апісанне",
        "delete": "Выдаліць",
        "undelete_short": "Аднавіць {{PLURAL:$1|адну праўку|$1 правак}}",
        "viewdeleted_short": "Паказаць {{PLURAL:$1|адну сцёртую праўку|$1 сцёртыя праўкі}}",
index 1a2ef9d..cc78e3f 100644 (file)
        "timezonelegend": "Časové pásmo:",
        "localtime": "Místní čas:",
        "timezoneuseserverdefault": "Použít časové pásmo wiki ($1)",
-       "timezoneuseoffset": "Jiné (zadejte posun)",
+       "timezoneuseoffset": "Jiné (níže zadejte posun)",
+       "timezone-useoffset-placeholder": "Příklady hodnot: „-07:00“ nebo „01:00“",
        "servertime": "Čas na serveru:",
        "guesstimezone": "Načíst z prohlížeče",
        "timezoneregion-africa": "Afrika",
index 462d835..c831929 100644 (file)
        "tog-watchlisthideminor": "Skjul mindre ændringer i overvågningslisten",
        "tog-watchlisthideliu": "Skjul indloggede brugeres redigeringer i overvågningslisten",
        "tog-watchlistreloadautomatically": "Opdater overvågningslisten automatisk, når et filter ændres (kræver JavaScript)",
-       "tog-watchlistunwatchlinks": "Tilføj direkte henvisninger for at overvåge/fjerne overvågning til overvågningsposter (JavaScript krævet for at skifte funktionalitet)",
+       "tog-watchlistunwatchlinks": "Føj mærker ({{int:Watchlist-unwatch}}/{{int:Watchlist-unwatch-undo}}) til at slå overvågning til og fra for overvågede sider med ændringer (JavaScript kræves for at kunne slå til og fra)",
        "tog-watchlisthideanons": "Skjul anonyme brugeres redigeringer i overvågningslisten",
        "tog-watchlisthidepatrolled": "Skjul patruljerede ændringer i overvågningslisten",
        "tog-watchlisthidecategorization": "Skjul kategorisering af sider",
index 07ace65..25545a4 100644 (file)
        "localtime": "Τοπική ώρα:",
        "timezoneuseserverdefault": "Χρήση της προεπιλογής του wiki ($1)",
        "timezoneuseoffset": "Ἀλλη (καθορισμός της διαφοράς)",
+       "timezone-useoffset-placeholder": "Τιμές ως παράδειγμα: \"-07:00\" or \"01:00\"",
        "servertime": "Η ώρα του διακομιστή:",
        "guesstimezone": "Συμπλήρωση μέσω του browser",
        "timezoneregion-africa": "Αφρική",
index dd94787..159e705 100644 (file)
        "edit-gone-missing": "Could not update the page.\nIt appears to have been deleted.",
        "edit-conflict": "Edit conflict.",
        "edit-no-change": "Your edit was ignored because no change was made to the text.",
+       "edit-slots-cannot-add": "The following {{PLURAL:$1|slot is|slots are}} not supported here: $2.",
+       "edit-slots-cannot-remove": "The following {{PLURAL:$1|slot is|slots are}} required and cannot be removed: $2.",
+       "edit-slots-missing": "The following {{PLURAL:$1|slot is|slots are}} missing: $2.",
        "postedit-confirmation-created": "The page has been created.",
        "postedit-confirmation-restored": "The page has been restored.",
        "postedit-confirmation-saved": "Your edit was saved.",
        "defaultmessagetext": "Default message text",
        "content-failed-to-parse": "Failed to parse $2 content for $1 model: $3",
        "invalid-content-data": "Invalid content data",
-       "content-not-allowed-here": "\"$1\" content is not allowed on page [[$2]]",
+       "content-not-allowed-here": "\"$1\" content is not allowed on page [[$2]] in slot \"$3\"",
        "editwarning-warning": "Leaving this page may cause you to lose any changes you have made.\nIf you are logged in, you can disable this warning in the \"{{int:prefs-editing}}\" section of your preferences.",
        "editpage-invalidcontentmodel-title": "Content model not supported",
        "editpage-invalidcontentmodel-text": "The content model \"$1\" is not supported.",
index df93640..4855e3a 100644 (file)
        "filedesc": "Resumo",
        "fileuploadsummary": "Resumo:",
        "filereuploadsummary": "Dosieraj ŝanĝoj:",
-       "filestatus": "Aŭtorrajta statuso:",
+       "filestatus": "Aŭtorrajta stato:",
        "filesource": "Fonto:",
        "ignorewarning": "Ignori averton kaj konservi dosieron ĉiukaze",
        "ignorewarnings": "Ignori ĉiajn avertojn",
        "exif-nickname": "Malformala nomo de bildo",
        "exif-rating": "Taksado (el 5)",
        "exif-rightscertificate": "Atestilo de rajtoj-administrado",
-       "exif-copyrighted": "Aŭtorrajta statuso:",
+       "exif-copyrighted": "Aŭtorrajta stato:",
        "exif-copyrightowner": "Posedanto de la aŭtorrajto",
        "exif-usageterms": "Regularo pri uzado",
        "exif-webstatement": "Interreta deklarado pri aŭtorrajtoj",
        "version-license-not-found": "Por tiu ĉi etendilo ne estis trovitaj pli detalaj permesilaj informoj.",
        "version-credits-title": "Agnosko por $1",
        "version-credits-not-found": "Por tiu ĉi etendilo ne estis trovitaj pli detalaj informoj pri aŭtoroj.",
-       "version-poweredby-credits": "Ĉi tiu vikio funkcias per '''[https://www.mediawiki.org/ MediaWiki]''', aŭtorrajto ©&thinsp;2001–$1 $2.",
+       "version-poweredby-credits": "Ĉi tiu vikio funkcias per <strong>[https://www.mediawiki.org/ MediaWiki]</strong>, aŭtorrajto © 2001–$1 $2.",
        "version-poweredby-others": "aliaj",
        "version-poweredby-translators": "tradukantoj de translatewiki.net",
        "version-credits-summary": "Ni ŝatus agnoski la sekvajn personojn pro siaj kontribuoj al [[Special:Version|MediaWiki]].",
index 48cfb2d..e54c2e1 100644 (file)
        "prefs-displaywatchlist": "Näyttöasetukset",
        "prefs-changesrc": "Näytettävät muutokset",
        "prefs-changeswatchlist": "Näytettävät muutokset",
-       "prefs-pageswatchlist": "Tarkkailtavat sivut",
+       "prefs-pageswatchlist": "Tarkkaillut sivut",
        "prefs-tokenwatchlist": "Avain",
        "prefs-diffs": "Eroavaisuudet",
        "prefs-help-prefershttps": "Tämä asetus tulee voimaan seuraavan sisäänkirjautumisesi yhteydessä.",
index efff545..7f9c58e 100644 (file)
        "subject-preview": "Tárgy előnézete:",
        "previewerrortext": "Hiba történt a változások előnézetének megjelenítése során.",
        "blockedtitle": "A szerkesztő blokkolva van",
+       "blocked-email-user": "<strong>Szerkesztőneved számára az e-mail küldési lehetőséget blokkoltuk. Továbbra is szerkeszthetsz egyéb lapokat.</strong> A blokkolás további részleteit a [[Special:MyContributions|fiók közreműködéseinél]] találod.\n\nA blokkolást $1 hajtotta végre.\n\nAz általa megadott indok: <em>$2.</em>\n\n* A blokk kezdete: $8\n* A blokk lejárata: $6\n* Blokkolt szerkesztő: $7\n* A blokkolás azonosítószáma: $5",
+       "blockedtext-partial": "<strong>Szerkesztőneved vagy IP-címed számára az oldal szerkesztését blokkoltuk. Továbbra is szerkeszthetsz egyéb lapokat.</strong> A blokkolás további részleteit a [[Special:MyContributions|fiók közreműködéseinél]] találod.\n\nA blokkolást $1 hajtotta végre.\n\nAz általa megadott indok: <em>$2.</em>\n\n* A blokk kezdete: $8\n* A blokk lejárata: $6\n* Blokkolt szerkesztő: $7\n* A blokkolás azonosítószáma: $5",
        "blockedtext": "<strong>A szerkesztőnevedet vagy az IP-címedet blokkoltuk.</strong>\n\nA blokkolást $1 végezte el.\nAz általa felhozott indok: <em>$2.</em>\n\n* A blokk kezdete: $8\n* A blokk lejárata: $6\n* Blokkolt szerkesztő: $7\n\nKapcsolatba léphetsz $1 szerkesztőnkkel vagy egy másik [[{{MediaWiki:Grouppage-sysop}}|adminisztrátorral]], és megbeszélheted vele a blokkolást.\nAz „{{int:emailuser}}” funkciót csak akkor használhatod, ha érvényes e-mail-címet adtál meg [[Special:Preferences|fiókbeállításaidban]], és nem blokkolták a használatát.\nJelenlegi IP-címed: $3, a blokkolás azonosítószáma: #$5.\nKérjük, hogy érdeklődés esetén minden fenti részletet adj meg.",
        "autoblockedtext": "Az IP-címed automatikusan blokkolva lett, mert korábban egy olyan szerkesztő használta, akit $1 blokkolt, az alábbi indoklással:\n\n:''$2''\n\n*A blokk kezdete: '''$8'''\n*A blokk lejárata: '''$6'''\n*Blokkolt szerkesztő: '''$7'''\n\nKapcsolatba léphetsz $1 szerkesztőnkkel, vagy egy másik [[{{MediaWiki:Grouppage-sysop}}|adminisztrátorral]], és megbeszélheted vele a blokkolást.\n\nAz „{{int:emailuser}}” funkciót csak akkor használhatod, ha érvényes e-mail címet adtál meg\n[[Special:Preferences|fiókbeállításaidban]], és nem blokkolták a használatát.\n\nJelenlegi IP-címed: $3, a blokkolás azonosítószáma: #$5.\nKérjük, hogy érdeklődés esetén mindkettőt add meg.",
        "systemblockedtext": "A felhasználónevedet vagy IP-címedet automatikusan blokkolta a MediaWiki.\nA blokkolás indoka:\n\n:<em>$2</em>\n\n* A blokk kezdete: $8\n* A blokk lejárata: $6\n* Blokkolt szerkesztő: $7\n\nA jelenlegi IP-címed: $3.\nKérjük, hogy érdeklődés esetén minden fenti részletet adj meg.",
        "converter-manual-rule-error": "Hiba van a kézi nyelvi konverziós szabályban",
        "undo-success": "A szerkesztés visszavonható. Kérlek ellenőrizd alább a változásokat, hogy valóban ezt szeretnéd-e tenni, majd kattints a lap mentése gombra a visszavonás véglegesítéséhez.",
        "undo-failure": "A szerkesztést nem lehet automatikusan visszavonni vele ütköző későbbi szerkesztések miatt.",
+       "undo-main-slot-only": "A szerkesztést nem lehet automatikusan visszavonni, mert érinti a tartalom más részét is.",
        "undo-norev": "A szerkesztés nem állítható vissza, mert nem létezik vagy törölve lett.",
        "undo-nochange": "A szerkesztés már vissza lett állítva.",
        "undo-summary": "Visszavontam [[Special:Contributions/$2|$2]] ([[User talk:$2|vita]]) szerkesztését (oldid: $1)",
        "localtime": "Helyi idő:",
        "timezoneuseserverdefault": "Az alapértelmezett beállítás használata ($1)",
        "timezoneuseoffset": "Egyéb (eltérés megadása)",
+       "timezone-useoffset-placeholder": "Példaértékek: „-07:00” vagy „01:00”",
        "servertime": "A kiszolgáló ideje:",
        "guesstimezone": "Töltse ki a böngésző",
        "timezoneregion-africa": "Afrika",
        "ipb-disableusertalk": "Megakadályozza, hogy a felhasználó szerkeszthesse a saját vitalapját, miközben blokkolva van",
        "ipb-change-block": "Blokk beállításainak megváltoztatása",
        "ipb-confirm": "Blokk megerősítése",
+       "ipb-sitewide": "Teljes körű",
+       "ipb-partial": "Részleges",
        "ipb-type-label": "Típus",
        "ipb-pages-label": "Lapok",
        "badipaddress": "Érvénytelen IP-cím",
        "createaccountblock": "új felhasználó létrehozása blokkolva",
        "emailblock": "e-mail-cím blokkolva",
        "blocklist-nousertalk": "nem szerkesztheti a vitalapját",
+       "blocklist-editing": "szerkesztés",
+       "blocklist-editing-sitewide": "szerkesztés (teljes körű)",
        "ipblocklist-empty": "A blokkoltak listája üres.",
        "ipblocklist-no-results": "A kért IP-cím vagy felhasználónév nem blokkolt.",
        "blocklink": "blokkolás",
        "move-watch": "Figyeld a lapot",
        "movepagebtn": "Lap átnevezése",
        "pagemovedsub": "Az átnevezés sikerült",
+       "cannotmove": "A lapot nem sikerült átnevezni a következő {{PLURAL:$1|ok|okok}} miatt:",
        "movepage-moved": "'''„$1” átnevezve „$2” névre'''",
        "movepage-moved-redirect": "Átirányítás létrehozva.",
        "movepage-moved-noredirect": "A régi címről nem készült átirányítás.",
+       "movepage-delete-first": "A céloldal túl sok változattal rendelkezik ahhoz, ezért az átnevezés részeként nem törölhető. Kérlek, előbb töröld a lapot kézzel, aztán próbáld újra.",
        "articleexists": "Ilyen névvel már létezik lap, vagy az általad választott név érvénytelen.\nKérlek, válassz egy másik nevet.",
        "cantmove-titleprotected": "Nem nevezheted át a lapot, mert az új cím le van védve a létrehozás ellen.",
        "movetalk": "Nevezd át a vitalapot is, ha lehetséges",
        "confirm-mcrundo-title": "Egy változtatás visszavonva",
        "mcrundofailed": "A visszavonás nem sikerült",
        "mcrundo-missingparam": "Kötelező paraméterek hiányoznak a kérésből.",
+       "mcrundo-changed": "A változtatások megtekintése óta az oldal megváltozott. Kérlek, tekintsd meg az új változtatásokat.",
        "ellipsis": "…",
        "quotation-marks": "„$1”",
        "imgmultipageprev": "← előző oldal",
        "logentry-block-block": "$1 {{GENDER:$2|blokkolta}} „{{GENDER:$4|$3}}”-t $5 időtartamra $6",
        "logentry-block-unblock": "$1 {{GENDER:$2|feloldotta}} {{GENDER:$4|$3}} blokkolását",
        "logentry-block-reblock": "$1 {{GENDER:$2|módosította}} a blokk beállításokat „{{GENDER:$4|$3}}” szerkesztőnél $5 időtartamra $6",
+       "logentry-partialblock-block": "$1 {{GENDER:$2|blokkolta}} „{{GENDER:$4|$3}}”-t $5 időtartamra $6 a következő {{PLURAL:$8|lap|lapok}} szerkesztésétől: $7",
+       "logentry-partialblock-reblock": "$1 {{GENDER:$2|módosította}} a(z) $7 {{PLURAL:$8|lap|lapok}} szerkesztését megakadályozó blokk beállítását „{{GENDER:$4|$3}}” szerkesztőnél $5 időtartamra $6",
+       "logentry-non-editing-block-block": "$1 {{GENDER:$2|blokkolta}} „{{GENDER:$4|$3}}”-t nem-szerkesztési műveletektől $5 időtartamra $6",
+       "logentry-non-editing-block-reblock": "$1 {{GENDER:$2|módosította}} a nem-szerkesztési műveletekre vonatkozó blokk beállításait „{{GENDER:$4|$3}}” szerkesztőnél $5 időtartamra $6",
        "logentry-suppress-block": "$1 {{GENDER:$2|blokkolta}} „{{GENDER:$4|$3}}”-t $5 időtartamra $6",
        "logentry-suppress-reblock": "$1 {{GENDER:$2|módosította}} a blokk beállításokat „{{GENDER:$4|$3}}” szerkesztőnél $5 időtartamra $6",
        "logentry-import-upload": "$1 {{GENDER:$2|importálta}} $3 lapot fájl feltöltéssel",
index be7811b..99db9e5 100644 (file)
        "timezonelegend": "Часовен појас:",
        "localtime": "Месно време:",
        "timezoneuseserverdefault": "Од викито ($1)",
-       "timezoneuseoffset": "Друго (посочете отстапување)",
+       "timezoneuseoffset": "Друго (подолу посочете отстапување)",
+       "timezone-useoffset-placeholder": "Примерни вредности: „-07:00“ или „01:00“",
        "servertime": "Време на опслужувачот:",
        "guesstimezone": "Пополни од прелистувачот",
        "timezoneregion-africa": "Африка",
index 52db680..f865446 100644 (file)
        "timezonelegend": "Tijdzone:",
        "localtime": "Plaatselijke tijd:",
        "timezoneuseserverdefault": "Wikistandaard gebruiken ($1)",
-       "timezoneuseoffset": "Anders (tijdverschil opgeven)",
+       "timezoneuseoffset": "Anders (vul tijdverschil hier beneden in)",
+       "timezone-useoffset-placeholder": "Voorbeeldinvoer: \"-07:00\" of \"01:00\"",
        "servertime": "Servertijd:",
        "guesstimezone": "Vanuit de browser toevoegen",
        "timezoneregion-africa": "Afrika",
index 70f4c47..ee10a52 100644 (file)
        "protect-expiry-options": "1 time:1 hour,1 dag:1 day,1 veke:1 week,2 veker:2 weeks,1 månad:1 month,3 månader:3 months,6 månader:6 months,1 år:1 year,endelaus:infinite",
        "restriction-type": "Tilgang:",
        "restriction-level": "Avgrensingsnivå:",
-       "minimum-size": "Minimumstorleik",
-       "maximum-size": "Maksimumstorleik:",
+       "minimum-size": "Minstestorleik",
+       "maximum-size": "Størstestorleik:",
        "pagesize": "(byte)",
        "restriction-edit": "Endring",
        "restriction-move": "Flytting",
index 393e920..4cbe2e5 100644 (file)
        "edit-gone-missing": "Used as error message.\n\nSee also:\n* {{msg-mw|edit-hook-aborted}}\n* {{msg-mw|edit-conflict}}\n* {{msg-mw|edit-no-change}}\n* {{msg-mw|edit-already-exists}}",
        "edit-conflict": "An 'Edit conflict' happens when more than one edit is being made to a page at the same time. This would usually be caused by separate individuals working on the same page. However, if the system is slow, several edits from one individual could back up and attempt to apply simultaneously - causing the conflict.\n\nSee also:\n* {{msg-mw|edit-hook-aborted}}\n* {{msg-mw|edit-gone-missing}}\n* {{msg-mw|edit-no-change}}\n* {{msg-mw|edit-already-exists}}",
        "edit-no-change": "Used as error message.\n\nSee also:\n* {{msg-mw|edit-hook-aborted}}\n* {{msg-mw|edit-gone-missing}}\n* {{msg-mw|edit-conflict}}\n* {{msg-mw|edit-already-exists}}",
+       "edit-slots-cannot-add": "An error message shown when trying to save an edit, if the edit tries to add a {{Identical|slot}} that is not allowed on the page.\n* $1 - the number of slots\n* $2 - the slots that were attempted to be added but are not allowed",
+       "edit-slots-cannot-remove": "An error message shown when trying to save an edit, if the edit tries to remove a {{Identical|slot}} that is required on the page.\n* $1 - the number of slots\n* $2 - the slots that were attempted to be removed but are required",
+       "edit-slots-missing": "An error message shown when trying to save an edit, if the edit is missing some required {{Identical|slot}}, which could not be inherited from a parent revision.\n* $1 - the number of slots\n* $2 - the slots that are required but missing from the new revision",
        "postedit-confirmation-created": "{{gender}}\nShown after a user creates a new page. Parameters:\n* $1 - the current user, for GENDER support",
        "postedit-confirmation-restored": "{{gender}}\nShown after a user restores a page to a previous revision. Parameters:\n* $1 - the current user, for GENDER support",
        "postedit-confirmation-saved": "{{gender}}\nShown after a user saves a page. Parameters:\n* $1 - the current user, for GENDER support",
        "defaultmessagetext": "Caption above the default message text shown on the left-hand side of a diff displayed after clicking \"Show changes\" when creating a new page in the MediaWiki: namespace",
        "content-failed-to-parse": "Error message indicating that the page's content can not be saved because it is syntactically invalid. This may occurr for content types using serialization or a strict markup syntax.\n\nParameters:\n* $1 – content model, any one of the following messages:\n** {{msg-mw|Content-model-wikitext}}\n** {{msg-mw|Content-model-javascript}}\n** {{msg-mw|Content-model-css}}\n** {{msg-mw|Content-model-json}}\n** {{msg-mw|Content-model-text}}\n* $2 – content format as MIME type (e.g. <code>text/css</code>)\n* $3 – specific error message",
        "invalid-content-data": "Error message indicating that the page's content can not be saved because it is invalid. This may occurr for content types with internal consistency constraints.",
-       "content-not-allowed-here": "Error message indicating that the desired content model is not supported in given localtion.\n* $1 - the human readable name of the content model: {{msg-mw|Content-model-wikitext}}, {{msg-mw|Content-model-javascript}}, {{msg-mw|Content-model-json}}, {{msg-mw|Content-model-css}} or {{msg-mw|Content-model-text}}\n* $2 - the title of the page in question",
+       "content-not-allowed-here": "Error message indicating that the desired content model is not supported in given localtion.\n* $1 - the human readable name of the content model: {{msg-mw|Content-model-wikitext}}, {{msg-mw|Content-model-javascript}}, {{msg-mw|Content-model-json}}, {{msg-mw|Content-model-css}} or {{msg-mw|Content-model-text}}\n* $2 - the title of the page in question\n* $3 - the role name of the slot the content is not allowed in",
        "editwarning-warning": "Uses {{msg-mw|Prefs-editing}}",
        "editpage-invalidcontentmodel-title": "Title of error page shown when using an unrecognized content model on EditPage",
        "editpage-invalidcontentmodel-text": "Error message shown when using an unrecognized content model on EditPage. $1 is the user's invalid input",
index 0e75cfb..73e36c1 100644 (file)
        "timezonelegend": "Часовой пояс:",
        "localtime": "Местное время:",
        "timezoneuseserverdefault": "Использовать настройки сервера ($1)",
-       "timezoneuseoffset": "Иное (укажите смещение)",
+       "timezoneuseoffset": "Иное (ниже укажите смещение)",
+       "timezone-useoffset-placeholder": "Например: «-07:00» или «01:00»",
        "servertime": "Время сервера:",
        "guesstimezone": "Заполнить из браузера",
        "timezoneregion-africa": "Африка",
index 5c13b34..aa8f6ee 100644 (file)
        "timezonelegend": "Časovni pas",
        "localtime": "Krajevni čas:",
        "timezoneuseserverdefault": "Uporabi privzeti wiki čas ($1)",
-       "timezoneuseoffset": "Drugo (navedite izravnavo)",
+       "timezoneuseoffset": "Drugo (spodaj navedite odmik)",
+       "timezone-useoffset-placeholder": "Primera vrednosti: »-07:00« ali »01:00«",
        "servertime": "Strežniški čas:",
        "guesstimezone": "Izpolni iz brskalnika",
        "timezoneregion-africa": "Afrika",
index c0acf9e..5826514 100644 (file)
        "lastmodifiedat": "Ова страница је последњи пут уређена на датум $1 у $2 ч.",
        "viewcount": "Овој страници је приступљено {{PLURAL:$1|једанпут|$1 пута}}.",
        "protectedpage": "Заштићена страница",
-       "jumpto": "Ð\98ди на:",
+       "jumpto": "Ð\9fÑ\80еÑ\92и на:",
        "jumptonavigation": "навигацију",
        "jumptosearch": "претрагу",
        "view-pool-error": "Сервери су тренутно преоптерећени.\nПревише корисника покушава да види ову страницу.\nСачекајте неко време пре него што поново покушате да јој приступите.\n\n$1",
        "updated": "(ажурирано)",
        "note": "<strong>Напомена:</strong>",
        "previewnote": "<strong>Не заборавите да је ово само претпреглед.</strong>\nВаше промене још нису сачуване!",
-       "continue-editing": "Ð\98ди Ð½Ð° Ñ\83Ñ\80еÑ\92иваÑ\87ки Ð¾ÐºÐ²Ð¸Ñ\80",
+       "continue-editing": "Ð\9fÑ\80еÑ\92и Ð½Ð° Ð¾Ð±Ð°Ñ\81Ñ\82 Ñ\83Ñ\80еÑ\92иваÑ\9aа",
        "previewconflict": "Овај преглед осликава како ће изгледати текст у текстуалном оквиру.",
        "session_fail_preview": "Извињавамо се! Нисмо могли да обрадимо вашу измену због губитка података сесије.\n\nМожда сте одјављени. <strong>Проверите да ли сте пријављени и покушајте поново</strong>.\nАко и даље не ради, покушајте да се [[Special:UserLogout|одјавите]] и поново пријавите, те проверите да ли су на вашем претраживачу дозвољени колачићи са овог сајта.",
        "session_fail_preview_html": "Нисмо могли да обрадимо вашу измену због губитка података сесије.\n\n<em>Будући да је на овом викију омогућен унос HTML ознака, преглед је сакривен као мера предострожности против напада преко јаваскрипта.</em>\n\n<strong>Ако сте покушали да направите праву измену, покушајте поново.<strong>\nАко и даље не ради, покушајте да се [[Special:UserLogout|одјавите]] и поново пријавите и проверите да ли ваш прегледач дозвољава колачиће са овог сајта.",
        "imgmultipageprev": "← претходна страница",
        "imgmultipagenext": "следећа страница →",
        "imgmultigo": "Иди!",
-       "imgmultigoto": "Ð\98ди на страницу $1",
+       "imgmultigoto": "Ð\9fÑ\80еÑ\92и на страницу $1",
        "img-lang-opt": "$2 ($1)",
        "img-lang-default": "(подразумевани језик)",
        "img-lang-info": "Рендеруј ову слику у $1. $2",
        "diff-form-submit": "Прикажи разлике",
        "permanentlink": "Трајна веза",
        "permanentlink-revid": "ID измене",
-       "permanentlink-submit": "Ð\98ди на измену",
+       "permanentlink-submit": "Ð\9fÑ\80еÑ\92и на измену",
        "dberr-problems": "Дошло је до техничких проблема.",
        "dberr-again": "Сачекајте неколико минута и поново учитајте страницу.",
        "dberr-info": "(Не могу приступити бази података: $1)",
index 8e3fd94..e4ed687 100644 (file)
        "timezonelegend": "Tidszon:",
        "localtime": "Lokal tid:",
        "timezoneuseserverdefault": "Använd wikins standard ($1)",
-       "timezoneuseoffset": "Annan (specificera skillnad)",
+       "timezoneuseoffset": "Annan (specificera skillnad nedan)",
+       "timezone-useoffset-placeholder": "Exempelvärden: \"-07:00\" eller \"01:00\"",
        "servertime": "Serverns tid:",
        "guesstimezone": "Fyll i från webbläsare",
        "timezoneregion-africa": "Afrika",
index 36ddafd..406b1b9 100644 (file)
        "timezonelegend": "Часовий пояс:",
        "localtime": "Місцевий час:",
        "timezoneuseserverdefault": "Використовувати стандартне налаштування вікі ($1)",
-       "timezoneuseoffset": "Інше (зазначте зміщення)",
+       "timezoneuseoffset": "Інше (нижче зазначте зміщення)",
+       "timezone-useoffset-placeholder": "Наприклад: «-07:00» або «01:00»",
        "servertime": "Час сервера:",
        "guesstimezone": "Заповнити з браузера",
        "timezoneregion-africa": "Африка",
        "move-watch": "Спостерігати за цією сторінкою",
        "movepagebtn": "Перейменувати сторінку",
        "pagemovedsub": "Перейменування виконано",
-       "cannotmove": "СÑ\82оÑ\80Ñ\96нка Ð½Ðµ Ð¼Ð¾Ð¶Ðµ Ð±Ñ\83Ñ\82и Ð¿ÐµÑ\80ейменована Ð· {{PLURAL:$1|1=Ñ\82акоÑ\8a причини|таких причин}}:",
+       "cannotmove": "СÑ\82оÑ\80Ñ\96нка Ð½Ðµ Ð¼Ð¾Ð¶Ðµ Ð±Ñ\83Ñ\82и Ð¿ÐµÑ\80ейменована Ð· {{PLURAL:$1|1=Ñ\82акоÑ\97 причини|таких причин}}:",
        "movepage-moved": "'''Сторінка «$1» перейменована на «$2»'''",
        "movepage-moved-redirect": "Створено перенаправлення.",
        "movepage-moved-noredirect": "Створення перенаправлення було заборонене.",
index 5c0ad6c..df7817d 100644 (file)
        "prefs-watchlist-edits": "زیر نظر فہرست میں نظر آنے والی تبدیلیوں کی زیادہ سے زیادہ تعداد:",
        "prefs-watchlist-edits-max": "زیادہ سے زیادہ تعداد: 1000",
        "prefs-watchlist-token": "زیر نظر فہرست کی کلید:",
+       "prefs-watchlist-managetokens": "انتظام ٹوکن",
        "prefs-misc": "دیگر",
        "prefs-resetpass": "پاس ورڈ تبدیل کریں",
        "prefs-changeemail": "برقی ڈاک پتا تبدیل یا حذف کریں",
        "recentchangescount": "حالیہ تبدیلیوں، تاریخچوں اور نوشتوں میں دکھائی جانے والی ترامیم کی تعداد:",
        "prefs-help-recentchangescount": "زیادہ سے زیادہ تعداد: 1000",
        "prefs-help-watchlist-token2": "یہ آپ کی زیر نظر فہرست کے ویب فیڈ کی خفیہ کلید ہے۔\nاسے خفیہ رکھیں، تاکہ کوئی دوسرا شخص آپ کی زیر نظر فہرست نہ دیکھ سکے۔\nاگر آپ کو کلید تبدیل کرنی ہو تو [[Special:ResetTokens|یہاں کلک کریں]]۔",
+       "prefs-help-tokenmanagement": "آپ کی زیر نظر فہرست کی ویب فیڈ تک رسائی کے لیے اپنے کھاتے کی خفیہ کلید آپ یہاں دیکھ اور بدل سکتے ہیں۔ جس کے پاس یہ کلید ہوگی وہ آپ کی زیر نظر فہرست کو دیکھ سکتا ہے، لہذا اس کلید کو خفیہ رکھیں۔",
        "savedprefs": "آپ کی ترجیحات محفوظ ہوگئیں۔",
        "savedrights": "{{GENDER:$1|$1}} کے اختیارات محفوظ ہو گئے۔",
        "timezonelegend": "منطقۂ وقت:",
        "prefs-advancedwatchlist": "اضافی اختیارات",
        "prefs-displayrc": "نمائش کے اختیارات",
        "prefs-displaywatchlist": "نمائش کے اختیارات",
+       "prefs-changesrc": "دکھائی جانے والی تبدیلیاں",
+       "prefs-changeswatchlist": "دکھائی جانے والی تبدیلیاں",
+       "prefs-pageswatchlist": "زیر نظر صفحات",
        "prefs-tokenwatchlist": "ٹوکن",
        "prefs-diffs": "فرق",
        "prefs-help-prefershttps": "یہ ترجیح آپ کے اگلے لاگ ان پر اثر انداز ہوگی۔",
index d226bf9..75be32d 100644 (file)
        "json-error-syntax": "語法錯咗",
        "json-error-utf8": "字符引導失敗,因為有非法UTF-8代碼。",
        "headline-anchor-title": "連結到呢一節",
-       "special-characters-group-latin": "拉丁",
-       "special-characters-group-latinextended": "Latin擴展左",
+       "special-characters-group-latin": "拉丁字母擴展",
+       "special-characters-group-latinextended": "拉丁字母擴展",
        "special-characters-group-ipa": "IPA",
        "special-characters-group-symbols": "符號",
        "special-characters-group-greek": "希臘文",
index 569a3c8..fd9c061 100644 (file)
        "timezonelegend": "时区:",
        "localtime": "当地时间:",
        "timezoneuseserverdefault": "使用wiki默认值($1)",
-       "timezoneuseoffset": "其(指定时差)",
+       "timezoneuseoffset": "其(指定时差)",
        "servertime": "服务器时间:",
        "guesstimezone": "使用浏览器设置",
        "timezoneregion-africa": "非洲",
index cb1f2bd..0385d80 100644 (file)
@@ -1372,7 +1372,6 @@ return [
                'dependencies' => [
                        'jquery.accessKeyLabel',
                        'mediawiki.RegExp',
-                       'mediawiki.notify',
                ],
                'targets' => [ 'desktop', 'mobile' ],
        ],
index 27031f1..65fe3d3 100644 (file)
                }
        };
 
-       /**
-        * Add a little box at the top of the screen to inform the user of
-        * something, replacing any previous message.
-        * Calling with no arguments, with an empty string or null will hide the message
-        *
-        * @method jsMessage
-        * @deprecated since 1.20 Use mw#notify
-        * @param {Mixed} message The DOM-element, jQuery object or HTML-string to be put inside the message box.
-        *  to allow CSS/JS to hide different boxes. null = no class used.
-        */
-       mw.log.deprecate( util, 'jsMessage', function ( message ) {
-               if ( !arguments.length || message === '' || message === null ) {
-                       return true;
-               }
-               if ( typeof message !== 'object' ) {
-                       message = $.parseHTML( message );
-               }
-               mw.notify( message, { autoHide: true, tag: 'legacy' } );
-               return true;
-       }, 'Use mw.notify instead.', 'mw.util.jsMessage' );
-
        /**
         * Initialisation of mw.util.$content
         */
diff --git a/tests/phpunit/includes/Revision/FallbackSlotRoleHandlerTest.php b/tests/phpunit/includes/Revision/FallbackSlotRoleHandlerTest.php
new file mode 100644 (file)
index 0000000..aedf292
--- /dev/null
@@ -0,0 +1,75 @@
+<?php
+
+namespace MediaWiki\Tests\Revision;
+
+use MediaWiki\Revision\FallbackSlotRoleHandler;
+use MediaWikiTestCase;
+use Title;
+
+/**
+ * @covers \MediaWiki\Revision\FallbackSlotRoleHandler
+ */
+class FallbackSlotRoleHandlerTest extends MediaWikiTestCase {
+
+       private function makeBlankTitleObject() {
+               /** @var Title $title */
+               $title = $this->getMockBuilder( Title::class )
+                       ->disableOriginalConstructor()
+                       ->getMock();
+
+               return $title;
+       }
+
+       /**
+        * @covers \MediaWiki\Revision\FallbackSlotRoleHandler::__construct
+        * @covers \MediaWiki\Revision\FallbackSlotRoleHandler::getRole()
+        * @covers \MediaWiki\Revision\FallbackSlotRoleHandler::getNameMessageKey()
+        * @covers \MediaWiki\Revision\FallbackSlotRoleHandler::getDefaultModel()
+        * @covers \MediaWiki\Revision\FallbackSlotRoleHandler::getOutputLayoutHints()
+        */
+       public function testConstruction() {
+               $handler = new FallbackSlotRoleHandler( 'foo' );
+               $this->assertSame( 'foo', $handler->getRole() );
+               $this->assertSame( 'slot-name-foo', $handler->getNameMessageKey() );
+
+               $title = $this->makeBlankTitleObject();
+               $this->assertSame( CONTENT_MODEL_TEXT, $handler->getDefaultModel( $title ) );
+
+               $hints = $handler->getOutputLayoutHints();
+               $this->assertArrayHasKey( 'display', $hints );
+               $this->assertArrayHasKey( 'region', $hints );
+               $this->assertArrayHasKey( 'placement', $hints );
+       }
+
+       /**
+        * @covers \MediaWiki\Revision\FallbackSlotRoleHandler::isAllowedModel()
+        */
+       public function testIsAllowedModel() {
+               $handler = new FallbackSlotRoleHandler( 'foo', 'FooModel' );
+
+               // For the fallback handler, no models are allowed
+               $title = $this->makeBlankTitleObject();
+               $this->assertFalse( $handler->isAllowedModel( 'FooModel', $title ) );
+               $this->assertFalse( $handler->isAllowedModel( 'QuaxModel', $title ) );
+       }
+
+       /**
+        * @covers \MediaWiki\Revision\SlotRoleHandler::isAllowedModel()
+        */
+       public function testIsAllowedOn() {
+               $handler = new FallbackSlotRoleHandler( 'foo', 'FooModel' );
+
+               $title = $this->makeBlankTitleObject();
+               $this->assertFalse( $handler->isAllowedOn( $title ) );
+       }
+
+       /**
+        * @covers \MediaWiki\Revision\FallbackSlotRoleHandler::supportsArticleCount()
+        */
+       public function testSupportsArticleCount() {
+               $handler = new FallbackSlotRoleHandler( 'foo', 'FooModel' );
+
+               $this->assertFalse( $handler->supportsArticleCount() );
+       }
+
+}
diff --git a/tests/phpunit/includes/Revision/MainSlotRoleHandlerTest.php b/tests/phpunit/includes/Revision/MainSlotRoleHandlerTest.php
new file mode 100644 (file)
index 0000000..f2f3da8
--- /dev/null
@@ -0,0 +1,79 @@
+<?php
+
+namespace MediaWiki\Tests\Revision;
+
+use MediaWiki\Revision\MainSlotRoleHandler;
+use MediaWikiTestCase;
+use PHPUnit\Framework\MockObject\MockObject;
+use Title;
+
+/**
+ * @covers \MediaWiki\Revision\MainSlotRoleHandler
+ */
+class MainSlotRoleHandlerTest extends MediaWikiTestCase {
+
+       private function makeTitleObject( $ns ) {
+               /** @var Title|MockObject $title */
+               $title = $this->getMockBuilder( Title::class )
+                       ->disableOriginalConstructor()
+                       ->getMock();
+
+               $title->method( 'getNamespace' )
+                       ->willReturn( $ns );
+
+               return $title;
+       }
+
+       /**
+        * @covers \MediaWiki\Revision\MainSlotRoleHandler::__construct
+        * @covers \MediaWiki\Revision\MainSlotRoleHandler::getRole()
+        * @covers \MediaWiki\Revision\MainSlotRoleHandler::getNameMessageKey()
+        * @covers \MediaWiki\Revision\MainSlotRoleHandler::getOutputLayoutHints()
+        */
+       public function testConstruction() {
+               $handler = new MainSlotRoleHandler( [] );
+               $this->assertSame( 'main', $handler->getRole() );
+               $this->assertSame( 'slot-name-main', $handler->getNameMessageKey() );
+
+               $hints = $handler->getOutputLayoutHints();
+               $this->assertArrayHasKey( 'display', $hints );
+               $this->assertArrayHasKey( 'region', $hints );
+               $this->assertArrayHasKey( 'placement', $hints );
+       }
+
+       /**
+        * @covers \MediaWiki\Revision\MainSlotRoleHandler::getDefaultModel()
+        */
+       public function testFetDefaultModel() {
+               $handler = new MainSlotRoleHandler( [ 100 => CONTENT_MODEL_TEXT ] );
+
+               // For the main handler, the namespace determins the defualt model
+               $titleMain = $this->makeTitleObject( NS_MAIN );
+               $this->assertSame( CONTENT_MODEL_WIKITEXT, $handler->getDefaultModel( $titleMain ) );
+
+               $title100 = $this->makeTitleObject( 100 );
+               $this->assertSame( CONTENT_MODEL_TEXT, $handler->getDefaultModel( $title100 ) );
+       }
+
+       /**
+        * @covers \MediaWiki\Revision\MainSlotRoleHandler::isAllowedModel()
+        */
+       public function testIsAllowedModel() {
+               $handler = new MainSlotRoleHandler( [] );
+
+               // For the main handler, (nearly) all models are allowed
+               $title = $this->makeTitleObject( NS_MAIN );
+               $this->assertTrue( $handler->isAllowedModel( CONTENT_MODEL_WIKITEXT, $title ) );
+               $this->assertTrue( $handler->isAllowedModel( CONTENT_MODEL_TEXT, $title ) );
+       }
+
+       /**
+        * @covers \MediaWiki\Revision\MainSlotRoleHandler::supportsArticleCount()
+        */
+       public function testSupportsArticleCount() {
+               $handler = new MainSlotRoleHandler( [] );
+
+               $this->assertTrue( $handler->supportsArticleCount() );
+       }
+
+}
index 5c75ede..59b5a2c 100644 (file)
@@ -7,9 +7,12 @@ use Content;
 use Language;
 use LogicException;
 use MediaWiki\Revision\MutableRevisionRecord;
+use MediaWiki\Revision\MainSlotRoleHandler;
 use MediaWiki\Revision\RevisionRecord;
 use MediaWiki\Revision\RevisionRenderer;
 use MediaWiki\Revision\SlotRecord;
+use MediaWiki\Revision\SlotRoleRegistry;
+use MediaWiki\Storage\NameTableStore;
 use MediaWikiTestCase;
 use MediaWiki\User\UserIdentityValue;
 use ParserOptions;
@@ -126,7 +129,20 @@ class RevisionRendererTest extends MediaWikiTestCase {
                        ->with( $dbIndex )
                        ->willReturn( $db );
 
-               return new RevisionRenderer( $lb );
+               /** @var NameTableStore|MockObject $slotRoles */
+               $slotRoles = $this->getMockBuilder( NameTableStore::class )
+                       ->disableOriginalConstructor()
+                       ->getMock();
+               $slotRoles->method( 'getMap' )
+                       ->willReturn( [] );
+
+               $roleReg = new SlotRoleRegistry( $slotRoles );
+               $roleReg->defineRole( 'main', function () {
+                       return new MainSlotRoleHandler( [] );
+               } );
+               $roleReg->defineRoleWithModel( 'aux', CONTENT_MODEL_WIKITEXT );
+
+               return new RevisionRenderer( $lb, $roleReg );
        }
 
        private function selectFieldCallback( $table, $fields, $cond, $maxRev ) {
index fcd5016..dee07da 100644 (file)
@@ -231,6 +231,7 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
                        MediaWikiServices::getInstance()->getCommentStore(),
                        MediaWikiServices::getInstance()->getContentModelStore(),
                        MediaWikiServices::getInstance()->getSlotRoleStore(),
+                       MediaWikiServices::getInstance()->getSlotRoleRegistry(),
                        $this->getMcrMigrationStage(),
                        MediaWikiServices::getInstance()->getActorMigration(),
                        $wikiId
index 9904b3b..2e61745 100644 (file)
@@ -7,6 +7,7 @@ use CommentStore;
 use MediaWiki\Logger\Spi as LoggerSpi;
 use MediaWiki\Revision\RevisionStore;
 use MediaWiki\Revision\RevisionStoreFactory;
+use MediaWiki\Revision\SlotRoleRegistry;
 use MediaWiki\Storage\BlobStore;
 use MediaWiki\Storage\BlobStoreFactory;
 use MediaWiki\Storage\NameTableStore;
@@ -27,6 +28,7 @@ class RevisionStoreFactoryTest extends MediaWikiTestCase {
                        $this->getMockLoadBalancerFactory(),
                        $this->getMockBlobStoreFactory(),
                        $this->getNameTableStoreFactory(),
+                       $this->getMockSlotRoleRegistry(),
                        $this->getHashWANObjectCache(),
                        $this->getMockCommentStore(),
                        ActorMigration::newMigration(),
@@ -56,6 +58,7 @@ class RevisionStoreFactoryTest extends MediaWikiTestCase {
                $lbFactory = $this->getMockLoadBalancerFactory();
                $blobStoreFactory = $this->getMockBlobStoreFactory();
                $nameTableStoreFactory = $this->getNameTableStoreFactory();
+               $slotRoleRegistry = $this->getMockSlotRoleRegistry();
                $cache = $this->getHashWANObjectCache();
                $commentStore = $this->getMockCommentStore();
                $actorMigration = ActorMigration::newMigration();
@@ -65,6 +68,7 @@ class RevisionStoreFactoryTest extends MediaWikiTestCase {
                        $lbFactory,
                        $blobStoreFactory,
                        $nameTableStoreFactory,
+                       $slotRoleRegistry,
                        $cache,
                        $commentStore,
                        $actorMigration,
@@ -142,6 +146,16 @@ class RevisionStoreFactoryTest extends MediaWikiTestCase {
                return $mock;
        }
 
+       /**
+        * @return \PHPUnit_Framework_MockObject_MockObject|SlotRoleRegistry
+        */
+       private function getMockSlotRoleRegistry() {
+               $mock = $this->getMockBuilder( SlotRoleRegistry::class )
+                       ->disableOriginalConstructor()->getMock();
+
+               return $mock;
+       }
+
        /**
         * @return NameTableStoreFactory
         */
index 2093b41..efc2952 100644 (file)
@@ -9,6 +9,7 @@ use Language;
 use MediaWiki\MediaWikiServices;
 use MediaWiki\Revision\RevisionAccessException;
 use MediaWiki\Revision\RevisionStore;
+use MediaWiki\Revision\SlotRoleRegistry;
 use MediaWiki\Revision\SlotRecord;
 use MediaWiki\Storage\SqlBlobStore;
 use MediaWikiTestCase;
@@ -51,6 +52,7 @@ class RevisionStoreTest extends MediaWikiTestCase {
                        MediaWikiServices::getInstance()->getCommentStore(),
                        MediaWikiServices::getInstance()->getContentModelStore(),
                        MediaWikiServices::getInstance()->getSlotRoleStore(),
+                       MediaWikiServices::getInstance()->getSlotRoleRegistry(),
                        $wgMultiContentRevisionSchemaMigrationStage,
                        MediaWikiServices::getInstance()->getActorMigration()
                );
@@ -88,6 +90,14 @@ class RevisionStoreTest extends MediaWikiTestCase {
                        ->disableOriginalConstructor()->getMock();
        }
 
+       /**
+        * @return \PHPUnit_Framework_MockObject_MockObject|SlotRoleRegistry
+        */
+       private function getMockSlotRoleRegistry() {
+               return $this->getMockBuilder( SlotRoleRegistry::class )
+                       ->disableOriginalConstructor()->getMock();
+       }
+
        private function getHashWANObjectCache() {
                return new WANObjectCache( [ 'cache' => new \HashBagOStuff() ] );
        }
@@ -127,6 +137,7 @@ class RevisionStoreTest extends MediaWikiTestCase {
                        $this->getMockCommentStore(),
                        $nameTables->getContentModels(),
                        $nameTables->getSlotRoles(),
+                       $this->getMockSlotRoleRegistry(),
                        $migrationMode,
                        MediaWikiServices::getInstance()->getActorMigration()
                );
@@ -541,6 +552,7 @@ class RevisionStoreTest extends MediaWikiTestCase {
                $nameTables = $services->getNameTableStoreFactory();
                $contentModelStore = $nameTables->getContentModels();
                $slotRoleStore = $nameTables->getSlotRoles();
+               $slotRoleRegistry = $services->getSlotRoleRegistry();
                $store = new RevisionStore(
                        $loadBalancer,
                        $blobStore,
@@ -548,6 +560,7 @@ class RevisionStoreTest extends MediaWikiTestCase {
                        $commentStore,
                        $nameTables->getContentModels(),
                        $nameTables->getSlotRoles(),
+                       $slotRoleRegistry,
                        $migration,
                        $services->getActorMigration()
                );
diff --git a/tests/phpunit/includes/Revision/SlotRoleHandlerTest.php b/tests/phpunit/includes/Revision/SlotRoleHandlerTest.php
new file mode 100644 (file)
index 0000000..67e9464
--- /dev/null
@@ -0,0 +1,67 @@
+<?php
+
+namespace MediaWiki\Tests\Revision;
+
+use MediaWiki\Revision\SlotRoleHandler;
+use MediaWikiTestCase;
+use Title;
+
+/**
+ * @covers \MediaWiki\Revision\SlotRoleHandler
+ */
+class SlotRoleHandlerTest extends MediaWikiTestCase {
+
+       private function makeBlankTitleObject() {
+               /** @var Title $title */
+               $title = $this->getMockBuilder( Title::class )
+                       ->disableOriginalConstructor()
+                       ->getMock();
+
+               return $title;
+       }
+
+       /**
+        * @covers \MediaWiki\Revision\SlotRoleHandler::__construct
+        * @covers \MediaWiki\Revision\SlotRoleHandler::getRole()
+        * @covers \MediaWiki\Revision\SlotRoleHandler::getNameMessageKey()
+        * @covers \MediaWiki\Revision\SlotRoleHandler::getDefaultModel()
+        * @covers \MediaWiki\Revision\SlotRoleHandler::getOutputLayoutHints()
+        */
+       public function testConstruction() {
+               $handler = new SlotRoleHandler( 'foo', 'FooModel', [ 'frob' => 'niz' ] );
+               $this->assertSame( 'foo', $handler->getRole() );
+               $this->assertSame( 'slot-name-foo', $handler->getNameMessageKey() );
+
+               $title = $this->makeBlankTitleObject();
+               $this->assertSame( 'FooModel', $handler->getDefaultModel( $title ) );
+
+               $hints = $handler->getOutputLayoutHints();
+               $this->assertArrayHasKey( 'frob', $hints );
+               $this->assertSame( 'niz', $hints['frob'] );
+
+               $this->assertArrayHasKey( 'display', $hints );
+               $this->assertArrayHasKey( 'region', $hints );
+               $this->assertArrayHasKey( 'placement', $hints );
+       }
+
+       /**
+        * @covers \MediaWiki\Revision\SlotRoleHandler::isAllowedModel()
+        */
+       public function testIsAllowedModel() {
+               $handler = new SlotRoleHandler( 'foo', 'FooModel' );
+
+               $title = $this->makeBlankTitleObject();
+               $this->assertTrue( $handler->isAllowedModel( 'FooModel', $title ) );
+               $this->assertFalse( $handler->isAllowedModel( 'QuaxModel', $title ) );
+       }
+
+       /**
+        * @covers \MediaWiki\Revision\SlotRoleHandler::supportsArticleCount()
+        */
+       public function testSupportsArticleCount() {
+               $handler = new SlotRoleHandler( 'foo', 'FooModel' );
+
+               $this->assertFalse( $handler->supportsArticleCount() );
+       }
+
+}
diff --git a/tests/phpunit/includes/Revision/SlotRoleRegistryTest.php b/tests/phpunit/includes/Revision/SlotRoleRegistryTest.php
new file mode 100644 (file)
index 0000000..4d8030d
--- /dev/null
@@ -0,0 +1,194 @@
+<?php
+
+namespace MediaWiki\Tests\Revision;
+
+use InvalidArgumentException;
+use LogicException;
+use MediaWiki\Revision\MainSlotRoleHandler;
+use MediaWiki\Revision\SlotRoleHandler;
+use MediaWiki\Revision\SlotRoleRegistry;
+use MediaWiki\Storage\NameTableStore;
+use MediaWikiTestCase;
+use Title;
+use Wikimedia\Assert\PostconditionException;
+
+/**
+ * @covers \MediaWiki\Revision\SlotRoleRegistry
+ */
+class SlotRoleRegistryTest extends MediaWikiTestCase {
+
+       private function makeBlankTitleObject() {
+               /** @var Title $title */
+               $title = $this->getMockBuilder( Title::class )
+                       ->disableOriginalConstructor()
+                       ->getMock();
+
+               return $title;
+       }
+
+       private function makeNameTableStore( array $names = [] ) {
+               $mock = $this->getMockBuilder( NameTableStore::class )
+                       ->disableOriginalConstructor()
+                       ->getMock();
+
+               $mock->method( 'getMap' )
+                       ->willReturn( $names );
+
+               return $mock;
+       }
+
+       private function newSlotRoleRegistry( NameTableStore $roleNameStore = null ) {
+               if ( !$roleNameStore ) {
+                       $roleNameStore = $this->makeNameTableStore();
+               }
+
+               return new SlotRoleRegistry( $roleNameStore );
+       }
+
+       /**
+        * @covers \MediaWiki\Revision\SlotRoleRegistry::defineRole()
+        * @covers \MediaWiki\Revision\SlotRoleRegistry::getDefinedRoles()
+        * @covers \MediaWiki\Revision\SlotRoleRegistry::getKnownRoles()
+        * @covers \MediaWiki\Revision\SlotRoleRegistry::getRoleHandler()
+        */
+       public function testDefineRole() {
+               $registry = $this->newSlotRoleRegistry();
+               $registry->defineRole( 'foo', function ( $role ) {
+                       return new SlotRoleHandler( $role, 'FooModel' );
+               } );
+
+               $this->assertTrue( $registry->isDefinedRole( 'foo' ) );
+               $this->assertContains( 'foo', $registry->getDefinedRoles() );
+               $this->assertContains( 'foo', $registry->getKnownRoles() );
+
+               $handler = $registry->getRoleHandler( 'foo' );
+               $this->assertSame( 'foo', $handler->getRole() );
+
+               $title = $this->makeBlankTitleObject();
+               $this->assertSame( 'FooModel', $handler->getDefaultModel( $title ) );
+       }
+
+       /**
+        * @covers \MediaWiki\Revision\SlotRoleRegistry::defineRole()
+        */
+       public function testDefineRoleFailsForDupe() {
+               $registry = $this->newSlotRoleRegistry();
+               $registry->defineRole( 'foo', function ( $role ) {
+                       return new SlotRoleHandler( $role, 'FooModel' );
+               } );
+
+               $this->setExpectedException( LogicException::class );
+               $registry->defineRole( 'foo', function ( $role ) {
+                       return new SlotRoleHandler( $role, 'FooModel' );
+               } );
+       }
+
+       /**
+        * @covers \MediaWiki\Revision\SlotRoleRegistry::defineRoleWithModel()
+        * @covers \MediaWiki\Revision\SlotRoleRegistry::getDefinedRoles()
+        * @covers \MediaWiki\Revision\SlotRoleRegistry::getKnownRoles()
+        * @covers \MediaWiki\Revision\SlotRoleRegistry::getRoleHandler()
+        */
+       public function testDefineRoleWithContentModel() {
+               $registry = $this->newSlotRoleRegistry();
+               $registry->defineRoleWithModel( 'foo', 'FooModel' );
+
+               $this->assertTrue( $registry->isDefinedRole( 'foo' ) );
+               $this->assertContains( 'foo', $registry->getDefinedRoles() );
+               $this->assertContains( 'foo', $registry->getKnownRoles() );
+
+               $handler = $registry->getRoleHandler( 'foo' );
+               $this->assertSame( 'foo', $handler->getRole() );
+
+               /** @var Title $title */
+               $title = $this->getMockBuilder( Title::class )
+                       ->disableOriginalConstructor()
+                       ->getMock();
+               $this->assertSame( 'FooModel', $handler->getDefaultModel( $title ) );
+       }
+
+       /**
+        * @covers \MediaWiki\Revision\SlotRoleRegistry::getRoleHandler()
+        */
+       public function testGetRoleHandlerForUnknownModel() {
+               $registry = $this->newSlotRoleRegistry();
+
+               $this->setExpectedException( InvalidArgumentException::class );
+
+               $registry->getRoleHandler( 'foo' );
+       }
+
+       /**
+        * @covers \MediaWiki\Revision\SlotRoleRegistry::getRoleHandler()
+        */
+       public function testGetRoleHandlerFallbackHandler() {
+               $registry = $this->newSlotRoleRegistry(
+                       $this->makeNameTableStore( [ 1 => 'foo' ] )
+               );
+
+               \Wikimedia\suppressWarnings();
+               $handler = $registry->getRoleHandler( 'foo' );
+               $this->assertSame( 'foo', $handler->getRole() );
+
+               \Wikimedia\restoreWarnings();
+       }
+
+       /**
+        * @covers \MediaWiki\Revision\SlotRoleRegistry::getRoleHandler()
+        */
+       public function testGetRoleHandlerWithBadInstantiator() {
+               $registry = $this->newSlotRoleRegistry();
+               $registry->defineRole( 'foo', function ( $role ) {
+                       return 'Not a SlotRoleHandler instance';
+               } );
+
+               $this->setExpectedException( PostconditionException::class );
+               $registry->getRoleHandler( 'foo' );
+       }
+
+       /**
+        * @covers \MediaWiki\Revision\SlotRoleRegistry::getRequiredRoles()
+        */
+       public function testGetRequiredRoles() {
+               $registry = $this->newSlotRoleRegistry();
+               $registry->defineRole( 'main', function ( $role ) {
+                       return new MainSlotRoleHandler( [] );
+               } );
+
+               $title = $this->makeBlankTitleObject();
+               $this->assertEquals( [ 'main' ], $registry->getRequiredRoles( $title ) );
+       }
+
+       /**
+        * @covers \MediaWiki\Revision\SlotRoleRegistry::getAllowedRoles()
+        */
+       public function testGetAllowedRoles() {
+               $registry = $this->newSlotRoleRegistry();
+               $registry->defineRole( 'main', function ( $role ) {
+                       return new MainSlotRoleHandler( [] );
+               } );
+               $registry->defineRoleWithModel( 'foo', CONTENT_MODEL_TEXT );
+
+               $title = $this->makeBlankTitleObject();
+               $this->assertEquals( [ 'main', 'foo' ], $registry->getAllowedRoles( $title ) );
+       }
+
+       /**
+        * @covers \MediaWiki\Revision\SlotRoleRegistry::getKnownRoles()
+        * @covers \MediaWiki\Revision\SlotRoleRegistry::isKnownRole()
+        */
+       public function testGetKnownRoles() {
+               $registry = $this->newSlotRoleRegistry(
+                       $this->makeNameTableStore( [ 1 => 'foo' ] )
+               );
+               $registry->defineRoleWithModel( 'bar', CONTENT_MODEL_TEXT );
+
+               $this->assertTrue( $registry->isKnownRole( 'foo' ) );
+               $this->assertTrue( $registry->isKnownRole( 'bar' ) );
+               $this->assertFalse( $registry->isKnownRole( 'xyzzy' ) );
+
+               $title = $this->makeBlankTitleObject();
+               $this->assertArrayEquals( [ 'foo', 'bar' ], $registry->getKnownRoles( $title ) );
+       }
+
+}
index cfff088..a2f2796 100644 (file)
@@ -458,6 +458,7 @@ abstract class RevisionDbTestBase extends MediaWikiTestCase {
                        $services->getCommentStore(),
                        $services->getContentModelStore(),
                        $services->getSlotRoleStore(),
+                       $services->getSlotRoleRegistry(),
                        $this->getMcrMigrationStage(),
                        $services->getActorMigration()
                );
index 04f0793..7ee800a 100644 (file)
@@ -478,6 +478,7 @@ class RevisionTest extends MediaWikiTestCase {
                        MediaWikiServices::getInstance()->getCommentStore(),
                        MediaWikiServices::getInstance()->getContentModelStore(),
                        MediaWikiServices::getInstance()->getSlotRoleStore(),
+                       MediaWikiServices::getInstance()->getSlotRoleRegistry(),
                        MIGRATION_OLD,
                        MediaWikiServices::getInstance()->getActorMigration()
                );
index 7320305..5f3cba3 100644 (file)
@@ -102,6 +102,9 @@ class DerivedPageDataUpdaterTest extends MediaWikiTestCase {
                }
 
                $rev = $updater->saveRevision( $comment );
+               if ( !$updater->wasSuccessful() ) {
+                       $this->fail( $updater->getStatus()->getWikiText() );
+               }
 
                $this->getDerivedPageDataUpdater( $page ); // flush cached instance after.
                return $rev;
@@ -186,6 +189,11 @@ class DerivedPageDataUpdaterTest extends MediaWikiTestCase {
         * @covers \MediaWiki\Storage\DerivedPageDataUpdater::getCanonicalParserOutput()
         */
        public function testPrepareContent() {
+               MediaWikiServices::getInstance()->getSlotRoleRegistry()->defineRoleWithModel(
+                       'aux',
+                       CONTENT_MODEL_WIKITEXT
+               );
+
                $sysop = $this->getTestUser( [ 'sysop' ] )->getUser();
                $updater = $this->getDerivedPageDataUpdater( __METHOD__ );
 
@@ -584,9 +592,7 @@ class DerivedPageDataUpdaterTest extends MediaWikiTestCase {
        }
 
        public function testGetSecondaryDataUpdatesWithSlotRemoval() {
-               global $wgMultiContentRevisionSchemaMigrationStage;
-
-               if ( ! ( $wgMultiContentRevisionSchemaMigrationStage & SCHEMA_COMPAT_READ_NEW ) ) {
+               if ( !$this->hasMultiSlotSupport() ) {
                        $this->markTestSkipped( 'Slot removal cannot happen with MCR being enabled' );
                }
 
@@ -594,6 +600,11 @@ class DerivedPageDataUpdaterTest extends MediaWikiTestCase {
                $a1 = $this->defineMockContentModelForUpdateTesting( 'A1' );
                $m2 = $this->defineMockContentModelForUpdateTesting( 'M2' );
 
+               MediaWikiServices::getInstance()->getSlotRoleRegistry()->defineRoleWithModel(
+                       'aux',
+                       $a1->getModelID()
+               );
+
                $mainContent1 = $this->createMockContent( $m1, 'main 1' );
                $auxContent1 = $this->createMockContent( $a1, 'aux 1' );
                $mainContent2 = $this->createMockContent( $m2, 'main 2' );
@@ -866,6 +877,11 @@ class DerivedPageDataUpdaterTest extends MediaWikiTestCase {
 
                if ( $this->hasMultiSlotSupport() ) {
                        $content['aux'] = new WikitextContent( 'Aux [[Nix]]' );
+
+                       MediaWikiServices::getInstance()->getSlotRoleRegistry()->defineRoleWithModel(
+                               'aux',
+                               CONTENT_MODEL_WIKITEXT
+                       );
                }
 
                $rev = $this->createRevision( $page, 'first', $content );
index 3ba9032..4e09077 100644 (file)
@@ -22,6 +22,15 @@ use WikiPage;
  */
 class PageUpdaterTest extends MediaWikiTestCase {
 
+       public function setUp() {
+               parent::setUp();
+
+               MediaWikiServices::getInstance()->getSlotRoleRegistry()->defineRoleWithModel(
+                       'aux',
+                       CONTENT_MODEL_WIKITEXT
+               );
+       }
+
        private function getDummyTitle( $method ) {
                return Title::newFromText( $method, $this->getDefaultWikitextNS() );
        }
@@ -337,6 +346,34 @@ class PageUpdaterTest extends MediaWikiTestCase {
                $this->assertTrue( $status->hasMessage( 'edit-already-exists' ), 'edit-already-exists' );
        }
 
+       /**
+        * @covers \MediaWiki\Storage\PageUpdater::saveRevision()
+        */
+       public function testFailureOnBadContentModel() {
+               $user = $this->getTestUser()->getUser();
+               $title = $this->getDummyTitle( __METHOD__ );
+
+               // start editing non-existing page
+               $page = WikiPage::factory( $title );
+               $updater = $page->newPageUpdater( $user );
+
+               // plain text content should fail in aux slot (the main slot doesn't care)
+               $updater->setContent( 'main', new TextContent( 'Main Content' ) );
+               $updater->setContent( 'aux', new TextContent( 'Aux Content' ) );
+
+               $summary = CommentStoreComment::newUnsavedComment( 'udpate?!' );
+               $updater->saveRevision( $summary, EDIT_UPDATE );
+               $status = $updater->getStatus();
+
+               $this->assertFalse( $updater->wasSuccessful(), 'wasSuccessful()' );
+               $this->assertNull( $updater->getNewRevision(), 'getNewRevision()' );
+               $this->assertFalse( $status->isOK(), 'getStatus()->isOK()' );
+               $this->assertTrue(
+                       $status->hasMessage( 'content-not-allowed-here' ),
+                       'content-not-allowed-here'
+               );
+       }
+
        public function provideSetRcPatrolStatus( $patrolled ) {
                yield [ RecentChange::PRC_UNPATROLLED ];
                yield [ RecentChange::PRC_AUTOPATROLLED ];
index cb5e1f8..11b9c01 100644 (file)
@@ -1,6 +1,5 @@
 <?php
 
-use MediaWiki\Block\Restriction\PageRestriction;
 use MediaWiki\MediaWikiServices;
 
 /**
@@ -893,7 +892,7 @@ class TitlePermissionTest extends MediaWikiLangTestCase {
                        'wgEmailAuthentication' => true,
                ] );
 
-               $this->setUserPerm( [ 'createpage', 'edit', 'move', 'rollback', 'patrol', 'upload', 'purge' ] );
+               $this->setUserPerm( [ "createpage", "move" ] );
                $this->setTitle( NS_HELP, "test page" );
 
                # $wgEmailConfirmToEdit only applies to 'edit' action
@@ -965,24 +964,11 @@ class TitlePermissionTest extends MediaWikiLangTestCase {
                        'expiry' => 10,
                        'systemBlock' => 'test',
                ] );
-
-               $errors = [ [ 'systemblockedtext',
+               $this->assertEquals( [ [ 'systemblockedtext',
                                '[[User:Useruser|Useruser]]', 'no reason given', '127.0.0.1',
                                'Useruser', 'test', '23:00, 31 December 1969', '127.0.8.1',
-                               $wgLang->timeanddate( wfTimestamp( TS_MW, $now ), true ) ] ];
-
-               $this->assertEquals( $errors,
-                       $this->title->getUserPermissionsErrors( 'edit', $this->user ) );
-               $this->assertEquals( $errors,
+                               $wgLang->timeanddate( wfTimestamp( TS_MW, $now ), true ) ] ],
                        $this->title->getUserPermissionsErrors( 'move-target', $this->user ) );
-               $this->assertEquals( $errors,
-                       $this->title->getUserPermissionsErrors( 'rollback', $this->user ) );
-               $this->assertEquals( $errors,
-                       $this->title->getUserPermissionsErrors( 'patrol', $this->user ) );
-               $this->assertEquals( $errors,
-                       $this->title->getUserPermissionsErrors( 'upload', $this->user ) );
-               $this->assertEquals( [],
-                       $this->title->getUserPermissionsErrors( 'purge', $this->user ) );
 
                // partial block message test
                $this->user->mBlockedby = $this->user->getName();
@@ -995,39 +981,10 @@ class TitlePermissionTest extends MediaWikiLangTestCase {
                        'expiry' => 10,
                ] );
 
-               $this->assertEquals( [],
-                       $this->title->getUserPermissionsErrors( 'edit', $this->user ) );
-               $this->assertEquals( [],
-                       $this->title->getUserPermissionsErrors( 'move-target', $this->user ) );
-               $this->assertEquals( [],
-                       $this->title->getUserPermissionsErrors( 'rollback', $this->user ) );
-               $this->assertEquals( [],
-                       $this->title->getUserPermissionsErrors( 'patrol', $this->user ) );
-               $this->assertEquals( [],
-                       $this->title->getUserPermissionsErrors( 'upload', $this->user ) );
-               $this->assertEquals( [],
-                       $this->title->getUserPermissionsErrors( 'purge', $this->user ) );
-
-               $this->user->mBlock->setRestrictions( [
-                               ( new PageRestriction( 0, $this->title->getArticleID() ) )->setTitle( $this->title ),
-               ] );
-
-               $errors = [ [ 'blockedtext-partial',
+               $this->assertEquals( [ [ 'blockedtext-partial',
                                '[[User:Useruser|Useruser]]', 'no reason given', '127.0.0.1',
                                'Useruser', null, '23:00, 31 December 1969', '127.0.8.1',
-                               $wgLang->timeanddate( wfTimestamp( TS_MW, $now ), true ) ] ];
-
-               $this->assertEquals( $errors,
-                       $this->title->getUserPermissionsErrors( 'edit', $this->user ) );
-               $this->assertEquals( $errors,
+                               $wgLang->timeanddate( wfTimestamp( TS_MW, $now ), true ) ] ],
                        $this->title->getUserPermissionsErrors( 'move-target', $this->user ) );
-               $this->assertEquals( $errors,
-                       $this->title->getUserPermissionsErrors( 'rollback', $this->user ) );
-               $this->assertEquals( $errors,
-                       $this->title->getUserPermissionsErrors( 'patrol', $this->user ) );
-               $this->assertEquals( [],
-                       $this->title->getUserPermissionsErrors( 'upload', $this->user ) );
-               $this->assertEquals( [],
-                       $this->title->getUserPermissionsErrors( 'purge', $this->user ) );
        }
 }
index 7665b78..215177d 100644 (file)
@@ -48,6 +48,11 @@ class RefreshLinksJobTest extends MediaWikiTestCase {
        // TODO: test partition
 
        public function testRunForSinglePage() {
+               MediaWikiServices::getInstance()->getSlotRoleRegistry()->defineRoleWithModel(
+                       'aux',
+                       CONTENT_MODEL_WIKITEXT
+               );
+
                $mainContent = new WikitextContent( 'MAIN [[Kittens]]' );
                $auxContent = new WikitextContent( 'AUX [[Category:Goats]]' );
                $page = $this->createPage( __METHOD__, [ 'main' => $mainContent, 'aux' => $auxContent ] );
index 439bd38..f7e5bd1 100644 (file)
@@ -135,6 +135,9 @@ abstract class WikiPageDbTestBase extends MediaWikiLangTestCase {
                }
 
                $updater->saveRevision( CommentStoreComment::newUnsavedComment( "testing" ) );
+               if ( !$updater->wasSuccessful() ) {
+                       $this->fail( $updater->getStatus()->getWikiText() );
+               }
 
                return $page;
        }
index 69d12e3..78c0ac3 100644 (file)
@@ -1,4 +1,6 @@
 <?php
+
+use MediaWiki\MediaWikiServices;
 use MediaWiki\Tests\Revision\McrReadNewSchemaOverride;
 
 /**
@@ -24,6 +26,11 @@ class WikiPageMcrReadNewDbTest extends WikiPageDbTestBase {
                $m1 = $this->defineMockContentModelForUpdateTesting( 'M1' );
                $a1 = $this->defineMockContentModelForUpdateTesting( 'A1' );
 
+               MediaWikiServices::getInstance()->getSlotRoleRegistry()->defineRoleWithModel(
+                       'aux',
+                       $a1->getModelID()
+               );
+
                $mainContent1 = $this->createMockContent( $m1, 'main 1' );
                $auxContent1 = $this->createMockContent( $a1, 'aux 1' );