Skip to content

Instantly share code, notes, and snippets.

@esolitos
Last active February 20, 2019 19:56
Show Gist options
  • Save esolitos/d14d423a310f139fa479a3290e75a7f8 to your computer and use it in GitHub Desktop.
Save esolitos/d14d423a310f139fa479a3290e75a7f8 to your computer and use it in GitHub Desktop.
SA-CORE-2019-003
diff --git a/core/lib/Drupal.php b/core/lib/Drupal.php
index 84dd672a93..04fe4a7871 100644
--- a/core/lib/Drupal.php
+++ b/core/lib/Drupal.php
@@ -82,7 +82,7 @@ class Drupal {
/**
* The current system version.
*/
- const VERSION = '8.5.10';
+ const VERSION = '8.5.11';
/**
* Core API compatibility.
diff --git a/core/lib/Drupal/Core/Access/CsrfRequestHeaderAccessCheck.php b/core/lib/Drupal/Core/Access/CsrfRequestHeaderAccessCheck.php
index 563355a6b7..c9266f5736 100644
--- a/core/lib/Drupal/Core/Access/CsrfRequestHeaderAccessCheck.php
+++ b/core/lib/Drupal/Core/Access/CsrfRequestHeaderAccessCheck.php
@@ -89,12 +89,15 @@ public function applies(Route $route) {
public function access(Request $request, AccountInterface $account) {
$method = $request->getMethod();
+ // Read-only operations are always allowed.
+ if (in_array($method, ['GET', 'HEAD', 'OPTIONS', 'TRACE'], TRUE)) {
+ return AccessResult::allowed();
+ }
+
// This check only applies if
- // 1. this is a write operation
- // 2. the user was successfully authenticated and
- // 3. the request comes with a session cookie.
- if (!in_array($method, ['GET', 'HEAD', 'OPTIONS', 'TRACE'])
- && $account->isAuthenticated()
+ // 1. the user was successfully authenticated and
+ // 2. the request comes with a session cookie.
+ if ($account->isAuthenticated()
&& $this->sessionConfiguration->hasSession($request)
) {
if (!$request->headers->has('X-CSRF-Token')) {
diff --git a/core/lib/Drupal/Core/Entity/EntityCreateAnyAccessCheck.php b/core/lib/Drupal/Core/Entity/EntityCreateAnyAccessCheck.php
index a6ff09430d..e0e24a2f9e 100644
--- a/core/lib/Drupal/Core/Entity/EntityCreateAnyAccessCheck.php
+++ b/core/lib/Drupal/Core/Entity/EntityCreateAnyAccessCheck.php
@@ -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;
+ }
}
}
diff --git a/core/lib/Drupal/Core/EventSubscriber/AuthenticationSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/AuthenticationSubscriber.php
index be3f55e4a3..9ab36f3e31 100644
--- a/core/lib/Drupal/Core/EventSubscriber/AuthenticationSubscriber.php
+++ b/core/lib/Drupal/Core/EventSubscriber/AuthenticationSubscriber.php
@@ -124,6 +124,21 @@ public function onExceptionSendChallenge(GetResponseForExceptionEvent $event) {
}
}
+ /**
+ * Detect disallowed authentication methods on access denied exceptions.
+ *
+ * @param \Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent $event
+ */
+ public function _onExceptionAccessDenied(GetResponseForExceptionEvent $event) {
+ if (isset($this->filter) && $event->isMasterRequest()) {
+ $request = $event->getRequest();
+ $exception = $event->getException();
+ if ($exception instanceof AccessDeniedHttpException && $this->authenticationProvider->applies($request) && !$this->filter->appliesToRoutedRequest($request, TRUE)) {
+ $event->setException(new AccessDeniedHttpException('The used authentication method is not allowed on this route.', $exception));
+ }
+ }
+ }
+
/**
* {@inheritdoc}
*/
@@ -137,6 +152,7 @@ public static function getSubscribedEvents() {
// Access check must be performed after routing.
$events[KernelEvents::REQUEST][] = ['onKernelRequestFilterProvider', 31];
$events[KernelEvents::EXCEPTION][] = ['onExceptionSendChallenge', 75];
+ $events[KernelEvents::EXCEPTION][] = ['_onExceptionAccessDenied', 80];
return $events;
}
diff --git a/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/MapItem.php b/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/MapItem.php
index e15fe848bf..5550ede34a 100644
--- a/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/MapItem.php
+++ b/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/MapItem.php
@@ -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);
+ }
}
}
diff --git a/core/lib/Drupal/Core/Routing/AccessAwareRouter.php b/core/lib/Drupal/Core/Routing/AccessAwareRouter.php
index d8487c6ef9..7607ffa29f 100644
--- a/core/lib/Drupal/Core/Routing/AccessAwareRouter.php
+++ b/core/lib/Drupal/Core/Routing/AccessAwareRouter.php
@@ -4,6 +4,8 @@
use Drupal\Core\Access\AccessManagerInterface;
use Drupal\Core\Access\AccessResultReasonInterface;
+use Drupal\Core\Cache\CacheableDependencyInterface;
+use Drupal\Core\Http\Exception\CacheableAccessDeniedHttpException;
use Drupal\Core\Session\AccountInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
@@ -111,7 +113,12 @@ protected function checkAccess(Request $request) {
$request->attributes->set(AccessAwareRouterInterface::ACCESS_RESULT, $access_result);
}
if (!$access_result->isAllowed()) {
- throw new AccessDeniedHttpException($access_result instanceof AccessResultReasonInterface ? $access_result->getReason() : NULL);
+ if ($access_result instanceof CacheableDependencyInterface && $request->isMethodCacheable()) {
+ throw new CacheableAccessDeniedHttpException($access_result, $access_result instanceof AccessResultReasonInterface ? $access_result->getReason() : NULL);
+ }
+ else {
+ throw new AccessDeniedHttpException($access_result instanceof AccessResultReasonInterface ? $access_result->getReason() : NULL);
+ }
}
}
diff --git a/core/modules/basic_auth/src/Authentication/Provider/BasicAuth.php b/core/modules/basic_auth/src/Authentication/Provider/BasicAuth.php
index 01034f4a8c..cc7c4949f4 100644
--- a/core/modules/basic_auth/src/Authentication/Provider/BasicAuth.php
+++ b/core/modules/basic_auth/src/Authentication/Provider/BasicAuth.php
@@ -12,6 +12,7 @@
use Drupal\Core\Http\Exception\CacheableUnauthorizedHttpException;
use Drupal\user\UserAuthInterface;
use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
/**
* HTTP Basic authentication provider.
@@ -155,7 +156,9 @@ public function challengeException(Request $request, \Exception $previous) {
$cacheability = CacheableMetadata::createFromObject($site_config)
->addCacheTags(['config:user.role.anonymous'])
->addCacheContexts(['user.roles:anonymous']);
- return new CacheableUnauthorizedHttpException($cacheability, (string) $challenge, 'No authentication credentials provided.', $previous);
+ return $request->isMethodCacheable()
+ ? new CacheableUnauthorizedHttpException($cacheability, (string) $challenge, 'No authentication credentials provided.', $previous)
+ : new UnauthorizedHttpException((string) $challenge, 'No authentication credentials provided.', $previous);
}
}
diff --git a/core/modules/block/src/BlockAccessControlHandler.php b/core/modules/block/src/BlockAccessControlHandler.php
index 35af61ed54..0ce8384788 100644
--- a/core/modules/block/src/BlockAccessControlHandler.php
+++ b/core/modules/block/src/BlockAccessControlHandler.php
@@ -128,7 +128,10 @@ protected function checkAccess(EntityInterface $entity, $operation, AccountInter
}
}
else {
- $access = AccessResult::forbidden();
+ $reason = count($conditions) > 1
+ ? "One of the block visibility conditions ('%s') denied access."
+ : "The block visibility condition '%s' denied access.";
+ $access = AccessResult::forbidden(sprintf($reason, implode("', '", array_keys($conditions))));
}
$this->mergeCacheabilityFromConditions($access, $conditions);
diff --git a/core/modules/block/tests/src/Functional/Rest/BlockResourceTestBase.php b/core/modules/block/tests/src/Functional/Rest/BlockResourceTestBase.php
index 998690caa8..a6af25805e 100644
--- a/core/modules/block/tests/src/Functional/Rest/BlockResourceTestBase.php
+++ b/core/modules/block/tests/src/Functional/Rest/BlockResourceTestBase.php
@@ -3,6 +3,7 @@
namespace Drupal\Tests\block\Functional\Rest;
use Drupal\block\Entity\Block;
+use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase;
abstract class BlockResourceTestBase extends EntityResourceTestBase {
@@ -135,7 +136,7 @@ protected function getExpectedUnauthorizedAccessMessage($method) {
switch ($method) {
case 'GET':
- return "You are not authorized to view this block entity.";
+ return "The block visibility condition 'user_role' denied access.";
default:
return parent::getExpectedUnauthorizedAccessMessage($method);
}
@@ -143,17 +144,25 @@ protected function getExpectedUnauthorizedAccessMessage($method) {
/**
* {@inheritdoc}
+ *
+ * @todo Fix this in https://www.drupal.org/node/2820315.
*/
protected function getExpectedUnauthorizedAccessCacheability() {
+ return (new CacheableMetadata())
+ ->setCacheTags(['4xx-response', 'http_response'])
+ ->setCacheContexts(['user.roles']);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function getExpectedUnauthorizedEntityAccessCacheability($is_authenticated) {
// @see \Drupal\block\BlockAccessControlHandler::checkAccess()
- return parent::getExpectedUnauthorizedAccessCacheability()
- ->setCacheTags([
- '4xx-response',
+ return parent::getExpectedUnauthorizedEntityAccessCacheability($is_authenticated)
+ ->addCacheTags([
'config:block.block.llama',
- 'http_response',
- static::$auth ? 'user:2' : 'user:0',
- ])
- ->setCacheContexts(['user.roles']);
+ $is_authenticated ? 'user:2' : 'user:0',
+ ]);
}
}
diff --git a/core/modules/block_content/src/BlockContentAccessControlHandler.php b/core/modules/block_content/src/BlockContentAccessControlHandler.php
index 7079ef4849..8112a0e001 100644
--- a/core/modules/block_content/src/BlockContentAccessControlHandler.php
+++ b/core/modules/block_content/src/BlockContentAccessControlHandler.php
@@ -22,7 +22,7 @@ protected function checkAccess(EntityInterface $entity, $operation, AccountInter
return AccessResult::allowedIf($entity->isPublished())->addCacheableDependency($entity)
->orIf(AccessResult::allowedIfHasPermission($account, 'administer blocks'));
}
- return parent::checkAccess($entity, $operation, $account);
+ return parent::checkAccess($entity, $operation, $account)->addCacheableDependency($entity);
}
}
diff --git a/core/modules/block_content/tests/src/Functional/Rest/BlockContentResourceTestBase.php b/core/modules/block_content/tests/src/Functional/Rest/BlockContentResourceTestBase.php
index 2bad7215d8..0017a8b432 100644
--- a/core/modules/block_content/tests/src/Functional/Rest/BlockContentResourceTestBase.php
+++ b/core/modules/block_content/tests/src/Functional/Rest/BlockContentResourceTestBase.php
@@ -176,9 +176,9 @@ protected function getExpectedUnauthorizedAccessMessage($method) {
/**
* {@inheritdoc}
*/
- protected function getExpectedUnauthorizedAccessCacheability() {
+ protected function getExpectedUnauthorizedEntityAccessCacheability($is_authenticated) {
// @see \Drupal\block_content\BlockContentAccessControlHandler()
- return parent::getExpectedUnauthorizedAccessCacheability()
+ return parent::getExpectedUnauthorizedEntityAccessCacheability($is_authenticated)
->addCacheTags(['block_content:1']);
}
diff --git a/core/modules/comment/tests/src/Functional/Rest/CommentResourceTestBase.php b/core/modules/comment/tests/src/Functional/Rest/CommentResourceTestBase.php
index f25567dd28..9c2f84408e 100644
--- a/core/modules/comment/tests/src/Functional/Rest/CommentResourceTestBase.php
+++ b/core/modules/comment/tests/src/Functional/Rest/CommentResourceTestBase.php
@@ -335,8 +335,11 @@ protected function getExpectedUnauthorizedAccessMessage($method) {
return "The 'access comments' permission is required and the comment must be published.";
case 'POST';
return "The 'post comments' permission is required.";
- default:
- return parent::getExpectedUnauthorizedAccessMessage($method);
+ case 'PATCH';
+ case 'DELETE':
+ // \Drupal\comment\CommentAccessControlHandler::checkAccess() does not
+ // specify a reason for not allowing a comment to be updated or deleted.
+ return '';
}
}
@@ -376,9 +379,9 @@ public function testPostSkipCommentApproval() {
/**
* {@inheritdoc}
*/
- protected function getExpectedUnauthorizedAccessCacheability() {
+ protected function getExpectedUnauthorizedEntityAccessCacheability($is_authenticated) {
// @see \Drupal\comment\CommentAccessControlHandler::checkAccess()
- return parent::getExpectedUnauthorizedAccessCacheability()
+ return parent::getExpectedUnauthorizedEntityAccessCacheability($is_authenticated)
->addCacheTags(['comment:1']);
}
diff --git a/core/modules/config/tests/config_test/tests/src/Functional/Rest/ConfigTestResourceTestBase.php b/core/modules/config/tests/config_test/tests/src/Functional/Rest/ConfigTestResourceTestBase.php
index 9ab65d76ae..dbd71b7778 100644
--- a/core/modules/config/tests/config_test/tests/src/Functional/Rest/ConfigTestResourceTestBase.php
+++ b/core/modules/config/tests/config_test/tests/src/Functional/Rest/ConfigTestResourceTestBase.php
@@ -70,4 +70,20 @@ protected function getNormalizedPostEntity() {
// @todo Update in https://www.drupal.org/node/2300677.
}
+ /**
+ * {@inheritdoc}
+ */
+ protected function getExpectedUnauthorizedAccessMessage($method) {
+ if ($this->config('rest.settings')->get('bc_entity_resource_permissions')) {
+ return parent::getExpectedUnauthorizedAccessMessage($method);
+ }
+
+ switch ($method) {
+ case 'GET':
+ return "The 'view config_test' permission is required.";
+ default:
+ return parent::getExpectedUnauthorizedAccessMessage($method);
+ }
+ }
+
}
diff --git a/core/modules/dblog/tests/src/Functional/DbLogResourceTest.php b/core/modules/dblog/tests/src/Functional/DbLogResourceTest.php
index 0d4630318e..904cd2919d 100644
--- a/core/modules/dblog/tests/src/Functional/DbLogResourceTest.php
+++ b/core/modules/dblog/tests/src/Functional/DbLogResourceTest.php
@@ -66,7 +66,7 @@ public function testWatchdog() {
$request_options = $this->getAuthenticationRequestOptions('GET');
$response = $this->request('GET', $url, $request_options);
- $this->assertResourceErrorResponse(403, "The 'restful get dblog' permission is required.", $response);
+ $this->assertResourceErrorResponse(403, "The 'restful get dblog' permission is required.", $response, ['4xx-response', 'http_response'], ['user.permissions'], FALSE, FALSE);
// Create a user account that has the required permissions to read
// the watchdog resource via the REST API.
diff --git a/core/modules/file/tests/src/Functional/Rest/FileResourceTestBase.php b/core/modules/file/tests/src/Functional/Rest/FileResourceTestBase.php
index a274a5f61e..3eb6b367cd 100644
--- a/core/modules/file/tests/src/Functional/Rest/FileResourceTestBase.php
+++ b/core/modules/file/tests/src/Functional/Rest/FileResourceTestBase.php
@@ -213,6 +213,9 @@ public function testPost() {
*/
protected function getExpectedUnauthorizedAccessMessage($method) {
if ($this->config('rest.settings')->get('bc_entity_resource_permissions')) {
+ if ($method === 'DELETE') {
+ return '';
+ }
return parent::getExpectedUnauthorizedAccessMessage($method);
}
@@ -220,7 +223,10 @@ protected function getExpectedUnauthorizedAccessMessage($method) {
return "The 'access content' permission is required.";
}
if ($method === 'PATCH') {
- return 'You are not authorized to update this file entity.';
+ return '';
+ }
+ if ($method === 'DELETE') {
+ return '';
}
return parent::getExpectedUnauthorizedAccessMessage($method);
}
diff --git a/core/modules/hal/src/Normalizer/FieldItemNormalizer.php b/core/modules/hal/src/Normalizer/FieldItemNormalizer.php
index 6d10b06f32..b2654d84ea 100644
--- a/core/modules/hal/src/Normalizer/FieldItemNormalizer.php
+++ b/core/modules/hal/src/Normalizer/FieldItemNormalizer.php
@@ -4,6 +4,7 @@
use Drupal\Core\Field\FieldItemInterface;
use Drupal\Core\TypedData\TypedDataInternalPropertiesHelper;
+use Drupal\serialization\Normalizer\SerializedColumnNormalizerTrait;
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
/**
@@ -11,6 +12,8 @@
*/
class FieldItemNormalizer extends NormalizerBase {
+ use SerializedColumnNormalizerTrait;
+
/**
* The interface or class that this Normalizer supports.
*
@@ -44,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'])) {
@@ -71,6 +75,19 @@ public function denormalize($data, $class, $format = NULL, array $context = [])
* The value to use in Entity::setValue().
*/
protected function constructValue($data, $context) {
+ /** @var \Drupal\Core\Field\FieldItemInterface $field_item */
+ $field_item = $context['target_instance'];
+ $serialized_property_names = $this->getCustomSerializedPropertyNames($field_item);
+
+ // Explicitly serialize the input, unlike properties that rely on
+ // being automatically serialized, manually managed serialized properties
+ // expect to receive serialized input.
+ foreach ($serialized_property_names as $serialized_property_name) {
+ if (!empty($data[$serialized_property_name])) {
+ $data[$serialized_property_name] = serialize($data[$serialized_property_name]);
+ }
+ }
+
return $data;
}
diff --git a/core/modules/hal/tests/src/Kernel/DenormalizeTest.php b/core/modules/hal/tests/src/Kernel/DenormalizeTest.php
index 87eb97329f..fde694fa69 100644
--- a/core/modules/hal/tests/src/Kernel/DenormalizeTest.php
+++ b/core/modules/hal/tests/src/Kernel/DenormalizeTest.php
@@ -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;
diff --git a/core/modules/language/tests/src/Functional/Hal/ConfigurableLanguageHalJsonBasicAuthTest.php b/core/modules/language/tests/src/Functional/Hal/ConfigurableLanguageHalJsonBasicAuthTest.php
index 3239b7e22c..4647acb82a 100644
--- a/core/modules/language/tests/src/Functional/Hal/ConfigurableLanguageHalJsonBasicAuthTest.php
+++ b/core/modules/language/tests/src/Functional/Hal/ConfigurableLanguageHalJsonBasicAuthTest.php
@@ -3,14 +3,14 @@
namespace Drupal\Tests\language\Functional\Hal;
use Drupal\Tests\language\Functional\Rest\ConfigurableLanguageResourceTestBase;
-use Drupal\Tests\rest\Functional\BasicAuthResourceWithInterfaceTranslationTestTrait;
+use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait;
/**
* @group hal
*/
class ConfigurableLanguageHalJsonBasicAuthTest extends ConfigurableLanguageResourceTestBase {
- use BasicAuthResourceWithInterfaceTranslationTestTrait;
+ use BasicAuthResourceTestTrait;
/**
* {@inheritdoc}
diff --git a/core/modules/language/tests/src/Functional/Hal/ContentLanguageSettingsHalJsonBasicAuthTest.php b/core/modules/language/tests/src/Functional/Hal/ContentLanguageSettingsHalJsonBasicAuthTest.php
index 61306eaf5e..fefd0db73a 100644
--- a/core/modules/language/tests/src/Functional/Hal/ContentLanguageSettingsHalJsonBasicAuthTest.php
+++ b/core/modules/language/tests/src/Functional/Hal/ContentLanguageSettingsHalJsonBasicAuthTest.php
@@ -3,14 +3,14 @@
namespace Drupal\Tests\language\Functional\Hal;
use Drupal\Tests\language\Functional\Rest\ContentLanguageSettingsResourceTestBase;
-use Drupal\Tests\rest\Functional\BasicAuthResourceWithInterfaceTranslationTestTrait;
+use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait;
/**
* @group hal
*/
class ContentLanguageSettingsHalJsonBasicAuthTest extends ContentLanguageSettingsResourceTestBase {
- use BasicAuthResourceWithInterfaceTranslationTestTrait;
+ use BasicAuthResourceTestTrait;
/**
* {@inheritdoc}
diff --git a/core/modules/language/tests/src/Functional/Rest/ConfigurableLanguageJsonBasicAuthTest.php b/core/modules/language/tests/src/Functional/Rest/ConfigurableLanguageJsonBasicAuthTest.php
index 847e781640..5352c9a951 100644
--- a/core/modules/language/tests/src/Functional/Rest/ConfigurableLanguageJsonBasicAuthTest.php
+++ b/core/modules/language/tests/src/Functional/Rest/ConfigurableLanguageJsonBasicAuthTest.php
@@ -2,14 +2,14 @@
namespace Drupal\Tests\language\Functional\Rest;
-use Drupal\Tests\rest\Functional\BasicAuthResourceWithInterfaceTranslationTestTrait;
+use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait;
/**
* @group rest
*/
class ConfigurableLanguageJsonBasicAuthTest extends ConfigurableLanguageResourceTestBase {
- use BasicAuthResourceWithInterfaceTranslationTestTrait;
+ use BasicAuthResourceTestTrait;
/**
* {@inheritdoc}
diff --git a/core/modules/language/tests/src/Functional/Rest/ConfigurableLanguageXmlBasicAuthTest.php b/core/modules/language/tests/src/Functional/Rest/ConfigurableLanguageXmlBasicAuthTest.php
index 7a79c4d433..afc017f7a0 100644
--- a/core/modules/language/tests/src/Functional/Rest/ConfigurableLanguageXmlBasicAuthTest.php
+++ b/core/modules/language/tests/src/Functional/Rest/ConfigurableLanguageXmlBasicAuthTest.php
@@ -2,7 +2,7 @@
namespace Drupal\Tests\language\Functional\Rest;
-use Drupal\Tests\rest\Functional\BasicAuthResourceWithInterfaceTranslationTestTrait;
+use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait;
use Drupal\Tests\rest\Functional\EntityResource\XmlEntityNormalizationQuirksTrait;
/**
@@ -10,7 +10,7 @@
*/
class ConfigurableLanguageXmlBasicAuthTest extends ConfigurableLanguageResourceTestBase {
- use BasicAuthResourceWithInterfaceTranslationTestTrait;
+ use BasicAuthResourceTestTrait;
use XmlEntityNormalizationQuirksTrait;
/**
diff --git a/core/modules/language/tests/src/Functional/Rest/ContentLanguageSettingsJsonBasicAuthTest.php b/core/modules/language/tests/src/Functional/Rest/ContentLanguageSettingsJsonBasicAuthTest.php
index 14437a9092..f1063a3dc1 100644
--- a/core/modules/language/tests/src/Functional/Rest/ContentLanguageSettingsJsonBasicAuthTest.php
+++ b/core/modules/language/tests/src/Functional/Rest/ContentLanguageSettingsJsonBasicAuthTest.php
@@ -2,14 +2,14 @@
namespace Drupal\Tests\language\Functional\Rest;
-use Drupal\Tests\rest\Functional\BasicAuthResourceWithInterfaceTranslationTestTrait;
+use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait;
/**
* @group rest
*/
class ContentLanguageSettingsJsonBasicAuthTest extends ContentLanguageSettingsResourceTestBase {
- use BasicAuthResourceWithInterfaceTranslationTestTrait;
+ use BasicAuthResourceTestTrait;
/**
* {@inheritdoc}
diff --git a/core/modules/language/tests/src/Functional/Rest/ContentLanguageSettingsXmlBasicAuthTest.php b/core/modules/language/tests/src/Functional/Rest/ContentLanguageSettingsXmlBasicAuthTest.php
index 741e75bcd3..dee7ad27d4 100644
--- a/core/modules/language/tests/src/Functional/Rest/ContentLanguageSettingsXmlBasicAuthTest.php
+++ b/core/modules/language/tests/src/Functional/Rest/ContentLanguageSettingsXmlBasicAuthTest.php
@@ -2,7 +2,7 @@
namespace Drupal\Tests\language\Functional\Rest;
-use Drupal\Tests\rest\Functional\BasicAuthResourceWithInterfaceTranslationTestTrait;
+use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait;
use Drupal\Tests\rest\Functional\EntityResource\XmlEntityNormalizationQuirksTrait;
/**
@@ -10,7 +10,7 @@
*/
class ContentLanguageSettingsXmlBasicAuthTest extends ContentLanguageSettingsResourceTestBase {
- use BasicAuthResourceWithInterfaceTranslationTestTrait;
+ use BasicAuthResourceTestTrait;
use XmlEntityNormalizationQuirksTrait;
/**
diff --git a/core/modules/link/src/Plugin/Field/FieldType/LinkItem.php b/core/modules/link/src/Plugin/Field/FieldType/LinkItem.php
index 5d772a98ce..160ce1998c 100644
--- a/core/modules/link/src/Plugin/Field/FieldType/LinkItem.php
+++ b/core/modules/link/src/Plugin/Field/FieldType/LinkItem.php
@@ -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);
}
diff --git a/core/modules/media/src/MediaAccessControlHandler.php b/core/modules/media/src/MediaAccessControlHandler.php
index d4203728e5..a665e5d4df 100644
--- a/core/modules/media/src/MediaAccessControlHandler.php
+++ b/core/modules/media/src/MediaAccessControlHandler.php
@@ -47,7 +47,7 @@ protected function checkAccess(EntityInterface $entity, $operation, AccountInter
if ($account->hasPermission('update media') && $is_owner) {
return AccessResult::allowed()->cachePerPermissions()->cachePerUser()->addCacheableDependency($entity);
}
- return AccessResult::neutral()->cachePerPermissions();
+ return AccessResult::neutral("The following permissions are required: 'update any media' OR 'update own media' OR '$type: edit any media' OR '$type: edit own media'.")->cachePerPermissions();
case 'delete':
if ($account->hasPermission('delete any ' . $type . ' media')) {
@@ -64,7 +64,7 @@ protected function checkAccess(EntityInterface $entity, $operation, AccountInter
if ($account->hasPermission('delete media') && $is_owner) {
return AccessResult::allowed()->cachePerPermissions()->cachePerUser()->addCacheableDependency($entity);
}
- return AccessResult::neutral()->cachePerPermissions();
+ return AccessResult::neutral("The following permissions are required: 'delete any media' OR 'delete own media' OR '$type: delete any media' OR '$type: delete own media'.")->cachePerPermissions();
default:
return AccessResult::neutral()->cachePerPermissions();
diff --git a/core/modules/media/tests/src/Functional/Rest/MediaResourceTestBase.php b/core/modules/media/tests/src/Functional/Rest/MediaResourceTestBase.php
index 4ca38abc49..7e9ae8db0c 100644
--- a/core/modules/media/tests/src/Functional/Rest/MediaResourceTestBase.php
+++ b/core/modules/media/tests/src/Functional/Rest/MediaResourceTestBase.php
@@ -246,10 +246,10 @@ protected function getExpectedUnauthorizedAccessMessage($method) {
return "The 'view media' permission is required and the media item must be published.";
case 'PATCH':
- return 'You are not authorized to update this media entity of bundle camelids.';
+ return "The following permissions are required: 'update any media' OR 'update own media' OR 'camelids: edit any media' OR 'camelids: edit own media'.";
case 'DELETE':
- return 'You are not authorized to delete this media entity of bundle camelids.';
+ return "The following permissions are required: 'delete any media' OR 'delete own media' OR 'camelids: delete any media' OR 'camelids: delete own media'.";
default:
return parent::getExpectedUnauthorizedAccessMessage($method);
@@ -266,9 +266,9 @@ public function testPost() {
/**
* {@inheritdoc}
*/
- protected function getExpectedUnauthorizedAccessCacheability() {
+ protected function getExpectedUnauthorizedEntityAccessCacheability($is_authenticated) {
// @see \Drupal\media\MediaAccessControlHandler::checkAccess()
- return parent::getExpectedUnauthorizedAccessCacheability()
+ return parent::getExpectedUnauthorizedEntityAccessCacheability($is_authenticated)
->addCacheTags(['media:1']);
}
diff --git a/core/modules/menu_link_content/src/MenuLinkContentAccessControlHandler.php b/core/modules/menu_link_content/src/MenuLinkContentAccessControlHandler.php
index eadf04532c..b663f27071 100644
--- a/core/modules/menu_link_content/src/MenuLinkContentAccessControlHandler.php
+++ b/core/modules/menu_link_content/src/MenuLinkContentAccessControlHandler.php
@@ -72,7 +72,8 @@ protected function checkAccess(EntityInterface $entity, $operation, AccountInter
}
case 'delete':
- return AccessResult::allowedIf(!$entity->isNew() && $account->hasPermission('administer menu'))->cachePerPermissions()->addCacheableDependency($entity);
+ return AccessResult::allowedIfHasPermission($account, 'administer menu')
+ ->andIf(AccessResult::allowedIf(!$entity->isNew())->addCacheableDependency($entity));
}
}
diff --git a/core/modules/menu_link_content/tests/src/Functional/Rest/MenuLinkContentResourceTestBase.php b/core/modules/menu_link_content/tests/src/Functional/Rest/MenuLinkContentResourceTestBase.php
index abfb356308..144e9d64ac 100644
--- a/core/modules/menu_link_content/tests/src/Functional/Rest/MenuLinkContentResourceTestBase.php
+++ b/core/modules/menu_link_content/tests/src/Functional/Rest/MenuLinkContentResourceTestBase.php
@@ -185,7 +185,7 @@ protected function getExpectedUnauthorizedAccessMessage($method) {
switch ($method) {
case 'DELETE':
- return "You are not authorized to delete this menu_link_content entity.";
+ return "The 'administer menu' permission is required.";
default:
return parent::getExpectedUnauthorizedAccessMessage($method);
}
diff --git a/core/modules/node/src/NodeAccessControlHandler.php b/core/modules/node/src/NodeAccessControlHandler.php
index a8a0bc6241..b0ac52088a 100644
--- a/core/modules/node/src/NodeAccessControlHandler.php
+++ b/core/modules/node/src/NodeAccessControlHandler.php
@@ -81,7 +81,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();
}
diff --git a/core/modules/node/tests/src/Functional/Rest/NodeResourceTestBase.php b/core/modules/node/tests/src/Functional/Rest/NodeResourceTestBase.php
index da80d27d8d..4682f050f5 100644
--- a/core/modules/node/tests/src/Functional/Rest/NodeResourceTestBase.php
+++ b/core/modules/node/tests/src/Functional/Rest/NodeResourceTestBase.php
@@ -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);
diff --git a/core/modules/rest/rest.post_update.php b/core/modules/rest/rest.post_update.php
index 6d45178804..c15d270495 100644
--- a/core/modules/rest/rest.post_update.php
+++ b/core/modules/rest/rest.post_update.php
@@ -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.
+}
diff --git a/core/modules/rest/src/Plugin/rest/resource/EntityResource.php b/core/modules/rest/src/Plugin/rest/resource/EntityResource.php
index 8b1c2fa979..d9a5619f95 100644
--- a/core/modules/rest/src/Plugin/rest/resource/EntityResource.php
+++ b/core/modules/rest/src/Plugin/rest/resource/EntityResource.php
@@ -12,12 +12,13 @@
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityStorageException;
use Drupal\Core\Field\FieldItemListInterface;
-use Drupal\Core\Http\Exception\CacheableAccessDeniedHttpException;
+use Drupal\Core\Routing\AccessAwareRouterInterface;
use Drupal\rest\Plugin\ResourceBase;
use Drupal\rest\ResourceResponse;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\rest\ModifiedResourceResponse;
+use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
@@ -120,14 +121,11 @@ public static function create(ContainerInterface $container, array $configuratio
* @throws \Symfony\Component\HttpKernel\Exception\HttpException
*/
public function get(EntityInterface $entity) {
- $entity_access = $entity->access('view', NULL, TRUE);
- if (!$entity_access->isAllowed()) {
- throw new CacheableAccessDeniedHttpException($entity_access, $entity_access->getReason() ?: $this->generateFallbackAccessDeniedMessage($entity, 'view'));
- }
-
+ $request = \Drupal::request();
$response = new ResourceResponse($entity, 200);
+ // @todo Either remove the line below or remove this todo in https://www.drupal.org/project/drupal/issues/2973356
+ $response->addCacheableDependency($request->attributes->get(AccessAwareRouterInterface::ACCESS_RESULT));
$response->addCacheableDependency($entity);
- $response->addCacheableDependency($entity_access);
if ($entity instanceof FieldableEntityInterface) {
foreach ($entity as $field_name => $field) {
@@ -222,10 +220,6 @@ public function patch(EntityInterface $original_entity, EntityInterface $entity
if ($entity->getEntityTypeId() != $definition['entity_type']) {
throw new BadRequestHttpException('Invalid entity type');
}
- $entity_access = $original_entity->access('update', NULL, TRUE);
- if (!$entity_access->isAllowed()) {
- throw new AccessDeniedHttpException($entity_access->getReason() ?: $this->generateFallbackAccessDeniedMessage($entity, 'update'));
- }
// Overwrite the received fields.
foreach ($entity->_restSubmittedFields as $field_name) {
@@ -310,10 +304,6 @@ protected function checkPatchFieldAccess(FieldItemListInterface $original_field,
* @throws \Symfony\Component\HttpKernel\Exception\HttpException
*/
public function delete(EntityInterface $entity) {
- $entity_access = $entity->access('delete', NULL, TRUE);
- if (!$entity_access->isAllowed()) {
- throw new AccessDeniedHttpException($entity_access->getReason() ?: $this->generateFallbackAccessDeniedMessage($entity, 'delete'));
- }
try {
$entity->delete();
$this->logger->notice('Deleted entity %type with ID %id.', ['%type' => $entity->getEntityTypeId(), '%id' => $entity->id()]);
@@ -366,6 +356,23 @@ public function permissions() {
*/
protected function getBaseRoute($canonical_path, $method) {
$route = parent::getBaseRoute($canonical_path, $method);
+
+ switch ($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;
+ case 'DELETE':
+ $route->setRequirement('_entity_access', $this->entityType->id() . '.delete');
+ break;
+ }
+
$definition = $this->getPluginDefinition();
$parameters = $route->getOption('parameters') ?: [];
diff --git a/core/modules/rest/tests/modules/config_test_rest/config_test_rest.module b/core/modules/rest/tests/modules/config_test_rest/config_test_rest.module
index fcd9979a11..aa9fababb0 100644
--- a/core/modules/rest/tests/modules/config_test_rest/config_test_rest.module
+++ b/core/modules/rest/tests/modules/config_test_rest/config_test_rest.module
@@ -6,6 +6,7 @@
*/
use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Access\AccessResultReasonInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Session\AccountInterface;
@@ -26,5 +27,9 @@ function config_test_rest_config_test_access(EntityInterface $entity, $operation
// Add permission, so that EntityResourceTestBase's scenarios can test access
// being denied. By default, all access is always allowed for the config_test
// config entity.
- return AccessResult::forbiddenIf(!$account->hasPermission('view config_test'))->cachePerPermissions();
+ $access_result = AccessResult::forbiddenIf(!$account->hasPermission('view config_test'))->cachePerPermissions();
+ if (!$access_result->isAllowed() && $access_result instanceof AccessResultReasonInterface) {
+ $access_result->setReason("The 'view config_test' permission is required.");
+ }
+ return $access_result;
}
diff --git a/core/modules/rest/tests/src/Functional/BasicAuthResourceTestTrait.php b/core/modules/rest/tests/src/Functional/BasicAuthResourceTestTrait.php
index e4dcd70ff6..4a58254db1 100644
--- a/core/modules/rest/tests/src/Functional/BasicAuthResourceTestTrait.php
+++ b/core/modules/rest/tests/src/Functional/BasicAuthResourceTestTrait.php
@@ -14,8 +14,6 @@
* authenticated, a 401 response must be sent.
* - Because every request must send an authorization, there is no danger of
* CSRF attacks.
- *
- * @see \Drupal\Tests\rest\Functional\BasicAuthResourceWithInterfaceTranslationTestTrait
*/
trait BasicAuthResourceTestTrait {
@@ -34,10 +32,23 @@ protected function getAuthenticationRequestOptions($method) {
* {@inheritdoc}
*/
protected function assertResponseWhenMissingAuthentication($method, ResponseInterface $response) {
+ if ($method !== 'GET') {
+ return $this->assertResourceErrorResponse(401, 'No authentication credentials provided.', $response);
+ }
+
$expected_page_cache_header_value = $method === 'GET' ? 'MISS' : FALSE;
- // @see \Drupal\basic_auth\Authentication\Provider\BasicAuth::challengeException()
- $expected_dynamic_page_cache_header_value = $expected_page_cache_header_value;
- $this->assertResourceErrorResponse(401, 'No authentication credentials provided.', $response, ['4xx-response', 'config:system.site', 'config:user.role.anonymous', 'http_response'], ['user.roles:anonymous'], $expected_page_cache_header_value, $expected_dynamic_page_cache_header_value);
+ $expected_cacheability = $this->getExpectedUnauthorizedAccessCacheability()
+ ->addCacheableDependency($this->getExpectedUnauthorizedEntityAccessCacheability(FALSE))
+ // @see \Drupal\basic_auth\Authentication\Provider\BasicAuth::challengeException()
+ ->addCacheableDependency($this->config('system.site'))
+ // @see \Drupal\Core\EventSubscriber\AnonymousUserResponseSubscriber::onRespond()
+ ->addCacheTags(['config:user.role.anonymous']);
+ // Only add the 'user.roles:anonymous' cache context if its parent cache
+ // context is not already present.
+ if (!in_array('user.roles', $expected_cacheability->getCacheContexts(), TRUE)) {
+ $expected_cacheability->addCacheContexts(['user.roles:anonymous']);
+ }
+ $this->assertResourceErrorResponse(401, 'No authentication credentials provided.', $response, $expected_cacheability->getCacheTags(), $expected_cacheability->getCacheContexts(), $expected_page_cache_header_value, FALSE);
}
/**
diff --git a/core/modules/rest/tests/src/Functional/BasicAuthResourceWithInterfaceTranslationTestTrait.php b/core/modules/rest/tests/src/Functional/BasicAuthResourceWithInterfaceTranslationTestTrait.php
deleted file mode 100644
index 37b8381eae..0000000000
--- a/core/modules/rest/tests/src/Functional/BasicAuthResourceWithInterfaceTranslationTestTrait.php
+++ /dev/null
@@ -1,28 +0,0 @@
-<?php
-
-namespace Drupal\Tests\rest\Functional;
-
-use Psr\Http\Message\ResponseInterface;
-
-/**
- * Trait for ResourceTestBase subclasses testing $auth=basic_auth + 'language'.
- *
- * @see \Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait
- */
-trait BasicAuthResourceWithInterfaceTranslationTestTrait {
-
- use BasicAuthResourceTestTrait;
-
- /**
- * {@inheritdoc}
- */
- protected function assertResponseWhenMissingAuthentication($method, ResponseInterface $response) {
- // Because BasicAuth::challengeException() relies on the 'system.site'
- // configuration, and this test installs the 'language' module, all config
- // may be translated and therefore gets the 'languages:language_interface'
- // cache context.
- $expected_page_cache_header_value = $method === 'GET' ? 'MISS' : FALSE;
- $this->assertResourceErrorResponse(401, 'No authentication credentials provided.', $response, ['4xx-response', 'config:system.site', 'config:user.role.anonymous', 'http_response'], ['languages:language_interface', 'user.roles:anonymous'], $expected_page_cache_header_value, $expected_page_cache_header_value);
- }
-
-}
diff --git a/core/modules/rest/tests/src/Functional/CookieResourceTestTrait.php b/core/modules/rest/tests/src/Functional/CookieResourceTestTrait.php
index 7ce381b889..2d25b946a5 100644
--- a/core/modules/rest/tests/src/Functional/CookieResourceTestTrait.php
+++ b/core/modules/rest/tests/src/Functional/CookieResourceTestTrait.php
@@ -99,7 +99,9 @@ protected function assertResponseWhenMissingAuthentication($method, ResponseInte
// @see \Drupal\user\Authentication\Provider\Cookie
// @todo https://www.drupal.org/node/2847623
if ($method === 'GET') {
- $expected_cookie_403_cacheability = $this->getExpectedUnauthorizedAccessCacheability();
+ $expected_cookie_403_cacheability = $this->getExpectedUnauthorizedAccessCacheability()
+ // @see \Drupal\Core\EventSubscriber\AnonymousUserResponseSubscriber::onRespond()
+ ->addCacheableDependency($this->getExpectedUnauthorizedEntityAccessCacheability(FALSE));
// - \Drupal\Core\EventSubscriber\AnonymousUserResponseSubscriber applies
// to cacheable anonymous responses: it updates their cacheability.
// - A 403 response to a GET request is cacheable.
@@ -111,7 +113,7 @@ protected function assertResponseWhenMissingAuthentication($method, ResponseInte
if (static::$entityTypeId === 'block') {
$expected_cookie_403_cacheability->setCacheTags(str_replace('user:2', 'user:0', $expected_cookie_403_cacheability->getCacheTags()));
}
- $this->assertResourceErrorResponse(403, FALSE, $response, $expected_cookie_403_cacheability->getCacheTags(), $expected_cookie_403_cacheability->getCacheContexts(), 'MISS', 'MISS');
+ $this->assertResourceErrorResponse(403, FALSE, $response, $expected_cookie_403_cacheability->getCacheTags(), $expected_cookie_403_cacheability->getCacheContexts(), 'MISS', FALSE);
}
else {
$this->assertResourceErrorResponse(403, FALSE, $response);
diff --git a/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
index d1ac74d8b2..8e0beeb913 100644
--- a/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
+++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
@@ -375,6 +375,20 @@ protected function getExpectedUnauthorizedAccessCacheability() {
->setCacheContexts(['user.permissions']);
}
+ /**
+ * The cacheability of unauthorized 'view' entity access.
+ *
+ * @param bool $is_authenticated
+ * Whether the current request is authenticated or not. This matters for
+ * some entity access control handlers, but not for most.
+ *
+ * @return \Drupal\Core\Cache\CacheableMetadata
+ * The expected cacheability.
+ */
+ protected function getExpectedUnauthorizedEntityAccessCacheability($is_authenticated) {
+ return new CacheableMetadata();
+ }
+
/**
* The expected cache tags for the GET/HEAD response of the test entity.
*
@@ -437,7 +451,11 @@ public function testGet() {
// response because ?_format query string is present.
$response = $this->request('GET', $url, $request_options);
if ($has_canonical_url) {
- $this->assertResourceErrorResponse(403, $this->getExpectedUnauthorizedAccessMessage('GET'), $response);
+ $expected_cacheability = $this->getExpectedUnauthorizedAccessCacheability()
+ // @see \Drupal\Core\EventSubscriber\AnonymousUserResponseSubscriber::onRespond()
+ ->addCacheTags(['config:user.role.anonymous']);
+ $expected_cacheability->addCacheableDependency($this->getExpectedUnauthorizedEntityAccessCacheability(FALSE));
+ $this->assertResourceErrorResponse(403, $this->getExpectedUnauthorizedAccessMessage('GET'), $response, $expected_cacheability->getCacheTags(), $expected_cacheability->getCacheContexts(), 'MISS', FALSE);
}
else {
$this->assertResourceErrorResponse(404, 'No route found for "GET ' . str_replace($this->baseUrl, '', $this->getEntityResourceUrl()->setAbsolute()->toString()) . '"', $response);
@@ -470,7 +488,8 @@ public function testGet() {
// First: single format. Drupal will automatically pick the only format.
$this->provisionEntityResource(TRUE);
- $expected_403_cacheability = $this->getExpectedUnauthorizedAccessCacheability();
+ $expected_403_cacheability = $this->getExpectedUnauthorizedAccessCacheability()
+ ->addCacheableDependency($this->getExpectedUnauthorizedEntityAccessCacheability(static::$auth !== FALSE));
// DX: 403 because unauthorized single-format route, ?_format is omittable.
$url->setOption('query', []);
$response = $this->request('GET', $url, $request_options);
@@ -479,13 +498,13 @@ public function testGet() {
$this->assertSame(['text/html; charset=UTF-8'], $response->getHeader('Content-Type'));
}
else {
- $this->assertResourceErrorResponse(403, FALSE, $response, $expected_403_cacheability->getCacheTags(), $expected_403_cacheability->getCacheContexts(), static::$auth ? FALSE : 'MISS', 'MISS');
+ $this->assertResourceErrorResponse(403, FALSE, $response, $expected_403_cacheability->getCacheTags(), $expected_403_cacheability->getCacheContexts(), static::$auth ? FALSE : 'MISS', FALSE);
}
$this->assertSame(static::$auth ? [] : ['MISS'], $response->getHeader('X-Drupal-Cache'));
// DX: 403 because unauthorized.
$url->setOption('query', ['_format' => static::$format]);
$response = $this->request('GET', $url, $request_options);
- $this->assertResourceErrorResponse(403, FALSE, $response, $expected_403_cacheability->getCacheTags(), $expected_403_cacheability->getCacheContexts(), static::$auth ? FALSE : 'MISS', $has_canonical_url ? 'MISS' : 'HIT');
+ $this->assertResourceErrorResponse(403, FALSE, $response, $expected_403_cacheability->getCacheTags(), $expected_403_cacheability->getCacheContexts(), static::$auth ? FALSE : 'MISS', FALSE);
// Then, what we'll use for the remainder of the test: multiple formats.
$this->provisionEntityResource();
@@ -505,7 +524,7 @@ public function testGet() {
// DX: 403 because unauthorized.
$url->setOption('query', ['_format' => static::$format]);
$response = $this->request('GET', $url, $request_options);
- $this->assertResourceErrorResponse(403, $this->getExpectedUnauthorizedAccessMessage('GET'), $response, $expected_403_cacheability->getCacheTags(), $expected_403_cacheability->getCacheContexts(), static::$auth ? FALSE : 'MISS', 'HIT');
+ $this->assertResourceErrorResponse(403, $this->getExpectedUnauthorizedAccessMessage('GET'), $response, $expected_403_cacheability->getCacheTags(), $expected_403_cacheability->getCacheContexts(), static::$auth ? FALSE : 'MISS', FALSE);
$this->assertArrayNotHasKey('Link', $response->getHeaders());
$this->setUpAuthorization('GET');
@@ -683,7 +702,15 @@ public function testGet() {
// DX: 403 when unauthorized.
$response = $this->request('GET', $url, $request_options);
- $this->assertResourceErrorResponse(403, $this->getExpectedUnauthorizedAccessMessage('GET'), $response);
+ $expected_403_cacheability = $this->getExpectedUnauthorizedAccessCacheability();
+ // Permission checking now happens first, so it's the only cache context we
+ // could possibly vary by.
+ $expected_403_cacheability->setCacheContexts(['user.permissions']);
+ // @see \Drupal\Core\EventSubscriber\AnonymousUserResponseSubscriber::onRespond()
+ if (static::$auth === FALSE) {
+ $expected_403_cacheability->addCacheTags(['config:user.role.anonymous']);
+ }
+ $this->assertResourceErrorResponse(403, $this->getExpectedUnauthorizedAccessMessage('GET'), $response, $expected_403_cacheability->getCacheTags(), $expected_403_cacheability->getCacheContexts(), static::$auth ? FALSE : 'MISS', FALSE);
$this->grantPermissionsToTestedRole(['restful get entity:' . static::$entityTypeId]);
@@ -856,18 +883,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.
@@ -883,6 +898,18 @@ public function testPost() {
$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;
@@ -1099,18 +1126,6 @@ public function testPatch() {
$request_options[RequestOptions::HEADERS]['Content-Type'] = static::$mimeType;
- // DX: 400 when no request body.
- $response = $this->request('PATCH', $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('PATCH', $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.
@@ -1126,6 +1141,18 @@ public function testPatch() {
$this->setUpAuthorization('PATCH');
+ // DX: 400 when no request body.
+ $response = $this->request('PATCH', $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('PATCH', $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('PATCH', $url, $request_options);
$label_field = $this->entity->getEntityType()->hasKey('label') ? $this->entity->getEntityType()->getKey('label') : static::$labelFieldName;
diff --git a/core/modules/search/tests/src/Functional/Rest/SearchPageResourceTestBase.php b/core/modules/search/tests/src/Functional/Rest/SearchPageResourceTestBase.php
index e711a34799..1730badd05 100644
--- a/core/modules/search/tests/src/Functional/Rest/SearchPageResourceTestBase.php
+++ b/core/modules/search/tests/src/Functional/Rest/SearchPageResourceTestBase.php
@@ -102,9 +102,9 @@ protected function getExpectedUnauthorizedAccessMessage($method) {
/**
* {@inheritdoc}
*/
- protected function getExpectedUnauthorizedAccessCacheability() {
+ protected function getExpectedUnauthorizedEntityAccessCacheability($is_authenticated) {
// @see \Drupal\search\SearchPageAccessControlHandler::checkAccess()
- return parent::getExpectedUnauthorizedAccessCacheability()
+ return parent::getExpectedUnauthorizedEntityAccessCacheability($is_authenticated)
->addCacheTags(['config:search.page.hinode_search']);
}
diff --git a/core/modules/serialization/src/Normalizer/FieldItemNormalizer.php b/core/modules/serialization/src/Normalizer/FieldItemNormalizer.php
index decca43227..085e5242d0 100644
--- a/core/modules/serialization/src/Normalizer/FieldItemNormalizer.php
+++ b/core/modules/serialization/src/Normalizer/FieldItemNormalizer.php
@@ -11,6 +11,8 @@
*/
class FieldItemNormalizer extends ComplexDataNormalizer implements DenormalizerInterface {
+ use SerializedColumnNormalizerTrait;
+
/**
* {@inheritdoc}
*/
@@ -30,6 +32,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;
@@ -51,6 +54,19 @@ public function denormalize($data, $class, $format = NULL, array $context = [])
* The value to use in Entity::setValue().
*/
protected function constructValue($data, $context) {
+ /** @var \Drupal\Core\Field\FieldItemInterface $field_item */
+ $field_item = $context['target_instance'];
+ $serialized_property_names = $this->getCustomSerializedPropertyNames($field_item);
+
+ // Explicitly serialize the input, unlike properties that rely on
+ // being automatically serialized, manually managed serialized properties
+ // expect to receive serialized input.
+ foreach ($serialized_property_names as $serialized_property_name) {
+ if (!empty($data[$serialized_property_name])) {
+ $data[$serialized_property_name] = serialize($data[$serialized_property_name]);
+ }
+ }
+
return $data;
}
diff --git a/core/modules/serialization/src/Normalizer/PrimitiveDataNormalizer.php b/core/modules/serialization/src/Normalizer/PrimitiveDataNormalizer.php
index cce108cacf..1774a11bef 100644
--- a/core/modules/serialization/src/Normalizer/PrimitiveDataNormalizer.php
+++ b/core/modules/serialization/src/Normalizer/PrimitiveDataNormalizer.php
@@ -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;
+
/**
* The interface or class that this Normalizer supports.
*
@@ -20,6 +23,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,
diff --git a/core/modules/serialization/src/Normalizer/SerializedColumnNormalizerTrait.php b/core/modules/serialization/src/Normalizer/SerializedColumnNormalizerTrait.php
new file mode 100644
index 0000000000..bf6eb0643c
--- /dev/null
+++ b/core/modules/serialization/src/Normalizer/SerializedColumnNormalizerTrait.php
@@ -0,0 +1,116 @@
+<?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 [];
+ }
+
+}
diff --git a/core/modules/serialization/tests/src/Kernel/EntitySerializationTest.php b/core/modules/serialization/tests/src/Kernel/EntitySerializationTest.php
index 72da3cb4eb..5a5df13a4c 100644
--- a/core/modules/serialization/tests/src/Kernel/EntitySerializationTest.php
+++ b/core/modules/serialization/tests/src/Kernel/EntitySerializationTest.php
@@ -4,6 +4,7 @@
use Drupal\Component\Serialization\Json;
use Drupal\Component\Utility\SafeMarkup;
+use Drupal\entity_test\Entity\EntitySerializedField;
use Drupal\entity_test\Entity\EntityTestMulRev;
use Drupal\filter\Entity\FilterFormat;
use Drupal\Tests\rest\Functional\BcTimestampNormalizerUnixTestTrait;
diff --git a/core/modules/serialization/tests/src/Unit/Normalizer/EntityReferenceFieldItemNormalizerTest.php b/core/modules/serialization/tests/src/Unit/Normalizer/EntityReferenceFieldItemNormalizerTest.php
index 5cc6467e8b..85ada16fef 100644
--- a/core/modules/serialization/tests/src/Unit/Normalizer/EntityReferenceFieldItemNormalizerTest.php
+++ b/core/modules/serialization/tests/src/Unit/Normalizer/EntityReferenceFieldItemNormalizerTest.php
@@ -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\TypedData\TypedDataInterface;
use Drupal\Core\Entity\EntityRepositoryInterface;
@@ -302,6 +303,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);
diff --git a/core/modules/serialization/tests/src/Unit/Normalizer/TimestampItemNormalizerTest.php b/core/modules/serialization/tests/src/Unit/Normalizer/TimestampItemNormalizerTest.php
index c4e351424a..47abc4601f 100644
--- a/core/modules/serialization/tests/src/Unit/Normalizer/TimestampItemNormalizerTest.php
+++ b/core/modules/serialization/tests/src/Unit/Normalizer/TimestampItemNormalizerTest.php
@@ -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;
@@ -110,6 +113,29 @@ public function testDenormalizeValidFormats($value, $expected) {
$timestamp_item->setValue(['value' => $expected])
->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()];
$denormalized = $this->normalizer->denormalize($normalized, TimestampItem::class, NULL, $context);
@@ -146,7 +172,32 @@ public function providerTestDenormalizeValidFormats() {
public function testDenormalizeException() {
$this->setExpectedException(UnexpectedValueException::class, 'The specified date "2016/11/06 09:02am GMT" is not in an accepted format: "U" (UNIX timestamp), "Y-m-d\TH:i:sO" (ISO 8601), "Y-m-d\TH:i:sP" (RFC 3339).');
- $context = ['target_instance' => $this->createTimestampItemProphecy()->reveal()];
+ $timestamp_item = $this->createTimestampItemProphecy();
+
+ // Avoid a static method call by returning dummy serialized 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()];
$normalized = ['value' => '2016/11/06 09:02am GMT'];
$this->normalizer->denormalize($normalized, TimestampItem::class, NULL, $context);
diff --git a/core/modules/shortcut/src/ShortcutSetAccessControlHandler.php b/core/modules/shortcut/src/ShortcutSetAccessControlHandler.php
index 3a55f74999..87be25d2d0 100644
--- a/core/modules/shortcut/src/ShortcutSetAccessControlHandler.php
+++ b/core/modules/shortcut/src/ShortcutSetAccessControlHandler.php
@@ -20,7 +20,7 @@ class ShortcutSetAccessControlHandler extends EntityAccessControlHandler {
protected function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) {
switch ($operation) {
case 'view':
- return AccessResult::allowedIf($account->hasPermission('access shortcuts'))->cachePerPermissions();
+ return AccessResult::allowedIfHasPermission($account, 'access shortcuts');
case 'update':
if ($account->hasPermission('administer shortcuts')) {
return AccessResult::allowed()->cachePerPermissions();
diff --git a/core/modules/shortcut/tests/src/Functional/Rest/ShortcutSetResourceTestBase.php b/core/modules/shortcut/tests/src/Functional/Rest/ShortcutSetResourceTestBase.php
index b25f028d1c..1510df288a 100644
--- a/core/modules/shortcut/tests/src/Functional/Rest/ShortcutSetResourceTestBase.php
+++ b/core/modules/shortcut/tests/src/Functional/Rest/ShortcutSetResourceTestBase.php
@@ -85,4 +85,20 @@ protected function getNormalizedPostEntity() {
// @todo Update in https://www.drupal.org/node/2300677.
}
+ /**
+ * {@inheritdoc}
+ */
+ protected function getExpectedUnauthorizedAccessMessage($method) {
+ if ($this->config('rest.settings')->get('bc_entity_resource_permissions')) {
+ return parent::getExpectedUnauthorizedAccessMessage($method);
+ }
+
+ switch ($method) {
+ case 'GET':
+ return "The 'access shortcuts' permission is required.";
+ default:
+ return parent::getExpectedUnauthorizedAccessMessage($method);
+ }
+ }
+
}
diff --git a/core/modules/user/src/UserAccessControlHandler.php b/core/modules/user/src/UserAccessControlHandler.php
index 8ff01d1447..27438c2a8f 100644
--- a/core/modules/user/src/UserAccessControlHandler.php
+++ b/core/modules/user/src/UserAccessControlHandler.php
@@ -4,6 +4,7 @@
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Access\AccessResultNeutral;
+use Drupal\Core\Access\AccessResultReasonInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityAccessControlHandler;
use Drupal\Core\Field\FieldDefinitionInterface;
@@ -64,11 +65,16 @@ protected function checkAccess(EntityInterface $entity, $operation, AccountInter
case 'update':
// Users can always edit their own account.
- return AccessResult::allowedIf($account->id() == $entity->id())->cachePerUser();
+ $access_result = AccessResult::allowedIf($account->id() == $entity->id())->cachePerUser();
+ if (!$access_result->isAllowed() && $access_result instanceof AccessResultReasonInterface) {
+ $access_result->setReason("Users can only update their own account, unless they have the 'administer users' permission.");
+ }
+ return $access_result;
case 'delete':
// Users with 'cancel account' permission can cancel their own account.
- return AccessResult::allowedIf($account->id() == $entity->id() && $account->hasPermission('cancel account'))->cachePerPermissions()->cachePerUser();
+ return AccessResult::allowedIfHasPermission($account, 'cancel account')
+ ->andIf(AccessResult::allowedIf($account->id() == $entity->id())->cachePerUser());
}
// No opinion.
diff --git a/core/modules/user/tests/src/Functional/Rest/UserResourceTestBase.php b/core/modules/user/tests/src/Functional/Rest/UserResourceTestBase.php
index 65a0caaaa7..0f2c6e3c40 100644
--- a/core/modules/user/tests/src/Functional/Rest/UserResourceTestBase.php
+++ b/core/modules/user/tests/src/Functional/Rest/UserResourceTestBase.php
@@ -309,9 +309,9 @@ protected function getExpectedUnauthorizedAccessMessage($method) {
case 'GET':
return "The 'access user profiles' permission is required and the user must be active.";
case 'PATCH':
- return "You are not authorized to update this user entity.";
+ return "Users can only update their own account, unless they have the 'administer users' permission.";
case 'DELETE':
- return 'You are not authorized to delete this user entity.';
+ return "The 'cancel account' permission is required.";
default:
return parent::getExpectedUnauthorizedAccessMessage($method);
}
@@ -320,9 +320,9 @@ protected function getExpectedUnauthorizedAccessMessage($method) {
/**
* {@inheritdoc}
*/
- protected function getExpectedUnauthorizedAccessCacheability() {
+ protected function getExpectedUnauthorizedEntityAccessCacheability($is_authenticated) {
// @see \Drupal\user\UserAccessControlHandler::checkAccess()
- return parent::getExpectedUnauthorizedAccessCacheability()
+ return parent::getExpectedUnauthorizedEntityAccessCacheability($is_authenticated)
->addCacheTags(['user:3']);
}
diff --git a/core/tests/Drupal/KernelTests/Core/Routing/ExceptionHandlingTest.php b/core/tests/Drupal/KernelTests/Core/Routing/ExceptionHandlingTest.php
index a791454f72..f500194666 100644
--- a/core/tests/Drupal/KernelTests/Core/Routing/ExceptionHandlingTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Routing/ExceptionHandlingTest.php
@@ -3,6 +3,7 @@
namespace Drupal\KernelTests\Core\Routing;
use Drupal\Component\Utility\Html;
+use Drupal\Core\Cache\CacheableJsonResponse;
use Drupal\KernelTests\KernelTestBase;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
@@ -56,6 +57,7 @@ public function testJson403() {
$this->assertEqual($response->getStatusCode(), Response::HTTP_FORBIDDEN);
$this->assertEqual($response->headers->get('Content-type'), 'application/json');
$this->assertEqual('{"message":""}', $response->getContent());
+ $this->assertInstanceOf(CacheableJsonResponse::class, $response);
}
/**
diff --git a/core/includes/update.inc b/core/includes/update.inc
index 8db643304e..6c3fc394b1 100644
--- a/core/includes/update.inc
+++ b/core/includes/update.inc
@@ -16,6 +16,10 @@
* Disables any extensions that are incompatible with the current core version.
*/
function update_fix_compatibility() {
+ // Fix extension objects if the update is being done via Drush 8. In non-Drush
+ // environments this will already be fixed by the UpdateKernel this point.
+ UpdateKernel::fixSerializedExtensionObjects(\Drupal::getContainer());
+
$extension_config = \Drupal::configFactory()->getEditable('core.extension');
$save = FALSE;
foreach (['module', 'theme'] as $type) {
@@ -30,10 +34,6 @@ function update_fix_compatibility() {
$extension_config->set('module', module_config_sort($extension_config->get('module')));
$extension_config->save();
}
-
- // Fix extension objects if the update is being done via Drush 8. In non-Drush
- // environments this will already be fixed by the UpdateKernel this point.
- UpdateKernel::fixSerializedExtensionObjects(\Drupal::getContainer());
}
/**
diff --git a/core/lib/Drupal.php b/core/lib/Drupal.php
index f3c30ec7ae..653f3d07db 100644
--- a/core/lib/Drupal.php
+++ b/core/lib/Drupal.php
@@ -82,7 +82,7 @@ class Drupal {
/**
* The current system version.
*/
- const VERSION = '8.6.9';
+ const VERSION = '8.6.10';
/**
* Core API compatibility.
diff --git a/core/lib/Drupal/Core/Access/CsrfRequestHeaderAccessCheck.php b/core/lib/Drupal/Core/Access/CsrfRequestHeaderAccessCheck.php
index 563355a6b7..c9266f5736 100644
--- a/core/lib/Drupal/Core/Access/CsrfRequestHeaderAccessCheck.php
+++ b/core/lib/Drupal/Core/Access/CsrfRequestHeaderAccessCheck.php
@@ -89,12 +89,15 @@ public function applies(Route $route) {
public function access(Request $request, AccountInterface $account) {
$method = $request->getMethod();
+ // Read-only operations are always allowed.
+ if (in_array($method, ['GET', 'HEAD', 'OPTIONS', 'TRACE'], TRUE)) {
+ return AccessResult::allowed();
+ }
+
// This check only applies if
- // 1. this is a write operation
- // 2. the user was successfully authenticated and
- // 3. the request comes with a session cookie.
- if (!in_array($method, ['GET', 'HEAD', 'OPTIONS', 'TRACE'])
- && $account->isAuthenticated()
+ // 1. the user was successfully authenticated and
+ // 2. the request comes with a session cookie.
+ if ($account->isAuthenticated()
&& $this->sessionConfiguration->hasSession($request)
) {
if (!$request->headers->has('X-CSRF-Token')) {
diff --git a/core/lib/Drupal/Core/Entity/EntityCreateAnyAccessCheck.php b/core/lib/Drupal/Core/Entity/EntityCreateAnyAccessCheck.php
index a6ff09430d..e0e24a2f9e 100644
--- a/core/lib/Drupal/Core/Entity/EntityCreateAnyAccessCheck.php
+++ b/core/lib/Drupal/Core/Entity/EntityCreateAnyAccessCheck.php
@@ -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;
+ }
}
}
diff --git a/core/lib/Drupal/Core/EventSubscriber/AuthenticationSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/AuthenticationSubscriber.php
index be3f55e4a3..9ab36f3e31 100644
--- a/core/lib/Drupal/Core/EventSubscriber/AuthenticationSubscriber.php
+++ b/core/lib/Drupal/Core/EventSubscriber/AuthenticationSubscriber.php
@@ -124,6 +124,21 @@ public function onExceptionSendChallenge(GetResponseForExceptionEvent $event) {
}
}
+ /**
+ * Detect disallowed authentication methods on access denied exceptions.
+ *
+ * @param \Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent $event
+ */
+ public function _onExceptionAccessDenied(GetResponseForExceptionEvent $event) {
+ if (isset($this->filter) && $event->isMasterRequest()) {
+ $request = $event->getRequest();
+ $exception = $event->getException();
+ if ($exception instanceof AccessDeniedHttpException && $this->authenticationProvider->applies($request) && !$this->filter->appliesToRoutedRequest($request, TRUE)) {
+ $event->setException(new AccessDeniedHttpException('The used authentication method is not allowed on this route.', $exception));
+ }
+ }
+ }
+
/**
* {@inheritdoc}
*/
@@ -137,6 +152,7 @@ public static function getSubscribedEvents() {
// Access check must be performed after routing.
$events[KernelEvents::REQUEST][] = ['onKernelRequestFilterProvider', 31];
$events[KernelEvents::EXCEPTION][] = ['onExceptionSendChallenge', 75];
+ $events[KernelEvents::EXCEPTION][] = ['_onExceptionAccessDenied', 80];
return $events;
}
diff --git a/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/MapItem.php b/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/MapItem.php
index e15fe848bf..5550ede34a 100644
--- a/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/MapItem.php
+++ b/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/MapItem.php
@@ -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);
+ }
}
}
diff --git a/core/lib/Drupal/Core/Routing/AccessAwareRouter.php b/core/lib/Drupal/Core/Routing/AccessAwareRouter.php
index 8f9e7beb29..1a93b42d35 100644
--- a/core/lib/Drupal/Core/Routing/AccessAwareRouter.php
+++ b/core/lib/Drupal/Core/Routing/AccessAwareRouter.php
@@ -4,6 +4,8 @@
use Drupal\Core\Access\AccessManagerInterface;
use Drupal\Core\Access\AccessResultReasonInterface;
+use Drupal\Core\Cache\CacheableDependencyInterface;
+use Drupal\Core\Http\Exception\CacheableAccessDeniedHttpException;
use Drupal\Core\Session\AccountInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
@@ -111,7 +113,12 @@ protected function checkAccess(Request $request) {
$request->attributes->set(AccessAwareRouterInterface::ACCESS_RESULT, $access_result);
}
if (!$access_result->isAllowed()) {
- throw new AccessDeniedHttpException($access_result instanceof AccessResultReasonInterface ? $access_result->getReason() : NULL);
+ if ($access_result instanceof CacheableDependencyInterface && $request->isMethodCacheable()) {
+ throw new CacheableAccessDeniedHttpException($access_result, $access_result instanceof AccessResultReasonInterface ? $access_result->getReason() : NULL);
+ }
+ else {
+ throw new AccessDeniedHttpException($access_result instanceof AccessResultReasonInterface ? $access_result->getReason() : NULL);
+ }
}
}
diff --git a/core/lib/Drupal/Core/Update/UpdateKernel.php b/core/lib/Drupal/Core/Update/UpdateKernel.php
index c38dad7ba6..d5051a7916 100644
--- a/core/lib/Drupal/Core/Update/UpdateKernel.php
+++ b/core/lib/Drupal/Core/Update/UpdateKernel.php
@@ -219,6 +219,9 @@ public static function fixSerializedExtensionObjects(ContainerInterface $contain
// will be PHP warnings. This silently fixes Drupal so that the update can
// continue.
$callable = function () use ($container) {
+ // Reset static caches in profile list so the module list is rebuilt
+ // correctly.
+ $container->get('extension.list.profile')->reset();
foreach ($container->getParameter('cache_bins') as $service_id => $bin) {
$container->get($service_id)->deleteAll();
}
diff --git a/core/modules/basic_auth/src/Authentication/Provider/BasicAuth.php b/core/modules/basic_auth/src/Authentication/Provider/BasicAuth.php
index 48732bc86d..72b81a7bf0 100644
--- a/core/modules/basic_auth/src/Authentication/Provider/BasicAuth.php
+++ b/core/modules/basic_auth/src/Authentication/Provider/BasicAuth.php
@@ -12,6 +12,7 @@
use Drupal\Core\Http\Exception\CacheableUnauthorizedHttpException;
use Drupal\user\UserAuthInterface;
use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
/**
* HTTP Basic authentication provider.
@@ -155,7 +156,9 @@ public function challengeException(Request $request, \Exception $previous) {
$cacheability = CacheableMetadata::createFromObject($site_config)
->addCacheTags(['config:user.role.anonymous'])
->addCacheContexts(['user.roles:anonymous']);
- return new CacheableUnauthorizedHttpException($cacheability, (string) $challenge, 'No authentication credentials provided.', $previous);
+ return $request->isMethodCacheable()
+ ? new CacheableUnauthorizedHttpException($cacheability, (string) $challenge, 'No authentication credentials provided.', $previous)
+ : new UnauthorizedHttpException((string) $challenge, 'No authentication credentials provided.', $previous);
}
}
diff --git a/core/modules/block/src/BlockAccessControlHandler.php b/core/modules/block/src/BlockAccessControlHandler.php
index e0abc0df67..e4e515206e 100644
--- a/core/modules/block/src/BlockAccessControlHandler.php
+++ b/core/modules/block/src/BlockAccessControlHandler.php
@@ -136,7 +136,10 @@ protected function checkAccess(EntityInterface $entity, $operation, AccountInter
}
}
else {
- $access = AccessResult::forbidden();
+ $reason = count($conditions) > 1
+ ? "One of the block visibility conditions ('%s') denied access."
+ : "The block visibility condition '%s' denied access.";
+ $access = AccessResult::forbidden(sprintf($reason, implode("', '", array_keys($conditions))));
}
$this->mergeCacheabilityFromConditions($access, $conditions);
diff --git a/core/modules/block/tests/src/Functional/Rest/BlockResourceTestBase.php b/core/modules/block/tests/src/Functional/Rest/BlockResourceTestBase.php
index 998690caa8..a6af25805e 100644
--- a/core/modules/block/tests/src/Functional/Rest/BlockResourceTestBase.php
+++ b/core/modules/block/tests/src/Functional/Rest/BlockResourceTestBase.php
@@ -3,6 +3,7 @@
namespace Drupal\Tests\block\Functional\Rest;
use Drupal\block\Entity\Block;
+use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase;
abstract class BlockResourceTestBase extends EntityResourceTestBase {
@@ -135,7 +136,7 @@ protected function getExpectedUnauthorizedAccessMessage($method) {
switch ($method) {
case 'GET':
- return "You are not authorized to view this block entity.";
+ return "The block visibility condition 'user_role' denied access.";
default:
return parent::getExpectedUnauthorizedAccessMessage($method);
}
@@ -143,17 +144,25 @@ protected function getExpectedUnauthorizedAccessMessage($method) {
/**
* {@inheritdoc}
+ *
+ * @todo Fix this in https://www.drupal.org/node/2820315.
*/
protected function getExpectedUnauthorizedAccessCacheability() {
+ return (new CacheableMetadata())
+ ->setCacheTags(['4xx-response', 'http_response'])
+ ->setCacheContexts(['user.roles']);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function getExpectedUnauthorizedEntityAccessCacheability($is_authenticated) {
// @see \Drupal\block\BlockAccessControlHandler::checkAccess()
- return parent::getExpectedUnauthorizedAccessCacheability()
- ->setCacheTags([
- '4xx-response',
+ return parent::getExpectedUnauthorizedEntityAccessCacheability($is_authenticated)
+ ->addCacheTags([
'config:block.block.llama',
- 'http_response',
- static::$auth ? 'user:2' : 'user:0',
- ])
- ->setCacheContexts(['user.roles']);
+ $is_authenticated ? 'user:2' : 'user:0',
+ ]);
}
}
diff --git a/core/modules/block_content/tests/src/Functional/Rest/BlockContentResourceTestBase.php b/core/modules/block_content/tests/src/Functional/Rest/BlockContentResourceTestBase.php
index 4a3ac11f4c..52e4870fad 100644
--- a/core/modules/block_content/tests/src/Functional/Rest/BlockContentResourceTestBase.php
+++ b/core/modules/block_content/tests/src/Functional/Rest/BlockContentResourceTestBase.php
@@ -180,9 +180,9 @@ protected function getExpectedUnauthorizedAccessMessage($method) {
/**
* {@inheritdoc}
*/
- protected function getExpectedUnauthorizedAccessCacheability() {
+ protected function getExpectedUnauthorizedEntityAccessCacheability($is_authenticated) {
// @see \Drupal\block_content\BlockContentAccessControlHandler()
- return parent::getExpectedUnauthorizedAccessCacheability()
+ return parent::getExpectedUnauthorizedEntityAccessCacheability($is_authenticated)
->addCacheTags(['block_content:1']);
}
diff --git a/core/modules/comment/tests/src/Functional/Rest/CommentResourceTestBase.php b/core/modules/comment/tests/src/Functional/Rest/CommentResourceTestBase.php
index bd71e4fa32..74efb4fbac 100644
--- a/core/modules/comment/tests/src/Functional/Rest/CommentResourceTestBase.php
+++ b/core/modules/comment/tests/src/Functional/Rest/CommentResourceTestBase.php
@@ -337,8 +337,10 @@ protected function getExpectedUnauthorizedAccessMessage($method) {
return "The 'post comments' permission is required.";
case 'PATCH';
return "The 'edit own comments' permission is required, the user must be the comment author, and the comment must be published.";
- default:
- return parent::getExpectedUnauthorizedAccessMessage($method);
+ case 'DELETE':
+ // \Drupal\comment\CommentAccessControlHandler::checkAccess() does not
+ // specify a reason for not allowing a comment to be deleted.
+ return '';
}
}
@@ -378,9 +380,9 @@ public function testPostSkipCommentApproval() {
/**
* {@inheritdoc}
*/
- protected function getExpectedUnauthorizedAccessCacheability() {
+ protected function getExpectedUnauthorizedEntityAccessCacheability($is_authenticated) {
// @see \Drupal\comment\CommentAccessControlHandler::checkAccess()
- return parent::getExpectedUnauthorizedAccessCacheability()
+ return parent::getExpectedUnauthorizedEntityAccessCacheability($is_authenticated)
->addCacheTags(['comment:1']);
}
diff --git a/core/modules/config/tests/config_test/tests/src/Functional/Rest/ConfigTestResourceTestBase.php b/core/modules/config/tests/config_test/tests/src/Functional/Rest/ConfigTestResourceTestBase.php
index 9ab65d76ae..dbd71b7778 100644
--- a/core/modules/config/tests/config_test/tests/src/Functional/Rest/ConfigTestResourceTestBase.php
+++ b/core/modules/config/tests/config_test/tests/src/Functional/Rest/ConfigTestResourceTestBase.php
@@ -70,4 +70,20 @@ protected function getNormalizedPostEntity() {
// @todo Update in https://www.drupal.org/node/2300677.
}
+ /**
+ * {@inheritdoc}
+ */
+ protected function getExpectedUnauthorizedAccessMessage($method) {
+ if ($this->config('rest.settings')->get('bc_entity_resource_permissions')) {
+ return parent::getExpectedUnauthorizedAccessMessage($method);
+ }
+
+ switch ($method) {
+ case 'GET':
+ return "The 'view config_test' permission is required.";
+ default:
+ return parent::getExpectedUnauthorizedAccessMessage($method);
+ }
+ }
+
}
diff --git a/core/modules/dblog/tests/src/Functional/DbLogResourceTest.php b/core/modules/dblog/tests/src/Functional/DbLogResourceTest.php
index 0d4630318e..904cd2919d 100644
--- a/core/modules/dblog/tests/src/Functional/DbLogResourceTest.php
+++ b/core/modules/dblog/tests/src/Functional/DbLogResourceTest.php
@@ -66,7 +66,7 @@ public function testWatchdog() {
$request_options = $this->getAuthenticationRequestOptions('GET');
$response = $this->request('GET', $url, $request_options);
- $this->assertResourceErrorResponse(403, "The 'restful get dblog' permission is required.", $response);
+ $this->assertResourceErrorResponse(403, "The 'restful get dblog' permission is required.", $response, ['4xx-response', 'http_response'], ['user.permissions'], FALSE, FALSE);
// Create a user account that has the required permissions to read
// the watchdog resource via the REST API.
diff --git a/core/modules/file/tests/src/Functional/Rest/FileResourceTestBase.php b/core/modules/file/tests/src/Functional/Rest/FileResourceTestBase.php
index 04b7c87ea3..d36f5b306a 100644
--- a/core/modules/file/tests/src/Functional/Rest/FileResourceTestBase.php
+++ b/core/modules/file/tests/src/Functional/Rest/FileResourceTestBase.php
@@ -218,6 +218,9 @@ public function testPost() {
*/
protected function getExpectedUnauthorizedAccessMessage($method) {
if ($this->config('rest.settings')->get('bc_entity_resource_permissions')) {
+ if ($method === 'DELETE') {
+ return 'Only the file owner can update or delete the file entity.';
+ }
return parent::getExpectedUnauthorizedAccessMessage($method);
}
diff --git a/core/modules/hal/src/Normalizer/FieldItemNormalizer.php b/core/modules/hal/src/Normalizer/FieldItemNormalizer.php
index f5b2ec07e8..5408cc436b 100644
--- a/core/modules/hal/src/Normalizer/FieldItemNormalizer.php
+++ b/core/modules/hal/src/Normalizer/FieldItemNormalizer.php
@@ -4,6 +4,7 @@
use Drupal\Core\Field\FieldItemInterface;
use Drupal\Core\TypedData\TypedDataInternalPropertiesHelper;
+use Drupal\serialization\Normalizer\SerializedColumnNormalizerTrait;
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
/**
@@ -11,6 +12,8 @@
*/
class FieldItemNormalizer extends NormalizerBase {
+ use SerializedColumnNormalizerTrait;
+
/**
* The interface or class that this Normalizer supports.
*
@@ -44,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'])) {
@@ -71,6 +75,19 @@ public function denormalize($data, $class, $format = NULL, array $context = [])
* The value to use in Entity::setValue().
*/
protected function constructValue($data, $context) {
+ /** @var \Drupal\Core\Field\FieldItemInterface $field_item */
+ $field_item = $context['target_instance'];
+ $serialized_property_names = $this->getCustomSerializedPropertyNames($field_item);
+
+ // Explicitly serialize the input, unlike properties that rely on
+ // being automatically serialized, manually managed serialized properties
+ // expect to receive serialized input.
+ foreach ($serialized_property_names as $serialized_property_name) {
+ if (!empty($data[$serialized_property_name])) {
+ $data[$serialized_property_name] = serialize($data[$serialized_property_name]);
+ }
+ }
+
return $data;
}
diff --git a/core/modules/hal/tests/src/Kernel/DenormalizeTest.php b/core/modules/hal/tests/src/Kernel/DenormalizeTest.php
index 87eb97329f..fde694fa69 100644
--- a/core/modules/hal/tests/src/Kernel/DenormalizeTest.php
+++ b/core/modules/hal/tests/src/Kernel/DenormalizeTest.php
@@ -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;
diff --git a/core/modules/language/tests/src/Functional/Hal/ConfigurableLanguageHalJsonBasicAuthTest.php b/core/modules/language/tests/src/Functional/Hal/ConfigurableLanguageHalJsonBasicAuthTest.php
index 3239b7e22c..4647acb82a 100644
--- a/core/modules/language/tests/src/Functional/Hal/ConfigurableLanguageHalJsonBasicAuthTest.php
+++ b/core/modules/language/tests/src/Functional/Hal/ConfigurableLanguageHalJsonBasicAuthTest.php
@@ -3,14 +3,14 @@
namespace Drupal\Tests\language\Functional\Hal;
use Drupal\Tests\language\Functional\Rest\ConfigurableLanguageResourceTestBase;
-use Drupal\Tests\rest\Functional\BasicAuthResourceWithInterfaceTranslationTestTrait;
+use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait;
/**
* @group hal
*/
class ConfigurableLanguageHalJsonBasicAuthTest extends ConfigurableLanguageResourceTestBase {
- use BasicAuthResourceWithInterfaceTranslationTestTrait;
+ use BasicAuthResourceTestTrait;
/**
* {@inheritdoc}
diff --git a/core/modules/language/tests/src/Functional/Hal/ContentLanguageSettingsHalJsonBasicAuthTest.php b/core/modules/language/tests/src/Functional/Hal/ContentLanguageSettingsHalJsonBasicAuthTest.php
index 61306eaf5e..fefd0db73a 100644
--- a/core/modules/language/tests/src/Functional/Hal/ContentLanguageSettingsHalJsonBasicAuthTest.php
+++ b/core/modules/language/tests/src/Functional/Hal/ContentLanguageSettingsHalJsonBasicAuthTest.php
@@ -3,14 +3,14 @@
namespace Drupal\Tests\language\Functional\Hal;
use Drupal\Tests\language\Functional\Rest\ContentLanguageSettingsResourceTestBase;
-use Drupal\Tests\rest\Functional\BasicAuthResourceWithInterfaceTranslationTestTrait;
+use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait;
/**
* @group hal
*/
class ContentLanguageSettingsHalJsonBasicAuthTest extends ContentLanguageSettingsResourceTestBase {
- use BasicAuthResourceWithInterfaceTranslationTestTrait;
+ use BasicAuthResourceTestTrait;
/**
* {@inheritdoc}
diff --git a/core/modules/language/tests/src/Functional/Rest/ConfigurableLanguageJsonBasicAuthTest.php b/core/modules/language/tests/src/Functional/Rest/ConfigurableLanguageJsonBasicAuthTest.php
index 847e781640..5352c9a951 100644
--- a/core/modules/language/tests/src/Functional/Rest/ConfigurableLanguageJsonBasicAuthTest.php
+++ b/core/modules/language/tests/src/Functional/Rest/ConfigurableLanguageJsonBasicAuthTest.php
@@ -2,14 +2,14 @@
namespace Drupal\Tests\language\Functional\Rest;
-use Drupal\Tests\rest\Functional\BasicAuthResourceWithInterfaceTranslationTestTrait;
+use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait;
/**
* @group rest
*/
class ConfigurableLanguageJsonBasicAuthTest extends ConfigurableLanguageResourceTestBase {
- use BasicAuthResourceWithInterfaceTranslationTestTrait;
+ use BasicAuthResourceTestTrait;
/**
* {@inheritdoc}
diff --git a/core/modules/language/tests/src/Functional/Rest/ConfigurableLanguageXmlBasicAuthTest.php b/core/modules/language/tests/src/Functional/Rest/ConfigurableLanguageXmlBasicAuthTest.php
index 7a79c4d433..afc017f7a0 100644
--- a/core/modules/language/tests/src/Functional/Rest/ConfigurableLanguageXmlBasicAuthTest.php
+++ b/core/modules/language/tests/src/Functional/Rest/ConfigurableLanguageXmlBasicAuthTest.php
@@ -2,7 +2,7 @@
namespace Drupal\Tests\language\Functional\Rest;
-use Drupal\Tests\rest\Functional\BasicAuthResourceWithInterfaceTranslationTestTrait;
+use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait;
use Drupal\Tests\rest\Functional\EntityResource\XmlEntityNormalizationQuirksTrait;
/**
@@ -10,7 +10,7 @@
*/
class ConfigurableLanguageXmlBasicAuthTest extends ConfigurableLanguageResourceTestBase {
- use BasicAuthResourceWithInterfaceTranslationTestTrait;
+ use BasicAuthResourceTestTrait;
use XmlEntityNormalizationQuirksTrait;
/**
diff --git a/core/modules/language/tests/src/Functional/Rest/ContentLanguageSettingsJsonBasicAuthTest.php b/core/modules/language/tests/src/Functional/Rest/ContentLanguageSettingsJsonBasicAuthTest.php
index 14437a9092..f1063a3dc1 100644
--- a/core/modules/language/tests/src/Functional/Rest/ContentLanguageSettingsJsonBasicAuthTest.php
+++ b/core/modules/language/tests/src/Functional/Rest/ContentLanguageSettingsJsonBasicAuthTest.php
@@ -2,14 +2,14 @@
namespace Drupal\Tests\language\Functional\Rest;
-use Drupal\Tests\rest\Functional\BasicAuthResourceWithInterfaceTranslationTestTrait;
+use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait;
/**
* @group rest
*/
class ContentLanguageSettingsJsonBasicAuthTest extends ContentLanguageSettingsResourceTestBase {
- use BasicAuthResourceWithInterfaceTranslationTestTrait;
+ use BasicAuthResourceTestTrait;
/**
* {@inheritdoc}
diff --git a/core/modules/language/tests/src/Functional/Rest/ContentLanguageSettingsXmlBasicAuthTest.php b/core/modules/language/tests/src/Functional/Rest/ContentLanguageSettingsXmlBasicAuthTest.php
index 741e75bcd3..dee7ad27d4 100644
--- a/core/modules/language/tests/src/Functional/Rest/ContentLanguageSettingsXmlBasicAuthTest.php
+++ b/core/modules/language/tests/src/Functional/Rest/ContentLanguageSettingsXmlBasicAuthTest.php
@@ -2,7 +2,7 @@
namespace Drupal\Tests\language\Functional\Rest;
-use Drupal\Tests\rest\Functional\BasicAuthResourceWithInterfaceTranslationTestTrait;
+use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait;
use Drupal\Tests\rest\Functional\EntityResource\XmlEntityNormalizationQuirksTrait;
/**
@@ -10,7 +10,7 @@
*/
class ContentLanguageSettingsXmlBasicAuthTest extends ContentLanguageSettingsResourceTestBase {
- use BasicAuthResourceWithInterfaceTranslationTestTrait;
+ use BasicAuthResourceTestTrait;
use XmlEntityNormalizationQuirksTrait;
/**
diff --git a/core/modules/link/src/Plugin/Field/FieldType/LinkItem.php b/core/modules/link/src/Plugin/Field/FieldType/LinkItem.php
index 8fa765e0b6..431512c290 100644
--- a/core/modules/link/src/Plugin/Field/FieldType/LinkItem.php
+++ b/core/modules/link/src/Plugin/Field/FieldType/LinkItem.php
@@ -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);
}
diff --git a/core/modules/media/tests/src/Functional/Rest/MediaResourceTestBase.php b/core/modules/media/tests/src/Functional/Rest/MediaResourceTestBase.php
index de352e001a..e1bb704b86 100644
--- a/core/modules/media/tests/src/Functional/Rest/MediaResourceTestBase.php
+++ b/core/modules/media/tests/src/Functional/Rest/MediaResourceTestBase.php
@@ -345,7 +345,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();
}
@@ -422,9 +422,9 @@ protected function getExpectedNormalizedFileEntity() {
/**
* {@inheritdoc}
*/
- protected function getExpectedUnauthorizedAccessCacheability() {
+ protected function getExpectedUnauthorizedEntityAccessCacheability($is_authenticated) {
// @see \Drupal\media\MediaAccessControlHandler::checkAccess()
- return parent::getExpectedUnauthorizedAccessCacheability()
+ return parent::getExpectedUnauthorizedEntityAccessCacheability($is_authenticated)
->addCacheTags(['media:1']);
}
diff --git a/core/modules/menu_link_content/src/MenuLinkContentAccessControlHandler.php b/core/modules/menu_link_content/src/MenuLinkContentAccessControlHandler.php
index eadf04532c..b663f27071 100644
--- a/core/modules/menu_link_content/src/MenuLinkContentAccessControlHandler.php
+++ b/core/modules/menu_link_content/src/MenuLinkContentAccessControlHandler.php
@@ -72,7 +72,8 @@ protected function checkAccess(EntityInterface $entity, $operation, AccountInter
}
case 'delete':
- return AccessResult::allowedIf(!$entity->isNew() && $account->hasPermission('administer menu'))->cachePerPermissions()->addCacheableDependency($entity);
+ return AccessResult::allowedIfHasPermission($account, 'administer menu')
+ ->andIf(AccessResult::allowedIf(!$entity->isNew())->addCacheableDependency($entity));
}
}
diff --git a/core/modules/menu_link_content/tests/src/Functional/Rest/MenuLinkContentResourceTestBase.php b/core/modules/menu_link_content/tests/src/Functional/Rest/MenuLinkContentResourceTestBase.php
index 9b03899e45..6c076e4ce6 100644
--- a/core/modules/menu_link_content/tests/src/Functional/Rest/MenuLinkContentResourceTestBase.php
+++ b/core/modules/menu_link_content/tests/src/Functional/Rest/MenuLinkContentResourceTestBase.php
@@ -204,7 +204,7 @@ protected function getExpectedUnauthorizedAccessMessage($method) {
switch ($method) {
case 'DELETE':
- return "You are not authorized to delete this menu_link_content entity.";
+ return "The 'administer menu' permission is required.";
default:
return parent::getExpectedUnauthorizedAccessMessage($method);
}
diff --git a/core/modules/node/src/NodeAccessControlHandler.php b/core/modules/node/src/NodeAccessControlHandler.php
index 5ccc6425e3..7fd27dea04 100644
--- a/core/modules/node/src/NodeAccessControlHandler.php
+++ b/core/modules/node/src/NodeAccessControlHandler.php
@@ -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();
}
diff --git a/core/modules/node/tests/src/Functional/Rest/NodeResourceTestBase.php b/core/modules/node/tests/src/Functional/Rest/NodeResourceTestBase.php
index 9f9a114924..ebf967ed64 100644
--- a/core/modules/node/tests/src/Functional/Rest/NodeResourceTestBase.php
+++ b/core/modules/node/tests/src/Functional/Rest/NodeResourceTestBase.php
@@ -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);
diff --git a/core/modules/rest/rest.post_update.php b/core/modules/rest/rest.post_update.php
index 8ed5a6dbff..e1e1d20301 100644
--- a/core/modules/rest/rest.post_update.php
+++ b/core/modules/rest/rest.post_update.php
@@ -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.
+}
diff --git a/core/modules/rest/src/Plugin/rest/resource/EntityResource.php b/core/modules/rest/src/Plugin/rest/resource/EntityResource.php
index ebfd075041..7aa56055c7 100644
--- a/core/modules/rest/src/Plugin/rest/resource/EntityResource.php
+++ b/core/modules/rest/src/Plugin/rest/resource/EntityResource.php
@@ -13,12 +13,13 @@
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityStorageException;
use Drupal\Core\Field\FieldItemListInterface;
-use Drupal\Core\Http\Exception\CacheableAccessDeniedHttpException;
+use Drupal\Core\Routing\AccessAwareRouterInterface;
use Drupal\rest\Plugin\ResourceBase;
use Drupal\rest\ResourceResponse;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\rest\ModifiedResourceResponse;
+use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
@@ -121,14 +122,11 @@ public static function create(ContainerInterface $container, array $configuratio
* @throws \Symfony\Component\HttpKernel\Exception\HttpException
*/
public function get(EntityInterface $entity) {
- $entity_access = $entity->access('view', NULL, TRUE);
- if (!$entity_access->isAllowed()) {
- throw new CacheableAccessDeniedHttpException($entity_access, $entity_access->getReason() ?: $this->generateFallbackAccessDeniedMessage($entity, 'view'));
- }
-
+ $request = \Drupal::request();
$response = new ResourceResponse($entity, 200);
+ // @todo Either remove the line below or remove this todo in https://www.drupal.org/project/drupal/issues/2973356
+ $response->addCacheableDependency($request->attributes->get(AccessAwareRouterInterface::ACCESS_RESULT));
$response->addCacheableDependency($entity);
- $response->addCacheableDependency($entity_access);
if ($entity instanceof FieldableEntityInterface) {
foreach ($entity as $field_name => $field) {
@@ -223,10 +221,6 @@ public function patch(EntityInterface $original_entity, EntityInterface $entity
if ($entity->getEntityTypeId() != $definition['entity_type']) {
throw new BadRequestHttpException('Invalid entity type');
}
- $entity_access = $original_entity->access('update', NULL, TRUE);
- if (!$entity_access->isAllowed()) {
- throw new AccessDeniedHttpException($entity_access->getReason() ?: $this->generateFallbackAccessDeniedMessage($entity, 'update'));
- }
// Overwrite the received fields.
// @todo Remove $changed_fields in https://www.drupal.org/project/drupal/issues/2862574.
@@ -327,10 +321,6 @@ protected function checkPatchFieldAccess(FieldItemListInterface $original_field,
* @throws \Symfony\Component\HttpKernel\Exception\HttpException
*/
public function delete(EntityInterface $entity) {
- $entity_access = $entity->access('delete', NULL, TRUE);
- if (!$entity_access->isAllowed()) {
- throw new AccessDeniedHttpException($entity_access->getReason() ?: $this->generateFallbackAccessDeniedMessage($entity, 'delete'));
- }
try {
$entity->delete();
$this->logger->notice('Deleted entity %type with ID %id.', ['%type' => $entity->getEntityTypeId(), '%id' => $entity->id()]);
@@ -383,6 +373,23 @@ public function permissions() {
*/
protected function getBaseRoute($canonical_path, $method) {
$route = parent::getBaseRoute($canonical_path, $method);
+
+ switch ($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;
+ case 'DELETE':
+ $route->setRequirement('_entity_access', $this->entityType->id() . '.delete');
+ break;
+ }
+
$definition = $this->getPluginDefinition();
$parameters = $route->getOption('parameters') ?: [];
diff --git a/core/modules/rest/tests/modules/config_test_rest/config_test_rest.module b/core/modules/rest/tests/modules/config_test_rest/config_test_rest.module
index fcd9979a11..aa9fababb0 100644
--- a/core/modules/rest/tests/modules/config_test_rest/config_test_rest.module
+++ b/core/modules/rest/tests/modules/config_test_rest/config_test_rest.module
@@ -6,6 +6,7 @@
*/
use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Access\AccessResultReasonInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Session\AccountInterface;
@@ -26,5 +27,9 @@ function config_test_rest_config_test_access(EntityInterface $entity, $operation
// Add permission, so that EntityResourceTestBase's scenarios can test access
// being denied. By default, all access is always allowed for the config_test
// config entity.
- return AccessResult::forbiddenIf(!$account->hasPermission('view config_test'))->cachePerPermissions();
+ $access_result = AccessResult::forbiddenIf(!$account->hasPermission('view config_test'))->cachePerPermissions();
+ if (!$access_result->isAllowed() && $access_result instanceof AccessResultReasonInterface) {
+ $access_result->setReason("The 'view config_test' permission is required.");
+ }
+ return $access_result;
}
diff --git a/core/modules/rest/tests/src/Functional/BasicAuthResourceTestTrait.php b/core/modules/rest/tests/src/Functional/BasicAuthResourceTestTrait.php
index e4dcd70ff6..4a58254db1 100644
--- a/core/modules/rest/tests/src/Functional/BasicAuthResourceTestTrait.php
+++ b/core/modules/rest/tests/src/Functional/BasicAuthResourceTestTrait.php
@@ -14,8 +14,6 @@
* authenticated, a 401 response must be sent.
* - Because every request must send an authorization, there is no danger of
* CSRF attacks.
- *
- * @see \Drupal\Tests\rest\Functional\BasicAuthResourceWithInterfaceTranslationTestTrait
*/
trait BasicAuthResourceTestTrait {
@@ -34,10 +32,23 @@ protected function getAuthenticationRequestOptions($method) {
* {@inheritdoc}
*/
protected function assertResponseWhenMissingAuthentication($method, ResponseInterface $response) {
+ if ($method !== 'GET') {
+ return $this->assertResourceErrorResponse(401, 'No authentication credentials provided.', $response);
+ }
+
$expected_page_cache_header_value = $method === 'GET' ? 'MISS' : FALSE;
- // @see \Drupal\basic_auth\Authentication\Provider\BasicAuth::challengeException()
- $expected_dynamic_page_cache_header_value = $expected_page_cache_header_value;
- $this->assertResourceErrorResponse(401, 'No authentication credentials provided.', $response, ['4xx-response', 'config:system.site', 'config:user.role.anonymous', 'http_response'], ['user.roles:anonymous'], $expected_page_cache_header_value, $expected_dynamic_page_cache_header_value);
+ $expected_cacheability = $this->getExpectedUnauthorizedAccessCacheability()
+ ->addCacheableDependency($this->getExpectedUnauthorizedEntityAccessCacheability(FALSE))
+ // @see \Drupal\basic_auth\Authentication\Provider\BasicAuth::challengeException()
+ ->addCacheableDependency($this->config('system.site'))
+ // @see \Drupal\Core\EventSubscriber\AnonymousUserResponseSubscriber::onRespond()
+ ->addCacheTags(['config:user.role.anonymous']);
+ // Only add the 'user.roles:anonymous' cache context if its parent cache
+ // context is not already present.
+ if (!in_array('user.roles', $expected_cacheability->getCacheContexts(), TRUE)) {
+ $expected_cacheability->addCacheContexts(['user.roles:anonymous']);
+ }
+ $this->assertResourceErrorResponse(401, 'No authentication credentials provided.', $response, $expected_cacheability->getCacheTags(), $expected_cacheability->getCacheContexts(), $expected_page_cache_header_value, FALSE);
}
/**
diff --git a/core/modules/rest/tests/src/Functional/BasicAuthResourceWithInterfaceTranslationTestTrait.php b/core/modules/rest/tests/src/Functional/BasicAuthResourceWithInterfaceTranslationTestTrait.php
deleted file mode 100644
index 37b8381eae..0000000000
--- a/core/modules/rest/tests/src/Functional/BasicAuthResourceWithInterfaceTranslationTestTrait.php
+++ /dev/null
@@ -1,28 +0,0 @@
-<?php
-
-namespace Drupal\Tests\rest\Functional;
-
-use Psr\Http\Message\ResponseInterface;
-
-/**
- * Trait for ResourceTestBase subclasses testing $auth=basic_auth + 'language'.
- *
- * @see \Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait
- */
-trait BasicAuthResourceWithInterfaceTranslationTestTrait {
-
- use BasicAuthResourceTestTrait;
-
- /**
- * {@inheritdoc}
- */
- protected function assertResponseWhenMissingAuthentication($method, ResponseInterface $response) {
- // Because BasicAuth::challengeException() relies on the 'system.site'
- // configuration, and this test installs the 'language' module, all config
- // may be translated and therefore gets the 'languages:language_interface'
- // cache context.
- $expected_page_cache_header_value = $method === 'GET' ? 'MISS' : FALSE;
- $this->assertResourceErrorResponse(401, 'No authentication credentials provided.', $response, ['4xx-response', 'config:system.site', 'config:user.role.anonymous', 'http_response'], ['languages:language_interface', 'user.roles:anonymous'], $expected_page_cache_header_value, $expected_page_cache_header_value);
- }
-
-}
diff --git a/core/modules/rest/tests/src/Functional/CookieResourceTestTrait.php b/core/modules/rest/tests/src/Functional/CookieResourceTestTrait.php
index 7ce381b889..2d25b946a5 100644
--- a/core/modules/rest/tests/src/Functional/CookieResourceTestTrait.php
+++ b/core/modules/rest/tests/src/Functional/CookieResourceTestTrait.php
@@ -99,7 +99,9 @@ protected function assertResponseWhenMissingAuthentication($method, ResponseInte
// @see \Drupal\user\Authentication\Provider\Cookie
// @todo https://www.drupal.org/node/2847623
if ($method === 'GET') {
- $expected_cookie_403_cacheability = $this->getExpectedUnauthorizedAccessCacheability();
+ $expected_cookie_403_cacheability = $this->getExpectedUnauthorizedAccessCacheability()
+ // @see \Drupal\Core\EventSubscriber\AnonymousUserResponseSubscriber::onRespond()
+ ->addCacheableDependency($this->getExpectedUnauthorizedEntityAccessCacheability(FALSE));
// - \Drupal\Core\EventSubscriber\AnonymousUserResponseSubscriber applies
// to cacheable anonymous responses: it updates their cacheability.
// - A 403 response to a GET request is cacheable.
@@ -111,7 +113,7 @@ protected function assertResponseWhenMissingAuthentication($method, ResponseInte
if (static::$entityTypeId === 'block') {
$expected_cookie_403_cacheability->setCacheTags(str_replace('user:2', 'user:0', $expected_cookie_403_cacheability->getCacheTags()));
}
- $this->assertResourceErrorResponse(403, FALSE, $response, $expected_cookie_403_cacheability->getCacheTags(), $expected_cookie_403_cacheability->getCacheContexts(), 'MISS', 'MISS');
+ $this->assertResourceErrorResponse(403, FALSE, $response, $expected_cookie_403_cacheability->getCacheTags(), $expected_cookie_403_cacheability->getCacheContexts(), 'MISS', FALSE);
}
else {
$this->assertResourceErrorResponse(403, FALSE, $response);
diff --git a/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
index 17e0c0a7b5..ebb5f4005b 100644
--- a/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
+++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
@@ -379,6 +379,20 @@ protected function getExpectedUnauthorizedAccessCacheability() {
->setCacheContexts(['user.permissions']);
}
+ /**
+ * The cacheability of unauthorized 'view' entity access.
+ *
+ * @param bool $is_authenticated
+ * Whether the current request is authenticated or not. This matters for
+ * some entity access control handlers, but not for most.
+ *
+ * @return \Drupal\Core\Cache\CacheableMetadata
+ * The expected cacheability.
+ */
+ protected function getExpectedUnauthorizedEntityAccessCacheability($is_authenticated) {
+ return new CacheableMetadata();
+ }
+
/**
* The expected cache tags for the GET/HEAD response of the test entity.
*
@@ -441,7 +455,11 @@ public function testGet() {
// response because ?_format query string is present.
$response = $this->request('GET', $url, $request_options);
if ($has_canonical_url) {
- $this->assertResourceErrorResponse(403, $this->getExpectedUnauthorizedAccessMessage('GET'), $response);
+ $expected_cacheability = $this->getExpectedUnauthorizedAccessCacheability()
+ // @see \Drupal\Core\EventSubscriber\AnonymousUserResponseSubscriber::onRespond()
+ ->addCacheTags(['config:user.role.anonymous']);
+ $expected_cacheability->addCacheableDependency($this->getExpectedUnauthorizedEntityAccessCacheability(FALSE));
+ $this->assertResourceErrorResponse(403, $this->getExpectedUnauthorizedAccessMessage('GET'), $response, $expected_cacheability->getCacheTags(), $expected_cacheability->getCacheContexts(), 'MISS', FALSE);
}
else {
$this->assertResourceErrorResponse(404, 'No route found for "GET ' . str_replace($this->baseUrl, '', $this->getEntityResourceUrl()->setAbsolute()->toString()) . '"', $response);
@@ -474,7 +492,8 @@ public function testGet() {
// First: single format. Drupal will automatically pick the only format.
$this->provisionEntityResource(TRUE);
- $expected_403_cacheability = $this->getExpectedUnauthorizedAccessCacheability();
+ $expected_403_cacheability = $this->getExpectedUnauthorizedAccessCacheability()
+ ->addCacheableDependency($this->getExpectedUnauthorizedEntityAccessCacheability(static::$auth !== FALSE));
// DX: 403 because unauthorized single-format route, ?_format is omittable.
$url->setOption('query', []);
$response = $this->request('GET', $url, $request_options);
@@ -483,13 +502,13 @@ public function testGet() {
$this->assertSame(['text/html; charset=UTF-8'], $response->getHeader('Content-Type'));
}
else {
- $this->assertResourceErrorResponse(403, FALSE, $response, $expected_403_cacheability->getCacheTags(), $expected_403_cacheability->getCacheContexts(), static::$auth ? FALSE : 'MISS', 'MISS');
+ $this->assertResourceErrorResponse(403, FALSE, $response, $expected_403_cacheability->getCacheTags(), $expected_403_cacheability->getCacheContexts(), static::$auth ? FALSE : 'MISS', FALSE);
}
$this->assertSame(static::$auth ? [] : ['MISS'], $response->getHeader('X-Drupal-Cache'));
// DX: 403 because unauthorized.
$url->setOption('query', ['_format' => static::$format]);
$response = $this->request('GET', $url, $request_options);
- $this->assertResourceErrorResponse(403, FALSE, $response, $expected_403_cacheability->getCacheTags(), $expected_403_cacheability->getCacheContexts(), static::$auth ? FALSE : 'MISS', $has_canonical_url ? 'MISS' : 'HIT');
+ $this->assertResourceErrorResponse(403, FALSE, $response, $expected_403_cacheability->getCacheTags(), $expected_403_cacheability->getCacheContexts(), static::$auth ? FALSE : 'MISS', FALSE);
// Then, what we'll use for the remainder of the test: multiple formats.
$this->provisionEntityResource();
@@ -509,7 +528,7 @@ public function testGet() {
// DX: 403 because unauthorized.
$url->setOption('query', ['_format' => static::$format]);
$response = $this->request('GET', $url, $request_options);
- $this->assertResourceErrorResponse(403, $this->getExpectedUnauthorizedAccessMessage('GET'), $response, $expected_403_cacheability->getCacheTags(), $expected_403_cacheability->getCacheContexts(), static::$auth ? FALSE : 'MISS', 'HIT');
+ $this->assertResourceErrorResponse(403, $this->getExpectedUnauthorizedAccessMessage('GET'), $response, $expected_403_cacheability->getCacheTags(), $expected_403_cacheability->getCacheContexts(), static::$auth ? FALSE : 'MISS', FALSE);
$this->assertArrayNotHasKey('Link', $response->getHeaders());
$this->setUpAuthorization('GET');
@@ -687,7 +706,15 @@ public function testGet() {
// DX: 403 when unauthorized.
$response = $this->request('GET', $url, $request_options);
- $this->assertResourceErrorResponse(403, $this->getExpectedUnauthorizedAccessMessage('GET'), $response);
+ $expected_403_cacheability = $this->getExpectedUnauthorizedAccessCacheability();
+ // Permission checking now happens first, so it's the only cache context we
+ // could possibly vary by.
+ $expected_403_cacheability->setCacheContexts(['user.permissions']);
+ // @see \Drupal\Core\EventSubscriber\AnonymousUserResponseSubscriber::onRespond()
+ if (static::$auth === FALSE) {
+ $expected_403_cacheability->addCacheTags(['config:user.role.anonymous']);
+ }
+ $this->assertResourceErrorResponse(403, $this->getExpectedUnauthorizedAccessMessage('GET'), $response, $expected_403_cacheability->getCacheTags(), $expected_403_cacheability->getCacheContexts(), static::$auth ? FALSE : 'MISS', FALSE);
$this->grantPermissionsToTestedRole(['restful get entity:' . static::$entityTypeId]);
@@ -839,18 +866,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.
@@ -862,16 +877,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;
@@ -1074,18 +1095,6 @@ public function testPatch() {
$request_options[RequestOptions::HEADERS]['Content-Type'] = static::$mimeType;
- // DX: 400 when no request body.
- $response = $this->request('PATCH', $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('PATCH', $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.
@@ -1101,6 +1110,18 @@ public function testPatch() {
$this->setUpAuthorization('PATCH');
+ // DX: 400 when no request body.
+ $response = $this->request('PATCH', $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('PATCH', $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('PATCH', $url, $request_options);
$label_field = $this->entity->getEntityType()->hasKey('label') ? $this->entity->getEntityType()->getKey('label') : static::$labelFieldName;
diff --git a/core/modules/search/tests/src/Functional/Rest/SearchPageResourceTestBase.php b/core/modules/search/tests/src/Functional/Rest/SearchPageResourceTestBase.php
index e711a34799..1730badd05 100644
--- a/core/modules/search/tests/src/Functional/Rest/SearchPageResourceTestBase.php
+++ b/core/modules/search/tests/src/Functional/Rest/SearchPageResourceTestBase.php
@@ -102,9 +102,9 @@ protected function getExpectedUnauthorizedAccessMessage($method) {
/**
* {@inheritdoc}
*/
- protected function getExpectedUnauthorizedAccessCacheability() {
+ protected function getExpectedUnauthorizedEntityAccessCacheability($is_authenticated) {
// @see \Drupal\search\SearchPageAccessControlHandler::checkAccess()
- return parent::getExpectedUnauthorizedAccessCacheability()
+ return parent::getExpectedUnauthorizedEntityAccessCacheability($is_authenticated)
->addCacheTags(['config:search.page.hinode_search']);
}
diff --git a/core/modules/serialization/src/Normalizer/FieldItemNormalizer.php b/core/modules/serialization/src/Normalizer/FieldItemNormalizer.php
index decca43227..085e5242d0 100644
--- a/core/modules/serialization/src/Normalizer/FieldItemNormalizer.php
+++ b/core/modules/serialization/src/Normalizer/FieldItemNormalizer.php
@@ -11,6 +11,8 @@
*/
class FieldItemNormalizer extends ComplexDataNormalizer implements DenormalizerInterface {
+ use SerializedColumnNormalizerTrait;
+
/**
* {@inheritdoc}
*/
@@ -30,6 +32,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;
@@ -51,6 +54,19 @@ public function denormalize($data, $class, $format = NULL, array $context = [])
* The value to use in Entity::setValue().
*/
protected function constructValue($data, $context) {
+ /** @var \Drupal\Core\Field\FieldItemInterface $field_item */
+ $field_item = $context['target_instance'];
+ $serialized_property_names = $this->getCustomSerializedPropertyNames($field_item);
+
+ // Explicitly serialize the input, unlike properties that rely on
+ // being automatically serialized, manually managed serialized properties
+ // expect to receive serialized input.
+ foreach ($serialized_property_names as $serialized_property_name) {
+ if (!empty($data[$serialized_property_name])) {
+ $data[$serialized_property_name] = serialize($data[$serialized_property_name]);
+ }
+ }
+
return $data;
}
diff --git a/core/modules/serialization/src/Normalizer/PrimitiveDataNormalizer.php b/core/modules/serialization/src/Normalizer/PrimitiveDataNormalizer.php
index cce108cacf..1774a11bef 100644
--- a/core/modules/serialization/src/Normalizer/PrimitiveDataNormalizer.php
+++ b/core/modules/serialization/src/Normalizer/PrimitiveDataNormalizer.php
@@ -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;
+
/**
* The interface or class that this Normalizer supports.
*
@@ -20,6 +23,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,
diff --git a/core/modules/serialization/src/Normalizer/SerializedColumnNormalizerTrait.php b/core/modules/serialization/src/Normalizer/SerializedColumnNormalizerTrait.php
new file mode 100644
index 0000000000..bf6eb0643c
--- /dev/null
+++ b/core/modules/serialization/src/Normalizer/SerializedColumnNormalizerTrait.php
@@ -0,0 +1,116 @@
+<?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 [];
+ }
+
+}
diff --git a/core/modules/serialization/tests/src/Kernel/EntitySerializationTest.php b/core/modules/serialization/tests/src/Kernel/EntitySerializationTest.php
index ef5362a0ed..487cbc1258 100644
--- a/core/modules/serialization/tests/src/Kernel/EntitySerializationTest.php
+++ b/core/modules/serialization/tests/src/Kernel/EntitySerializationTest.php
@@ -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;
diff --git a/core/modules/serialization/tests/src/Unit/Normalizer/EntityReferenceFieldItemNormalizerTest.php b/core/modules/serialization/tests/src/Unit/Normalizer/EntityReferenceFieldItemNormalizerTest.php
index 96274a675f..bad2e7e7a4 100644
--- a/core/modules/serialization/tests/src/Unit/Normalizer/EntityReferenceFieldItemNormalizerTest.php
+++ b/core/modules/serialization/tests/src/Unit/Normalizer/EntityReferenceFieldItemNormalizerTest.php
@@ -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\TypedData\Type\IntegerInterface;
use Drupal\Core\TypedData\TypedDataInterface;
@@ -364,6 +365,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);
diff --git a/core/modules/serialization/tests/src/Unit/Normalizer/TimestampItemNormalizerTest.php b/core/modules/serialization/tests/src/Unit/Normalizer/TimestampItemNormalizerTest.php
index c4e351424a..47abc4601f 100644
--- a/core/modules/serialization/tests/src/Unit/Normalizer/TimestampItemNormalizerTest.php
+++ b/core/modules/serialization/tests/src/Unit/Normalizer/TimestampItemNormalizerTest.php
@@ -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;
@@ -110,6 +113,29 @@ public function testDenormalizeValidFormats($value, $expected) {
$timestamp_item->setValue(['value' => $expected])
->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()];
$denormalized = $this->normalizer->denormalize($normalized, TimestampItem::class, NULL, $context);
@@ -146,7 +172,32 @@ public function providerTestDenormalizeValidFormats() {
public function testDenormalizeException() {
$this->setExpectedException(UnexpectedValueException::class, 'The specified date "2016/11/06 09:02am GMT" is not in an accepted format: "U" (UNIX timestamp), "Y-m-d\TH:i:sO" (ISO 8601), "Y-m-d\TH:i:sP" (RFC 3339).');
- $context = ['target_instance' => $this->createTimestampItemProphecy()->reveal()];
+ $timestamp_item = $this->createTimestampItemProphecy();
+
+ // Avoid a static method call by returning dummy serialized 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()];
$normalized = ['value' => '2016/11/06 09:02am GMT'];
$this->normalizer->denormalize($normalized, TimestampItem::class, NULL, $context);
diff --git a/core/modules/shortcut/src/ShortcutSetAccessControlHandler.php b/core/modules/shortcut/src/ShortcutSetAccessControlHandler.php
index 3a55f74999..87be25d2d0 100644
--- a/core/modules/shortcut/src/ShortcutSetAccessControlHandler.php
+++ b/core/modules/shortcut/src/ShortcutSetAccessControlHandler.php
@@ -20,7 +20,7 @@ class ShortcutSetAccessControlHandler extends EntityAccessControlHandler {
protected function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) {
switch ($operation) {
case 'view':
- return AccessResult::allowedIf($account->hasPermission('access shortcuts'))->cachePerPermissions();
+ return AccessResult::allowedIfHasPermission($account, 'access shortcuts');
case 'update':
if ($account->hasPermission('administer shortcuts')) {
return AccessResult::allowed()->cachePerPermissions();
diff --git a/core/modules/shortcut/tests/src/Functional/Rest/ShortcutSetResourceTestBase.php b/core/modules/shortcut/tests/src/Functional/Rest/ShortcutSetResourceTestBase.php
index b25f028d1c..1510df288a 100644
--- a/core/modules/shortcut/tests/src/Functional/Rest/ShortcutSetResourceTestBase.php
+++ b/core/modules/shortcut/tests/src/Functional/Rest/ShortcutSetResourceTestBase.php
@@ -85,4 +85,20 @@ protected function getNormalizedPostEntity() {
// @todo Update in https://www.drupal.org/node/2300677.
}
+ /**
+ * {@inheritdoc}
+ */
+ protected function getExpectedUnauthorizedAccessMessage($method) {
+ if ($this->config('rest.settings')->get('bc_entity_resource_permissions')) {
+ return parent::getExpectedUnauthorizedAccessMessage($method);
+ }
+
+ switch ($method) {
+ case 'GET':
+ return "The 'access shortcuts' permission is required.";
+ default:
+ return parent::getExpectedUnauthorizedAccessMessage($method);
+ }
+ }
+
}
diff --git a/core/modules/system/system.install b/core/modules/system/system.install
index 6eb8c12005..91d92ec9a0 100644
--- a/core/modules/system/system.install
+++ b/core/modules/system/system.install
@@ -18,6 +18,7 @@
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\DrupalKernel;
+use Drupal\Core\Extension\Extension;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\Core\Site\Settings;
use Drupal\Core\StreamWrapper\PrivateStream;
@@ -2173,3 +2174,67 @@ function system_update_8501() {
}
}
}
+
+/**
+* Fix missing install profile after updating to Drupal 8.6.9 with Drush 8.
+*/
+function system_update_8601() {
+ $extension_config = \Drupal::configFactory()->getEditable('core.extension');
+ $install_profile = $extension_config->get('profile');
+ if (!$install_profile) {
+ // There's no install profile configured.
+ return;
+ }
+ $modules = $extension_config->get('module');
+ if (isset($modules[$install_profile])) {
+ // The install profile is already in the installed module list.
+ return;
+ }
+
+ // Ensure the install profile is available.
+ if (!\Drupal::service('extension.list.module')->exists($install_profile)) {
+ return t('The %install_profile install profile configured in core.extension is not available.', ['%install_profile' => $install_profile]);
+ }
+
+ // Add the install profile to the list of enabled modules.
+ $modules[$install_profile] = 1000;
+ $modules = module_config_sort($modules);
+ $extension_config
+ ->set('module', $modules)
+ ->save(TRUE);
+
+ // Build a module list from the updated extension configuration.
+ $current_module_filenames = \Drupal::moduleHandler()->getModuleList();
+ $current_modules = array_fill_keys(array_keys($current_module_filenames), 0);
+ $current_modules = module_config_sort(array_merge($current_modules, $extension_config->get('module')));
+ $module_filenames = [];
+ foreach ($current_modules as $name => $weight) {
+ if (isset($current_module_filenames[$name])) {
+ $module_filenames[$name] = $current_module_filenames[$name];
+ }
+ else {
+ $module_path = \Drupal::service('extension.list.module')->getPath($name);
+ $pathname = "$module_path/$name.info.yml";
+ $filename = file_exists($module_path . "/$name.module") ? "$name.module" : NULL;
+ $module_filenames[$name] = new Extension(\Drupal::root(), 'module', $pathname, $filename);
+ }
+ }
+
+ // Update the module handler list to contain the missing install profile.
+ \Drupal::moduleHandler()->setModuleList($module_filenames);
+ \Drupal::moduleHandler()->load($install_profile);
+
+ // Clear the static cache of the "extension.list.module" service to pick
+ // up the new install profile correctly.
+ \Drupal::service('extension.list.profile')->reset();
+
+ // Clear the static cache of the "extension.list.module" service to pick
+ // up the new module, since it merges the installation status of modules
+ // into its statically cached list.
+ \Drupal::service('extension.list.module')->reset();
+
+ // Update the kernel to include the missing profile.
+ \Drupal::service('kernel')->updateModules($module_filenames, $module_filenames);
+
+ return t('The %install_profile install profile has been added to the installed module list.', ['%install_profile' => $install_profile]);
+}
diff --git a/core/modules/system/tests/src/Functional/Update/WarmCacheUpdateFrom8dot6Test.php b/core/modules/system/tests/src/Functional/Update/WarmCacheUpdateFrom8dot6Test.php
index a0ebdac38b..663ba96bdb 100644
--- a/core/modules/system/tests/src/Functional/Update/WarmCacheUpdateFrom8dot6Test.php
+++ b/core/modules/system/tests/src/Functional/Update/WarmCacheUpdateFrom8dot6Test.php
@@ -2,6 +2,7 @@
namespace Drupal\Tests\system\Functional\Update;
+use Drupal\Core\Database\Database;
use Drupal\FunctionalTests\Update\UpdatePathTestBase;
use Symfony\Component\DependencyInjection\ContainerInterface;
@@ -39,6 +40,41 @@ public function testUpdatedSite() {
$this->assertSame('Australia/Sydney', $this->config('system.date')->get('timezone.default'));
}
+ /**
+ * Tests system_update_8601().
+ */
+ public function testWithMissingProfile() {
+ // Remove the install profile from the module list to simulate how Drush 8
+ // and update_fix_compatibility() worked together to remove the install
+ // profile. See https://www.drupal.org/project/drupal/issues/3031740.
+ $connection = Database::getConnection();
+ $config = $connection->select('config')
+ ->fields('config', ['data'])
+ ->condition('collection', '')
+ ->condition('name', 'core.extension')
+ ->execute()
+ ->fetchField();
+ $config = unserialize($config);
+ unset($config['module']['minimal']);
+ $connection->update('config')
+ ->fields([
+ 'data' => serialize($config),
+ 'collection' => '',
+ 'name' => 'core.extension',
+ ])
+ ->condition('collection', '')
+ ->condition('name', 'core.extension')
+ ->execute();
+
+ $this->runUpdates();
+ $this->assertSession()->pageTextContains('The minimal install profile has been added to the installed module list.');
+
+ // Login and check that the status report is working correctly.
+ $this->drupalLogin($this->rootUser);
+ $this->drupalGet('admin/reports/status');
+ $this->assertSession()->pageTextContains("Installation Profile Minimal");
+ }
+
/**
* {@inheritdoc}
*/
diff --git a/core/modules/taxonomy/tests/src/Functional/Rest/TermResourceTestBase.php b/core/modules/taxonomy/tests/src/Functional/Rest/TermResourceTestBase.php
index 3a1a5a241d..f303072a5f 100644
--- a/core/modules/taxonomy/tests/src/Functional/Rest/TermResourceTestBase.php
+++ b/core/modules/taxonomy/tests/src/Functional/Rest/TermResourceTestBase.php
@@ -356,10 +356,10 @@ public function providerTestGetTermWithParent() {
/**
* {@inheritdoc}
*/
- protected function getExpectedUnauthorizedAccessCacheability() {
+ protected function getExpectedUnauthorizedEntityAccessCacheability($is_authenticated) {
// @see \Drupal\taxonomy\TermAccessControlHandler::checkAccess()
- return parent::getExpectedUnauthorizedAccessCacheability()
- ->addCacheTags(['taxonomy_term:1']);
+ return parent::getExpectedUnauthorizedEntityAccessCacheability($is_authenticated)
+ ->addCacheTags(['taxonomy_term:1']);;
}
}
diff --git a/core/modules/user/src/UserAccessControlHandler.php b/core/modules/user/src/UserAccessControlHandler.php
index 19fe6b692c..00e53fe7d8 100644
--- a/core/modules/user/src/UserAccessControlHandler.php
+++ b/core/modules/user/src/UserAccessControlHandler.php
@@ -4,6 +4,7 @@
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Access\AccessResultNeutral;
+use Drupal\Core\Access\AccessResultReasonInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityAccessControlHandler;
use Drupal\Core\Field\FieldDefinitionInterface;
@@ -64,11 +65,16 @@ protected function checkAccess(EntityInterface $entity, $operation, AccountInter
case 'update':
// Users can always edit their own account.
- return AccessResult::allowedIf($account->id() == $entity->id())->cachePerUser();
+ $access_result = AccessResult::allowedIf($account->id() == $entity->id())->cachePerUser();
+ if (!$access_result->isAllowed() && $access_result instanceof AccessResultReasonInterface) {
+ $access_result->setReason("Users can only update their own account, unless they have the 'administer users' permission.");
+ }
+ return $access_result;
case 'delete':
// Users with 'cancel account' permission can cancel their own account.
- return AccessResult::allowedIf($account->id() == $entity->id() && $account->hasPermission('cancel account'))->cachePerPermissions()->cachePerUser();
+ return AccessResult::allowedIfHasPermission($account, 'cancel account')
+ ->andIf(AccessResult::allowedIf($account->id() == $entity->id())->cachePerUser());
}
// No opinion.
diff --git a/core/modules/user/tests/src/Functional/Rest/UserResourceTestBase.php b/core/modules/user/tests/src/Functional/Rest/UserResourceTestBase.php
index d8fe60e40c..6b544f4235 100644
--- a/core/modules/user/tests/src/Functional/Rest/UserResourceTestBase.php
+++ b/core/modules/user/tests/src/Functional/Rest/UserResourceTestBase.php
@@ -309,9 +309,9 @@ protected function getExpectedUnauthorizedAccessMessage($method) {
case 'GET':
return "The 'access user profiles' permission is required and the user must be active.";
case 'PATCH':
- return "You are not authorized to update this user entity.";
+ return "Users can only update their own account, unless they have the 'administer users' permission.";
case 'DELETE':
- return 'You are not authorized to delete this user entity.';
+ return "The 'cancel account' permission is required.";
default:
return parent::getExpectedUnauthorizedAccessMessage($method);
}
@@ -320,9 +320,9 @@ protected function getExpectedUnauthorizedAccessMessage($method) {
/**
* {@inheritdoc}
*/
- protected function getExpectedUnauthorizedAccessCacheability() {
+ protected function getExpectedUnauthorizedEntityAccessCacheability($is_authenticated) {
// @see \Drupal\user\UserAccessControlHandler::checkAccess()
- return parent::getExpectedUnauthorizedAccessCacheability()
+ return parent::getExpectedUnauthorizedEntityAccessCacheability($is_authenticated)
->addCacheTags(['user:3']);
}
diff --git a/core/tests/Drupal/KernelTests/Core/Routing/ExceptionHandlingTest.php b/core/tests/Drupal/KernelTests/Core/Routing/ExceptionHandlingTest.php
index 5d68f95e79..3215d9ce56 100644
--- a/core/tests/Drupal/KernelTests/Core/Routing/ExceptionHandlingTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Routing/ExceptionHandlingTest.php
@@ -3,6 +3,7 @@
namespace Drupal\KernelTests\Core\Routing;
use Drupal\Component\Utility\Html;
+use Drupal\Core\Cache\CacheableJsonResponse;
use Drupal\KernelTests\KernelTestBase;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
@@ -56,6 +57,7 @@ public function testJson403() {
$this->assertEqual($response->getStatusCode(), Response::HTTP_FORBIDDEN);
$this->assertEqual($response->headers->get('Content-type'), 'application/json');
$this->assertEqual('{"message":""}', $response->getContent());
+ $this->assertInstanceOf(CacheableJsonResponse::class, $response);
}
/**
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment