diff --git a/core/modules/dblog/src/Tests/Rest/DbLogResourceTest.php b/core/modules/dblog/src/Tests/Rest/DbLogResourceTest.php index 279d202a14564f92afbb0b44107ec5a071c3ba2a..1dd1f1145460a0cdcbb3eccfcb2a2614db82291e 100644 --- a/core/modules/dblog/src/Tests/Rest/DbLogResourceTest.php +++ b/core/modules/dblog/src/Tests/Rest/DbLogResourceTest.php @@ -53,13 +53,13 @@ public function testWatchdog() { $response = $this->httpRequest(Url::fromRoute('rest.dblog.GET.' . $this->defaultFormat, ['id' => 9999, '_format' => $this->defaultFormat]), 'GET'); $this->assertResponse(404); $decoded = Json::decode($response); - $this->assertEqual($decoded['message'], 'Log entry with ID 9999 was not found', 'Response message is correct.'); + $this->assertEqual($decoded['error'], 'Log entry with ID 9999 was not found', 'Response message is correct.'); // Make a bad request (a true malformed request would never be a route match). $response = $this->httpRequest(Url::fromRoute('rest.dblog.GET.' . $this->defaultFormat, ['id' => 0, '_format' => $this->defaultFormat]), 'GET'); $this->assertResponse(400); $decoded = Json::decode($response); - $this->assertEqual($decoded['message'], 'No log entry ID was provided', 'Response message is correct.'); + $this->assertEqual($decoded['error'], 'No log entry ID was provided', 'Response message is correct.'); } } diff --git a/core/modules/page_cache/src/Tests/PageCacheTest.php b/core/modules/page_cache/src/Tests/PageCacheTest.php index 22b8b51a1675dd6bae6bbb2d7ed53dbbe7efcec1..64e4ee9f8c37ee40514d4ea14040511fc6e91d23 100644 --- a/core/modules/page_cache/src/Tests/PageCacheTest.php +++ b/core/modules/page_cache/src/Tests/PageCacheTest.php @@ -8,6 +8,7 @@ use Drupal\entity_test\Entity\EntityTest; use Drupal\simpletest\WebTestBase; use Drupal\Core\Cache\Cache; +use Drupal\user\Entity\Role; use Drupal\user\RoleInterface; /** @@ -135,6 +136,10 @@ function testQueryParameterFormatRequests() { $node = $this->drupalCreateNode(['type' => 'article']); $node_uri = $node->urlInfo(); $node_url_with_hal_json_format = $node->urlInfo('canonical')->setRouteParameter('_format', 'hal_json'); + /** @var \Drupal\user\RoleInterface $role */ + $role = Role::load('anonymous'); + $role->grantPermission('restful get entity:node'); + $role->save(); $this->drupalGet($node_uri); $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'MISS'); diff --git a/core/modules/rest/config/install/rest.settings.yml b/core/modules/rest/config/install/rest.settings.yml index eb3da2df4ca6d77149ab67513f4e7e7de6b7e9d1..2d8185e5c45d67895b8965b0f1c01e4bcd4f9d02 100644 --- a/core/modules/rest/config/install/rest.settings.yml +++ b/core/modules/rest/config/install/rest.settings.yml @@ -1,11 +1,3 @@ # Set the domain for REST type and relation links. # If left blank, the site's domain will be used. link_domain: ~ - -# Before Drupal 8.2, EntityResource used permissions as well as the entity -# access system for access checking. This was confusing, and it only did this -# for historical reasons. New Drupal installations opt out from this by default -# (hence this is set to false), existing installations opt in to it. -# @see rest_update_8203() -# @see https://www.drupal.org/node/2664780 -bc_entity_resource_permissions: false diff --git a/core/modules/rest/config/schema/rest.schema.yml b/core/modules/rest/config/schema/rest.schema.yml index 2c255ab88cd4f810fc973995a11a439f01088622..04f88a6fada02a9bdbb3ca4105f5e2c383c8c598 100644 --- a/core/modules/rest/config/schema/rest.schema.yml +++ b/core/modules/rest/config/schema/rest.schema.yml @@ -6,9 +6,6 @@ rest.settings: link_domain: type: string label: 'Domain of the relation' - bc_entity_resource_permissions: - type: boolean - label: 'Whether the pre Drupal 8.2.x behavior of having permissions for EntityResource is enabled or not.' # Method-level granularity of REST resource configuration. rest_resource.method: diff --git a/core/modules/rest/rest.install b/core/modules/rest/rest.install index 90f4ec92b292fc346e57c8996bc947f8d4fa3368..4cfaa112fb0a2571c6cb7def60689bc5d1a33ffe 100644 --- a/core/modules/rest/rest.install +++ b/core/modules/rest/rest.install @@ -65,16 +65,6 @@ function rest_update_8202() { } } -/** - * Enable BC for EntityResource: continue to use permissions. - */ -function rest_update_8203() { - $config_factory = \Drupal::configFactory(); - $rest_settings = $config_factory->getEditable('rest.settings'); - $rest_settings->set('bc_entity_resource_permissions', TRUE) - ->save(TRUE); -} - /** * @} End of "defgroup updates-8.1.x-to-8.2.x". */ diff --git a/core/modules/rest/src/Plugin/ResourceBase.php b/core/modules/rest/src/Plugin/ResourceBase.php index 93684dbbc415df5d8fc3a7258b44490d389e4c70..549ac544a30d4e60c278283545f074a44fe0ed7d 100644 --- a/core/modules/rest/src/Plugin/ResourceBase.php +++ b/core/modules/rest/src/Plugin/ResourceBase.php @@ -12,11 +12,6 @@ /** * Common base class for resource plugins. * - * Note that this base class' implementation of the permissions() method - * generates a permission for every method for a resource. If your resource - * already has its own access control mechanism, you should opt out from this - * default permissions() method by overriding it. - * * @see \Drupal\rest\Annotation\RestResource * @see \Drupal\rest\Plugin\Type\ResourcePluginManager * @see \Drupal\rest\Plugin\ResourceInterface @@ -184,7 +179,7 @@ public function availableMethods() { } /** - * Gets the base route for a particular method. + * Setups the base route for all HTTP methods. * * @param string $canonical_path * The canonical path for the resource. @@ -195,48 +190,20 @@ public function availableMethods() { * The created base route. */ protected function getBaseRoute($canonical_path, $method) { - return new Route($canonical_path, array( + $lower_method = strtolower($method); + + $route = new Route($canonical_path, array( '_controller' => 'Drupal\rest\RequestHandler::handle', + ), array( + '_permission' => "restful $lower_method $this->pluginId", ), - $this->getBaseRouteRequirements($method), array(), '', array(), // The HTTP method is a requirement for this route. array($method) ); - } - - /** - * Gets the base route requirements for a particular method. - * - * @param $method - * The HTTP method to be used for the route. - * - * @return array - * An array of requirements for parameters. - */ - protected function getBaseRouteRequirements($method) { - $lower_method = strtolower($method); - // Every route MUST have requirements that result in the access manager - // having access checks to check. If it does not, the route is made - // inaccessible. So, we default to granting access to everyone. If a - // permission exists, then we add that below. The access manager requires - // that ALL access checks must grant access, so this still results in - // correct behavior. - $requirements = [ - '_access' => 'TRUE', - ]; - - // Only specify route requirements if the default permission exists. For any - // more advanced route definition, resource plugins extending this base - // class must override this method. - $permission = "restful $lower_method $this->pluginId"; - if (isset($this->permissions()[$permission])) { - $requirements['_permission'] = $permission; - } - - return $requirements; + return $route; } } diff --git a/core/modules/rest/src/Plugin/ResourceInterface.php b/core/modules/rest/src/Plugin/ResourceInterface.php index 7e92c571c49426e5e513aeb75812141c07c7a16a..0bc2bfb2a83beb79df8f864ea646462e330f180f 100644 --- a/core/modules/rest/src/Plugin/ResourceInterface.php +++ b/core/modules/rest/src/Plugin/ResourceInterface.php @@ -33,10 +33,6 @@ public function routes(); * A resource plugin can define a set of user permissions that are used on the * routes for this resource or for other purposes. * - * It is not required for a resource plugin to specify permissions: if they - * have their own access control mechanism, they can use that, and return the - * empty array. - * * @return array * The permission array. */ diff --git a/core/modules/rest/src/Plugin/rest/resource/EntityResource.php b/core/modules/rest/src/Plugin/rest/resource/EntityResource.php index 5cf42ddf64c9ca2c6f675781ea4a2bc315eba909..8b96940e317bc5f02fdcf17737d72606cfc97d3a 100644 --- a/core/modules/rest/src/Plugin/rest/resource/EntityResource.php +++ b/core/modules/rest/src/Plugin/rest/resource/EntityResource.php @@ -6,7 +6,6 @@ use Drupal\Core\Config\Entity\ConfigEntityType; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Entity\FieldableEntityInterface; -use Drupal\Core\Config\ConfigFactoryInterface; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityStorageException; use Drupal\rest\Plugin\ResourceBase; @@ -43,13 +42,6 @@ class EntityResource extends ResourceBase implements DependentPluginInterface { */ protected $entityType; - /** - * The config factory. - * - * @var \Drupal\Core\Config\ConfigFactoryInterface - */ - protected $configFactory; - /** * Constructs a Drupal\rest\Plugin\rest\resource\EntityResource object. * @@ -65,13 +57,10 @@ class EntityResource extends ResourceBase implements DependentPluginInterface { * The available serialization formats. * @param \Psr\Log\LoggerInterface $logger * A logger instance. - * @param \Drupal\Core\Config\ConfigFactoryInterface - * The config factory. */ - public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, $serializer_formats, LoggerInterface $logger, ConfigFactoryInterface $config_factory) { + public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, $serializer_formats, LoggerInterface $logger) { parent::__construct($configuration, $plugin_id, $plugin_definition, $serializer_formats, $logger); $this->entityType = $entity_type_manager->getDefinition($plugin_definition['entity_type']); - $this->configFactory = $config_factory; } /** @@ -84,8 +73,7 @@ public static function create(ContainerInterface $container, array $configuratio $plugin_definition, $container->get('entity_type.manager'), $container->getParameter('serializer.formats'), - $container->get('logger.factory')->get('rest'), - $container->get('config.factory') + $container->get('logger.factory')->get('rest') ); } @@ -309,21 +297,6 @@ protected function validate(EntityInterface $entity) { } } - /** - * {@inheritdoc} - */ - public function permissions() { - // @see https://www.drupal.org/node/2664780 - if ($this->configFactory->get('rest.settings')->get('bc_entity_resource_permissions')) { - // The default Drupal 8.0.x and 8.1.x behavior. - return parent::permissions(); - } - else { - // The default Drupal 8.2.x behavior. - return []; - } - } - /** * {@inheritdoc} */ diff --git a/core/modules/rest/src/RequestHandler.php b/core/modules/rest/src/RequestHandler.php index 088dca298932cb0c1fdb819b8479d94352514c01..d75cfbbd73a2f1f325f0d3a075bdd70381d960b0 100644 --- a/core/modules/rest/src/RequestHandler.php +++ b/core/modules/rest/src/RequestHandler.php @@ -12,6 +12,7 @@ use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Exception\HttpException; use Symfony\Component\HttpKernel\Exception\UnsupportedMediaTypeHttpException; use Symfony\Component\Serializer\Exception\UnexpectedValueException; use Symfony\Component\Serializer\SerializerInterface; @@ -130,7 +131,17 @@ public function handle(RouteMatchInterface $route_match, Request $request) { // Invoke the operation on the resource plugin. $format = $this->getResponseFormat($route_match, $request); - $response = call_user_func_array(array($resource, $method), array_merge($parameters, array($unserialized, $request))); + try { + $response = call_user_func_array(array($resource, $method), array_merge($parameters, array($unserialized, $request))); + } + catch (HttpException $e) { + $error['error'] = $e->getMessage(); + $content = $serializer->serialize($error, $format); + // Add the default content type, but only if the headers from the + // exception have not specified it already. + $headers = $e->getHeaders() + array('Content-Type' => $request->getMimeType($format)); + return new Response($content, $e->getStatusCode(), $headers); + } return $response instanceof ResourceResponseInterface ? $this->renderResponse($request, $response, $serializer, $format, $resource_config) : diff --git a/core/modules/rest/src/Tests/AuthTest.php b/core/modules/rest/src/Tests/AuthTest.php index 9f4224f64f1a58e4ce494f67a0e136e8f9f0b383..e9fb7d736a37b257d7465fc8fcd75b0dc9cf07ac 100644 --- a/core/modules/rest/src/Tests/AuthTest.php +++ b/core/modules/rest/src/Tests/AuthTest.php @@ -43,6 +43,7 @@ public function testRead() { // resources via the REST API, but the request is authenticated // with session cookies. $permissions = $this->entityPermissions($entity_type, 'view'); + $permissions[] = 'restful get entity:' . $entity_type; $account = $this->drupalCreateUser($permissions); $this->drupalLogin($account); diff --git a/core/modules/rest/src/Tests/CreateTest.php b/core/modules/rest/src/Tests/CreateTest.php index 8c7db07bac1cb550aebb27c662908b6de7a8969b..d71b9e4a0f793e4343c8f13b7436f2f793b7143f 100644 --- a/core/modules/rest/src/Tests/CreateTest.php +++ b/core/modules/rest/src/Tests/CreateTest.php @@ -5,7 +5,6 @@ use Drupal\comment\Tests\CommentTestTrait; use Drupal\Component\Serialization\Json; use Drupal\Core\Entity\EntityInterface; -use Drupal\Core\Url; use Drupal\entity_test\Entity\EntityTest; use Drupal\node\Entity\Node; use Drupal\user\Entity\User; @@ -50,6 +49,8 @@ public function testCreateResourceRestApiNotEnabled() { // Get the necessary user permissions to create the current entity type. $permissions = $this->entityPermissions($entity_type, 'create'); + // POST method must be allowed for the current entity type. + $permissions[] = 'restful post entity:' . $entity_type; // Create the user. $account = $this->drupalCreateUser($permissions); @@ -76,11 +77,7 @@ public function testCreateResourceRestApiNotEnabled() { /** * Ensure that an entity cannot be created without the restful permission. */ - public function testCreateWithoutPermissionIfBcFlagIsOn() { - $rest_settings = $this->config('rest.settings'); - $rest_settings->set('bc_entity_resource_permissions', TRUE) - ->save(TRUE); - + public function testCreateWithoutPermission() { $entity_type = 'entity_test'; // Enables the REST service for 'entity_test' entity type. $this->enableService('entity:' . $entity_type, 'POST'); @@ -99,14 +96,6 @@ public function testCreateWithoutPermissionIfBcFlagIsOn() { $this->httpRequest('entity/' . $entity_type, 'POST', $serialized, $this->defaultMimeType); $this->assertResponse(403); $this->assertFalse(EntityTest::loadMultiple(), 'No entity has been created in the database.'); - - // Create a user with the 'restful post entity:entity_test permission and - // try again. This time, we should be able to create an entity. - $permissions[] = 'restful post entity:' . $entity_type; - $account = $this->drupalCreateUser($permissions); - $this->drupalLogin($account); - $this->httpRequest('entity/' . $entity_type, 'POST', $serialized, $this->defaultMimeType); - $this->assertResponse(201); } /** @@ -342,6 +331,8 @@ public function createAccountPerEntity($entity_type) { $accounts = array(); // Get the necessary user permissions for the current $entity_type creation. $permissions = $this->entityPermissions($entity_type, 'create'); + // POST method must be allowed for the current entity type. + $permissions[] = 'restful post entity:' . $entity_type; // Create user without administrative permissions. $accounts[] = $this->drupalCreateUser($permissions); // Add administrative permissions for nodes and users. @@ -449,14 +440,14 @@ public function assertCreateEntityInvalidSerialized(EntityInterface $entity, $en $entity->set('uuid', $this->randomMachineName(129)); $invalid_serialized = $this->serializer->serialize($entity, $this->defaultFormat, $context); - $response = $this->httpRequest(Url::fromRoute("rest.entity.$entity_type.POST")->setRouteParameter('_format', $this->defaultFormat), 'POST', $invalid_serialized, $this->defaultMimeType); + $response = $this->httpRequest('entity/' . $entity_type, 'POST', $invalid_serialized, $this->defaultMimeType); // Unprocessable Entity as response. $this->assertResponse(422); // Verify that the text of the response is correct. $error = Json::decode($response); - $this->assertEqual($error['message'], "Unprocessable Entity: validation failed.\nuuid.0.value: UUID: may not be longer than 128 characters.\n"); + $this->assertEqual($error['error'], "Unprocessable Entity: validation failed.\nuuid.0.value: UUID: may not be longer than 128 characters.\n"); } /** diff --git a/core/modules/rest/src/Tests/CsrfTest.php b/core/modules/rest/src/Tests/CsrfTest.php index f1c41a6fa78dc01effd15c04970e66e08f09456c..b23e23d433f480f9a970d66129c6f88312b58302 100644 --- a/core/modules/rest/src/Tests/CsrfTest.php +++ b/core/modules/rest/src/Tests/CsrfTest.php @@ -43,6 +43,7 @@ protected function setUp() { // Create a user account that has the required permissions to create // resources via the REST API. $permissions = $this->entityPermissions($this->testEntityType, 'create'); + $permissions[] = 'restful post entity:' . $this->testEntityType; $this->account = $this->drupalCreateUser($permissions); // Serialize an entity to a string to use in the content body of the POST diff --git a/core/modules/rest/src/Tests/DeleteTest.php b/core/modules/rest/src/Tests/DeleteTest.php index 60302bcda28fe10b1bdbe07c51180e5f329d5012..7de3fb48e4a75eda1495bb857a5d0c276aac31bd 100644 --- a/core/modules/rest/src/Tests/DeleteTest.php +++ b/core/modules/rest/src/Tests/DeleteTest.php @@ -31,6 +31,7 @@ public function testDelete() { // Create a user account that has the required permissions to delete // resources via the REST API. $permissions = $this->entityPermissions($entity_type, 'delete'); + $permissions[] = 'restful delete entity:' . $entity_type; $account = $this->drupalCreateUser($permissions); $this->drupalLogin($account); diff --git a/core/modules/rest/src/Tests/NodeTest.php b/core/modules/rest/src/Tests/NodeTest.php index 95dc475e0e83d07ecb1ba0af7406cdf054b4cc76..dbdeaec0c5c6fc6722420833c80009e3528a2ee8 100644 --- a/core/modules/rest/src/Tests/NodeTest.php +++ b/core/modules/rest/src/Tests/NodeTest.php @@ -32,6 +32,7 @@ class NodeTest extends RESTTestBase { protected function enableNodeConfiguration($method, $operation) { $this->enableService('entity:node', $method); $permissions = $this->entityPermissions('node', $operation); + $permissions[] = 'restful ' . strtolower($method) . ' entity:node'; $account = $this->drupalCreateUser($permissions); $this->drupalLogin($account); } diff --git a/core/modules/rest/src/Tests/PageCacheTest.php b/core/modules/rest/src/Tests/PageCacheTest.php index 66e57f5a54d65da3dc37084f845cc0b922afc690..d4b09e98fcd2c9c2f83e60cbe1ef46aad8865f8f 100644 --- a/core/modules/rest/src/Tests/PageCacheTest.php +++ b/core/modules/rest/src/Tests/PageCacheTest.php @@ -48,6 +48,10 @@ public function testConfigChangePageCache() { $this->enableService('entity:entity_test', 'POST'); $permissions = [ 'administer entity_test content', + 'restful post entity:entity_test', + 'restful get entity:entity_test', + 'restful patch entity:entity_test', + 'restful delete entity:entity_test', ]; $account = $this->drupalCreateUser($permissions); diff --git a/core/modules/rest/src/Tests/ReadTest.php b/core/modules/rest/src/Tests/ReadTest.php index dc6f5743793df6bc3f42ee7693468a9eb488e8bf..99ebbaab53793ddaf2bf6a6fc5df8ef6131d6507 100644 --- a/core/modules/rest/src/Tests/ReadTest.php +++ b/core/modules/rest/src/Tests/ReadTest.php @@ -48,6 +48,7 @@ public function testRead() { // Create a user account that has the required permissions to read // resources via the REST API. $permissions = $this->entityPermissions($entity_type, 'view'); + $permissions[] = 'restful get entity:' . $entity_type; $account = $this->drupalCreateUser($permissions); $this->drupalLogin($account); @@ -122,6 +123,12 @@ public function testRead() { $data = Json::decode($response); $this->assertFalse(isset($data['field_test_text']), 'Field access protected field is not visible in the response.'); } + + // Try to read an entity without proper permissions. + $this->drupalLogout(); + $response = $this->httpRequest($this->getReadUrl($entity), 'GET'); + $this->assertResponse(403); + $this->assertIdentical('{"message":""}', $response); } // Try to read a resource, the user entity, which is not REST API enabled. $account = $this->drupalCreateUser(); @@ -148,6 +155,7 @@ public function testResourceStructure() { // Create a user account that has the required permissions to read // resources via the REST API. $permissions = $this->entityPermissions('node', 'view'); + $permissions[] = 'restful get entity:node'; $account = $this->drupalCreateUser($permissions); $this->drupalLogin($account); diff --git a/core/modules/rest/src/Tests/Update/EntityResourcePermissionsUpdateTest.php b/core/modules/rest/src/Tests/Update/EntityResourcePermissionsUpdateTest.php deleted file mode 100644 index 989159e1ca801140d4ae50aa88abf1f77302666a..0000000000000000000000000000000000000000 --- a/core/modules/rest/src/Tests/Update/EntityResourcePermissionsUpdateTest.php +++ /dev/null @@ -1,56 +0,0 @@ -databaseDumpFiles = [ - __DIR__ . '/../../../../system/tests/fixtures/update/drupal-8.bare.standard.php.gz', - __DIR__ . '/../../../../rest/tests/fixtures/update/drupal-8.rest-rest_update_8203.php', - ]; - } - - /** - * Tests rest_update_8203(). - */ - public function testBcEntityResourcePermissionSettingAdded() { - $permission_handler = $this->container->get('user.permissions'); - - $is_rest_resource_permission = function ($permission) { - return $permission['provider'] === 'rest' && (string) $permission['title'] !== 'Administer REST resource configuration'; - }; - - // Make sure we have the expected values before the update. - $rest_settings = $this->config('rest.settings'); - $this->assertFalse(array_key_exists('bc_entity_resource_permissions', $rest_settings->getRawData())); - $this->assertEqual([], array_filter($permission_handler->getPermissions(), $is_rest_resource_permission)); - - $this->runUpdates(); - - // Make sure we have the expected values after the update. - $rest_settings = $this->config('rest.settings'); - $this->assertTrue(array_key_exists('bc_entity_resource_permissions', $rest_settings->getRawData())); - $this->assertTrue($rest_settings->get('bc_entity_resource_permissions')); - $rest_permissions = array_keys(array_filter($permission_handler->getPermissions(), $is_rest_resource_permission)); - $this->assertEqual(['restful delete entity:node', 'restful get entity:node', 'restful patch entity:node', 'restful post entity:node'], $rest_permissions); - } - -} diff --git a/core/modules/rest/src/Tests/UpdateTest.php b/core/modules/rest/src/Tests/UpdateTest.php index 2f8030486fe9921594e0e97228a2765fdbfb0eec..dcab1ff76bd8b1a072d917afc7712c2c04894c84 100644 --- a/core/modules/rest/src/Tests/UpdateTest.php +++ b/core/modules/rest/src/Tests/UpdateTest.php @@ -46,6 +46,7 @@ public function testPatchUpdate() { // Create a user account that has the required permissions to create // resources via the REST API. $permissions = $this->entityPermissions($entity_type, 'update'); + $permissions[] = 'restful patch entity:' . $entity_type; $account = $this->drupalCreateUser($permissions); $this->drupalLogin($account); @@ -175,10 +176,10 @@ public function testPatchUpdate() { // Send a UUID that is too long. $entity->set('uuid', $this->randomMachineName(129)); $invalid_serialized = $serializer->serialize($entity, $this->defaultFormat, $context); - $response = $this->httpRequest($entity->toUrl()->setRouteParameter('_format', $this->defaultFormat), 'PATCH', $invalid_serialized, $this->defaultMimeType); + $response = $this->httpRequest($entity->urlInfo(), 'PATCH', $invalid_serialized, $this->defaultMimeType); $this->assertResponse(422); $error = Json::decode($response); - $this->assertEqual($error['message'], "Unprocessable Entity: validation failed.\nuuid.0.value: UUID: may not be longer than 128 characters.\n"); + $this->assertEqual($error['error'], "Unprocessable Entity: validation failed.\nuuid.0.value: UUID: may not be longer than 128 characters.\n"); // Try to update an entity without proper permissions. $this->drupalLogout(); @@ -201,6 +202,7 @@ public function testUpdateUser() { // Enables the REST service for 'user' entity type. $this->enableService('entity:' . $entity_type, 'PATCH'); $permissions = $this->entityPermissions($entity_type, 'update'); + $permissions[] = 'restful patch entity:' . $entity_type; $account = $this->drupalCreateUser($permissions); $account->set('mail', 'old-email@example.com'); $this->drupalLogin($account); @@ -214,18 +216,18 @@ public function testUpdateUser() { $context = ['account' => $account]; $normalized = $serializer->normalize($account, $this->defaultFormat, $context); $serialized = $serializer->serialize($normalized, $this->defaultFormat, $context); - $response = $this->httpRequest($account->toUrl()->setRouteParameter('_format', $this->defaultFormat), 'PATCH', $serialized, $this->defaultMimeType); + $response = $this->httpRequest($account->urlInfo(), 'PATCH', $serialized, $this->defaultMimeType); $this->assertResponse(422); $error = Json::decode($response); - $this->assertEqual($error['message'], "Unprocessable Entity: validation failed.\nmail: Your current password is missing or incorrect; it's required to change the Email.\n"); + $this->assertEqual($error['error'], "Unprocessable Entity: validation failed.\nmail: Your current password is missing or incorrect; it's required to change the Email.\n"); // Try and send the new email with a password. $normalized['pass'][0]['existing'] = 'wrong'; $serialized = $serializer->serialize($normalized, $this->defaultFormat, $context); - $response = $this->httpRequest($account->toUrl()->setRouteParameter('_format', $this->defaultFormat), 'PATCH', $serialized, $this->defaultMimeType); + $response = $this->httpRequest($account->urlInfo(), 'PATCH', $serialized, $this->defaultMimeType); $this->assertResponse(422); $error = Json::decode($response); - $this->assertEqual($error['message'], "Unprocessable Entity: validation failed.\nmail: Your current password is missing or incorrect; it's required to change the Email.\n"); + $this->assertEqual($error['error'], "Unprocessable Entity: validation failed.\nmail: Your current password is missing or incorrect; it's required to change the Email.\n"); // Try again with the password. $normalized['pass'][0]['existing'] = $account->pass_raw; @@ -238,10 +240,10 @@ public function testUpdateUser() { $normalized = $serializer->normalize($account, $this->defaultFormat, $context); $normalized['pass'][0]['value'] = $new_password; $serialized = $serializer->serialize($normalized, $this->defaultFormat, $context); - $response = $this->httpRequest($account->toUrl()->setRouteParameter('_format', $this->defaultFormat), 'PATCH', $serialized, $this->defaultMimeType); + $response = $this->httpRequest($account->urlInfo(), 'PATCH', $serialized, $this->defaultMimeType); $this->assertResponse(422); $error = Json::decode($response); - $this->assertEqual($error['message'], "Unprocessable Entity: validation failed.\npass: Your current password is missing or incorrect; it's required to change the Password.\n"); + $this->assertEqual($error['error'], "Unprocessable Entity: validation failed.\npass: Your current password is missing or incorrect; it's required to change the Password.\n"); // Try again with the password. $normalized['pass'][0]['existing'] = $account->pass_raw; @@ -262,6 +264,7 @@ public function testUpdateComment() { // Enables the REST service for 'comment' entity type. $this->enableService('entity:' . $entity_type, 'PATCH', ['hal_json', 'json']); $permissions = $this->entityPermissions($entity_type, 'update'); + $permissions[] = 'restful patch entity:' . $entity_type; $account = $this->drupalCreateUser($permissions); $account->set('mail', 'old-email@example.com'); $this->drupalLogin($account); @@ -333,7 +336,7 @@ public function testUpdateComment() { protected function patchEntity(EntityInterface $entity, array $read_only_fields, AccountInterface $account, $format, $mime_type) { $serializer = $this->container->get('serializer'); - $url = $entity->toUrl()->setRouteParameter('_format', $this->defaultFormat); + $url = $entity->toUrl(); $context = ['account' => $account]; // Certain fields are always read-only, others this user simply is not // allowed to modify. For all of them, ensure they are not serialized, else @@ -356,7 +359,7 @@ protected function patchEntity(EntityInterface $entity, array $read_only_fields, $this->httpRequest($url, 'PATCH', $serialized, $mime_type); $this->assertResponse(403); - $this->assertResponseBody('{"message":"Access denied on updating field \\u0027' . $field . '\\u0027."}'); + $this->assertResponseBody('{"error":"Access denied on updating field \'' . $field . '\'."}'); if ($format === 'hal_json') { // We've just tried with this read-only field, now unset it. diff --git a/core/modules/rest/tests/fixtures/update/drupal-8.rest-rest_update_8203.php b/core/modules/rest/tests/fixtures/update/drupal-8.rest-rest_update_8203.php deleted file mode 100644 index 3ce8b6abdc04d1778e615800760d060fc3d8260e..0000000000000000000000000000000000000000 --- a/core/modules/rest/tests/fixtures/update/drupal-8.rest-rest_update_8203.php +++ /dev/null @@ -1,63 +0,0 @@ -insert('key_value') - ->fields([ - 'collection' => 'system.schema', - 'name' => 'rest', - 'value' => 'i:8000;', - ]) - ->fields([ - 'collection' => 'system.schema', - 'name' => 'serialization', - 'value' => 'i:8000;', - ]) - ->execute(); - -// Update core.extension. -$extensions = $connection->select('config') - ->fields('config', ['data']) - ->condition('collection', '') - ->condition('name', 'core.extension') - ->execute() - ->fetchField(); -$extensions = unserialize($extensions); -$extensions['module']['rest'] = 8000; -$extensions['module']['serialization'] = 8000; -$connection->update('config') - ->fields([ - 'data' => serialize($extensions), - ]) - ->condition('collection', '') - ->condition('name', 'core.extension') - ->execute(); - -// Install the rest configuration. -$config = [ - 'resources' => [ - 'entity:node' => [ - 'GET' => [ - 'supported_formats' => ['json'], - 'supported_auth' => ['basic_auth'], - ], - ], - ], - 'link_domain' => '~', -]; -$data = $connection->insert('config') - ->fields([ - 'name' => 'rest.settings', - 'data' => serialize($config), - 'collection' => '' - ]) - ->execute(); diff --git a/core/modules/system/src/Tests/System/ResponseGeneratorTest.php b/core/modules/system/src/Tests/System/ResponseGeneratorTest.php index f54c6516e84c760a7f86de498b897fca473d621b..d2f8f5fbd51e08e2dceeb56365a6e97dddc935b4 100644 --- a/core/modules/system/src/Tests/System/ResponseGeneratorTest.php +++ b/core/modules/system/src/Tests/System/ResponseGeneratorTest.php @@ -26,6 +26,7 @@ protected function setUp() { $this->drupalCreateContentType(array('type' => 'page', 'name' => 'Basic page')); $permissions = $this->entityPermissions('node', 'view'); + $permissions[] = 'restful get entity:node'; $account = $this->drupalCreateUser($permissions); $this->drupalLogin($account); } diff --git a/core/modules/views/src/Entity/Render/ConfigurableLanguageRenderer.php b/core/modules/views/src/Entity/Render/ConfigurableLanguageRenderer.php index eeea6945f351b8e5fe2653dc1777011e38d7d3a1..2e28f9df3bbe37ca52fdd0c585f706c2023f3c58 100644 --- a/core/modules/views/src/Entity/Render/ConfigurableLanguageRenderer.php +++ b/core/modules/views/src/Entity/Render/ConfigurableLanguageRenderer.php @@ -39,7 +39,7 @@ public function __construct(ViewExecutable $view, LanguageManagerInterface $lang /** * {@inheritdoc} */ - public function getLangcode(ResultRow $row, $relationship = 'none') { + public function getLangcode(ResultRow $row) { return $this->langcode; } diff --git a/core/modules/views/src/Entity/Render/DefaultLanguageRenderer.php b/core/modules/views/src/Entity/Render/DefaultLanguageRenderer.php index 4f90ced9794ec0943702cb15790e79aba498259a..11dbb4ab87cdfafd5dbc5943b2bd830d3447caef 100644 --- a/core/modules/views/src/Entity/Render/DefaultLanguageRenderer.php +++ b/core/modules/views/src/Entity/Render/DefaultLanguageRenderer.php @@ -12,10 +12,8 @@ class DefaultLanguageRenderer extends EntityTranslationRendererBase { /** * {@inheritdoc} */ - public function getLangcode(ResultRow $row, $relationship = 'none') { - if ($entity = $this->getEntity($row, $relationship)) { - return $entity->getUntranslated()->language()->getId(); - } + public function getLangcode(ResultRow $row) { + return $row->_entity->getUntranslated()->language()->getId(); } } diff --git a/core/modules/views/src/Entity/Render/EntityFieldRenderer.php b/core/modules/views/src/Entity/Render/EntityFieldRenderer.php index 93a3dd444d0cff1814b7c08c93c7603ab8114a1c..1b05c1b2a3605952b197aba5ea94a9cf8c2d4b36 100644 --- a/core/modules/views/src/Entity/Render/EntityFieldRenderer.php +++ b/core/modules/views/src/Entity/Render/EntityFieldRenderer.php @@ -200,8 +200,7 @@ protected function buildFields(array $values) { $field = $this->view->field[current($field_ids)]; foreach ($values as $result_row) { if ($entity = $field->getEntity($result_row)) { - $relationship = isset($field->options['relationship']) ? $field->options['relationship'] : 'none'; - $entities_by_bundles[$entity->bundle()][$result_row->index] = $this->getEntityTranslation($entity, $result_row, $relationship); + $entities_by_bundles[$entity->bundle()][$result_row->index] = $this->getEntityTranslation($entity, $result_row); } } diff --git a/core/modules/views/src/Entity/Render/EntityTranslationRenderTrait.php b/core/modules/views/src/Entity/Render/EntityTranslationRenderTrait.php index cdbe3ada53670ced965ffbf39057d50c947ce447..e08ef04d301e4c8b5de3c4f27fda46fd465285b6 100644 --- a/core/modules/views/src/Entity/Render/EntityTranslationRenderTrait.php +++ b/core/modules/views/src/Entity/Render/EntityTranslationRenderTrait.php @@ -62,20 +62,18 @@ protected function getEntityTranslationRenderer() { * The entity object the field value being processed is attached to. * @param \Drupal\views\ResultRow $row * The result row the field value being processed belongs to. - * @param string $relationship - * The relationship to be used, or 'none' by default. * * @return \Drupal\Core\Entity\FieldableEntityInterface * The entity translation object for the specified row. */ - public function getEntityTranslation(EntityInterface $entity, ResultRow $row, $relationship = 'none') { + public function getEntityTranslation(EntityInterface $entity, ResultRow $row) { // We assume the same language should be used for all entity fields // belonging to a single row, even if they are attached to different entity // types. Below we apply language fallback to ensure a valid value is always // picked. $translation = $entity; if ($entity instanceof TranslatableInterface && count($entity->getTranslationLanguages()) > 1) { - $langcode = $this->getEntityTranslationRenderer()->getLangcode($row, $relationship); + $langcode = $this->getEntityTranslationRenderer()->getLangcode($row); $translation = $this->getEntityManager()->getTranslationFromContext($entity, $langcode); } return $translation; diff --git a/core/modules/views/src/Entity/Render/EntityTranslationRendererBase.php b/core/modules/views/src/Entity/Render/EntityTranslationRendererBase.php index c4787a413a753278ff368f44e14de98394f82ad2..e77992dd01969d98bda406379a875cca3b87a6e7 100644 --- a/core/modules/views/src/Entity/Render/EntityTranslationRendererBase.php +++ b/core/modules/views/src/Entity/Render/EntityTranslationRendererBase.php @@ -15,13 +15,11 @@ abstract class EntityTranslationRendererBase extends RendererBase { * * @param \Drupal\views\ResultRow $row * The result row. - * @param string $relationship - * The relationship to be used, or 'none' by default. * * @return string * A language code. */ - abstract public function getLangcode(ResultRow $row, $relationship = 'none'); + abstract public function getLangcode(ResultRow $row); /** * {@inheritdoc} @@ -30,62 +28,27 @@ public function query(QueryPluginBase $query, $relationship = NULL) { } /** - * Runs before each entity is rendered. - * - * @param \Drupal\views\ResultRow[] $result - * The full array of results from the query. - * @param string $relationship - * The relationship to be used, or 'none' by default. + * {@inheritdoc} */ - public function preRender(array $result, $relationship = 'none') { + public function preRender(array $result) { $view_builder = $this->view->rowPlugin->entityManager->getViewBuilder($this->entityType->id()); + /** @var \Drupal\views\ResultRow $row */ foreach ($result as $row) { - if ($entity = $this->getEntity($row, $relationship)) { - $entity->view = $this->view; - $this->build[$entity->id()] = $view_builder->view($entity, $this->view->rowPlugin->options['view_mode'], $this->getLangcode($row, $relationship)); - } + // @todo Take relationships into account. + // See https://www.drupal.org/node/2457999. + $entity = $row->_entity; + $entity->view = $this->view; + $this->build[$entity->id()] = $view_builder->view($entity, $this->view->rowPlugin->options['view_mode'], $this->getLangcode($row)); } } /** - * Renders entity data. - * - * @param \Drupal\views\ResultRow $row - * A single row of the query result. - * @param string $relationship - * The relationship to be used, or 'none' by default. - * - * @return array - * A renderable array for the entity data contained in the result row. - */ - public function render(ResultRow $row, $relationship = 'none') { - if ($entity = $this->getEntity($row, $relationship)) { - $entity_id = $entity->id(); - return $this->build[$entity_id]; - } - } - - /** - * Gets the entity assosiated with a row. - * - * @param \Drupal\views\ResultRow $row - * The result row. - * @param string $relationship - * (optional) The relationship. - * - * @return \Drupal\Core\Entity\EntityInterface|null - * The entity might be optional, because the relationship entity might not - * always exist. + * {@inheritdoc} */ - protected function getEntity($row, $relationship = 'none') { - if ($relationship === 'none') { - return $row->_entity; - } - elseif (isset($row->_relationship_entities[$relationship])) { - return $row->_relationship_entities[$relationship]; - } - return NULL; + public function render(ResultRow $row) { + $entity_id = $row->_entity->id(); + return $this->build[$entity_id]; } } diff --git a/core/modules/views/src/Entity/Render/RendererBase.php b/core/modules/views/src/Entity/Render/RendererBase.php index b47dd430812ce28f1222c9c0378132aa95f0436e..60327f615f87e6b5c0e43514e07bc3cea95ef2ac 100644 --- a/core/modules/views/src/Entity/Render/RendererBase.php +++ b/core/modules/views/src/Entity/Render/RendererBase.php @@ -93,7 +93,7 @@ abstract public function query(QueryPluginBase $query, $relationship = NULL); /** * Runs before each entity is rendered. * - * @param \Drupal\views\ResultRow[] $result + * @param $result * The full array of results from the query. */ public function preRender(array $result) { diff --git a/core/modules/views/src/Entity/Render/TranslationLanguageRenderer.php b/core/modules/views/src/Entity/Render/TranslationLanguageRenderer.php index 1d78c9d0951d9e8d4faa5ad10acdc1f6fc004395..00ae5248718570134b5d101152cd5aec40632379 100644 --- a/core/modules/views/src/Entity/Render/TranslationLanguageRenderer.php +++ b/core/modules/views/src/Entity/Render/TranslationLanguageRenderer.php @@ -40,34 +40,31 @@ public function query(QueryPluginBase $query, $relationship = NULL) { /** * {@inheritdoc} */ - public function preRender(array $result, $relationship = 'none') { + public function preRender(array $result) { $view_builder = $this->view->rowPlugin->entityManager->getViewBuilder($this->entityType->id()); /** @var \Drupal\views\ResultRow $row */ foreach ($result as $row) { - if ($entity = $this->getEntity($row, $relationship)) { - $entity->view = $this->view; - $langcode = $this->getLangcode($row, $relationship); - $this->build[$entity->id()][$langcode] = $view_builder->view($entity, $this->view->rowPlugin->options['view_mode'], $langcode); - } + $entity = $row->_entity; + $entity->view = $this->view; + $langcode = $this->getLangcode($row); + $this->build[$entity->id()][$langcode] = $view_builder->view($entity, $this->view->rowPlugin->options['view_mode'], $this->getLangcode($row)); } } /** * {@inheritdoc} */ - public function render(ResultRow $row, $relationship = 'none') { - if ($entity = $this->getEntity($row, $relationship)) { - $entity_id = $entity->id(); - $langcode = $this->getLangcode($row, $relationship); - return $this->build[$entity_id][$langcode]; - } + public function render(ResultRow $row) { + $entity_id = $row->_entity->id(); + $langcode = $this->getLangcode($row); + return $this->build[$entity_id][$langcode]; } /** * {@inheritdoc} */ - public function getLangcode(ResultRow $row, $relationship = 'none') { + public function getLangcode(ResultRow $row) { return isset($row->{$this->langcodeAlias}) ? $row->{$this->langcodeAlias} : $this->languageManager->getDefaultLanguage()->getId(); } diff --git a/core/modules/views/src/Plugin/views/row/EntityRow.php b/core/modules/views/src/Plugin/views/row/EntityRow.php index d54e03295ca14bf7e1767f3635812f5fede3380c..f5365296531c2d10ebc3addf30f0188179d573af 100644 --- a/core/modules/views/src/Plugin/views/row/EntityRow.php +++ b/core/modules/views/src/Plugin/views/row/EntityRow.php @@ -166,13 +166,7 @@ public function summaryTitle() { */ public function query() { parent::query(); - if (isset($this->options['relationship'], $this->view->relationship[$this->options['relationship']])) { - $relationship = $this->view->relationship[$this->options['relationship']]->alias; - } - else { - $relationship = NULL; - } - $this->getEntityTranslationRenderer()->query($this->view->getQuery(), $relationship); + $this->getEntityTranslationRenderer()->query($this->view->getQuery()); } /** @@ -181,7 +175,7 @@ public function query() { public function preRender($result) { parent::preRender($result); if ($result) { - $this->getEntityTranslationRenderer()->preRender($result, isset($this->options['relationship']) ? $this->options['relationship'] : 'none'); + $this->getEntityTranslationRenderer()->preRender($result); } } @@ -189,7 +183,7 @@ public function preRender($result) { * {@inheritdoc} */ public function render($row) { - return $this->getEntityTranslationRenderer()->render($row, isset($this->options['relationship']) ? $this->options['relationship'] : 'none'); + return $this->getEntityTranslationRenderer()->render($row); } /** diff --git a/core/modules/views/src/Plugin/views/row/RowPluginBase.php b/core/modules/views/src/Plugin/views/row/RowPluginBase.php index a60070bf86dc2513b12200453bd08c0ce9d1ea4b..d1e090e0aac7f7bb79f1b708f42610b88de7bdf6 100644 --- a/core/modules/views/src/Plugin/views/row/RowPluginBase.php +++ b/core/modules/views/src/Plugin/views/row/RowPluginBase.php @@ -90,7 +90,7 @@ public function buildOptionsForm(&$form, FormStateInterface $form_state) { $data = Views::viewsData()->get($relationship['table']); $base = $data[$relationship['field']]['relationship']['base']; if ($base == $this->base_table) { - $relationship_handler->init($executable, $this->displayHandler, $relationship); + $relationship_handler->init($executable, $relationship); $relationship_options[$relationship['id']] = $relationship_handler->adminLabel(); } } diff --git a/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_entity_row.yml b/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_entity_row.yml index c786e20443be757aa10cdbadb0f2828fb268b32f..ad9f748197ea9fd427d74af46d1023e2c5224697 100644 --- a/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_entity_row.yml +++ b/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_entity_row.yml @@ -6,8 +6,8 @@ label: '' module: views description: '' tag: '' -base_table: entity_test -base_field: id +base_table: taxonomy_term_field_data +base_field: nid core: '8' display: default: @@ -21,17 +21,10 @@ display: offset: 0 type: none row: - type: 'entity:entity_test' + type: 'entity:taxonomy_term' options: relationship: none view_mode: full - relationships: - user_id: - table: entity_test - field: user_id - id: user_id - relationship: none - plugin_id: standard display_plugin: default display_title: Master id: default diff --git a/core/modules/views/tests/src/Kernel/Plugin/RowEntityTest.php b/core/modules/views/tests/src/Kernel/Plugin/RowEntityTest.php index 0236b595d6dbbb1533d616dcac26a8ae91704052..8d2d86632a6614accf4fbb6eba1259d8f5905796 100644 --- a/core/modules/views/tests/src/Kernel/Plugin/RowEntityTest.php +++ b/core/modules/views/tests/src/Kernel/Plugin/RowEntityTest.php @@ -3,10 +3,10 @@ namespace Drupal\Tests\views\Kernel\Plugin; use Drupal\Core\Form\FormState; -use Drupal\entity_test\Entity\EntityTest; -use Drupal\user\Entity\User; use Drupal\views\Views; use Drupal\Tests\views\Kernel\ViewsKernelTestBase; +use Drupal\taxonomy\Entity\Vocabulary; +use Drupal\taxonomy\Entity\Term; /** * Tests the generic entity row plugin. @@ -21,7 +21,7 @@ class RowEntityTest extends ViewsKernelTestBase { * * @var array */ - public static $modules = ['entity_test', 'field', 'system', 'user']; + public static $modules = ['taxonomy', 'text', 'filter', 'field', 'system', 'node', 'user']; /** * Views used by this test. @@ -34,55 +34,27 @@ class RowEntityTest extends ViewsKernelTestBase { * {@inheritdoc} */ protected function setUp($import_test_views = TRUE) { - parent::setUp($import_test_views); + parent::setUp(); - $this->installEntitySchema('entity_test'); - $this->installEntitySchema('user'); + $this->installEntitySchema('taxonomy_term'); + $this->installConfig(array('taxonomy')); + \Drupal::service('router.builder')->rebuild(); } /** * Tests the entity row handler. */ public function testEntityRow() { - $user = User::create([ - 'name' => 'test user', - ]); - $user->save(); + $vocab = Vocabulary::create(['name' => $this->randomMachineName(), 'vid' => strtolower($this->randomMachineName())]); + $vocab->save(); + $term = Term::create(['name' => $this->randomMachineName(), 'vid' => $vocab->id() ]); + $term->save(); - $entity_test = EntityTest::create([ - 'user_id' => $user->id(), - 'name' => 'test entity test', - ]); - $entity_test->save(); - - // Ensure entities have different ids. - if ($entity_test->id() == $user->id()) { - $entity_test->delete(); - $entity_test = EntityTest::create([ - 'user_id' => $user->id(), - 'name' => 'test entity test', - ]); - $entity_test->save(); - } - - $view = Views::getView('test_entity_row'); - $build = $view->preview(); - $this->render($build); - - $this->assertText('test entity test'); - $this->assertNoText('Member for'); - - // Change the view to use a relationship to render the row. $view = Views::getView('test_entity_row'); - $display = &$view->storage->getDisplay('default'); - $display['display_options']['row']['type'] = 'entity:user'; - $display['display_options']['row']['options']['relationship'] = 'user_id'; - $view->setDisplay('default'); $build = $view->preview(); $this->render($build); - $this->assertNoText('test entity test'); - $this->assertText('Member for'); + $this->assertText($term->getName(), 'The rendered entity appears as row in the view.'); // Tests the available view mode options. $form = array();