Issue #3028301 by tedbow, tim.plunkett, Wim Leers, xjm: Do not expose to Layout Builder's sections either in defaults or overrides to REST and other external APIs
parent
902611a1dc
commit
d8859389a3
|
@ -2,8 +2,10 @@
|
|||
|
||||
namespace Drupal\layout_builder\Field;
|
||||
|
||||
use Drupal\Core\Access\AccessResult;
|
||||
use Drupal\Core\Field\FieldItemList;
|
||||
use Drupal\Core\Field\FieldItemListInterface;
|
||||
use Drupal\Core\Session\AccountInterface;
|
||||
use Drupal\layout_builder\Section;
|
||||
use Drupal\layout_builder\SectionListInterface;
|
||||
use Drupal\layout_builder\SectionStorage\SectionStorageTrait;
|
||||
|
@ -74,4 +76,12 @@ class LayoutSectionItemList extends FieldItemList implements SectionListInterfac
|
|||
return $convert($this) === $convert($list_to_compare);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function defaultAccess($operation = 'view', AccountInterface $account = NULL) {
|
||||
// @todo Allow access in https://www.drupal.org/node/2942975.
|
||||
return AccessResult::forbidden();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -5,6 +5,8 @@ namespace Drupal\layout_builder;
|
|||
use Drupal\Core\DependencyInjection\ContainerBuilder;
|
||||
use Drupal\Core\DependencyInjection\ServiceProviderInterface;
|
||||
use Drupal\layout_builder\EventSubscriber\SetInlineBlockDependency;
|
||||
use Drupal\layout_builder\Normalizer\LayoutEntityDisplayNormalizer;
|
||||
use Symfony\Component\DependencyInjection\ChildDefinition;
|
||||
use Symfony\Component\DependencyInjection\Definition;
|
||||
use Symfony\Component\DependencyInjection\Reference;
|
||||
|
||||
|
@ -35,6 +37,14 @@ class LayoutBuilderServiceProvider implements ServiceProviderInterface {
|
|||
$definition->addTag('event_subscriber');
|
||||
$container->setDefinition('layout_builder.get_block_dependency_subscriber', $definition);
|
||||
}
|
||||
if (isset($modules['serialization'])) {
|
||||
$definition = (new ChildDefinition('serializer.normalizer.config_entity'))
|
||||
->setClass(LayoutEntityDisplayNormalizer::class)
|
||||
// Ensure that this normalizer takes precedence for Layout Builder data
|
||||
// over the generic serializer.normalizer.config_entity.
|
||||
->addTag('normalizer', ['priority' => 5]);
|
||||
$container->setDefinition('layout_builder.normalizer.layout_entity_display', $definition);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\layout_builder\Normalizer;
|
||||
|
||||
use Drupal\layout_builder\Entity\LayoutEntityDisplayInterface;
|
||||
use Drupal\serialization\Normalizer\ConfigEntityNormalizer;
|
||||
|
||||
/**
|
||||
* Normalizes/denormalizes LayoutEntityDisplay objects into an array structure.
|
||||
*
|
||||
* @internal
|
||||
* All tagged services are internal.
|
||||
*/
|
||||
class LayoutEntityDisplayNormalizer extends ConfigEntityNormalizer {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $supportedInterfaceOrClass = LayoutEntityDisplayInterface::class;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static function getDataWithoutInternals(array $data) {
|
||||
$data = parent::getDataWithoutInternals($data);
|
||||
// Do not expose the actual layout sections in normalization.
|
||||
// @todo Determine what to expose here in
|
||||
// https://www.drupal.org/node/2942975.
|
||||
unset($data['third_party_settings']['layout_builder']['sections']);
|
||||
return $data;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\Tests\layout_builder\Functional\Rest;
|
||||
|
||||
use Drupal\Core\Entity\Entity\EntityViewDisplay;
|
||||
use Drupal\Core\Url;
|
||||
|
||||
/**
|
||||
* Tests that default layout sections are not exposed via the REST API.
|
||||
*
|
||||
* @group layout_builder
|
||||
* @group rest
|
||||
*/
|
||||
class EntityDisplaySectionsTest extends LayoutRestTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $resourceConfigId = 'entity.entity_view_display';
|
||||
|
||||
/**
|
||||
* Tests the normalization does not contain layout sections.
|
||||
*/
|
||||
public function testLayoutEntityDisplay() {
|
||||
$display_id = 'node.bundle_with_section_field.default';
|
||||
$display = EntityViewDisplay::load($display_id);
|
||||
|
||||
// Assert the display has 1 section.
|
||||
$this->assertCount(1, $display->getThirdPartySetting('layout_builder', 'sections'));
|
||||
$response = $this->request(
|
||||
'GET',
|
||||
Url::fromRoute(
|
||||
'rest.entity.entity_view_display.GET',
|
||||
['entity_view_display' => 'node.bundle_with_section_field.default'])
|
||||
);
|
||||
$this->assertResourceResponse(
|
||||
200,
|
||||
FALSE,
|
||||
$response,
|
||||
[
|
||||
'config:core.entity_view_display.node.bundle_with_section_field.default',
|
||||
'config:rest.resource.entity.entity_view_display',
|
||||
'config:rest.settings',
|
||||
'http_response',
|
||||
],
|
||||
[
|
||||
'user.permissions',
|
||||
],
|
||||
FALSE,
|
||||
'MISS'
|
||||
);
|
||||
$response_data = $this->getDecodedContents($response);
|
||||
$this->assertSame($display_id, $response_data['id']);
|
||||
// Ensure the sections are not present in the serialized data, but other
|
||||
// Layout Builder data is.
|
||||
$this->assertArrayHasKey('layout_builder', $response_data['third_party_settings']);
|
||||
$this->assertArrayNotHasKey('sections', $response_data['third_party_settings']['layout_builder']);
|
||||
$this->assertEquals(['enabled' => TRUE, 'allow_custom' => TRUE], $response_data['third_party_settings']['layout_builder']);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,154 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\Tests\layout_builder\Functional\Rest;
|
||||
|
||||
use Drupal\Component\Utility\NestedArray;
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait;
|
||||
use Drupal\Tests\rest\Functional\ResourceTestBase;
|
||||
use GuzzleHttp\RequestOptions;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
/**
|
||||
* Base class for Layout Builder REST tests.
|
||||
*/
|
||||
abstract class LayoutRestTestBase extends ResourceTestBase {
|
||||
|
||||
use BasicAuthResourceTestTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static $modules = [
|
||||
'node',
|
||||
'layout_builder',
|
||||
'serialization',
|
||||
'basic_auth',
|
||||
];
|
||||
|
||||
/**
|
||||
* A test node.
|
||||
*
|
||||
* @var \Drupal\node\NodeInterface
|
||||
*/
|
||||
protected $node;
|
||||
|
||||
/**
|
||||
* The node storage.
|
||||
*
|
||||
* @var \Drupal\node\NodeStorageInterface
|
||||
*/
|
||||
protected $nodeStorage;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setUp() {
|
||||
parent::setUp();
|
||||
$assert_session = $this->assertSession();
|
||||
|
||||
// @todo The Layout Builder UI relies on local tasks; fix in
|
||||
// https://www.drupal.org/project/drupal/issues/2917777.
|
||||
$this->drupalPlaceBlock('local_tasks_block');
|
||||
|
||||
$this->createContentType(['type' => 'bundle_with_section_field']);
|
||||
|
||||
$this->drupalLogin($this->drupalCreateUser([
|
||||
'configure any layout',
|
||||
'administer node display',
|
||||
'administer display modes',
|
||||
'bypass node access',
|
||||
'create bundle_with_section_field content',
|
||||
'edit any bundle_with_section_field content',
|
||||
]));
|
||||
$page = $this->getSession()->getPage();
|
||||
$field_ui_prefix = 'admin/structure/types/manage/bundle_with_section_field/display';
|
||||
|
||||
// Enable Layout Builder for the default view modes, and overrides.
|
||||
$this->drupalGet("$field_ui_prefix/default");
|
||||
$page->checkField('layout[enabled]');
|
||||
$page->pressButton('Save');
|
||||
$page->checkField('layout[allow_custom]');
|
||||
$page->pressButton('Save');
|
||||
|
||||
// Create a node.
|
||||
$this->node = $this->createNode([
|
||||
'type' => 'bundle_with_section_field',
|
||||
'title' => 'A node at rest will stay at rest.',
|
||||
]);
|
||||
|
||||
$this->drupalGet('node/' . $this->node->id() . '/layout');
|
||||
$page->clickLink('Layout');
|
||||
$page->clickLink('Add Block');
|
||||
$page->clickLink('Powered by Drupal');
|
||||
$page->fillField('settings[label]', 'This is an override');
|
||||
$page->checkField('settings[label_display]');
|
||||
$page->pressButton('Add Block');
|
||||
$page->clickLink('Save Layout');
|
||||
$assert_session->pageTextContains('This is an override');
|
||||
|
||||
$this->nodeStorage = $this->container->get('entity_type.manager')->getStorage('node');
|
||||
$this->node = $this->nodeStorage->load($this->node->id());
|
||||
|
||||
$this->drupalLogout();
|
||||
$this->setUpAuthorization('ALL');
|
||||
|
||||
$this->provisionResource([static::$format], ['basic_auth']);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function request($method, Url $url, array $request_options = []) {
|
||||
$request_options[RequestOptions::HEADERS] = [
|
||||
'Content-Type' => static::$mimeType,
|
||||
];
|
||||
$request_options = NestedArray::mergeDeep($request_options, $this->getAuthenticationRequestOptions($method));
|
||||
$request_options[RequestOptions::QUERY] = ['_format' => static::$format];
|
||||
return parent::request($method, $url, $request_options);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUpAuthorization($method) {
|
||||
$permissions = array_keys($this->container->get('user.permissions')->getPermissions());
|
||||
// Give the test user all permissions on the site. There should be no
|
||||
// permission that gives the user access to layout sections over REST.
|
||||
$this->account = $this->drupalCreateUser($permissions);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function assertResponseWhenMissingAuthentication($method, ResponseInterface $response) {}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function assertNormalizationEdgeCases($method, Url $url, array $request_options) {}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function assertAuthenticationEdgeCases($method, Url $url, array $request_options) {}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExpectedUnauthorizedAccessCacheability() {}
|
||||
|
||||
/**
|
||||
* Gets the decoded contents.
|
||||
*
|
||||
* @param \Psr\Http\Message\ResponseInterface $response
|
||||
* The response.
|
||||
*
|
||||
* @return array
|
||||
* The decoded contents.
|
||||
*/
|
||||
protected function getDecodedContents(ResponseInterface $response) {
|
||||
return $this->serializer->decode((string) $response->getBody(), static::$format);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,153 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\Tests\layout_builder\Functional\Rest;
|
||||
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage;
|
||||
use Drupal\node\Entity\Node;
|
||||
use GuzzleHttp\RequestOptions;
|
||||
|
||||
/**
|
||||
* Tests that override layout sections are not exposed via the REST API.
|
||||
*
|
||||
* @group layout_builder
|
||||
* @group rest
|
||||
*/
|
||||
class OverrideSectionsTest extends LayoutRestTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $resourceConfigId = 'entity.node';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setUp() {
|
||||
parent::setUp();
|
||||
// @todo Figure why field definitions have to cleared in
|
||||
// https://www.drupal.org/project/drupal/issues/2985882.
|
||||
$this->container->get('entity_field.manager')->clearCachedFieldDefinitions();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that the layout override field is not normalized.
|
||||
*/
|
||||
public function testOverrideField() {
|
||||
$this->assertCount(1, $this->node->get(OverridesSectionStorage::FIELD_NAME));
|
||||
|
||||
// Make a GET request and ensure override field is not included.
|
||||
$response = $this->request(
|
||||
'GET',
|
||||
Url::fromRoute('rest.entity.node.GET', ['node' => $this->node->id()])
|
||||
);
|
||||
$this->assertResourceResponse(
|
||||
200,
|
||||
FALSE,
|
||||
$response,
|
||||
[
|
||||
'config:filter.format.plain_text',
|
||||
'config:rest.resource.entity.node',
|
||||
'config:rest.settings',
|
||||
'http_response',
|
||||
'node:1',
|
||||
],
|
||||
[
|
||||
'languages:language_interface',
|
||||
'theme',
|
||||
'url.site',
|
||||
'user.permissions',
|
||||
],
|
||||
FALSE,
|
||||
'MISS'
|
||||
);
|
||||
$get_data = $this->getDecodedContents($response);
|
||||
$this->assertSame('A node at rest will stay at rest.', $get_data['title'][0]['value']);
|
||||
$this->assertArrayNotHasKey('layout_builder__layout', $get_data);
|
||||
|
||||
// Make a POST request without the override field.
|
||||
$new_node = [
|
||||
'type' => [
|
||||
[
|
||||
'target_id' => 'bundle_with_section_field',
|
||||
],
|
||||
],
|
||||
'title' => [
|
||||
[
|
||||
'value' => 'On with the rest of the test.',
|
||||
],
|
||||
],
|
||||
];
|
||||
$response = $this->request(
|
||||
'POST',
|
||||
Url::fromRoute(
|
||||
'rest.entity.node.POST'),
|
||||
[
|
||||
RequestOptions::BODY => $this->serializer->encode($new_node, static::$format),
|
||||
]
|
||||
);
|
||||
$this->assertResourceResponse(201, FALSE, $response);
|
||||
$posted_node = $this->nodeStorage->load(2);
|
||||
$this->assertEquals('On with the rest of the test.', $posted_node->getTitle());
|
||||
|
||||
// Make a POST request with override field.
|
||||
$new_node['layout_builder__layout'] = [];
|
||||
$post_contents = $this->serializer->encode($new_node, static::$format);
|
||||
$response = $this->request(
|
||||
'POST',
|
||||
Url::fromRoute(
|
||||
'rest.entity.node.POST'),
|
||||
[
|
||||
RequestOptions::BODY => $post_contents,
|
||||
]
|
||||
);
|
||||
$this->assertResourceErrorResponse(403, 'Access denied on creating field \'layout_builder__layout\'.', $response);
|
||||
|
||||
// Make a PATCH request without the override field.
|
||||
$patch_data = [
|
||||
'title' => [
|
||||
[
|
||||
'value' => 'New and improved title',
|
||||
],
|
||||
],
|
||||
'type' => [
|
||||
[
|
||||
'target_id' => 'bundle_with_section_field',
|
||||
],
|
||||
],
|
||||
];
|
||||
$response = $this->request(
|
||||
'PATCH',
|
||||
Url::fromRoute(
|
||||
'rest.entity.node.PATCH',
|
||||
['node' => 1]
|
||||
),
|
||||
[
|
||||
RequestOptions::BODY => $this->serializer->encode($patch_data, static::$format),
|
||||
]
|
||||
);
|
||||
$this->assertResourceResponse(200, FALSE, $response);
|
||||
$this->nodeStorage->resetCache([1]);
|
||||
$this->node = $this->nodeStorage->load(1);
|
||||
$this->assertEquals('New and improved title', $this->node->getTitle());
|
||||
|
||||
// Make a PATCH request with the override field.
|
||||
$patch_data['title'][0]['value'] = 'This title will not save.';
|
||||
$patch_data['layout_builder__layout'] = [];
|
||||
$response = $this->request(
|
||||
'PATCH',
|
||||
Url::fromRoute(
|
||||
'rest.entity.node.PATCH',
|
||||
['node' => 1]
|
||||
),
|
||||
[
|
||||
RequestOptions::BODY => $this->serializer->encode($patch_data, static::$format),
|
||||
]
|
||||
);
|
||||
|
||||
$this->assertResourceErrorResponse(403, 'Access denied on updating field \'layout_builder__layout\'.', $response);
|
||||
// Ensure the title has not changed.
|
||||
$this->assertEquals('New and improved title', Node::load(1)->getTitle());
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue