Commit a12f2100 authored by xjm's avatar xjm

SA-CORE-2019-003 by samuel.mortenson, Berdir, pwolanin, dawehner,...

SA-CORE-2019-003 by samuel.mortenson, Berdir, pwolanin, dawehner, cashwilliams, Wim Leers, xjm, larowlan, alexpott, plach, damiankloip, tstoeckler, tedbow, DamienMcKenna, effulgentsia, RobLoach, gabesullice, drumm, heshanlk, dsnopek, fago, miro_dietiker, truls1502
parent 48378221
......@@ -78,13 +78,15 @@ public function access(Route $route, RouteMatchInterface $route_match, AccountIn
if ($entity_type->getBundleEntityType()) {
$access->addCacheTags($this->entityTypeManager->getDefinition($entity_type->getBundleEntityType())->getListCacheTags());
// Check if the user is allowed to create new bundles. If so, allow
// access, so the add page can show a link to create one.
// @see \Drupal\Core\Entity\Controller\EntityController::addPage()
$bundle_access_control_handler = $this->entityTypeManager->getAccessControlHandler($entity_type->getBundleEntityType());
$access = $access->orIf($bundle_access_control_handler->createAccess(NULL, $account, [], TRUE));
if ($access->isAllowed()) {
return $access;
if (empty($route->getOption('_ignore_create_bundle_access'))) {
// Check if the user is allowed to create new bundles. If so, allow
// access, so the add page can show a link to create one.
// @see \Drupal\Core\Entity\Controller\EntityController::addPage()
$bundle_access_control_handler = $this->entityTypeManager->getAccessControlHandler($entity_type->getBundleEntityType());
$access = $access->orIf($bundle_access_control_handler->createAccess(NULL, $account, [], TRUE));
if ($access->isAllowed()) {
return $access;
}
}
}
......
......@@ -64,7 +64,12 @@ public function setValue($values, $notify = TRUE) {
$values = $values->getValue();
}
else {
$values = unserialize($values);
if (version_compare(PHP_VERSION, '7.0.0', '>=')) {
$values = unserialize($values, ['allowed_classes' => FALSE]);
}
else {
$values = unserialize($values);
}
}
}
......
......@@ -5,6 +5,7 @@
use Drupal\Core\Field\FieldItemInterface;
use Drupal\Core\TypedData\TypedDataInternalPropertiesHelper;
use Drupal\serialization\Normalizer\FieldableEntityNormalizerTrait;
use Drupal\serialization\Normalizer\SerializedColumnNormalizerTrait;
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
/**
......@@ -13,6 +14,7 @@
class FieldItemNormalizer extends NormalizerBase {
use FieldableEntityNormalizerTrait;
use SerializedColumnNormalizerTrait;
/**
* {@inheritdoc}
......@@ -45,6 +47,7 @@ public function denormalize($data, $class, $format = NULL, array $context = [])
}
$field_item = $context['target_instance'];
$this->checkForSerializedStrings($data, $class, $field_item);
// If this field is translatable, we need to create a translated instance.
if (isset($data['lang'])) {
......
......@@ -3,6 +3,7 @@
namespace Drupal\Tests\hal\Kernel;
use Drupal\Core\Url;
use Drupal\entity_test\Entity\EntitySerializedField;
use Drupal\field\Entity\FieldConfig;
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
......
......@@ -191,7 +191,12 @@ public function setValue($values, $notify = TRUE) {
// SqlContentEntityStorage::loadFieldItems, see
// https://www.drupal.org/node/2414835
if (is_string($values['options'])) {
$values['options'] = unserialize($values['options']);
if (version_compare(PHP_VERSION, '7.0.0', '>=')) {
$values['options'] = unserialize($values['options'], ['allowed_classes' => FALSE]);
}
else {
$values['options'] = unserialize($values['options']);
}
}
parent::setValue($values, $notify);
}
......
......@@ -359,7 +359,7 @@ protected function uploadFile() {
// To still run the complete test coverage for POSTing a Media entity, we
// must revoke the additional permissions that we granted.
$role = Role::load(static::$auth ? RoleInterface::AUTHENTICATED_ID : RoleInterface::AUTHENTICATED_ID);
$role = Role::load(static::$auth ? RoleInterface::AUTHENTICATED_ID : RoleInterface::ANONYMOUS_ID);
$role->revokePermission('create camelids media');
$role->trustData()->save();
}
......
......@@ -80,7 +80,7 @@ public function createAccess($entity_bundle = NULL, AccountInterface $account =
return $return_as_object ? $result : $result->isAllowed();
}
if (!$account->hasPermission('access content')) {
$result = AccessResult::forbidden()->cachePerPermissions();
$result = AccessResult::forbidden("The 'access content' permission is required.")->cachePerPermissions();
return $return_as_object ? $result : $result->isAllowed();
}
......
......@@ -210,7 +210,7 @@ protected function getExpectedUnauthorizedAccessMessage($method) {
return parent::getExpectedUnauthorizedAccessMessage($method);
}
if ($method === 'GET' || $method == 'PATCH' || $method == 'DELETE') {
if ($method === 'GET' || $method == 'PATCH' || $method == 'DELETE' || $method == 'POST') {
return "The 'access content' permission is required.";
}
return parent::getExpectedUnauthorizedAccessMessage($method);
......
......@@ -61,3 +61,10 @@ function rest_post_update_resource_granularity() {
}
}
}
/**
* Clear caches due to changes in route definitions.
*/
function rest_post_update_161923() {
// Empty post-update hook.
}
......@@ -377,6 +377,10 @@ protected function getBaseRoute($canonical_path, $method) {
case 'GET':
$route->setRequirement('_entity_access', $this->entityType->id() . '.view');
break;
case 'POST':
$route->setRequirement('_entity_create_any_access', $this->entityType->id());
$route->setOption('_ignore_create_bundle_access', TRUE);
break;
case 'PATCH':
$route->setRequirement('_entity_access', $this->entityType->id() . '.update');
break;
......
......@@ -872,18 +872,6 @@ public function testPost() {
$request_options[RequestOptions::HEADERS]['Content-Type'] = static::$mimeType;
// DX: 400 when no request body.
$response = $this->request('POST', $url, $request_options);
$this->assertResourceErrorResponse(400, 'No entity content received.', $response);
$request_options[RequestOptions::BODY] = $unparseable_request_body;
// DX: 400 when unparseable request body.
$response = $this->request('POST', $url, $request_options);
$this->assertResourceErrorResponse(400, 'Syntax error', $response);
$request_options[RequestOptions::BODY] = $parseable_invalid_request_body;
if (static::$auth) {
// DX: forgetting authentication: authentication provider-specific error
// response.
......@@ -895,16 +883,22 @@ public function testPost() {
// DX: 403 when unauthorized.
$response = $this->request('POST', $url, $request_options);
// @todo Remove this if-test in https://www.drupal.org/project/drupal/issues/2820364
if (static::$entityTypeId === 'media' && !static::$auth) {
$this->assertResourceErrorResponse(422, "Unprocessable Entity: validation failed.\nname: Name: this field cannot hold more than 1 values.\nfield_media_file.0: You do not have access to the referenced entity (file: 3).\n", $response);
}
else {
$this->assertResourceErrorResponse(403, $this->getExpectedUnauthorizedAccessMessage('POST'), $response);
}
$this->assertResourceErrorResponse(403, $this->getExpectedUnauthorizedAccessMessage('POST'), $response);
$this->setUpAuthorization('POST');
// DX: 400 when no request body.
$response = $this->request('POST', $url, $request_options);
$this->assertResourceErrorResponse(400, 'No entity content received.', $response);
$request_options[RequestOptions::BODY] = $unparseable_request_body;
// DX: 400 when unparseable request body.
$response = $this->request('POST', $url, $request_options);
$this->assertResourceErrorResponse(400, 'Syntax error', $response);
$request_options[RequestOptions::BODY] = $parseable_invalid_request_body;
// DX: 422 when invalid entity: multiple values sent for single-value field.
$response = $this->request('POST', $url, $request_options);
$label_field = $this->entity->getEntityType()->hasKey('label') ? $this->entity->getEntityType()->getKey('label') : static::$labelFieldName;
......
......@@ -12,6 +12,7 @@
class FieldItemNormalizer extends ComplexDataNormalizer implements DenormalizerInterface {
use FieldableEntityNormalizerTrait;
use SerializedColumnNormalizerTrait;
/**
* {@inheritdoc}
......@@ -32,6 +33,7 @@ public function denormalize($data, $class, $format = NULL, array $context = [])
/** @var \Drupal\Core\Field\FieldItemInterface $field_item */
$field_item = $context['target_instance'];
$this->checkForSerializedStrings($data, $class, $field_item);
$field_item->setValue($this->constructValue($data, $context));
return $field_item;
......
......@@ -223,15 +223,24 @@ protected function constructValue($data, $context) {
assert($item_definition instanceof FieldItemDataDefinitionInterface);
$property_definitions = $item_definition->getPropertyDefinitions();
if (!is_array($data)) {
$property_value = $data;
$property_value_class = $property_definitions[$item_definition->getMainPropertyName()]->getClass();
$serialized_property_names = $this->getCustomSerializedPropertyNames($field_item);
$denormalize_property = function ($property_name, $property_value, $property_value_class, $context) use ($serialized_property_names) {
if ($this->serializer->supportsDenormalization($property_value, $property_value_class, NULL, $context)) {
return $this->serializer->denormalize($property_value, $property_value_class, NULL, $context);
}
else {
if (in_array($property_name, $serialized_property_names, TRUE)) {
$property_value = serialize($property_value);
}
return $property_value;
}
};
if (!is_array($data)) {
$property_value = $data;
$property_name = $item_definition->getMainPropertyName();
$property_value_class = $property_definitions[$property_name]->getClass();
return $denormalize_property($property_name, $property_value, $property_value_class, $context);
}
$data_internal = [];
......@@ -243,12 +252,7 @@ protected function constructValue($data, $context) {
}
$property_value = $data[$property_name];
$property_value_class = $property_definition->getClass();
if ($this->serializer->supportsDenormalization($property_value, $property_value_class, NULL, $context)) {
$data_internal[$property_name] = $this->serializer->denormalize($property_value, $property_value_class, NULL, $context);
}
else {
$data_internal[$property_name] = $property_value;
}
$data_internal[$property_name] = $denormalize_property($property_name, $property_value, $property_value_class, $context);
}
}
else {
......
......@@ -2,6 +2,7 @@
namespace Drupal\serialization\Normalizer;
use Drupal\Core\Field\FieldItemInterface;
use Drupal\Core\TypedData\PrimitiveInterface;
/**
......@@ -9,6 +10,8 @@
*/
class PrimitiveDataNormalizer extends NormalizerBase {
use SerializedColumnNormalizerTrait;
/**
* {@inheritdoc}
*/
......@@ -18,6 +21,14 @@ class PrimitiveDataNormalizer extends NormalizerBase {
* {@inheritdoc}
*/
public function normalize($object, $format = NULL, array $context = []) {
$parent = $object->getParent();
if ($parent instanceof FieldItemInterface && $object->getValue()) {
$serialized_property_names = $this->getCustomSerializedPropertyNames($parent);
if (in_array($object->getName(), $serialized_property_names, TRUE)) {
return unserialize($object->getValue());
}
}
// Typed data casts NULL objects to their empty variants, so for example
// the empty string ('') for string type data, or 0 for integer typed data.
// In a better world with typed data implementing algebraic data types,
......
<?php
namespace Drupal\serialization\Normalizer;
use Drupal\Component\Plugin\PluginInspectionInterface;
use Drupal\Core\Field\FieldItemInterface;
/**
* A trait providing methods for serialized columns.
*/
trait SerializedColumnNormalizerTrait {
/**
* Checks if there is a serialized string for a column.
*
* @param mixed $data
* The field item data to denormalize.
* @param string $class
* The expected class to instantiate.
* @param \Drupal\Core\Field\FieldItemInterface $field_item
* The field item.
*/
protected function checkForSerializedStrings($data, $class, FieldItemInterface $field_item) {
// Require specialized denormalizers for fields with 'serialize' columns.
// Note: this cannot be checked in ::supportsDenormalization() because at
// that time we only have the field item class. ::hasSerializeColumn()
// must be able to call $field_item->schema(), which requires a field
// storage definition. To determine that, the entity type and bundle
// must be known, which is contextual information that the Symfony
// serializer does not pass to ::supportsDenormalization().
if (!is_array($data)) {
$data = [$field_item->getDataDefinition()->getMainPropertyName() => $data];
}
if ($this->dataHasStringForSerializeColumn($field_item, $data)) {
$field_name = $field_item->getParent() ? $field_item->getParent()->getName() : $field_item->getName();
throw new \LogicException(sprintf('The generic FieldItemNormalizer cannot denormalize string values for "%s" properties of the "%s" field (field item class: %s).', implode('", "', $this->getSerializedPropertyNames($field_item)), $field_name, $class));
}
}
/**
* Checks if the data contains string value for serialize column.
*
* @param \Drupal\Core\Field\FieldItemInterface $field_item
* The field item.
* @param array $data
* The data being denormalized.
*
* @return bool
* TRUE if there is a string value for serialize column, otherwise FALSE.
*/
protected function dataHasStringForSerializeColumn(FieldItemInterface $field_item, array $data) {
foreach ($this->getSerializedPropertyNames($field_item) as $property_name) {
if (isset($data[$property_name]) && is_string($data[$property_name])) {
return TRUE;
}
}
return FALSE;
}
/**
* Gets the names of all serialized properties.
*
* @param \Drupal\Core\Field\FieldItemInterface $field_item
* The field item.
*
* @return string[]
* The property names for serialized properties.
*/
protected function getSerializedPropertyNames(FieldItemInterface $field_item) {
$field_storage_definition = $field_item->getFieldDefinition()->getFieldStorageDefinition();
if ($custom_property_names = $this->getCustomSerializedPropertyNames($field_item)) {
return $custom_property_names;
}
$field_storage_schema = $field_item->schema($field_storage_definition);
// If there are no columns then there are no serialized properties.
if (!isset($field_storage_schema['columns'])) {
return [];
}
$serialized_columns = array_filter($field_storage_schema['columns'], function ($column_schema) {
return isset($column_schema['serialize']) && $column_schema['serialize'] === TRUE;
});
return array_keys($serialized_columns);
}
/**
* Gets the names of all properties the plugin treats as serialized data.
*
* This allows the field storage definition or entity type to provide a
* setting for serialized properties. This can be used for fields that
* handle serialized data themselves and do not rely on the serialized schema
* flag.
*
* @param \Drupal\Core\Field\FieldItemInterface $field_item
* The field item.
*
* @return string[]
* The property names for serialized properties.
*/
protected function getCustomSerializedPropertyNames(FieldItemInterface $field_item) {
if ($field_item instanceof PluginInspectionInterface) {
$definition = $field_item->getPluginDefinition();
$serialized_fields = $field_item->getEntity()->getEntityType()->get('serialized_field_property_names');
$field_name = $field_item->getFieldDefinition()->getName();
if (is_array($serialized_fields) && isset($serialized_fields[$field_name]) && is_array($serialized_fields[$field_name])) {
return $serialized_fields[$field_name];
}
if (isset($definition['serialized_property_names']) && is_array($definition['serialized_property_names'])) {
return $definition['serialized_property_names'];
}
}
return [];
}
}
......@@ -4,6 +4,7 @@
use Drupal\Component\Serialization\Json;
use Drupal\Component\Render\FormattableMarkup;
use Drupal\entity_test\Entity\EntitySerializedField;
use Drupal\entity_test\Entity\EntityTestMulRev;
use Drupal\filter\Entity\FilterFormat;
use Drupal\Tests\rest\Functional\BcTimestampNormalizerUnixTestTrait;
......
......@@ -3,6 +3,7 @@
namespace Drupal\Tests\serialization\Unit\Normalizer;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\TypedData\FieldItemDataDefinition;
use Drupal\Core\GeneratedUrl;
......@@ -435,6 +436,33 @@ protected function assertDenormalize(array $data) {
->shouldBeCalled();
}
// Avoid a static method call by returning dummy property data.
$this->fieldDefinition
->getFieldStorageDefinition()
->willReturn()
->shouldBeCalled();
$this->fieldDefinition
->getName()
->willReturn('field_reference')
->shouldBeCalled();
$entity = $this->prophesize(EntityInterface::class);
$entity_type = $this->prophesize(EntityTypeInterface::class);
$entity->getEntityType()
->willReturn($entity_type->reveal())
->shouldBeCalled();
$this->fieldItem
->getPluginDefinition()
->willReturn([
'serialized_property_names' => [
'foo' => 'bar',
],
])
->shouldBeCalled();
$this->fieldItem
->getEntity()
->willReturn($entity->reveal())
->shouldBeCalled();
$context = ['target_instance' => $this->fieldItem->reveal()];
$denormalized = $this->normalizer->denormalize($data, EntityReferenceItem::class, 'json', $context);
$this->assertSame($context['target_instance'], $denormalized);
......
......@@ -2,6 +2,9 @@
namespace Drupal\Tests\serialization\Unit\Normalizer;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\Plugin\Field\FieldType\CreatedItem;
use Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem;
use Drupal\Core\Field\Plugin\Field\FieldType\TimestampItem;
......@@ -120,6 +123,29 @@ public function testDenormalize() {
$timestamp_item->setValue(['value' => $timestamp_data_denormalization])
->shouldBeCalled();
// Avoid a static method call by returning dummy property data.
$field_definition = $this->prophesize(FieldDefinitionInterface::class);
$timestamp_item
->getFieldDefinition()
->willReturn($field_definition->reveal())
->shouldBeCalled();
$timestamp_item->getPluginDefinition()
->willReturn([
'serialized_property_names' => [
'foo' => 'bar',
],
])
->shouldBeCalled();
$entity = $this->prophesize(EntityInterface::class);
$entity_type = $this->prophesize(EntityTypeInterface::class);
$entity->getEntityType()
->willReturn($entity_type->reveal())
->shouldBeCalled();
$timestamp_item
->getEntity()
->willReturn($entity->reveal())
->shouldBeCalled();
$context = [
'target_instance' => $timestamp_item->reveal(),
'datetime_allowed_formats' => [\DateTime::RFC3339],
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment