diff --git a/core/config/schema/core.data_types.schema.yml b/core/config/schema/core.data_types.schema.yml index bb4993a172..01a1eeee95 100644 --- a/core/config/schema/core.data_types.schema.yml +++ b/core/config/schema/core.data_types.schema.yml @@ -277,6 +277,10 @@ config_entity: uuid: type: string label: 'UUID' + constraints: + Length: + max: 128 + maxMessage: 'UUID: may not be longer than 128 characters.' langcode: type: string label: 'Language code' diff --git a/core/core.services.yml b/core/core.services.yml index 40c6a8a6b4..31733463b4 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -331,9 +331,11 @@ services: arguments: ['@config.storage', 'config/schema', '', true, '%install_profile%'] config.typed: class: Drupal\Core\Config\TypedConfigManager - arguments: ['@config.storage', '@config.storage.schema', '@cache.discovery', '@module_handler'] + arguments: ['@config.storage', '@config.storage.schema', '@cache.discovery', '@module_handler', '@class_resolver'] tags: - { name: plugin_manager_cache_clear } + calls: + - [setValidationConstraintManager, ['@validation.constraint']] context.handler: class: Drupal\Core\Plugin\Context\ContextHandler arguments: ['@typed_data_manager'] diff --git a/core/lib/Drupal/Core/Config/Schema/ArrayElement.php b/core/lib/Drupal/Core/Config/Schema/ArrayElement.php index c507655447..3dd4cac49b 100644 --- a/core/lib/Drupal/Core/Config/Schema/ArrayElement.php +++ b/core/lib/Drupal/Core/Config/Schema/ArrayElement.php @@ -2,10 +2,12 @@ namespace Drupal\Core\Config\Schema; +use Drupal\Core\TypedData\ComplexDataInterface; + /** * Defines a generic configuration element that contains multiple properties. */ -abstract class ArrayElement extends Element implements \IteratorAggregate, TypedConfigInterface { +abstract class ArrayElement extends Element implements \IteratorAggregate, TypedConfigInterface, ComplexDataInterface { /** * Parsed elements. @@ -161,4 +163,25 @@ public function isNullable() { return isset($this->definition['nullable']) && $this->definition['nullable'] == TRUE; } + /** + * {@inheritdoc} + */ + public function set($property_name, $value, $notify = TRUE) { + $this->value[$property_name] = $value; + // Config schema elements to not make use of notifications. Thus, we skip + // notifying parents. + return $this; + } + + /** + * {@inheritdoc} + */ + public function getProperties($include_computed = FALSE) { + $properties = array(); + foreach (array_keys($this->value) as $name) { + $properties[$name] = $this->get($name); + } + return $properties; + } + } diff --git a/core/lib/Drupal/Core/Config/Schema/Sequence.php b/core/lib/Drupal/Core/Config/Schema/Sequence.php index ce8dc1bc0b..afa3ed3bb0 100644 --- a/core/lib/Drupal/Core/Config/Schema/Sequence.php +++ b/core/lib/Drupal/Core/Config/Schema/Sequence.php @@ -10,6 +10,12 @@ * * Read https://www.drupal.org/node/1905070 for more details about configuration * schema, types and type resolution. + * + * Note that sequences implement the typed data ComplexDataInterface (via the + * parent ArrayElement) rather than the ListInterface. This is, as sequences + * have named keys, what is not covered by lists. From the typed data API + * perspective sequences are handled as ordered mappings without metadata about + * existing properties. */ class Sequence extends ArrayElement { diff --git a/core/lib/Drupal/Core/Config/StorableConfigBase.php b/core/lib/Drupal/Core/Config/StorableConfigBase.php index 0751e9f59f..1c8fb2c828 100644 --- a/core/lib/Drupal/Core/Config/StorableConfigBase.php +++ b/core/lib/Drupal/Core/Config/StorableConfigBase.php @@ -129,7 +129,7 @@ public function getStorage() { * * @return \Drupal\Core\Config\Schema\Element */ - protected function getSchemaWrapper() { + public function getSchemaWrapper() { if (!isset($this->schemaWrapper)) { $definition = $this->typedConfigManager->getDefinition($this->name); $data_definition = $this->typedConfigManager->buildDataDefinition($definition, $this->data); diff --git a/core/lib/Drupal/Core/Config/TypedConfigManager.php b/core/lib/Drupal/Core/Config/TypedConfigManager.php index 22795ad6ff..bd5fa99812 100644 --- a/core/lib/Drupal/Core/Config/TypedConfigManager.php +++ b/core/lib/Drupal/Core/Config/TypedConfigManager.php @@ -6,6 +6,7 @@ use Drupal\Core\Cache\CacheBackendInterface; use Drupal\Core\Config\Schema\ConfigSchemaAlterException; use Drupal\Core\Config\Schema\ConfigSchemaDiscovery; +use Drupal\Core\DependencyInjection\ClassResolverInterface; use Drupal\Core\Config\Schema\Undefined; use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\TypedData\TypedDataManager; @@ -45,13 +46,18 @@ class TypedConfigManager extends TypedDataManager implements TypedConfigManagerI * The storage object to use for reading schema data * @param \Drupal\Core\Cache\CacheBackendInterface $cache * The cache backend to use for caching the definitions. + * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler + * The module handler. + * @param \Drupal\Core\DependencyInjection\ClassResolverInterface $class_resolver + * (optional) The class resolver. */ - public function __construct(StorageInterface $configStorage, StorageInterface $schemaStorage, CacheBackendInterface $cache, ModuleHandlerInterface $module_handler) { + public function __construct(StorageInterface $configStorage, StorageInterface $schemaStorage, CacheBackendInterface $cache, ModuleHandlerInterface $module_handler, ClassResolverInterface $class_resolver = NULL) { $this->configStorage = $configStorage; $this->schemaStorage = $schemaStorage; $this->setCacheBackend($cache, 'typed_config_definitions'); $this->alterInfo('config_schema_info'); $this->moduleHandler = $module_handler; + $this->classResolver = $class_resolver ?: \Drupal::service('class_resolver'); } /** @@ -184,6 +190,7 @@ protected function getDefinitionWithReplacements($base_plugin_id, array $replace $definition += [ 'definition_class' => '\Drupal\Core\TypedData\DataDefinition', 'type' => $type, + 'unwrap_for_canonical_representation' => TRUE, ]; return $definition; } diff --git a/core/lib/Drupal/Core/Field/BaseFieldDefinition.php b/core/lib/Drupal/Core/Field/BaseFieldDefinition.php index 5194d85e9a..37cc103875 100644 --- a/core/lib/Drupal/Core/Field/BaseFieldDefinition.php +++ b/core/lib/Drupal/Core/Field/BaseFieldDefinition.php @@ -585,7 +585,7 @@ protected function getFieldItemClass() { public function __sleep() { // Do not serialize the statically cached property definitions. $vars = get_object_vars($this); - unset($vars['propertyDefinitions']); + unset($vars['propertyDefinitions'], $vars['typedDataManager']); return array_keys($vars); } diff --git a/core/lib/Drupal/Core/TypedData/ComplexDataDefinitionBase.php b/core/lib/Drupal/Core/TypedData/ComplexDataDefinitionBase.php index 0acdba468d..0002497e94 100644 --- a/core/lib/Drupal/Core/TypedData/ComplexDataDefinitionBase.php +++ b/core/lib/Drupal/Core/TypedData/ComplexDataDefinitionBase.php @@ -42,7 +42,7 @@ public function getMainPropertyName() { public function __sleep() { // Do not serialize the cached property definitions. $vars = get_object_vars($this); - unset($vars['propertyDefinitions']); + unset($vars['propertyDefinitions'], $vars['typedDataManager']); return array_keys($vars); } diff --git a/core/lib/Drupal/Core/TypedData/DataDefinition.php b/core/lib/Drupal/Core/TypedData/DataDefinition.php index 7eec1a90c4..52a4394cd7 100644 --- a/core/lib/Drupal/Core/TypedData/DataDefinition.php +++ b/core/lib/Drupal/Core/TypedData/DataDefinition.php @@ -7,6 +7,8 @@ */ class DataDefinition implements DataDefinitionInterface, \ArrayAccess { + use TypedDataTrait; + /** * The array holding values for all definition keys. * @@ -258,7 +260,7 @@ public function setSetting($setting_name, $value) { */ public function getConstraints() { $constraints = isset($this->definition['constraints']) ? $this->definition['constraints'] : []; - $constraints += \Drupal::typedDataManager()->getDefaultConstraints($this); + $constraints += $this->getTypedDataManager()->getDefaultConstraints($this); return $constraints; } @@ -340,4 +342,14 @@ public function toArray() { return $this->definition; } + /** + * {@inheritdoc} + */ + public function __sleep() { + // Never serialize the typed data manager. + $vars = get_object_vars($this); + unset($vars['typedDataManager']); + return array_keys($vars); + } + } diff --git a/core/lib/Drupal/Core/TypedData/TypedDataManager.php b/core/lib/Drupal/Core/TypedData/TypedDataManager.php index 8c4f265ebd..dbf0d5fec5 100644 --- a/core/lib/Drupal/Core/TypedData/TypedDataManager.php +++ b/core/lib/Drupal/Core/TypedData/TypedDataManager.php @@ -117,7 +117,13 @@ public function createDataDefinition($data_type) { throw new \InvalidArgumentException("Invalid data type '$data_type' has been given"); } $class = $type_definition['definition_class']; - return $class::createFromDataType($data_type); + $data_definition = $class::createFromDataType($data_type); + + if (method_exists($data_definition, 'setTypedDataManager')) { + $data_definition->setTypedDataManager($this); + } + + return $data_definition; } /** diff --git a/core/modules/config/tests/config_test/config/install/config_test.validation.yml b/core/modules/config/tests/config_test/config/install/config_test.validation.yml new file mode 100644 index 0000000000..37414689fd --- /dev/null +++ b/core/modules/config/tests/config_test/config/install/config_test.validation.yml @@ -0,0 +1,7 @@ +llama: llama +cat: + type: kitten + count: 2 +giraffe: + hum1: hum1 + hum2: hum2 diff --git a/core/modules/config/tests/config_test/config/schema/config_test.schema.yml b/core/modules/config/tests/config_test/config/schema/config_test.schema.yml index e5b75d1430..65787afd7a 100644 --- a/core/modules/config/tests/config_test/config/schema/config_test.schema.yml +++ b/core/modules/config/tests/config_test/config/schema/config_test.schema.yml @@ -158,3 +158,33 @@ config_test.foo: config_test.bar: type: config_test.foo + +config_test.validation: + type: config_object + label: 'Configuration type' + mapping: + llama: + type: string + constraints: + Callback: + callback: [\Drupal\config_test\ConfigValidation, validateLlama] + cat: + type: mapping + mapping: + type: + type: string + constraints: + Callback: + callback: [\Drupal\config_test\ConfigValidation, validateCats] + count: + type: integer + constraints: + Callback: + callback: [\Drupal\config_test\ConfigValidation, validateCatCount] + giraffe: + type: sequence + sequence: + type: string + constraints: + Callback: + callback: [\Drupal\config_test\ConfigValidation, validateGiraffes] diff --git a/core/modules/config/tests/config_test/src/ConfigTestAccessControlHandler.php b/core/modules/config/tests/config_test/src/ConfigTestAccessControlHandler.php index 88896f0b96..4df889f3d1 100644 --- a/core/modules/config/tests/config_test/src/ConfigTestAccessControlHandler.php +++ b/core/modules/config/tests/config_test/src/ConfigTestAccessControlHandler.php @@ -6,6 +6,7 @@ use Drupal\Core\Session\AccountInterface; use Drupal\Core\Entity\EntityAccessControlHandler; use Drupal\Core\Entity\EntityInterface; +use Drupal\node\Plugin\views\filter\Access; /** * Defines the access control handler for the config_test entity type. @@ -18,14 +19,17 @@ class ConfigTestAccessControlHandler extends EntityAccessControlHandler { * {@inheritdoc} */ public function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) { - return AccessResult::allowed(); + if ($operation === 'view') { + return AccessResult::allowedIfHasPermission($account, 'view config_test'); + } + return AccessResult::allowedIfHasPermission($account, 'administer config_test'); } /** * {@inheritdoc} */ protected function checkCreateAccess(AccountInterface $account, array $context, $entity_bundle = NULL) { - return AccessResult::allowed(); + return AccessResult::allowedIfHasPermission($account, 'administer config_test'); } } diff --git a/core/modules/config/tests/config_test/src/ConfigValidation.php b/core/modules/config/tests/config_test/src/ConfigValidation.php new file mode 100644 index 0000000000..8cb57b8eed --- /dev/null +++ b/core/modules/config/tests/config_test/src/ConfigValidation.php @@ -0,0 +1,68 @@ +addViolation('no valid llama'); + } + } + + /** + * Validates cats. + * + * @param string $object + * The string to validate. + * @param \Symfony\Component\Validator\Context\ExecutionContextInterface $context + * The validation execution context. + */ + public static function validateCats($object, ExecutionContextInterface $context) { + if (!in_array($object, ['kitten', 'cats', 'nyans'])) { + $context->addViolation('no valid cat'); + } + } + + /** + * Validates a number. + * + * @param string $object + * The string to validate. + * @param \Symfony\Component\Validator\Context\ExecutionContextInterface $context + * The validation execution context. + */ + public static function validateCatCount($object, ExecutionContextInterface $context) { + if ($object <= 1) { + $context->addViolation('no enough cats'); + } + } + + /** + * Validates giraffes. + * + * @param string $object + * The string to validate. + * @param \Symfony\Component\Validator\Context\ExecutionContextInterface $context + * The validation execution context. + */ + public static function validateGiraffes($object, ExecutionContextInterface $context) { + if (strpos($object, 'hum') !== 0) { + $context->addViolation('Giraffes just hum'); + } + } + +} diff --git a/core/modules/rest/src/Plugin/Deriver/EntityDeriver.php b/core/modules/rest/src/Plugin/Deriver/EntityDeriver.php index 010fad7363..634a1bdd31 100644 --- a/core/modules/rest/src/Plugin/Deriver/EntityDeriver.php +++ b/core/modules/rest/src/Plugin/Deriver/EntityDeriver.php @@ -2,8 +2,10 @@ namespace Drupal\rest\Plugin\Deriver; +use Drupal\Core\Config\Entity\ConfigEntityTypeInterface; use Drupal\Core\Entity\EntityManagerInterface; use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface; +use Drupal\rest\Plugin\rest\resource\ConfigEntityResource; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -88,6 +90,10 @@ public function getDerivativeDefinitions($base_plugin_definition) { } } + if ($entity_type instanceof ConfigEntityTypeInterface) { + $this->derivatives[$entity_type_id]['class'] = ConfigEntityResource::class; + } + $this->derivatives[$entity_type_id] += $base_plugin_definition; } } diff --git a/core/modules/rest/src/Plugin/rest/resource/ConfigEntityResource.php b/core/modules/rest/src/Plugin/rest/resource/ConfigEntityResource.php new file mode 100644 index 0000000000..7e16e6e3ac --- /dev/null +++ b/core/modules/rest/src/Plugin/rest/resource/ConfigEntityResource.php @@ -0,0 +1,39 @@ +getEditable($entity->getConfigDependencyName()); + $config->setData($entity->toArray()); + + $typed_config = $config->getSchemaWrapper(); + + $violations = $typed_config->validate(); + + if ($violations->count() > 0) { + $message = "Unprocessable Entity: validation failed.\n"; + foreach ($violations as $violation) { + // We strip every HTML from the error message to have a nicer to read + // message on REST responses. + $message .= $violation->getPropertyPath() . ': ' . PlainTextOutput::renderFromHtml($violation->getMessage()) . "\n"; + } + throw new UnprocessableEntityHttpException($message); + } + } + +} diff --git a/core/modules/rest/src/Plugin/rest/resource/EntityResource.php b/core/modules/rest/src/Plugin/rest/resource/EntityResource.php index 9b1d15f005..9111fafccc 100644 --- a/core/modules/rest/src/Plugin/rest/resource/EntityResource.php +++ b/core/modules/rest/src/Plugin/rest/resource/EntityResource.php @@ -264,36 +264,38 @@ public function patch(EntityInterface $original_entity, EntityInterface $entity // Overwrite the received properties. $entity_keys = $entity->getEntityType()->getKeys(); - foreach ($entity->_restSubmittedFields as $field_name) { - $field = $entity->get($field_name); - - // Entity key fields need special treatment: together they uniquely - // identify the entity. Therefore it does not make sense to modify any of - // them. However, rather than throwing an error, we just ignore them as - // long as their specified values match their current values. - if (in_array($field_name, $entity_keys, TRUE)) { - // @todo Work around the wrong assumption that entity keys need special - // treatment, when only read-only fields need it. - // This will be fixed in https://www.drupal.org/node/2824851. - if ($entity->getEntityTypeId() == 'comment' && $field_name == 'status' && !$original_entity->get($field_name)->access('edit')) { - throw new AccessDeniedHttpException("Access denied on updating field '$field_name'."); + if ($entity instanceof FieldableEntityInterface) { + foreach ($entity->_restSubmittedFields as $field_name) { + $field = $entity->get($field_name); + + // Entity key fields need special treatment: together they uniquely + // identify the entity. Therefore it does not make sense to modify any of + // them. However, rather than throwing an error, we just ignore them as + // long as their specified values match their current values. + if (in_array($field_name, $entity_keys, TRUE)) { + // @todo Work around the wrong assumption that entity keys need special + // treatment, when only read-only fields need it. + // This will be fixed in https://www.drupal.org/node/2824851. + if ($entity->getEntityTypeId() == 'comment' && $field_name == 'status' && !$original_entity->get($field_name)->access('edit')) { + throw new AccessDeniedHttpException("Access denied on updating field '$field_name'."); + } + + // Unchanged values for entity keys don't need access checking. + if ($this->getCastedValueFromFieldItemList($original_entity->get($field_name)) === $this->getCastedValueFromFieldItemList($entity->get($field_name))) { + continue; + } + // It is not possible to set the language to NULL as it is automatically + // re-initialized. As it must not be empty, skip it if it is. + elseif (isset($entity_keys['langcode']) && $field_name === $entity_keys['langcode'] && $field->isEmpty()) { + continue; + } } - // Unchanged values for entity keys don't need access checking. - if ($this->getCastedValueFromFieldItemList($original_entity->get($field_name)) === $this->getCastedValueFromFieldItemList($entity->get($field_name))) { - continue; - } - // It is not possible to set the language to NULL as it is automatically - // re-initialized. As it must not be empty, skip it if it is. - elseif (isset($entity_keys['langcode']) && $field_name === $entity_keys['langcode'] && $field->isEmpty()) { - continue; + if (!$original_entity->get($field_name)->access('edit')) { + throw new AccessDeniedHttpException("Access denied on updating field '$field_name'."); } + $original_entity->set($field_name, $field->getValue()); } - - if (!$original_entity->get($field_name)->access('edit')) { - throw new AccessDeniedHttpException("Access denied on updating field '$field_name'."); - } - $original_entity->set($field_name, $field->getValue()); } // Validate the received data before saving. @@ -388,20 +390,6 @@ protected function getBaseRoute($canonical_path, $method) { } /** - * {@inheritdoc} - */ - public function availableMethods() { - $methods = parent::availableMethods(); - if ($this->isConfigEntityResource()) { - // Currently only GET is supported for Config Entities. - // @todo Remove when supported https://www.drupal.org/node/2300677 - $unsupported_methods = ['POST', 'PUT', 'DELETE', 'PATCH']; - $methods = array_diff($methods, $unsupported_methods); - } - return $methods; - } - - /** * Checks if this resource is for a Config Entity. * * @return bool diff --git a/core/modules/rest/src/Plugin/rest/resource/EntityResourceAccessTrait.php b/core/modules/rest/src/Plugin/rest/resource/EntityResourceAccessTrait.php index 7bf8e824e1..cb286dc0b4 100644 --- a/core/modules/rest/src/Plugin/rest/resource/EntityResourceAccessTrait.php +++ b/core/modules/rest/src/Plugin/rest/resource/EntityResourceAccessTrait.php @@ -3,6 +3,7 @@ namespace Drupal\rest\Plugin\rest\resource; use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Entity\FieldableEntityInterface; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; /** @@ -25,9 +26,11 @@ protected function checkEditFieldAccess(EntityInterface $entity) { // Only check 'edit' permissions for fields that were actually submitted by // the user. Field access makes no difference between 'create' and 'update', // so the 'edit' operation is used here. - foreach ($entity->_restSubmittedFields as $key => $field_name) { - if (!$entity->get($field_name)->access('edit')) { - throw new AccessDeniedHttpException("Access denied on creating field '$field_name'."); + if ($entity instanceof FieldableEntityInterface) { + foreach ($entity->_restSubmittedFields as $key => $field_name) { + if (!$entity->get($field_name)->access('edit')) { + throw new AccessDeniedHttpException("Access denied on creating field '$field_name'."); + } } } } diff --git a/core/modules/rest/tests/src/Functional/EntityResource/ConfigTest/ConfigTestResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/ConfigTest/ConfigTestResourceTestBase.php index 9fe073b097..b96879540c 100644 --- a/core/modules/rest/tests/src/Functional/EntityResource/ConfigTest/ConfigTestResourceTestBase.php +++ b/core/modules/rest/tests/src/Functional/EntityResource/ConfigTest/ConfigTestResourceTestBase.php @@ -22,11 +22,26 @@ */ protected $entity; + protected $counter = 0; + /** * {@inheritdoc} */ protected function setUpAuthorization($method) { - $this->grantPermissionsToTestedRole(['view config_test']); + switch ($method) { + case 'GET': + $this->grantPermissionsToTestedRole(['view config_test']); + break; + case 'POST': + $this->grantPermissionsToTestedRole(['administer config_test']); + break; + case 'PATCH': + $this->grantPermissionsToTestedRole(['administer config_test']); + break; + case 'DELETE': + $this->grantPermissionsToTestedRole(['administer config_test']); + break; + } } /** @@ -67,7 +82,33 @@ protected function getExpectedNormalizedEntity() { * {@inheritdoc} */ protected function getNormalizedPostEntity() { - // @todo Update in https://www.drupal.org/node/2300677. + $this->counter++; + return [ + 'id' => 'llama' . (string) $this->counter, + 'label' => 'Llamam', + ]; + } + + /** + * {@inheritdoc} + */ + protected function makeNormalizationInvalid(array $normalization) { + $normalization['label'] = ['foo', 'bar']; + return $normalization; + } + + /** + * {@inheritdoc} + */ + protected function getExpectedUnauthorizedAccessMessage($method) { + if ($this->config('rest.settings')->get('bc_entity_resource_permissions')) { + return parent::getExpectedUnauthorizedAccessMessage($method); + } + + switch ($method) { + default: + return "The 'administer config_test' permission is required."; + } } } diff --git a/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php index 81baf9ee17..0adb23aff2 100644 --- a/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php +++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php @@ -9,6 +9,7 @@ use Drupal\Core\Url; use Drupal\field\Entity\FieldConfig; use Drupal\field\Entity\FieldStorageConfig; +use Drupal\Tests\Core\Batch\PercentagesTest; use Drupal\Tests\rest\Functional\ResourceTestBase; use GuzzleHttp\RequestOptions; use Psr\Http\Message\ResponseInterface; @@ -584,12 +585,6 @@ protected static function castToString(array $normalization) { * Tests a POST request for an entity, plus edge cases to ensure good DX. */ public function testPost() { - // @todo Remove this in https://www.drupal.org/node/2300677. - if ($this->entity instanceof ConfigEntityInterface) { - $this->assertTrue(TRUE, 'POSTing config entities is not yet supported.'); - return; - } - $this->initAuthentication(); $has_canonical_url = $this->entity->hasLinkTemplate('canonical'); @@ -598,7 +593,12 @@ public function testPost() { $parseable_valid_request_body = $this->serializer->encode($this->getNormalizedPostEntity(), static::$format); $parseable_valid_request_body_2 = $this->serializer->encode($this->getNormalizedPostEntity(), static::$format); $parseable_invalid_request_body = $this->serializer->encode($this->makeNormalizationInvalid($this->getNormalizedPostEntity()), static::$format); - $parseable_invalid_request_body_2 = $this->serializer->encode($this->getNormalizedPostEntity() + ['uuid' => [$this->randomMachineName(129)]], static::$format); + if ($this->entity instanceof FieldableEntityInterface) { + $parseable_invalid_request_body_2 = $this->serializer->encode($this->getNormalizedPostEntity() + ['uuid' => [$this->randomMachineName(129)]], static::$format); + } + else { + $parseable_invalid_request_body_2 = $this->serializer->encode($this->getNormalizedPostEntity() + ['uuid' => $this->randomMachineName(129)], static::$format); + } $parseable_invalid_request_body_3 = $this->serializer->encode($this->getNormalizedPostEntity() + ['field_rest_test' => [['value' => $this->randomString()]]], static::$format); // The URL and Guzzle request options that will be used in this test. The @@ -684,12 +684,13 @@ public function testPost() { $this->setUpAuthorization('POST'); - // 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; - $label_field_capitalized = $this->entity->getFieldDefinition($label_field)->getLabel(); - $this->assertResourceErrorResponse(422, "Unprocessable Entity: validation failed.\n$label_field: $label_field_capitalized: this field cannot hold more than 1 values.\n", $response); - + if ($this->entity instanceof FieldableEntityInterface) { + // 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; + $label_field_capitalized = $this->entity->getFieldDefinition($label_field) ->getLabel(); + $this->assertResourceErrorResponse(422, "Unprocessable Entity: validation failed.\n$label_field: $label_field_capitalized: this field cannot hold more than 1 values.\n", $response); + } $request_options[RequestOptions::BODY] = $parseable_invalid_request_body_2; @@ -698,16 +699,23 @@ public function testPost() { // @todo Fix this in https://www.drupal.org/node/2149851. if ($this->entity->getEntityType()->hasKey('uuid')) { $response = $this->request('POST', $url, $request_options); - $this->assertResourceErrorResponse(422, "Unprocessable Entity: validation failed.\nuuid.0.value: UUID: may not be longer than 128 characters.\n", $response); + if ($this->entity instanceof FieldableEntityInterface) { + $this->assertResourceErrorResponse(422, "Unprocessable Entity: validation failed.\nuuid.0.value: UUID: may not be longer than 128 characters.\n", $response); + } + else { + $this->assertResourceErrorResponse(422, "Unprocessable Entity: validation failed.\nuuid: UUID: may not be longer than 128 characters.\n", $response); + } } - $request_options[RequestOptions::BODY] = $parseable_invalid_request_body_3; + if ($this->entity instanceof FieldableEntityInterface) { + $request_options[RequestOptions::BODY] = $parseable_invalid_request_body_3; - // DX: 403 when entity contains field without 'edit' access. - $response = $this->request('POST', $url, $request_options); - $this->assertResourceErrorResponse(403, "Access denied on creating field 'field_rest_test'.", $response); + // DX: 403 when entity contains field without 'edit' access. + $response = $this->request('POST', $url, $request_options); + $this->assertResourceErrorResponse(403, "Access denied on creating field 'field_rest_test'.", $response); + } $request_options[RequestOptions::BODY] = $parseable_valid_request_body; diff --git a/core/modules/system/config/schema/system.schema.yml b/core/modules/system/config/schema/system.schema.yml index c23ed7e0ae..dce4e975bc 100644 --- a/core/modules/system/config/schema/system.schema.yml +++ b/core/modules/system/config/schema/system.schema.yml @@ -7,6 +7,8 @@ system.site: uuid: type: string label: 'Site UUID' + constraints: + NotNull: [] name: type: label label: 'Site name' diff --git a/core/modules/views/tests/src/Kernel/TestViewsTest.php b/core/modules/views/tests/src/Kernel/TestViewsTest.php index f9aa673421..dcd1859be3 100644 --- a/core/modules/views/tests/src/Kernel/TestViewsTest.php +++ b/core/modules/views/tests/src/Kernel/TestViewsTest.php @@ -34,7 +34,8 @@ public function testDefaultConfig() { \Drupal::service('config.storage'), new TestInstallStorage(InstallStorage::CONFIG_SCHEMA_DIRECTORY), \Drupal::service('cache.discovery'), - \Drupal::service('module_handler') + \Drupal::service('module_handler'), + \Drupal::service('class_resolver') ); // Create a configuration storage with access to default configuration in diff --git a/core/tests/Drupal/KernelTests/Config/TypedConfigTest.php b/core/tests/Drupal/KernelTests/Config/TypedConfigTest.php new file mode 100644 index 0000000000..f8042dfcfa --- /dev/null +++ b/core/tests/Drupal/KernelTests/Config/TypedConfigTest.php @@ -0,0 +1,126 @@ +installConfig('config_test'); + } + + /** + * Verifies that the Typed Data API is implemented correctly. + */ + public function testTypedDataAPI() { + /** @var \Drupal\Core\Config\TypedConfigManagerInterface $typed_config_manager */ + $typed_config_manager = \Drupal::service('config.typed'); + /** @var \Drupal\Core\Config\Schema\TypedConfigInterface $typed_config */ + $typed_config = $typed_config_manager->get('config_test.validation'); + + // Test a primitive. + $string_data = $typed_config->get('llama'); + $this->assertInstanceOf(StringInterface::class, $string_data); + $this->assertEquals('llama', $string_data->getValue()); + + // Test complex data. + $mapping = $typed_config->get('cat'); + /** @var \Drupal\Core\TypedData\ComplexDataInterface $mapping */ + $this->assertInstanceOf(ComplexDataInterface::class, $mapping); + $this->assertInstanceOf(StringInterface::class, $mapping->get('type')); + $this->assertEquals('kitten', $mapping->get('type')->getValue()); + $this->assertInstanceOf(IntegerInterface::class, $mapping->get('count')); + $this->assertEquals(2, $mapping->get('count')->getValue()); + // Verify the item metadata is available. + $this->assertInstanceOf(ComplexDataDefinitionInterface::class, $mapping->getDataDefinition()); + $this->assertArrayHasKey('type', $mapping->getProperties()); + $this->assertArrayHasKey('count', $mapping->getProperties()); + + // Test accessing sequences. + $sequence = $typed_config->get('giraffe'); + /** @var \Drupal\Core\TypedData\ListInterface $sequence */ + $this->assertInstanceOf(ComplexDataInterface::class, $sequence); + $this->assertInstanceOf(StringInterface::class, $sequence->get('hum1')); + $this->assertEquals('hum1', $sequence->get('hum1')->getValue()); + $this->assertEquals('hum2', $sequence->get('hum2')->getValue()); + $this->assertEquals(2, count($sequence->getIterator())); + // Verify the item metadata is available. + $this->assertInstanceOf(SequenceDataDefinition::class, $sequence->getDataDefinition()); + } + + /** + * Tests config validation via the typed data api. + */ + public function testSimpleConfigValidation() { + $config = \Drupal::configFactory()->getEditable('config_test.validation'); + /** @var \Drupal\Core\Config\TypedConfigManagerInterface $typed_config_manager */ + $typed_config_manager = \Drupal::service('config.typed'); + /** @var \Drupal\Core\Config\Schema\TypedConfigInterface $typed_config */ + $typed_config = $typed_config_manager->get('config_test.validation'); + + $result = $typed_config->validate(); + $this->assertInstanceOf(ConstraintViolationListInterface::class, $result); + $this->assertEmpty($result); + + // Test constraints on primitive types. + $config->set('llama', 'elephant'); + $config->save(); + + $typed_config = $typed_config_manager->get('config_test.validation'); + $result = $typed_config->validate(); + // Its not a valid llama anymore. + $this->assertCount(1, $result); + $this->assertEquals('no valid llama', $result->get(0)->getMessage()); + + // Test constraints on mapping. + $config->set('llama', 'llama'); + $config->set('cat.type', 'nyans'); + $config->save(); + + $typed_config = $typed_config_manager->get('config_test.validation'); + $result = $typed_config->validate(); + $this->assertEmpty($result); + + // Test constrains on nested mapping. + $config->set('cat.type', 'miaus'); + $config->save(); + + $typed_config = $typed_config_manager->get('config_test.validation'); + $result = $typed_config->validate(); + $this->assertCount(1, $result); + $this->assertEquals('no valid cat', $result->get(0)->getMessage()); + + // Test constrains on sequences. + $config->set('cat.type', 'nyans'); + $config->set('giraffe', ['muh', 'hum2']); + $config->save(); + + $typed_config = $typed_config_manager->get('config_test.validation'); + $result = $typed_config->validate(); + $this->assertCount(1, $result); + $this->assertEquals('Giraffes just hum', $result->get(0)->getMessage()); + } + +} diff --git a/core/tests/Drupal/KernelTests/Core/Config/ConfigSchemaTest.php b/core/tests/Drupal/KernelTests/Core/Config/ConfigSchemaTest.php index 96bd18c0a2..fb3573eec9 100644 --- a/core/tests/Drupal/KernelTests/Core/Config/ConfigSchemaTest.php +++ b/core/tests/Drupal/KernelTests/Core/Config/ConfigSchemaTest.php @@ -47,6 +47,7 @@ public function testSchemaMapping() { $expected['class'] = Undefined::class; $expected['type'] = 'undefined'; $expected['definition_class'] = '\Drupal\Core\TypedData\DataDefinition'; + $expected['unwrap_for_canonical_representation'] = TRUE; $this->assertEqual($definition, $expected, 'Retrieved the right metadata for nonexistent configuration.'); // Configuration file without schema will return Undefined as well. @@ -67,6 +68,7 @@ public function testSchemaMapping() { $expected['mapping']['testlist'] = ['label' => 'Test list']; $expected['type'] = 'config_schema_test.someschema'; $expected['definition_class'] = '\Drupal\Core\TypedData\MapDataDefinition'; + $expected['unwrap_for_canonical_representation'] = TRUE; $this->assertEqual($definition, $expected, 'Retrieved the right metadata for configuration with only some schema.'); // Check type detection on elements with undefined types. @@ -77,6 +79,7 @@ public function testSchemaMapping() { $expected['class'] = Undefined::class; $expected['type'] = 'undefined'; $expected['definition_class'] = '\Drupal\Core\TypedData\DataDefinition'; + $expected['unwrap_for_canonical_representation'] = TRUE; $this->assertEqual($definition, $expected, 'Automatic type detected for a scalar is undefined.'); $definition = $config->get('testlist')->getDataDefinition()->toArray(); $expected = []; @@ -84,6 +87,7 @@ public function testSchemaMapping() { $expected['class'] = Undefined::class; $expected['type'] = 'undefined'; $expected['definition_class'] = '\Drupal\Core\TypedData\DataDefinition'; + $expected['unwrap_for_canonical_representation'] = TRUE; $this->assertEqual($definition, $expected, 'Automatic type detected for a list is undefined.'); $definition = $config->get('testnoschema')->getDataDefinition()->toArray(); $expected = []; @@ -91,6 +95,7 @@ public function testSchemaMapping() { $expected['class'] = Undefined::class; $expected['type'] = 'undefined'; $expected['definition_class'] = '\Drupal\Core\TypedData\DataDefinition'; + $expected['unwrap_for_canonical_representation'] = TRUE; $this->assertEqual($definition, $expected, 'Automatic type detected for an undefined integer is undefined.'); // Simple case, straight metadata. @@ -109,6 +114,7 @@ public function testSchemaMapping() { $expected['mapping']['_core']['type'] = '_core_config_info'; $expected['type'] = 'system.maintenance'; $expected['definition_class'] = '\Drupal\Core\TypedData\MapDataDefinition'; + $expected['unwrap_for_canonical_representation'] = TRUE; $this->assertEqual($definition, $expected, 'Retrieved the right metadata for system.maintenance'); // Mixed schema with ignore elements. @@ -139,6 +145,7 @@ public function testSchemaMapping() { 'type' => 'integer', ]; $expected['type'] = 'config_schema_test.ignore'; + $expected['unwrap_for_canonical_representation'] = TRUE; $this->assertEqual($definition, $expected); @@ -149,6 +156,7 @@ public function testSchemaMapping() { $expected['label'] = 'Irrelevant'; $expected['class'] = Ignore::class; $expected['definition_class'] = '\Drupal\Core\TypedData\DataDefinition'; + $expected['unwrap_for_canonical_representation'] = TRUE; $this->assertEqual($definition, $expected); $definition = \Drupal::service('config.typed')->get('config_schema_test.ignore')->get('indescribable')->getDataDefinition()->toArray(); $expected['label'] = 'Indescribable'; @@ -160,6 +168,7 @@ public function testSchemaMapping() { $expected['label'] = 'Image style'; $expected['class'] = Mapping::class; $expected['definition_class'] = '\Drupal\Core\TypedData\MapDataDefinition'; + $expected['unwrap_for_canonical_representation'] = TRUE; $expected['mapping']['name']['type'] = 'string'; $expected['mapping']['uuid']['type'] = 'string'; $expected['mapping']['uuid']['label'] = 'UUID'; @@ -193,6 +202,7 @@ public function testSchemaMapping() { $expected['label'] = 'Image scale'; $expected['class'] = Mapping::class; $expected['definition_class'] = '\Drupal\Core\TypedData\MapDataDefinition'; + $expected['unwrap_for_canonical_representation'] = TRUE; $expected['mapping']['width']['type'] = 'integer'; $expected['mapping']['width']['label'] = 'Width'; $expected['mapping']['height']['type'] = 'integer'; @@ -220,6 +230,7 @@ public function testSchemaMapping() { $expected['label'] = 'Mapping'; $expected['class'] = Mapping::class; $expected['definition_class'] = '\Drupal\Core\TypedData\MapDataDefinition'; + $expected['unwrap_for_canonical_representation'] = TRUE; $expected['mapping'] = [ 'integer' => ['type' => 'integer'], 'string' => ['type' => 'string'], @@ -241,6 +252,7 @@ public function testSchemaMapping() { $expected['mapping']['testdescription']['label'] = 'Description'; $expected['type'] = 'config_schema_test.someschema.somemodule.*.*'; $expected['definition_class'] = '\Drupal\Core\TypedData\MapDataDefinition'; + $expected['unwrap_for_canonical_representation'] = TRUE; $this->assertEqual($definition, $expected, 'Retrieved the right metadata for config_schema_test.someschema.somemodule.section_one.subsection'); @@ -263,6 +275,7 @@ public function testSchemaMappingWithParents() { 'label' => 'Test item nested one level', 'class' => StringData::class, 'definition_class' => '\Drupal\Core\TypedData\DataDefinition', + 'unwrap_for_canonical_representation' => TRUE, ]; $this->assertEqual($definition, $expected); @@ -274,6 +287,7 @@ public function testSchemaMappingWithParents() { 'label' => 'Test item nested two levels', 'class' => StringData::class, 'definition_class' => '\Drupal\Core\TypedData\DataDefinition', + 'unwrap_for_canonical_representation' => TRUE, ]; $this->assertEqual($definition, $expected); @@ -285,6 +299,7 @@ public function testSchemaMappingWithParents() { 'label' => 'Test item nested three levels', 'class' => StringData::class, 'definition_class' => '\Drupal\Core\TypedData\DataDefinition', + 'unwrap_for_canonical_representation' => TRUE, ]; $this->assertEqual($definition, $expected); } @@ -475,6 +490,7 @@ public function testSchemaFallback() { $expected['label'] = 'Schema wildcard fallback test'; $expected['class'] = Mapping::class; $expected['definition_class'] = '\Drupal\Core\TypedData\MapDataDefinition'; + $expected['unwrap_for_canonical_representation'] = TRUE; $expected['mapping']['langcode']['type'] = 'string'; $expected['mapping']['langcode']['label'] = 'Language code'; $expected['mapping']['_core']['type'] = '_core_config_info'; diff --git a/core/tests/Drupal/KernelTests/Core/Entity/EntityTypedDataDefinitionTest.php b/core/tests/Drupal/KernelTests/Core/Entity/EntityTypedDataDefinitionTest.php index 9d5095d149..5e755d9584 100644 --- a/core/tests/Drupal/KernelTests/Core/Entity/EntityTypedDataDefinitionTest.php +++ b/core/tests/Drupal/KernelTests/Core/Entity/EntityTypedDataDefinitionTest.php @@ -101,8 +101,8 @@ public function testEntities() { // Test that the definition factory creates the right definitions for all // entity data types variants. - $this->assertEqual($this->typedDataManager->createDataDefinition('entity'), EntityDataDefinition::create()); - $this->assertEqual($this->typedDataManager->createDataDefinition('entity:node'), EntityDataDefinition::create('node')); + $this->assertEqual(serialize($this->typedDataManager->createDataDefinition('entity')), serialize(EntityDataDefinition::create())); + $this->assertEqual(serialize($this->typedDataManager->createDataDefinition('entity:node')), serialize(EntityDataDefinition::create('node'))); // Config entities don't support typed data. $entity_definition = EntityDataDefinition::create('node_type'); @@ -123,7 +123,7 @@ public function testEntityReferences() { // Test that the definition factory creates the right definition object. $reference_definition2 = $this->typedDataManager->createDataDefinition('entity_reference'); $this->assertTrue($reference_definition2 instanceof DataReferenceDefinitionInterface); - $this->assertEqual($reference_definition2, $reference_definition); + $this->assertEqual(serialize($reference_definition2), serialize($reference_definition)); } } diff --git a/core/tests/Drupal/KernelTests/Core/TypedData/TypedDataDefinitionTest.php b/core/tests/Drupal/KernelTests/Core/TypedData/TypedDataDefinitionTest.php index 8aeab575e9..ad266c2852 100644 --- a/core/tests/Drupal/KernelTests/Core/TypedData/TypedDataDefinitionTest.php +++ b/core/tests/Drupal/KernelTests/Core/TypedData/TypedDataDefinitionTest.php @@ -77,7 +77,7 @@ public function testMaps() { $map_definition2->setPropertyDefinition('one', DataDefinition::create('string')) ->setPropertyDefinition('two', DataDefinition::create('string')) ->setPropertyDefinition('three', DataDefinition::create('string')); - $this->assertEqual($map_definition, $map_definition2); + $this->assertEqual(serialize($map_definition), serialize($map_definition2)); } /** @@ -93,7 +93,7 @@ public function testDataReferences() { // Test using the definition factory. $language_reference_definition2 = $this->typedDataManager->createDataDefinition('language_reference'); $this->assertTrue($language_reference_definition2 instanceof DataReferenceDefinitionInterface); - $this->assertEqual($language_reference_definition, $language_reference_definition2); + $this->assertEqual(serialize($language_reference_definition), serialize($language_reference_definition2)); } }