From: daniel Date: Mon, 19 Nov 2018 11:39:56 +0000 (+0100) Subject: [MCR] Introduce SlotRoleHandler and SlotRoleRegistry X-Git-Tag: 1.34.0-rc.0~3361^2 X-Git-Url: http://git.cyclocoop.org/%22%20.%20generer_url_ecrire%28%22sites_tous%22%29%20.%20%22?a=commitdiff_plain;h=db987c700adfe8766316d56f2b4e05935e09d6a4;p=lhc%2Fweb%2Fwiklou.git [MCR] Introduce SlotRoleHandler and SlotRoleRegistry These new classes provide a mechanism for defining the behavior of slots, like the content models it supports. This acts as an extension point for extensions that need to define custom slots, like the MediaInfo extension for the SDC project. Bug: T194046 Change-Id: Ia20c98eee819293199e541be75b5521f6413bc2f --- diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php index 2d1681cc3e..cfa8afec2c 100644 --- a/includes/DefaultSettings.php +++ b/includes/DefaultSettings.php @@ -8606,6 +8606,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 = []; diff --git a/includes/MWNamespace.php b/includes/MWNamespace.php index e03a29b80f..98e70bf9f7 100644 --- a/includes/MWNamespace.php +++ b/includes/MWNamespace.php @@ -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; } /** diff --git a/includes/MediaWikiServices.php b/includes/MediaWikiServices.php index f3ca7d469e..0e36b22367 100644 --- a/includes/MediaWikiServices.php +++ b/includes/MediaWikiServices.php @@ -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 diff --git a/includes/MovePage.php b/includes/MovePage.php index 0fd697b5bd..bb76395dca 100644 --- a/includes/MovePage.php +++ b/includes/MovePage.php @@ -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 index 0000000000..78dfd39861 --- /dev/null +++ b/includes/Revision/FallbackSlotRoleHandler.php @@ -0,0 +1,71 @@ + '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 index 0000000000..6c6fdd6d05 --- /dev/null +++ b/includes/Revision/MainSlotRoleHandler.php @@ -0,0 +1,132 @@ +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; + } + +} diff --git a/includes/Revision/RenderedRevision.php b/includes/Revision/RenderedRevision.php index 6eee3c4cf6..8e50a1b95c 100644 --- a/includes/Revision/RenderedRevision.php +++ b/includes/Revision/RenderedRevision.php @@ -208,6 +208,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() ) { diff --git a/includes/Revision/RevisionRenderer.php b/includes/Revision/RevisionRenderer.php index e2e84b60ca..eb3f231d76 100644 --- a/includes/Revision/RevisionRenderer.php +++ b/includes/Revision/RevisionRenderer.php @@ -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(); @@ -175,8 +184,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; @@ -192,6 +199,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 ); } @@ -201,6 +209,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; @@ -210,6 +228,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 ); } diff --git a/includes/Revision/RevisionStore.php b/includes/Revision/RevisionStore.php index 6d3b72cdf9..b04994550f 100644 --- a/includes/Revision/RevisionStore.php +++ b/includes/Revision/RevisionStore.php @@ -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 ); }; } diff --git a/includes/Revision/RevisionStoreFactory.php b/includes/Revision/RevisionStoreFactory.php index 30ffc997ef..6b3117fc78 100644 --- a/includes/Revision/RevisionStoreFactory.php +++ b/includes/Revision/RevisionStoreFactory.php @@ -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 index 0000000000..85b4c5ab34 --- /dev/null +++ b/includes/Revision/SlotRoleHandler.php @@ -0,0 +1,159 @@ + '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 index 0000000000..b108b98d43 --- /dev/null +++ b/includes/Revision/SlotRoleRegistry.php @@ -0,0 +1,236 @@ +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 ); + } + +} diff --git a/includes/ServiceWiring.php b/includes/ServiceWiring.php index 33517a0665..9a94389f74 100644 --- a/includes/ServiceWiring.php +++ b/includes/ServiceWiring.php @@ -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(), diff --git a/includes/Storage/DerivedPageDataUpdater.php b/includes/Storage/DerivedPageDataUpdater.php index ad29f91e99..552dbae32e 100644 --- a/includes/Storage/DerivedPageDataUpdater.php +++ b/includes/Storage/DerivedPageDataUpdater.php @@ -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; @@ -660,12 +667,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; } /** @@ -673,6 +694,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(); diff --git a/includes/Storage/PageUpdater.php b/includes/Storage/PageUpdater.php index 043e00ebf6..6cbdcc6e91 100644 --- a/includes/Storage/PageUpdater.php +++ b/includes/Storage/PageUpdater.php @@ -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 ) + ); + } + } + } diff --git a/includes/Title.php b/includes/Title.php index 997063b3d1..8b4075b4de 100644 --- a/includes/Title.php +++ b/includes/Title.php @@ -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 */ diff --git a/includes/api/ApiComparePages.php b/includes/api/ApiComparePages.php index 76b7bce67b..393f43542d 100644 --- a/includes/api/ApiComparePages.php +++ b/includes/api/ApiComparePages.php @@ -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 diff --git a/includes/api/ApiQueryRevisionsBase.php b/includes/api/ApiQueryRevisionsBase.php index c00010a131..3d0a0fba62 100644 --- a/includes/api/ApiQueryRevisionsBase.php +++ b/includes/api/ApiQueryRevisionsBase.php @@ -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 [ diff --git a/includes/content/Content.php b/includes/content/Content.php index bb3fb107d7..1bb43f83b6 100644 --- a/includes/content/Content.php +++ b/includes/content/Content.php @@ -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 diff --git a/includes/content/ContentHandler.php b/includes/content/ContentHandler.php index fab043a2ba..5c18a330cb 100644 --- a/includes/content/ContentHandler.php +++ b/includes/content/ContentHandler.php @@ -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. diff --git a/includes/diff/DifferenceEngine.php b/includes/diff/DifferenceEngine.php index 8d0971e659..63cc2a85ea 100644 --- a/includes/diff/DifferenceEngine.php +++ b/includes/diff/DifferenceEngine.php @@ -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 ); } diff --git a/includes/page/WikiPage.php b/includes/page/WikiPage.php index 6a6b2a63e4..62ed0a99ba 100644 --- a/includes/page/WikiPage.php +++ b/includes/page/WikiPage.php @@ -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 ); diff --git a/languages/i18n/en.json b/languages/i18n/en.json index ba6353f795..daabc52602 100644 --- a/languages/i18n/en.json +++ b/languages/i18n/en.json @@ -760,6 +760,9 @@ "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.", @@ -770,7 +773,7 @@ "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.", diff --git a/languages/i18n/qqq.json b/languages/i18n/qqq.json index 1d058893ff..4b07586b53 100644 --- a/languages/i18n/qqq.json +++ b/languages/i18n/qqq.json @@ -963,6 +963,9 @@ "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", @@ -973,7 +976,7 @@ "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. text/css)\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", diff --git a/tests/phpunit/includes/Revision/FallbackSlotRoleHandlerTest.php b/tests/phpunit/includes/Revision/FallbackSlotRoleHandlerTest.php new file mode 100644 index 0000000000..aedf292eca --- /dev/null +++ b/tests/phpunit/includes/Revision/FallbackSlotRoleHandlerTest.php @@ -0,0 +1,75 @@ +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 index 0000000000..f2f3da8ac5 --- /dev/null +++ b/tests/phpunit/includes/Revision/MainSlotRoleHandlerTest.php @@ -0,0 +1,79 @@ +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() ); + } + +} diff --git a/tests/phpunit/includes/Revision/RevisionRendererTest.php b/tests/phpunit/includes/Revision/RevisionRendererTest.php index 469f2816dd..d797515e0b 100644 --- a/tests/phpunit/includes/Revision/RevisionRendererTest.php +++ b/tests/phpunit/includes/Revision/RevisionRendererTest.php @@ -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 ) { diff --git a/tests/phpunit/includes/Revision/RevisionStoreDbTestBase.php b/tests/phpunit/includes/Revision/RevisionStoreDbTestBase.php index 0d6a439dc3..61187ee223 100644 --- a/tests/phpunit/includes/Revision/RevisionStoreDbTestBase.php +++ b/tests/phpunit/includes/Revision/RevisionStoreDbTestBase.php @@ -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 diff --git a/tests/phpunit/includes/Revision/RevisionStoreFactoryTest.php b/tests/phpunit/includes/Revision/RevisionStoreFactoryTest.php index 9904b3bc65..2e61745d22 100644 --- a/tests/phpunit/includes/Revision/RevisionStoreFactoryTest.php +++ b/tests/phpunit/includes/Revision/RevisionStoreFactoryTest.php @@ -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 */ diff --git a/tests/phpunit/includes/Revision/RevisionStoreTest.php b/tests/phpunit/includes/Revision/RevisionStoreTest.php index 2093b41faa..efc2952856 100644 --- a/tests/phpunit/includes/Revision/RevisionStoreTest.php +++ b/tests/phpunit/includes/Revision/RevisionStoreTest.php @@ -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 index 0000000000..67e9464f33 --- /dev/null +++ b/tests/phpunit/includes/Revision/SlotRoleHandlerTest.php @@ -0,0 +1,67 @@ +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 index 0000000000..4d8030d519 --- /dev/null +++ b/tests/phpunit/includes/Revision/SlotRoleRegistryTest.php @@ -0,0 +1,194 @@ +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 ) ); + } + +} diff --git a/tests/phpunit/includes/RevisionDbTestBase.php b/tests/phpunit/includes/RevisionDbTestBase.php index e5e5551e9f..32c9e5a2d3 100644 --- a/tests/phpunit/includes/RevisionDbTestBase.php +++ b/tests/phpunit/includes/RevisionDbTestBase.php @@ -458,6 +458,7 @@ abstract class RevisionDbTestBase extends MediaWikiTestCase { $services->getCommentStore(), $services->getContentModelStore(), $services->getSlotRoleStore(), + $services->getSlotRoleRegistry(), $this->getMcrMigrationStage(), $services->getActorMigration() ); diff --git a/tests/phpunit/includes/RevisionTest.php b/tests/phpunit/includes/RevisionTest.php index c053104d6f..d6c33f0014 100644 --- a/tests/phpunit/includes/RevisionTest.php +++ b/tests/phpunit/includes/RevisionTest.php @@ -474,6 +474,7 @@ class RevisionTest extends MediaWikiTestCase { MediaWikiServices::getInstance()->getCommentStore(), MediaWikiServices::getInstance()->getContentModelStore(), MediaWikiServices::getInstance()->getSlotRoleStore(), + MediaWikiServices::getInstance()->getSlotRoleRegistry(), MIGRATION_OLD, MediaWikiServices::getInstance()->getActorMigration() ); diff --git a/tests/phpunit/includes/Storage/DerivedPageDataUpdaterTest.php b/tests/phpunit/includes/Storage/DerivedPageDataUpdaterTest.php index c175e2fd09..8b7e7a8241 100644 --- a/tests/phpunit/includes/Storage/DerivedPageDataUpdaterTest.php +++ b/tests/phpunit/includes/Storage/DerivedPageDataUpdaterTest.php @@ -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' ); @@ -876,6 +887,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 ); diff --git a/tests/phpunit/includes/Storage/PageUpdaterTest.php b/tests/phpunit/includes/Storage/PageUpdaterTest.php index 3ba90327ec..4e090775b5 100644 --- a/tests/phpunit/includes/Storage/PageUpdaterTest.php +++ b/tests/phpunit/includes/Storage/PageUpdaterTest.php @@ -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 ]; diff --git a/tests/phpunit/includes/jobqueue/jobs/RefreshLinksJobTest.php b/tests/phpunit/includes/jobqueue/jobs/RefreshLinksJobTest.php index 7665b788fd..215177d354 100644 --- a/tests/phpunit/includes/jobqueue/jobs/RefreshLinksJobTest.php +++ b/tests/phpunit/includes/jobqueue/jobs/RefreshLinksJobTest.php @@ -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 ] ); diff --git a/tests/phpunit/includes/page/WikiPageDbTestBase.php b/tests/phpunit/includes/page/WikiPageDbTestBase.php index fee45838b5..ef7c4bd39f 100644 --- a/tests/phpunit/includes/page/WikiPageDbTestBase.php +++ b/tests/phpunit/includes/page/WikiPageDbTestBase.php @@ -135,6 +135,9 @@ abstract class WikiPageDbTestBase extends MediaWikiLangTestCase { } $updater->saveRevision( CommentStoreComment::newUnsavedComment( "testing" ) ); + if ( !$updater->wasSuccessful() ) { + $this->fail( $updater->getStatus()->getWikiText() ); + } return $page; } diff --git a/tests/phpunit/includes/page/WikiPageMcrReadNewDbTest.php b/tests/phpunit/includes/page/WikiPageMcrReadNewDbTest.php index 69d12e31a3..78c0ac364c 100644 --- a/tests/phpunit/includes/page/WikiPageMcrReadNewDbTest.php +++ b/tests/phpunit/includes/page/WikiPageMcrReadNewDbTest.php @@ -1,4 +1,6 @@ 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' );