Last active
February 20, 2019 19:56
-
-
Save esolitos/d14d423a310f139fa479a3290e75a7f8 to your computer and use it in GitHub Desktop.
SA-CORE-2019-003
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
/** |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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