Drupal 8.0.0 beta 12. More info: https://www.drupal.org/node/2514176
This commit is contained in:
commit
9921556621
13277 changed files with 1459781 additions and 0 deletions
50
core/modules/rest/config/install/rest.settings.yml
Normal file
50
core/modules/rest/config/install/rest.settings.yml
Normal file
|
@ -0,0 +1,50 @@
|
|||
# Enable all methods on nodes.
|
||||
# You must install Hal and Basic_auth modules for this to work. Also, if you are
|
||||
# going to use Basic_auth in a production environment then you should consider
|
||||
# setting up SSL.
|
||||
# There are alternatives to Basic_auth in contrib such as OAuth module.
|
||||
resources:
|
||||
entity:node:
|
||||
GET:
|
||||
supported_formats:
|
||||
- hal_json
|
||||
supported_auth:
|
||||
- basic_auth
|
||||
POST:
|
||||
supported_formats:
|
||||
- hal_json
|
||||
supported_auth:
|
||||
- basic_auth
|
||||
PATCH:
|
||||
supported_formats:
|
||||
- hal_json
|
||||
supported_auth:
|
||||
- basic_auth
|
||||
DELETE:
|
||||
supported_formats:
|
||||
- hal_json
|
||||
supported_auth:
|
||||
- basic_auth
|
||||
|
||||
# Multiple formats and multiple authentication providers can be defined for a
|
||||
# resource:
|
||||
#
|
||||
# resources:
|
||||
# entity:node:
|
||||
# GET:
|
||||
# supported_formats:
|
||||
# - json
|
||||
# - hal_json
|
||||
# - xml
|
||||
# supported_auth:
|
||||
# - cookie
|
||||
# - basic_auth
|
||||
#
|
||||
# hal_json is the only format supported for POST and PATCH methods.
|
||||
#
|
||||
# The full documentation is located at
|
||||
# https://www.drupal.org/documentation/modules/rest.
|
||||
|
||||
# Set the domain for REST type and relation links.
|
||||
# If left blank, the site's domain will be used.
|
||||
link_domain: ~
|
47
core/modules/rest/config/schema/rest.schema.yml
Normal file
47
core/modules/rest/config/schema/rest.schema.yml
Normal file
|
@ -0,0 +1,47 @@
|
|||
# Schema for the configuration files of the REST module.
|
||||
|
||||
rest.settings:
|
||||
type: config_object
|
||||
label: 'REST settings'
|
||||
mapping:
|
||||
resources:
|
||||
type: sequence
|
||||
label: 'Resources'
|
||||
sequence:
|
||||
type: rest_resource
|
||||
label: 'Resource'
|
||||
link_domain:
|
||||
type: string
|
||||
label: 'Domain of the relation'
|
||||
|
||||
rest_resource:
|
||||
type: mapping
|
||||
mapping:
|
||||
GET:
|
||||
type: rest_request
|
||||
label: 'GET method settings'
|
||||
POST:
|
||||
type: rest_request
|
||||
label: 'POST method settings'
|
||||
PATCH:
|
||||
type: rest_request
|
||||
label: 'PATCH method settings'
|
||||
DELETE:
|
||||
type: rest_request
|
||||
label: 'DELETE method settings'
|
||||
|
||||
rest_request:
|
||||
type: mapping
|
||||
mapping:
|
||||
supported_formats:
|
||||
type: sequence
|
||||
label: 'Supported format'
|
||||
sequence:
|
||||
type: string
|
||||
label: 'Format'
|
||||
supported_auth:
|
||||
type: sequence
|
||||
label: 'Supported authentication'
|
||||
sequence:
|
||||
type: string
|
||||
label: 'Authentication'
|
34
core/modules/rest/config/schema/rest.views.schema.yml
Normal file
34
core/modules/rest/config/schema/rest.views.schema.yml
Normal file
|
@ -0,0 +1,34 @@
|
|||
# Schema for the views plugins of the REST module.
|
||||
|
||||
views.display.rest_export:
|
||||
type: views_display_path
|
||||
label: 'REST display options'
|
||||
|
||||
views.row.data_field:
|
||||
type: views_row
|
||||
label: 'Field row'
|
||||
mapping:
|
||||
field_options:
|
||||
type: sequence
|
||||
label: 'Options'
|
||||
sequence:
|
||||
type: mapping
|
||||
label: 'Row'
|
||||
mapping:
|
||||
alias:
|
||||
type: string
|
||||
label: 'Alias for ID'
|
||||
raw_output:
|
||||
type: boolean
|
||||
label: 'Raw output for ID'
|
||||
|
||||
views.style.serializer:
|
||||
type: views_style
|
||||
label: 'Serialized output format'
|
||||
mapping:
|
||||
formats:
|
||||
type: sequence
|
||||
label: 'Formats'
|
||||
sequence:
|
||||
type: string
|
||||
label: 'Format'
|
80
core/modules/rest/rest.api.php
Normal file
80
core/modules/rest/rest.api.php
Normal file
|
@ -0,0 +1,80 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Describes hooks provided by the RESTful Web Services module.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @addtogroup hooks
|
||||
* @{
|
||||
*/
|
||||
|
||||
/**
|
||||
* Alter the resource plugin definitions.
|
||||
*
|
||||
* @param array $definitions
|
||||
* The collection of resource definitions.
|
||||
*/
|
||||
function hook_rest_resource_alter(&$definitions) {
|
||||
if (isset($definitions['entity:node'])) {
|
||||
// We want to handle REST requests regarding nodes with our own plugin
|
||||
// class.
|
||||
$definitions['entity:node']['class'] = 'Drupal\mymodule\Plugin\rest\resource\NodeResource';
|
||||
// Serialized nodes should be expanded to my specific node class.
|
||||
$definitions['entity:node']['serialization_class'] = 'Drupal\mymodule\Entity\MyNode';
|
||||
}
|
||||
// We don't want Views to show up in the array of plugins at all.
|
||||
unset($definitions['entity:view']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Alter the REST type URI.
|
||||
*
|
||||
* Modules may wish to alter the type URI generated for a resource based on the
|
||||
* context of the serializer/normalizer operation.
|
||||
*
|
||||
* @param string $uri
|
||||
* The URI to alter.
|
||||
* @param array $context
|
||||
* The context from the serializer/normalizer operation.
|
||||
*
|
||||
* @see \Symfony\Component\Serializer\SerializerInterface::serialize()
|
||||
* @see \Symfony\Component\Serializer\SerializerInterface::deserialize()
|
||||
* @see \Symfony\Component\Serializer\NormalizerInterface::normalize()
|
||||
* @see \Symfony\Component\Serializer\DenormalizerInterface::denormalize()
|
||||
*/
|
||||
function hook_rest_type_uri_alter(&$uri, $context = array()) {
|
||||
if ($context['mymodule'] == TRUE) {
|
||||
$base = \Drupal::config('rest.settings')->get('link_domain');
|
||||
$uri = str_replace($base, 'http://mymodule.domain', $uri);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Alter the REST relation URI.
|
||||
*
|
||||
* Modules may wish to alter the relation URI generated for a resource based on
|
||||
* the context of the serializer/normalizer operation.
|
||||
*
|
||||
* @param string $uri
|
||||
* The URI to alter.
|
||||
* @param array $context
|
||||
* The context from the serializer/normalizer operation.
|
||||
*
|
||||
* @see \Symfony\Component\Serializer\SerializerInterface::serialize()
|
||||
* @see \Symfony\Component\Serializer\SerializerInterface::deserialize()
|
||||
* @see \Symfony\Component\Serializer\NormalizerInterface::normalize()
|
||||
* @see \Symfony\Component\Serializer\DenormalizerInterface::denormalize()
|
||||
*/
|
||||
function hook_rest_relation_uri_alter(&$uri, $context = array()) {
|
||||
if ($context['mymodule'] == TRUE) {
|
||||
$base = \Drupal::config('rest.settings')->get('link_domain');
|
||||
$uri = str_replace($base, 'http://mymodule.domain', $uri);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @} End of "addtogroup hooks".
|
||||
*/
|
8
core/modules/rest/rest.info.yml
Normal file
8
core/modules/rest/rest.info.yml
Normal file
|
@ -0,0 +1,8 @@
|
|||
name: 'RESTful Web Services'
|
||||
type: module
|
||||
description: 'Exposes entities and other resources as RESTful web API'
|
||||
package: Web services
|
||||
version: VERSION
|
||||
core: 8.x
|
||||
dependencies:
|
||||
- serialization
|
23
core/modules/rest/rest.install
Normal file
23
core/modules/rest/rest.install
Normal file
|
@ -0,0 +1,23 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Install, update and uninstall functions for the rest module.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Implements hook_requirements().
|
||||
*/
|
||||
function rest_requirements($phase) {
|
||||
$requirements = array();
|
||||
|
||||
if (version_compare(PHP_VERSION, '5.6.0', '>=') && version_compare(PHP_VERSION, '7', '<') && ini_get('always_populate_raw_post_data') != -1) {
|
||||
$requirements['always_populate_raw_post_data'] = array(
|
||||
'title' => t('always_populate_raw_post_data PHP setting'),
|
||||
'value' => t('Not set to -1.'),
|
||||
'severity' => REQUIREMENT_ERROR,
|
||||
'description' => t('The always_populate_raw_post_data PHP setting should be set to -1 in PHP version 5.6. Please check the <a href="https://php.net/manual/en/ini.core.php#ini.always-populate-raw-post-data">PHP manual</a> for information on how to correct this.'),
|
||||
);
|
||||
}
|
||||
return $requirements;
|
||||
}
|
29
core/modules/rest/rest.module
Normal file
29
core/modules/rest/rest.module
Normal file
|
@ -0,0 +1,29 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* RESTful web services module.
|
||||
*/
|
||||
|
||||
use Drupal\Core\Routing\RouteMatchInterface;
|
||||
|
||||
/**
|
||||
* Implements hook_help().
|
||||
*/
|
||||
function rest_help($route_name, RouteMatchInterface $route_match) {
|
||||
switch ($route_name) {
|
||||
case 'help.page.rest':
|
||||
$output = '';
|
||||
$output .= '<h3>' . t('About') . '</h3>';
|
||||
$output .= '<p>' . t('The RESTful Web Services module provides a framework for exposing REST resources on your site. It provides support for content entities (see the <a href="!field">Field module help page</a> for more information about entities) such as content, users, taxonomy terms, etc.; REST support for content items of the Node module is enabled by default, and support for other types of content entities can be enabled. Other modules may add support for other types of REST resources. For more information, see the <a href="!rest">online documentation for the RESTful Web Services module</a>.', array('!rest' => 'https://www.drupal.org/documentation/modules/rest', '!field' => (\Drupal::moduleHandler()->moduleExists('field')) ? \Drupal::url('help.page', array('name' => 'field')) : '#')) . '</p>';
|
||||
$output .= '<h3>' . t('Uses') . '</h3>';
|
||||
$output .= '<dl>';
|
||||
$output .= '<dt>' . t('Installing supporting modules') . '</dt>';
|
||||
$output .= '<dd>' . t('In order to use REST on a web site, you need to install modules that provide serialization and authentication services. You can use the Core module <a href="!hal">HAL</a> for serialization and <a href="!basic_auth">HTTP Basic Authentication</a> for authentication, or install a contributed or custom module.', array('!hal' => (\Drupal::moduleHandler()->moduleExists('hal')) ? \Drupal::url('help.page', array('name' => 'hal')) : '#', '!basic_auth' => (\Drupal::moduleHandler()->moduleExists('basic_auth')) ? \Drupal::url('help.page', array('name' => 'basic_auth')) : '#')) . '</dd>';
|
||||
$output .= '<dt>' . t('Enabling REST support for an entity type') . '</dt>';
|
||||
$output .= '<dd>' . t('REST support for content items of the Node module is enabled by default, and support for other types of content entities can be enabled. To enable support, you can use a <a href="!config">process based on configuration editing</a> or the contributed <a href="!restui">Rest UI module</a>.', array('!config' => 'https://www.drupal.org/documentation/modules/rest', '!restui' => 'https://www.drupal.org/project/restui')) . '</dd>';
|
||||
$output .= '<dd>' . t('You will also need to grant anonymous users permission to perform each of the REST operations you want to be available, and set up authentication properly to authorize web requests.') . '</dd>';
|
||||
$output .= '</dl>';
|
||||
return $output;
|
||||
}
|
||||
}
|
2
core/modules/rest/rest.permissions.yml
Normal file
2
core/modules/rest/rest.permissions.yml
Normal file
|
@ -0,0 +1,2 @@
|
|||
permission_callbacks:
|
||||
- Drupal\rest\RestPermissions::permissions
|
6
core/modules/rest/rest.routing.yml
Normal file
6
core/modules/rest/rest.routing.yml
Normal file
|
@ -0,0 +1,6 @@
|
|||
rest.csrftoken:
|
||||
path: '/rest/session/token'
|
||||
defaults:
|
||||
_controller: '\Drupal\rest\RequestHandler::csrfToken'
|
||||
requirements:
|
||||
_access: 'TRUE'
|
32
core/modules/rest/rest.services.yml
Normal file
32
core/modules/rest/rest.services.yml
Normal file
|
@ -0,0 +1,32 @@
|
|||
services:
|
||||
plugin.manager.rest:
|
||||
class: Drupal\rest\Plugin\Type\ResourcePluginManager
|
||||
arguments: ['@container.namespaces', '@cache.discovery', '@module_handler']
|
||||
cache.rest:
|
||||
class: Drupal\Core\Cache\CacheBackendInterface
|
||||
tags:
|
||||
- { name: cache.bin }
|
||||
factory: cache_factory:get
|
||||
arguments: [rest]
|
||||
access_check.rest.csrf:
|
||||
class: Drupal\rest\Access\CSRFAccessCheck
|
||||
arguments: ['@session_configuration']
|
||||
tags:
|
||||
- { name: access_check }
|
||||
rest.link_manager:
|
||||
class: Drupal\rest\LinkManager\LinkManager
|
||||
arguments: ['@rest.link_manager.type', '@rest.link_manager.relation']
|
||||
rest.link_manager.type:
|
||||
class: Drupal\rest\LinkManager\TypeLinkManager
|
||||
arguments: ['@cache.default', '@module_handler', '@config.factory', '@request_stack']
|
||||
rest.link_manager.relation:
|
||||
class: Drupal\rest\LinkManager\RelationLinkManager
|
||||
arguments: ['@cache.default', '@entity.manager', '@module_handler', '@config.factory', '@request_stack']
|
||||
rest.resource_routes:
|
||||
class: Drupal\rest\Routing\ResourceRoutes
|
||||
arguments: ['@plugin.manager.rest', '@config.factory', '@logger.channel.rest']
|
||||
tags:
|
||||
- { name: 'event_subscriber' }
|
||||
logger.channel.rest:
|
||||
parent: logger.channel_base
|
||||
arguments: ['rest']
|
93
core/modules/rest/src/Access/CSRFAccessCheck.php
Normal file
93
core/modules/rest/src/Access/CSRFAccessCheck.php
Normal file
|
@ -0,0 +1,93 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\rest\Access\CSRFAccessCheck.
|
||||
*/
|
||||
|
||||
namespace Drupal\rest\Access;
|
||||
|
||||
use Drupal\Core\Access\AccessCheckInterface;
|
||||
use Drupal\Core\Access\AccessResult;
|
||||
use Drupal\Core\Session\AccountInterface;
|
||||
use Drupal\Core\Session\SessionConfigurationInterface;
|
||||
use Symfony\Component\Routing\Route;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
/**
|
||||
* Access protection against CSRF attacks.
|
||||
*/
|
||||
class CSRFAccessCheck implements AccessCheckInterface {
|
||||
|
||||
/**
|
||||
* The session configuration.
|
||||
*
|
||||
* @var \Drupal\Core\Session\SessionConfigurationInterface
|
||||
*/
|
||||
protected $sessionConfiguration;
|
||||
|
||||
/**
|
||||
* Constructs a new rest CSRF access check.
|
||||
*
|
||||
* @param \Drupal\Core\Session\SessionConfigurationInterface $session_configuration
|
||||
* The session configuration.
|
||||
*/
|
||||
public function __construct(SessionConfigurationInterface $session_configuration) {
|
||||
$this->sessionConfiguration = $session_configuration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements AccessCheckInterface::applies().
|
||||
*/
|
||||
public function applies(Route $route) {
|
||||
$requirements = $route->getRequirements();
|
||||
|
||||
if (array_key_exists('_access_rest_csrf', $requirements)) {
|
||||
if (isset($requirements['_method'])) {
|
||||
// There could be more than one method requirement separated with '|'.
|
||||
$methods = explode('|', $requirements['_method']);
|
||||
// CSRF protection only applies to write operations, so we can filter
|
||||
// out any routes that require reading methods only.
|
||||
$write_methods = array_diff($methods, array('GET', 'HEAD', 'OPTIONS', 'TRACE'));
|
||||
if (empty($write_methods)) {
|
||||
return FALSE;
|
||||
}
|
||||
}
|
||||
// No method requirement given, so we run this access check to be on the
|
||||
// safe side.
|
||||
return TRUE;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks access.
|
||||
*
|
||||
* @param \Symfony\Component\HttpFoundation\Request $request
|
||||
* The request object.
|
||||
* @param \Drupal\Core\Session\AccountInterface $account
|
||||
* The currently logged in account.
|
||||
*
|
||||
* @return \Drupal\Core\Access\AccessResultInterface
|
||||
* The access result.
|
||||
*/
|
||||
public function access(Request $request, AccountInterface $account) {
|
||||
$method = $request->getMethod();
|
||||
|
||||
// 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, array('GET', 'HEAD', 'OPTIONS', 'TRACE'))
|
||||
&& $account->isAuthenticated()
|
||||
&& $this->sessionConfiguration->hasSession($request)
|
||||
) {
|
||||
$csrf_token = $request->headers->get('X-CSRF-Token');
|
||||
if (!\Drupal::csrfToken()->validate($csrf_token, 'rest')) {
|
||||
return AccessResult::forbidden()->setCacheMaxAge(0);
|
||||
}
|
||||
}
|
||||
// Let other access checkers decide if the request is legit.
|
||||
return AccessResult::allowed()->setCacheMaxAge(0);
|
||||
}
|
||||
|
||||
}
|
46
core/modules/rest/src/Annotation/RestResource.php
Normal file
46
core/modules/rest/src/Annotation/RestResource.php
Normal file
|
@ -0,0 +1,46 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\rest\Annotation\RestResource.
|
||||
*/
|
||||
|
||||
namespace Drupal\rest\Annotation;
|
||||
|
||||
use \Drupal\Component\Annotation\Plugin;
|
||||
|
||||
/**
|
||||
* Defines a REST resource annotation object.
|
||||
*
|
||||
* Plugin Namespace: Plugin\rest\resource
|
||||
*
|
||||
* For a working example, see \Drupal\rest\Plugin\rest\resource\DBLogResource
|
||||
*
|
||||
* @see \Drupal\rest\Plugin\Type\ResourcePluginManager
|
||||
* @see \Drupal\rest\Plugin\ResourceBase
|
||||
* @see \Drupal\rest\Plugin\ResourceInterface
|
||||
* @see plugin_api
|
||||
*
|
||||
* @ingroup third_party
|
||||
*
|
||||
* @Annotation
|
||||
*/
|
||||
class RestResource extends Plugin {
|
||||
|
||||
/**
|
||||
* The resource plugin ID.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $id;
|
||||
|
||||
/**
|
||||
* The human-readable name of the resource plugin.
|
||||
*
|
||||
* @ingroup plugin_translatable
|
||||
*
|
||||
* @var \Drupal\Core\Annotation\Translation
|
||||
*/
|
||||
public $label;
|
||||
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\rest\LinkManager\ConfigurableLinkManagerInterface.
|
||||
*/
|
||||
|
||||
namespace Drupal\rest\LinkManager;
|
||||
|
||||
/**
|
||||
* Defines an interface for a link manager with a configurable domain.
|
||||
*/
|
||||
interface ConfigurableLinkManagerInterface {
|
||||
|
||||
/**
|
||||
* Sets the link domain used in constructing link URIs.
|
||||
*
|
||||
* @param string $domain
|
||||
* The link domain to use for constructing link URIs.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setLinkDomain($domain);
|
||||
|
||||
}
|
75
core/modules/rest/src/LinkManager/LinkManager.php
Normal file
75
core/modules/rest/src/LinkManager/LinkManager.php
Normal file
|
@ -0,0 +1,75 @@
|
|||
<?php
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\rest\LinkManager\LinkManager.
|
||||
*/
|
||||
|
||||
namespace Drupal\rest\LinkManager;
|
||||
|
||||
class LinkManager implements LinkManagerInterface {
|
||||
|
||||
/**
|
||||
* The type link manager.
|
||||
*
|
||||
* @var \Drupal\rest\LinkManager\TypeLinkManagerInterface
|
||||
*/
|
||||
protected $typeLinkManager;
|
||||
|
||||
/**
|
||||
* The relation link manager.
|
||||
*
|
||||
* @var \Drupal\rest\LinkManager\RelationLinkManagerInterface
|
||||
*/
|
||||
protected $relationLinkManager;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param \Drupal\rest\LinkManager\TypeLinkManagerInterface $type_link_manager
|
||||
* Manager for handling bundle URIs.
|
||||
* @param \Drupal\rest\LinkManager\RelationLinkManagerInterface $relation_link_manager
|
||||
* Manager for handling bundle URIs.
|
||||
*/
|
||||
public function __construct(TypeLinkManagerInterface $type_link_manager, RelationLinkManagerInterface $relation_link_manager) {
|
||||
$this->typeLinkManager = $type_link_manager;
|
||||
$this->relationLinkManager = $relation_link_manager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements \Drupal\rest\LinkManager\TypeLinkManagerInterface::getTypeUri().
|
||||
*/
|
||||
public function getTypeUri($entity_type, $bundle, $context = array()) {
|
||||
return $this->typeLinkManager->getTypeUri($entity_type, $bundle, $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements \Drupal\rest\LinkManager\TypeLinkManagerInterface::getTypeInternalIds().
|
||||
*/
|
||||
public function getTypeInternalIds($type_uri, $context = array()) {
|
||||
return $this->typeLinkManager->getTypeInternalIds($type_uri, $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements \Drupal\rest\LinkManager\RelationLinkManagerInterface::getRelationUri().
|
||||
*/
|
||||
public function getRelationUri($entity_type, $bundle, $field_name, $context = array()) {
|
||||
return $this->relationLinkManager->getRelationUri($entity_type, $bundle, $field_name, $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements \Drupal\rest\LinkManager\RelationLinkManagerInterface::getRelationInternalIds().
|
||||
*/
|
||||
public function getRelationInternalIds($relation_uri) {
|
||||
return $this->relationLinkManager->getRelationInternalIds($relation_uri);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setLinkDomain($domain) {
|
||||
$this->relationLinkManager->setLinkDomain($domain);
|
||||
$this->typeLinkManager->setLinkDomain($domain);
|
||||
return $this;
|
||||
}
|
||||
|
||||
}
|
64
core/modules/rest/src/LinkManager/LinkManagerBase.php
Normal file
64
core/modules/rest/src/LinkManager/LinkManagerBase.php
Normal file
|
@ -0,0 +1,64 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\rest\LinkManager\LinkManagerBase.
|
||||
*/
|
||||
|
||||
namespace Drupal\rest\LinkManager;
|
||||
use Drupal\Core\Url;
|
||||
|
||||
/**
|
||||
* Defines an abstract base-class for REST link manager objects.
|
||||
*/
|
||||
abstract class LinkManagerBase {
|
||||
|
||||
/**
|
||||
* Link domain used for type links URIs.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $linkDomain;
|
||||
|
||||
/**
|
||||
* Config factory service.
|
||||
*
|
||||
* @var \Drupal\Core\Config\ConfigFactoryInterface
|
||||
*/
|
||||
protected $configFactory;
|
||||
|
||||
/**
|
||||
* The request stack.
|
||||
*
|
||||
* @var \Symfony\Component\HttpFoundation\RequestStack
|
||||
*/
|
||||
protected $requestStack;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setLinkDomain($domain) {
|
||||
$this->linkDomain = rtrim($domain, '/');
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the link domain.
|
||||
*
|
||||
* @return string
|
||||
* The link domain.
|
||||
*/
|
||||
protected function getLinkDomain() {
|
||||
if (empty($this->linkDomain)) {
|
||||
if ($domain = $this->configFactory->get('rest.settings')->get('link_domain')) {
|
||||
$this->linkDomain = rtrim($domain, '/');
|
||||
}
|
||||
else {
|
||||
$request = $this->requestStack->getCurrentRequest();
|
||||
$this->linkDomain = $request->getSchemeAndHttpHost() . $request->getBasePath();
|
||||
}
|
||||
}
|
||||
return $this->linkDomain;
|
||||
}
|
||||
|
||||
}
|
23
core/modules/rest/src/LinkManager/LinkManagerInterface.php
Normal file
23
core/modules/rest/src/LinkManager/LinkManagerInterface.php
Normal file
|
@ -0,0 +1,23 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\rest\LinkManager\LinkManagerInterface.
|
||||
*/
|
||||
|
||||
namespace Drupal\rest\LinkManager;
|
||||
|
||||
/**
|
||||
* Interface implemented by link managers.
|
||||
*
|
||||
* There are no explicit methods on the manager interface. Instead link managers
|
||||
* broker the interactions of the different components, and therefore must
|
||||
* implement each component interface, which is enforced by this interface
|
||||
* extending all of the component ones.
|
||||
*
|
||||
* While a link manager may directly implement these interface methods with
|
||||
* custom logic, it is expected to be more common for plugin managers to proxy
|
||||
* the method invocations to the respective components.
|
||||
*/
|
||||
interface LinkManagerInterface extends TypeLinkManagerInterface, RelationLinkManagerInterface {
|
||||
}
|
134
core/modules/rest/src/LinkManager/RelationLinkManager.php
Normal file
134
core/modules/rest/src/LinkManager/RelationLinkManager.php
Normal file
|
@ -0,0 +1,134 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\rest\LinkManager\RelationLinkManager.
|
||||
*/
|
||||
|
||||
namespace Drupal\rest\LinkManager;
|
||||
|
||||
use Drupal\Core\Cache\Cache;
|
||||
use Drupal\Core\Cache\CacheBackendInterface;
|
||||
use Drupal\Core\Config\ConfigFactoryInterface;
|
||||
use Drupal\Core\Entity\ContentEntityTypeInterface;
|
||||
use Drupal\Core\Entity\EntityManagerInterface;
|
||||
use Drupal\Core\Extension\ModuleHandlerInterface;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
|
||||
class RelationLinkManager extends LinkManagerBase implements RelationLinkManagerInterface {
|
||||
|
||||
/**
|
||||
* @var \Drupal\Core\Cache\CacheBackendInterface;
|
||||
*/
|
||||
protected $cache;
|
||||
|
||||
/**
|
||||
* Entity manager.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityManagerInterface
|
||||
*/
|
||||
protected $entityManager;
|
||||
|
||||
/**
|
||||
* Module handler service.
|
||||
*
|
||||
* @var \Drupal\Core\Extension\ModuleHandlerInterface
|
||||
*/
|
||||
protected $moduleHandler;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param \Drupal\Core\Cache\CacheBackendInterface $cache
|
||||
* The cache of relation URIs and their associated Typed Data IDs.
|
||||
* @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
|
||||
* The entity manager.
|
||||
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
|
||||
* The module handler service.
|
||||
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
|
||||
* The config factory service.
|
||||
* @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
|
||||
* The request stack.
|
||||
*/
|
||||
public function __construct(CacheBackendInterface $cache, EntityManagerInterface $entity_manager, ModuleHandlerInterface $module_handler, ConfigFactoryInterface $config_factory, RequestStack $request_stack) {
|
||||
$this->cache = $cache;
|
||||
$this->entityManager = $entity_manager;
|
||||
$this->configFactory = $config_factory;
|
||||
$this->moduleHandler = $module_handler;
|
||||
$this->requestStack = $request_stack;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getRelationUri($entity_type, $bundle, $field_name, $context = array()) {
|
||||
$uri = $this->getLinkDomain() . "/rest/relation/$entity_type/$bundle/$field_name";
|
||||
$this->moduleHandler->alter('rest_relation_uri', $uri, $context);
|
||||
return $uri;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getRelationInternalIds($relation_uri, $context = array()) {
|
||||
$relations = $this->getRelations($context);
|
||||
if (isset($relations[$relation_uri])) {
|
||||
return $relations[$relation_uri];
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the array of relation links.
|
||||
*
|
||||
* Any field can be handled as a relation simply by changing how it is
|
||||
* normalized. Therefore, there is no prior knowledge that can be used here
|
||||
* to determine which fields to assign relation URIs. Instead, each field,
|
||||
* even primitives, are given a relation URI. It is up to the caller to
|
||||
* determine which URIs to use.
|
||||
*
|
||||
* @param array $context
|
||||
* Context from the normalizer/serializer operation.
|
||||
*
|
||||
* @return array
|
||||
* An array of typed data ids (entity_type, bundle, and field name) keyed
|
||||
* by corresponding relation URI.
|
||||
*/
|
||||
protected function getRelations($context = array()) {
|
||||
$cid = 'rest:links:relations';
|
||||
$cache = $this->cache->get($cid);
|
||||
if (!$cache) {
|
||||
$this->writeCache($context);
|
||||
$cache = $this->cache->get($cid);
|
||||
}
|
||||
return $cache->data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the cache of relation links.
|
||||
*
|
||||
* @param array $context
|
||||
* Context from the normalizer/serializer operation.
|
||||
*/
|
||||
protected function writeCache($context = array()) {
|
||||
$data = array();
|
||||
|
||||
foreach ($this->entityManager->getDefinitions() as $entity_type) {
|
||||
if ($entity_type instanceof ContentEntityTypeInterface) {
|
||||
foreach ($this->entityManager->getBundleInfo($entity_type->id()) as $bundle => $bundle_info) {
|
||||
foreach ($this->entityManager->getFieldDefinitions($entity_type->id(), $bundle) as $field_definition) {
|
||||
$relation_uri = $this->getRelationUri($entity_type->id(), $bundle, $field_definition->getName(), $context);
|
||||
$data[$relation_uri] = array(
|
||||
'entity_type' => $entity_type,
|
||||
'bundle' => $bundle,
|
||||
'field_name' => $field_definition->getName(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// These URIs only change when field info changes, so cache it permanently
|
||||
// and only clear it when the fields cache is cleared.
|
||||
$this->cache->set('rest:links:relations', $data, Cache::PERMANENT, array('entity_field_info'));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\rest\LinkManager\RelationLinkManagerInterface.
|
||||
*/
|
||||
|
||||
namespace Drupal\rest\LinkManager;
|
||||
|
||||
interface RelationLinkManagerInterface extends ConfigurableLinkManagerInterface {
|
||||
|
||||
/**
|
||||
* Gets the URI that corresponds to a field.
|
||||
*
|
||||
* @param string $entity_type
|
||||
* The bundle's entity type.
|
||||
* @param string $bundle
|
||||
* The bundle name.
|
||||
* @param string $field_name
|
||||
* The field name.
|
||||
* @param array $context
|
||||
* (optional) Optional serializer/normalizer context.
|
||||
*
|
||||
* @return string
|
||||
* The corresponding URI for the field.
|
||||
*/
|
||||
public function getRelationUri($entity_type, $bundle, $field_name, $context = array());
|
||||
|
||||
/**
|
||||
* Translates a REST URI into internal IDs.
|
||||
*
|
||||
* @param string $relation_uri
|
||||
* Relation URI to transform into internal IDs
|
||||
*
|
||||
* @return array
|
||||
* Array with keys 'entity_type', 'bundle' and 'field_name'.
|
||||
*/
|
||||
public function getRelationInternalIds($relation_uri);
|
||||
|
||||
}
|
133
core/modules/rest/src/LinkManager/TypeLinkManager.php
Normal file
133
core/modules/rest/src/LinkManager/TypeLinkManager.php
Normal file
|
@ -0,0 +1,133 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\rest\LinkManager\TypeLinkManager.
|
||||
*/
|
||||
|
||||
namespace Drupal\rest\LinkManager;
|
||||
|
||||
use Drupal\Core\Cache\Cache;
|
||||
use Drupal\Core\Cache\CacheBackendInterface;
|
||||
use Drupal\Core\Config\ConfigFactoryInterface;
|
||||
use Drupal\Core\Extension\ModuleHandlerInterface;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
|
||||
class TypeLinkManager extends LinkManagerBase implements TypeLinkManagerInterface {
|
||||
|
||||
/**
|
||||
* Injected cache backend.
|
||||
*
|
||||
* @var \Drupal\Core\Cache\CacheBackendInterface;
|
||||
*/
|
||||
protected $cache;
|
||||
|
||||
/**
|
||||
* Module handler service.
|
||||
*
|
||||
* @var \Drupal\Core\Extension\ModuleHandlerInterface
|
||||
*/
|
||||
protected $moduleHandler;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param \Drupal\Core\Cache\CacheBackendInterface $cache
|
||||
* The injected cache backend for caching type URIs.
|
||||
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
|
||||
* The module handler service.
|
||||
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
|
||||
* The config factory service.
|
||||
* @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
|
||||
* The request stack.
|
||||
*/
|
||||
public function __construct(CacheBackendInterface $cache, ModuleHandlerInterface $module_handler, ConfigFactoryInterface $config_factory, RequestStack $request_stack) {
|
||||
$this->cache = $cache;
|
||||
$this->configFactory = $config_factory;
|
||||
$this->moduleHandler = $module_handler;
|
||||
$this->requestStack = $request_stack;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a type link for a bundle.
|
||||
*
|
||||
* @param string $entity_type
|
||||
* The bundle's entity type.
|
||||
* @param string $bundle
|
||||
* The name of the bundle.
|
||||
* @param array $context
|
||||
* Context of normalizer/serializer.
|
||||
*
|
||||
* @return string
|
||||
* The URI that identifies this bundle.
|
||||
*/
|
||||
public function getTypeUri($entity_type, $bundle, $context = array()) {
|
||||
$uri = $this->getLinkDomain() . "/rest/type/$entity_type/$bundle";
|
||||
$this->moduleHandler->alter('rest_type_uri', $uri, $context);
|
||||
return $uri;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements \Drupal\rest\LinkManager\TypeLinkManagerInterface::getTypeInternalIds().
|
||||
*/
|
||||
public function getTypeInternalIds($type_uri, $context = array()) {
|
||||
$types = $this->getTypes($context);
|
||||
if (isset($types[$type_uri])) {
|
||||
return $types[$type_uri];
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the array of type links.
|
||||
*
|
||||
* @param array $context
|
||||
* Context from the normalizer/serializer operation.
|
||||
*
|
||||
* @return array
|
||||
* An array of typed data ids (entity_type and bundle) keyed by
|
||||
* corresponding type URI.
|
||||
*/
|
||||
protected function getTypes($context = array()) {
|
||||
$cid = 'rest:links:types';
|
||||
$cache = $this->cache->get($cid);
|
||||
if (!$cache) {
|
||||
$this->writeCache($context);
|
||||
$cache = $this->cache->get($cid);
|
||||
}
|
||||
return $cache->data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the cache of type links.
|
||||
*
|
||||
* @param array $context
|
||||
* Context from the normalizer/serializer operation.
|
||||
*/
|
||||
protected function writeCache($context = array()) {
|
||||
$data = array();
|
||||
|
||||
// Type URIs correspond to bundles. Iterate through the bundles to get the
|
||||
// URI and data for them.
|
||||
$entity_types = \Drupal::entityManager()->getDefinitions();
|
||||
foreach (entity_get_bundles() as $entity_type_id => $bundles) {
|
||||
// Only content entities are supported currently.
|
||||
// @todo Consider supporting config entities.
|
||||
if ($entity_types[$entity_type_id]->isSubclassOf('\Drupal\Core\Config\Entity\ConfigEntityInterface')) {
|
||||
continue;
|
||||
}
|
||||
foreach ($bundles as $bundle => $bundle_info) {
|
||||
// Get a type URI for the bundle.
|
||||
$bundle_uri = $this->getTypeUri($entity_type_id, $bundle, $context);
|
||||
$data[$bundle_uri] = array(
|
||||
'entity_type' => $entity_type_id,
|
||||
'bundle' => $bundle,
|
||||
);
|
||||
}
|
||||
}
|
||||
// These URIs only change when entity info changes, so cache it permanently
|
||||
// and only clear it when entity_info is cleared.
|
||||
$this->cache->set('rest:links:types', $data, Cache::PERMANENT, array('entity_types'));
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\rest\LinkManager\TypeLinkManagerInterface.
|
||||
*/
|
||||
|
||||
namespace Drupal\rest\LinkManager;
|
||||
|
||||
interface TypeLinkManagerInterface extends ConfigurableLinkManagerInterface {
|
||||
|
||||
/**
|
||||
* Gets the URI that corresponds to a bundle.
|
||||
*
|
||||
* When using hypermedia formats, this URI can be used to indicate which
|
||||
* bundle the data represents. Documentation about required and optional
|
||||
* fields can also be provided at this URI.
|
||||
*
|
||||
* @param $entity_type
|
||||
* The bundle's entity type.
|
||||
* @param $bundle
|
||||
* The bundle name.
|
||||
* @param array $context
|
||||
* (optional) Optional serializer/normalizer context.
|
||||
*
|
||||
* @return string
|
||||
* The corresponding URI for the bundle.
|
||||
*/
|
||||
public function getTypeUri($entity_type, $bundle, $context = array());
|
||||
|
||||
/**
|
||||
* Get a bundle's Typed Data IDs based on a URI.
|
||||
*
|
||||
* @param string $type_uri
|
||||
* The type URI.
|
||||
* @param array $context
|
||||
* Context from the normalizer/serializer operation.
|
||||
*
|
||||
* @return array | boolean
|
||||
* If the URI matches a bundle, returns an array containing entity_type and
|
||||
* bundle. Otherwise, returns false.
|
||||
*/
|
||||
public function getTypeInternalIds($type_uri, $context = array());
|
||||
}
|
104
core/modules/rest/src/Plugin/Deriver/EntityDeriver.php
Normal file
104
core/modules/rest/src/Plugin/Deriver/EntityDeriver.php
Normal file
|
@ -0,0 +1,104 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\rest\Plugin\Deriver\EntityDeriver.
|
||||
*/
|
||||
|
||||
namespace Drupal\rest\Plugin\Deriver;
|
||||
|
||||
use Drupal\Core\Entity\EntityManagerInterface;
|
||||
use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
|
||||
use Drupal\Core\Routing\RouteBuilderInterface;
|
||||
use Drupal\Core\Routing\RouteProviderInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
use Symfony\Component\Routing\Exception\RouteNotFoundException;
|
||||
|
||||
/**
|
||||
* Provides a resource plugin definition for every entity type.
|
||||
*
|
||||
* @see \Drupal\rest\Plugin\rest\resource\EntityResource
|
||||
*/
|
||||
class EntityDeriver implements ContainerDeriverInterface {
|
||||
|
||||
/**
|
||||
* List of derivative definitions.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $derivatives;
|
||||
|
||||
/**
|
||||
* The entity manager.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityManagerInterface
|
||||
*/
|
||||
protected $entityManager;
|
||||
|
||||
/**
|
||||
* Constructs an EntityDerivative object.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
|
||||
* The entity manager.
|
||||
*/
|
||||
public function __construct(EntityManagerInterface $entity_manager) {
|
||||
$this->entityManager = $entity_manager;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container, $base_plugin_id) {
|
||||
return new static(
|
||||
$container->get('entity.manager')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements DerivativeInterface::getDerivativeDefinition().
|
||||
*/
|
||||
public function getDerivativeDefinition($derivative_id, $base_plugin_definition) {
|
||||
if (!isset($this->derivatives)) {
|
||||
$this->getDerivativeDefinitions($base_plugin_definition);
|
||||
}
|
||||
if (isset($this->derivatives[$derivative_id])) {
|
||||
return $this->derivatives[$derivative_id];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements DerivativeInterface::getDerivativeDefinitions().
|
||||
*/
|
||||
public function getDerivativeDefinitions($base_plugin_definition) {
|
||||
if (!isset($this->derivatives)) {
|
||||
// Add in the default plugin configuration and the resource type.
|
||||
foreach ($this->entityManager->getDefinitions() as $entity_type_id => $entity_type) {
|
||||
$this->derivatives[$entity_type_id] = array(
|
||||
'id' => 'entity:' . $entity_type_id,
|
||||
'entity_type' => $entity_type_id,
|
||||
'serialization_class' => $entity_type->getClass(),
|
||||
'label' => $entity_type->getLabel(),
|
||||
);
|
||||
|
||||
$default_uris = array(
|
||||
'canonical' => "/entity/$entity_type_id/" . '{' . $entity_type_id . '}',
|
||||
'https://www.drupal.org/link-relations/create' => "/entity/$entity_type_id",
|
||||
);
|
||||
|
||||
foreach ($default_uris as $link_relation => $default_uri) {
|
||||
// Check if there are link templates defined for the entity type and
|
||||
// use the path from the route instead of the default.
|
||||
if ($link_template = $entity_type->getLinkTemplate($link_relation)) {
|
||||
$this->derivatives[$entity_type_id]['uri_paths'][$link_relation] = '/' . $link_template;
|
||||
}
|
||||
else {
|
||||
$this->derivatives[$entity_type_id]['uri_paths'][$link_relation] = $default_uri;
|
||||
}
|
||||
}
|
||||
|
||||
$this->derivatives[$entity_type_id] += $base_plugin_definition;
|
||||
}
|
||||
}
|
||||
return $this->derivatives;
|
||||
}
|
||||
}
|
217
core/modules/rest/src/Plugin/ResourceBase.php
Normal file
217
core/modules/rest/src/Plugin/ResourceBase.php
Normal file
|
@ -0,0 +1,217 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\rest\Plugin\ResourceBase.
|
||||
*/
|
||||
|
||||
namespace Drupal\rest\Plugin;
|
||||
|
||||
use Drupal\Core\Access\AccessManagerInterface;
|
||||
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
|
||||
use Drupal\Core\Plugin\PluginBase;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
use Symfony\Component\Routing\Route;
|
||||
use Symfony\Component\Routing\RouteCollection;
|
||||
|
||||
/**
|
||||
* Common base class for resource plugins.
|
||||
*
|
||||
* @see \Drupal\rest\Annotation\RestResource
|
||||
* @see \Drupal\rest\Plugin\Type\ResourcePluginManager
|
||||
* @see \Drupal\rest\Plugin\ResourceInterface
|
||||
* @see plugin_api
|
||||
*
|
||||
* @ingroup third_party
|
||||
*/
|
||||
abstract class ResourceBase extends PluginBase implements ContainerFactoryPluginInterface, ResourceInterface {
|
||||
|
||||
/**
|
||||
* The available serialization formats.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $serializerFormats = array();
|
||||
|
||||
/**
|
||||
* A logger instance.
|
||||
*
|
||||
* @var \Psr\Log\LoggerInterface
|
||||
*/
|
||||
protected $logger;
|
||||
|
||||
/**
|
||||
* Constructs a Drupal\rest\Plugin\ResourceBase object.
|
||||
*
|
||||
* @param array $configuration
|
||||
* A configuration array containing information about the plugin instance.
|
||||
* @param string $plugin_id
|
||||
* The plugin_id for the plugin instance.
|
||||
* @param mixed $plugin_definition
|
||||
* The plugin implementation definition.
|
||||
* @param array $serializer_formats
|
||||
* The available serialization formats.
|
||||
* @param \Psr\Log\LoggerInterface $logger
|
||||
* A logger instance.
|
||||
*/
|
||||
public function __construct(array $configuration, $plugin_id, $plugin_definition, array $serializer_formats, LoggerInterface $logger) {
|
||||
parent::__construct($configuration, $plugin_id, $plugin_definition);
|
||||
$this->serializerFormats = $serializer_formats;
|
||||
$this->logger = $logger;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
|
||||
return new static(
|
||||
$configuration,
|
||||
$plugin_id,
|
||||
$plugin_definition,
|
||||
$container->getParameter('serializer.formats'),
|
||||
$container->get('logger.factory')->get('rest')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements ResourceInterface::permissions().
|
||||
*
|
||||
* Every plugin operation method gets its own user permission. Example:
|
||||
* "restful delete entity:node" with the title "Access DELETE on Node
|
||||
* resource".
|
||||
*/
|
||||
public function permissions() {
|
||||
$permissions = array();
|
||||
$definition = $this->getPluginDefinition();
|
||||
foreach ($this->availableMethods() as $method) {
|
||||
$lowered_method = strtolower($method);
|
||||
$permissions["restful $lowered_method $this->pluginId"] = array(
|
||||
'title' => $this->t('Access @method on %label resource', array('@method' => $method, '%label' => $definition['label'])),
|
||||
);
|
||||
}
|
||||
return $permissions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements ResourceInterface::routes().
|
||||
*/
|
||||
public function routes() {
|
||||
$collection = new RouteCollection();
|
||||
|
||||
$definition = $this->getPluginDefinition();
|
||||
$canonical_path = isset($definition['uri_paths']['canonical']) ? $definition['uri_paths']['canonical'] : '/' . strtr($this->pluginId, ':', '/') . '/{id}';
|
||||
$create_path = isset($definition['uri_paths']['https://www.drupal.org/link-relations/create']) ? $definition['uri_paths']['https://www.drupal.org/link-relations/create'] : '/' . strtr($this->pluginId, ':', '/');
|
||||
|
||||
$route_name = strtr($this->pluginId, ':', '.');
|
||||
|
||||
$methods = $this->availableMethods();
|
||||
foreach ($methods as $method) {
|
||||
$route = $this->getBaseRoute($canonical_path, $method);
|
||||
|
||||
switch ($method) {
|
||||
case 'POST':
|
||||
$route->setPath($create_path);
|
||||
// Restrict the incoming HTTP Content-type header to the known
|
||||
// serialization formats.
|
||||
$route->addRequirements(array('_content_type_format' => implode('|', $this->serializerFormats)));
|
||||
$collection->add("$route_name.$method", $route);
|
||||
break;
|
||||
|
||||
case 'PATCH':
|
||||
// Restrict the incoming HTTP Content-type header to the known
|
||||
// serialization formats.
|
||||
$route->addRequirements(array('_content_type_format' => implode('|', $this->serializerFormats)));
|
||||
$collection->add("$route_name.$method", $route);
|
||||
break;
|
||||
|
||||
case 'GET':
|
||||
case 'HEAD':
|
||||
// Restrict GET and HEAD requests to the media type specified in the
|
||||
// HTTP Accept headers.
|
||||
foreach ($this->serializerFormats as $format_name) {
|
||||
// Expose one route per available format.
|
||||
$format_route = clone $route;
|
||||
$format_route->addRequirements(array('_format' => $format_name));
|
||||
$collection->add("$route_name.$method.$format_name", $format_route);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
$collection->add("$route_name.$method", $route);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $collection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides predefined HTTP request methods.
|
||||
*
|
||||
* Plugins can override this method to provide additional custom request
|
||||
* methods.
|
||||
*
|
||||
* @return array
|
||||
* The list of allowed HTTP request method strings.
|
||||
*/
|
||||
protected function requestMethods() {
|
||||
return array(
|
||||
'HEAD',
|
||||
'GET',
|
||||
'POST',
|
||||
'PUT',
|
||||
'DELETE',
|
||||
'TRACE',
|
||||
'OPTIONS',
|
||||
'CONNECT',
|
||||
'PATCH',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements ResourceInterface::availableMethods().
|
||||
*/
|
||||
public function availableMethods() {
|
||||
$methods = $this->requestMethods();
|
||||
$available = array();
|
||||
foreach ($methods as $method) {
|
||||
// Only expose methods where the HTTP request method exists on the plugin.
|
||||
if (method_exists($this, strtolower($method))) {
|
||||
$available[] = $method;
|
||||
}
|
||||
}
|
||||
return $available;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setups the base route for all HTTP methods.
|
||||
*
|
||||
* @param string $canonical_path
|
||||
* The canonical path for the resource.
|
||||
* @param string $method
|
||||
* The HTTP method to be used for the route.
|
||||
*
|
||||
* @return \Symfony\Component\Routing\Route
|
||||
* The created base route.
|
||||
*/
|
||||
protected function getBaseRoute($canonical_path, $method) {
|
||||
$lower_method = strtolower($method);
|
||||
|
||||
$route = new Route($canonical_path, array(
|
||||
'_controller' => 'Drupal\rest\RequestHandler::handle',
|
||||
// Pass the resource plugin ID along as default property.
|
||||
'_plugin' => $this->pluginId,
|
||||
), array(
|
||||
'_permission' => "restful $lower_method $this->pluginId",
|
||||
),
|
||||
array(),
|
||||
'',
|
||||
array(),
|
||||
// The HTTP method is a requirement for this route.
|
||||
array($method)
|
||||
);
|
||||
return $route;
|
||||
}
|
||||
|
||||
}
|
54
core/modules/rest/src/Plugin/ResourceInterface.php
Normal file
54
core/modules/rest/src/Plugin/ResourceInterface.php
Normal file
|
@ -0,0 +1,54 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\rest\Plugin\ResourceInterface.
|
||||
*/
|
||||
|
||||
namespace Drupal\rest\Plugin;
|
||||
|
||||
use Drupal\Component\Plugin\PluginInspectionInterface;
|
||||
|
||||
/**
|
||||
* Specifies the publicly available methods of a resource plugin.
|
||||
*
|
||||
* @see \Drupal\rest\Annotation\RestResource
|
||||
* @see \Drupal\rest\Plugin\Type\ResourcePluginManager
|
||||
* @see \Drupal\rest\Plugin\ResourceBase
|
||||
* @see plugin_api
|
||||
*
|
||||
* @ingroup third_party
|
||||
*/
|
||||
interface ResourceInterface extends PluginInspectionInterface {
|
||||
|
||||
/**
|
||||
* Returns a collection of routes with URL path information for the resource.
|
||||
*
|
||||
* This method determines where a resource is reachable, what path
|
||||
* replacements are used, the required HTTP method for the operation etc.
|
||||
*
|
||||
* @return \Symfony\Component\Routing\RouteCollection
|
||||
* A collection of routes that should be registered for this resource.
|
||||
*/
|
||||
public function routes();
|
||||
|
||||
/**
|
||||
* Provides an array of permissions suitable for .permissions.yml files.
|
||||
*
|
||||
* A resource plugin can define a set of user permissions that are used on the
|
||||
* routes for this resource or for other purposes.
|
||||
*
|
||||
* @return array
|
||||
* The permission array.
|
||||
*/
|
||||
public function permissions();
|
||||
|
||||
/**
|
||||
* Returns the available HTTP request methods on this plugin.
|
||||
*
|
||||
* @return array
|
||||
* The list of supported methods. Example: array('GET', 'POST', 'PATCH').
|
||||
*/
|
||||
public function availableMethods();
|
||||
|
||||
}
|
50
core/modules/rest/src/Plugin/Type/ResourcePluginManager.php
Normal file
50
core/modules/rest/src/Plugin/Type/ResourcePluginManager.php
Normal file
|
@ -0,0 +1,50 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\rest\Plugin\Type\ResourcePluginManager.
|
||||
*/
|
||||
|
||||
namespace Drupal\rest\Plugin\Type;
|
||||
|
||||
use Drupal\Core\Cache\CacheBackendInterface;
|
||||
use Drupal\Core\Extension\ModuleHandlerInterface;
|
||||
use Drupal\Core\Plugin\DefaultPluginManager;
|
||||
|
||||
/**
|
||||
* Manages discovery and instantiation of resource plugins.
|
||||
*
|
||||
* @see \Drupal\rest\Annotation\RestResource
|
||||
* @see \Drupal\rest\Plugin\ResourceBase
|
||||
* @see \Drupal\rest\Plugin\ResourceInterface
|
||||
* @see plugin_api
|
||||
*/
|
||||
class ResourcePluginManager extends DefaultPluginManager {
|
||||
|
||||
/**
|
||||
* Constructs a new \Drupal\rest\Plugin\Type\ResourcePluginManager object.
|
||||
*
|
||||
* @param \Traversable $namespaces
|
||||
* An object that implements \Traversable which contains the root paths
|
||||
* keyed by the corresponding namespace to look for plugin implementations.
|
||||
* @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
|
||||
* Cache backend instance to use.
|
||||
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
|
||||
* The module handler to invoke the alter hook with.
|
||||
*/
|
||||
public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) {
|
||||
parent::__construct('Plugin/rest/resource', $namespaces, $module_handler, 'Drupal\rest\Plugin\ResourceInterface', 'Drupal\rest\Annotation\RestResource');
|
||||
|
||||
$this->setCacheBackend($cache_backend, 'rest_plugins');
|
||||
$this->alterInfo('rest_resource');
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides Drupal\Component\Plugin\PluginManagerBase::getInstance().
|
||||
*/
|
||||
public function getInstance(array $options){
|
||||
if (isset($options['id'])) {
|
||||
return $this->createInstance($options['id']);
|
||||
}
|
||||
}
|
||||
}
|
245
core/modules/rest/src/Plugin/rest/resource/EntityResource.php
Normal file
245
core/modules/rest/src/Plugin/rest/resource/EntityResource.php
Normal file
|
@ -0,0 +1,245 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\rest\Plugin\rest\resource\EntityResource.
|
||||
*/
|
||||
|
||||
namespace Drupal\rest\Plugin\rest\resource;
|
||||
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
use Drupal\Core\Entity\EntityStorageException;
|
||||
use Drupal\rest\Plugin\ResourceBase;
|
||||
use Drupal\rest\ResourceResponse;
|
||||
use Drupal\Component\Utility\SafeMarkup;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||
|
||||
/**
|
||||
* Represents entities as resources.
|
||||
*
|
||||
* @RestResource(
|
||||
* id = "entity",
|
||||
* label = @Translation("Entity"),
|
||||
* serialization_class = "Drupal\Core\Entity\Entity",
|
||||
* deriver = "Drupal\rest\Plugin\Deriver\EntityDeriver",
|
||||
* uri_paths = {
|
||||
* "canonical" = "/entity/{entity_type}/{entity}",
|
||||
* "https://www.drupal.org/link-relations/create" = "/entity/{entity_type}"
|
||||
* }
|
||||
* )
|
||||
*
|
||||
* @see \Drupal\rest\Plugin\Derivative\EntityDerivative
|
||||
*/
|
||||
class EntityResource extends ResourceBase {
|
||||
|
||||
/**
|
||||
* Responds to entity GET requests.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityInterface $entity
|
||||
* The entity object.
|
||||
*
|
||||
* @return \Drupal\rest\ResourceResponse
|
||||
* The response containing the entity with its accessible fields.
|
||||
*
|
||||
* @throws \Symfony\Component\HttpKernel\Exception\HttpException
|
||||
*/
|
||||
public function get(EntityInterface $entity) {
|
||||
if (!$entity->access('view')) {
|
||||
throw new AccessDeniedHttpException();
|
||||
}
|
||||
foreach ($entity as $field_name => $field) {
|
||||
if (!$field->access('view')) {
|
||||
unset($entity->{$field_name});
|
||||
}
|
||||
}
|
||||
|
||||
$response = new ResourceResponse($entity, 200);
|
||||
// Make the response use the entity's cacheability metadata.
|
||||
// @todo include access cacheability metadata, for the access checks above.
|
||||
$response->addCacheableDependency($entity);
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Responds to entity POST requests and saves the new entity.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityInterface $entity
|
||||
* The entity.
|
||||
*
|
||||
* @return \Drupal\rest\ResourceResponse
|
||||
* The HTTP response object.
|
||||
*
|
||||
* @throws \Symfony\Component\HttpKernel\Exception\HttpException
|
||||
*/
|
||||
public function post(EntityInterface $entity = NULL) {
|
||||
if ($entity == NULL) {
|
||||
throw new BadRequestHttpException('No entity content received.');
|
||||
}
|
||||
|
||||
if (!$entity->access('create')) {
|
||||
throw new AccessDeniedHttpException();
|
||||
}
|
||||
$definition = $this->getPluginDefinition();
|
||||
// Verify that the deserialized entity is of the type that we expect to
|
||||
// prevent security issues.
|
||||
if ($entity->getEntityTypeId() != $definition['entity_type']) {
|
||||
throw new BadRequestHttpException('Invalid entity type');
|
||||
}
|
||||
// POSTed entities must not have an ID set, because we always want to create
|
||||
// new entities here.
|
||||
if (!$entity->isNew()) {
|
||||
throw new BadRequestHttpException('Only new entities can be created');
|
||||
}
|
||||
|
||||
// Only check 'edit' permissions for fields that were actually
|
||||
// submitted by the user. Field access makes no difference between 'create'
|
||||
// and 'update', so the 'edit' operation is used here.
|
||||
foreach ($entity->_restSubmittedFields as $key => $field_name) {
|
||||
if (!$entity->get($field_name)->access('edit')) {
|
||||
throw new AccessDeniedHttpException(SafeMarkup::format('Access denied on creating field @field', array('@field' => $field_name)));
|
||||
}
|
||||
}
|
||||
|
||||
// Validate the received data before saving.
|
||||
$this->validate($entity);
|
||||
try {
|
||||
$entity->save();
|
||||
$this->logger->notice('Created entity %type with ID %id.', array('%type' => $entity->getEntityTypeId(), '%id' => $entity->id()));
|
||||
|
||||
// 201 Created responses have an empty body.
|
||||
return new ResourceResponse(NULL, 201, array('Location' => $entity->url('canonical', ['absolute' => TRUE])));
|
||||
}
|
||||
catch (EntityStorageException $e) {
|
||||
throw new HttpException(500, 'Internal Server Error', $e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Responds to entity PATCH requests.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityInterface $original_entity
|
||||
* The original entity object.
|
||||
* @param \Drupal\Core\Entity\EntityInterface $entity
|
||||
* The entity.
|
||||
*
|
||||
* @return \Drupal\rest\ResourceResponse
|
||||
* The HTTP response object.
|
||||
*
|
||||
* @throws \Symfony\Component\HttpKernel\Exception\HttpException
|
||||
*/
|
||||
public function patch(EntityInterface $original_entity, EntityInterface $entity = NULL) {
|
||||
if ($entity == NULL) {
|
||||
throw new BadRequestHttpException('No entity content received.');
|
||||
}
|
||||
$definition = $this->getPluginDefinition();
|
||||
if ($entity->getEntityTypeId() != $definition['entity_type']) {
|
||||
throw new BadRequestHttpException('Invalid entity type');
|
||||
}
|
||||
if (!$original_entity->access('update')) {
|
||||
throw new AccessDeniedHttpException();
|
||||
}
|
||||
|
||||
// Overwrite the received properties.
|
||||
$langcode_key = $entity->getEntityType()->getKey('langcode');
|
||||
foreach ($entity->_restSubmittedFields as $field_name) {
|
||||
$field = $entity->get($field_name);
|
||||
// It is not possible to set the language to NULL as it is automatically
|
||||
// re-initialized. As it must not be empty, skip it if it is.
|
||||
if ($field_name == $langcode_key && $field->isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$original_entity->get($field_name)->access('edit')) {
|
||||
throw new AccessDeniedHttpException(SafeMarkup::format('Access denied on updating field @field.', array('@field' => $field_name)));
|
||||
}
|
||||
$original_entity->set($field_name, $field->getValue());
|
||||
}
|
||||
|
||||
// Validate the received data before saving.
|
||||
$this->validate($original_entity);
|
||||
try {
|
||||
$original_entity->save();
|
||||
$this->logger->notice('Updated entity %type with ID %id.', array('%type' => $entity->getEntityTypeId(), '%id' => $entity->id()));
|
||||
|
||||
// Update responses have an empty body.
|
||||
return new ResourceResponse(NULL, 204);
|
||||
}
|
||||
catch (EntityStorageException $e) {
|
||||
throw new HttpException(500, 'Internal Server Error', $e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Responds to entity DELETE requests.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityInterface $entity
|
||||
* The entity object.
|
||||
*
|
||||
* @return \Drupal\rest\ResourceResponse
|
||||
* The HTTP response object.
|
||||
*
|
||||
* @throws \Symfony\Component\HttpKernel\Exception\HttpException
|
||||
*/
|
||||
public function delete(EntityInterface $entity) {
|
||||
if (!$entity->access('delete')) {
|
||||
throw new AccessDeniedHttpException();
|
||||
}
|
||||
try {
|
||||
$entity->delete();
|
||||
$this->logger->notice('Deleted entity %type with ID %id.', array('%type' => $entity->getEntityTypeId(), '%id' => $entity->id()));
|
||||
|
||||
// Delete responses have an empty body.
|
||||
return new ResourceResponse(NULL, 204);
|
||||
}
|
||||
catch (EntityStorageException $e) {
|
||||
throw new HttpException(500, 'Internal Server Error', $e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that the whole entity does not violate any validation constraints.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityInterface $entity
|
||||
* The entity object.
|
||||
*
|
||||
* @throws \Symfony\Component\HttpKernel\Exception\HttpException
|
||||
* If validation errors are found.
|
||||
*/
|
||||
protected function validate(EntityInterface $entity) {
|
||||
$violations = $entity->validate();
|
||||
|
||||
// Remove violations of inaccessible fields as they cannot stem from our
|
||||
// changes.
|
||||
$violations->filterByFieldAccess();
|
||||
|
||||
if (count($violations) > 0) {
|
||||
$message = "Unprocessable Entity: validation failed.\n";
|
||||
foreach ($violations as $violation) {
|
||||
$message .= $violation->getPropertyPath() . ': ' . $violation->getMessage() . "\n";
|
||||
}
|
||||
// Instead of returning a generic 400 response we use the more specific
|
||||
// 422 Unprocessable Entity code from RFC 4918. That way clients can
|
||||
// distinguish between general syntax errors in bad serializations (code
|
||||
// 400) and semantic errors in well-formed requests (code 422).
|
||||
throw new HttpException(422, $message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getBaseRoute($canonical_path, $method) {
|
||||
$route = parent::getBaseRoute($canonical_path, $method);
|
||||
$definition = $this->getPluginDefinition();
|
||||
|
||||
$parameters = $route->getOption('parameters') ?: array();
|
||||
$parameters[$definition['entity_type']]['type'] = 'entity:' . $definition['entity_type'];
|
||||
$route->setOption('parameters', $parameters);
|
||||
|
||||
return $route;
|
||||
}
|
||||
|
||||
|
||||
}
|
306
core/modules/rest/src/Plugin/views/display/RestExport.php
Normal file
306
core/modules/rest/src/Plugin/views/display/RestExport.php
Normal file
|
@ -0,0 +1,306 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\rest\Plugin\views\display\RestExport.
|
||||
*/
|
||||
|
||||
namespace Drupal\rest\Plugin\views\display;
|
||||
|
||||
use Drupal\Component\Utility\SafeMarkup;
|
||||
use Drupal\Core\Cache\CacheableMetadata;
|
||||
use Drupal\Core\Cache\CacheableResponse;
|
||||
use Drupal\views\Plugin\views\display\ResponseDisplayPluginInterface;
|
||||
use Drupal\views\ViewExecutable;
|
||||
use Drupal\views\Plugin\views\display\PathPluginBase;
|
||||
use Symfony\Component\Routing\RouteCollection;
|
||||
|
||||
/**
|
||||
* The plugin that handles Data response callbacks for REST resources.
|
||||
*
|
||||
* @ingroup views_display_plugins
|
||||
*
|
||||
* @ViewsDisplay(
|
||||
* id = "rest_export",
|
||||
* title = @Translation("REST export"),
|
||||
* help = @Translation("Create a REST export resource."),
|
||||
* uses_route = TRUE,
|
||||
* admin = @Translation("REST export"),
|
||||
* returns_response = TRUE
|
||||
* )
|
||||
*/
|
||||
class RestExport extends PathPluginBase implements ResponseDisplayPluginInterface {
|
||||
|
||||
/**
|
||||
* Overrides \Drupal\views\Plugin\views\display\DisplayPluginBase::$usesAJAX.
|
||||
*/
|
||||
protected $usesAJAX = FALSE;
|
||||
|
||||
/**
|
||||
* Overrides \Drupal\views\Plugin\views\display\DisplayPluginBase::$usesPager.
|
||||
*/
|
||||
protected $usesPager = FALSE;
|
||||
|
||||
/**
|
||||
* Overrides \Drupal\views\Plugin\views\display\DisplayPluginBase::$usesMore.
|
||||
*/
|
||||
protected $usesMore = FALSE;
|
||||
|
||||
/**
|
||||
* Overrides \Drupal\views\Plugin\views\display\DisplayPluginBase::$usesAreas.
|
||||
*/
|
||||
protected $usesAreas = FALSE;
|
||||
|
||||
/**
|
||||
* Overrides \Drupal\views\Plugin\views\display\DisplayPluginBase::$usesAreas.
|
||||
*/
|
||||
protected $usesOptions = FALSE;
|
||||
|
||||
/**
|
||||
* Overrides the content type of the data response, if needed.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $contentType = 'json';
|
||||
|
||||
/**
|
||||
* The mime type for the response.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $mimeType;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function initDisplay(ViewExecutable $view, array &$display, array &$options = NULL) {
|
||||
parent::initDisplay($view, $display, $options);
|
||||
|
||||
$request_content_type = $this->view->getRequest()->getRequestFormat();
|
||||
// Only use the requested content type if it's not 'html'. If it is then
|
||||
// default to 'json' to aid debugging.
|
||||
// @todo Remove the need for this when we have better content negotiation.
|
||||
if ($request_content_type != 'html') {
|
||||
$this->setContentType($request_content_type);
|
||||
}
|
||||
// If the requested content type is 'html' and the default 'json' is not
|
||||
// selected as a format option in the view display, fallback to the first
|
||||
// format in the array.
|
||||
elseif (!empty($options['style']['options']['formats']) && !isset($options['style']['options']['formats'][$this->getContentType()])) {
|
||||
$this->setContentType(reset($options['style']['options']['formats']));
|
||||
}
|
||||
|
||||
$this->setMimeType($this->view->getRequest()->getMimeType($this->contentType));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getType() {
|
||||
return 'data';
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function usesExposed() {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function displaysExposed() {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the request content type.
|
||||
*
|
||||
* @param string $mime_type
|
||||
* The response mime type. E.g. 'application/json'.
|
||||
*/
|
||||
public function setMimeType($mime_type) {
|
||||
$this->mimeType = $mime_type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the mime type.
|
||||
*
|
||||
* This will return any overridden mime type, otherwise returns the mime type
|
||||
* from the request.
|
||||
*
|
||||
* @return string
|
||||
* The response mime type. E.g. 'application/json'.
|
||||
*/
|
||||
public function getMimeType() {
|
||||
return $this->mimeType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the content type.
|
||||
*
|
||||
* @param string $content_type
|
||||
* The content type machine name. E.g. 'json'.
|
||||
*/
|
||||
public function setContentType($content_type) {
|
||||
$this->contentType = $content_type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the content type.
|
||||
*
|
||||
* @return string
|
||||
* The content type machine name. E.g. 'json'.
|
||||
*/
|
||||
public function getContentType() {
|
||||
return $this->contentType;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function defineOptions() {
|
||||
$options = parent::defineOptions();
|
||||
|
||||
// Set the default style plugin to 'json'.
|
||||
$options['style']['contains']['type']['default'] = 'serializer';
|
||||
$options['row']['contains']['type']['default'] = 'data_entity';
|
||||
$options['defaults']['default']['style'] = FALSE;
|
||||
$options['defaults']['default']['row'] = FALSE;
|
||||
|
||||
// Remove css/exposed form settings, as they are not used for the data display.
|
||||
unset($options['exposed_form']);
|
||||
unset($options['exposed_block']);
|
||||
unset($options['css_class']);
|
||||
|
||||
return $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function optionsSummary(&$categories, &$options) {
|
||||
parent::optionsSummary($categories, $options);
|
||||
|
||||
unset($categories['page'], $categories['exposed']);
|
||||
// Hide some settings, as they aren't useful for pure data output.
|
||||
unset($options['show_admin_links'], $options['analyze-theme']);
|
||||
|
||||
$categories['path'] = array(
|
||||
'title' => $this->t('Path settings'),
|
||||
'column' => 'second',
|
||||
'build' => array(
|
||||
'#weight' => -10,
|
||||
),
|
||||
);
|
||||
|
||||
$options['path']['category'] = 'path';
|
||||
$options['path']['title'] = $this->t('Path');
|
||||
|
||||
// Remove css/exposed form settings, as they are not used for the data
|
||||
// display.
|
||||
unset($options['exposed_form']);
|
||||
unset($options['exposed_block']);
|
||||
unset($options['css_class']);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function collectRoutes(RouteCollection $collection) {
|
||||
parent::collectRoutes($collection);
|
||||
$view_id = $this->view->storage->id();
|
||||
$display_id = $this->display['id'];
|
||||
|
||||
if ($route = $collection->get("view.$view_id.$display_id")) {
|
||||
$style_plugin = $this->getPlugin('style');
|
||||
// REST exports should only respond to get methods.
|
||||
$route->setMethods(['GET']);
|
||||
|
||||
// Format as a string using pipes as a delimiter.
|
||||
if ($formats = $style_plugin->getFormats()) {
|
||||
// Allow a REST Export View to be returned with an HTML-only accept
|
||||
// format. That allows browsers or other non-compliant systems to access
|
||||
// the view, as it is unlikely to have a conflicting HTML representation
|
||||
// anyway.
|
||||
$route->setRequirement('_format', implode('|', $formats + ['html']));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function buildResponse($view_id, $display_id, array $args = []) {
|
||||
$build = static::buildBasicRenderable($view_id, $display_id, $args);
|
||||
|
||||
/** @var \Drupal\Core\Render\RendererInterface $renderer */
|
||||
$renderer = \Drupal::service('renderer');
|
||||
|
||||
$output = $renderer->renderRoot($build);
|
||||
|
||||
$response = new CacheableResponse($output, 200);
|
||||
$cache_metadata = CacheableMetadata::createFromRenderArray($build);
|
||||
$response->addCacheableDependency($cache_metadata);
|
||||
|
||||
$response->headers->set('Content-type', $build['#content_type']);
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function execute() {
|
||||
parent::execute();
|
||||
|
||||
return $this->view->render();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function render() {
|
||||
$build = array();
|
||||
$build['#markup'] = $this->view->style_plugin->render();
|
||||
|
||||
$this->view->element['#content_type'] = $this->getMimeType();
|
||||
$this->view->element['#cache_properties'][] = '#content_type';
|
||||
|
||||
// Wrap the output in a pre tag if this is for a live preview.
|
||||
if (!empty($this->view->live_preview)) {
|
||||
$build['#prefix'] = '<pre>';
|
||||
$build['#markup'] = SafeMarkup::checkPlain($build['#markup']);
|
||||
$build['#suffix'] = '</pre>';
|
||||
}
|
||||
elseif ($this->view->getRequest()->getFormat($this->view->element['#content_type']) !== 'html') {
|
||||
// This display plugin is primarily for returning non-HTML formats.
|
||||
// However, we still invoke the renderer to collect cacheability metadata.
|
||||
// Because the renderer is designed for HTML rendering, it filters
|
||||
// #markup for XSS unless it is already known to be safe, but that filter
|
||||
// only works for HTML. Therefore, we mark the contents as safe to bypass
|
||||
// the filter. So long as we are returning this in a non-HTML response
|
||||
// (checked above), this is safe, because an XSS attack only works when
|
||||
// executed by an HTML agent.
|
||||
// @todo Decide how to support non-HTML in the render API in
|
||||
// https://www.drupal.org/node/2501313.
|
||||
$build['#markup'] = SafeMarkup::set($build['#markup']);
|
||||
}
|
||||
|
||||
parent::applyDisplayCachablityMetadata($build);
|
||||
|
||||
return $build;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* The DisplayPluginBase preview method assumes we will be returning a render
|
||||
* array. The data plugin will already return the serialized string.
|
||||
*/
|
||||
public function preview() {
|
||||
return $this->view->render();
|
||||
}
|
||||
|
||||
}
|
39
core/modules/rest/src/Plugin/views/row/DataEntityRow.php
Normal file
39
core/modules/rest/src/Plugin/views/row/DataEntityRow.php
Normal file
|
@ -0,0 +1,39 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\rest\Plugin\views\row\DataEntityRow.
|
||||
*/
|
||||
|
||||
namespace Drupal\rest\Plugin\views\row;
|
||||
|
||||
use Drupal\views\ViewExecutable;
|
||||
use Drupal\views\Plugin\views\row\RowPluginBase;
|
||||
|
||||
/**
|
||||
* Plugin which displays entities as raw data.
|
||||
*
|
||||
* @ingroup views_row_plugins
|
||||
*
|
||||
* @ViewsRow(
|
||||
* id = "data_entity",
|
||||
* title = @Translation("Entity"),
|
||||
* help = @Translation("Use entities as row data."),
|
||||
* display_types = {"data"}
|
||||
* )
|
||||
*/
|
||||
class DataEntityRow extends RowPluginBase {
|
||||
|
||||
/**
|
||||
* Overrides \Drupal\views\Plugin\Plugin::$usesOptions.
|
||||
*/
|
||||
protected $usesOptions = FALSE;
|
||||
|
||||
/**
|
||||
* Overrides \Drupal\views\Plugin\views\row\RowPluginBase::render().
|
||||
*/
|
||||
public function render($row) {
|
||||
return $row->_entity;
|
||||
}
|
||||
|
||||
}
|
191
core/modules/rest/src/Plugin/views/row/DataFieldRow.php
Normal file
191
core/modules/rest/src/Plugin/views/row/DataFieldRow.php
Normal file
|
@ -0,0 +1,191 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\rest\Plugin\views\row\DataFieldRow.
|
||||
*/
|
||||
|
||||
namespace Drupal\rest\Plugin\views\row;
|
||||
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\views\ViewExecutable;
|
||||
use Drupal\views\Plugin\views\display\DisplayPluginBase;
|
||||
use Drupal\views\Plugin\views\row\RowPluginBase;
|
||||
|
||||
/**
|
||||
* Plugin which displays fields as raw data.
|
||||
*
|
||||
* @ingroup views_row_plugins
|
||||
*
|
||||
* @ViewsRow(
|
||||
* id = "data_field",
|
||||
* title = @Translation("Fields"),
|
||||
* help = @Translation("Use fields as row data."),
|
||||
* display_types = {"data"}
|
||||
* )
|
||||
*/
|
||||
class DataFieldRow extends RowPluginBase {
|
||||
|
||||
/**
|
||||
* Overrides \Drupal\views\Plugin\views\row\RowPluginBase::$usesFields.
|
||||
*/
|
||||
protected $usesFields = TRUE;
|
||||
|
||||
/**
|
||||
* Stores an array of prepared field aliases from options.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $replacementAliases = array();
|
||||
|
||||
/**
|
||||
* Stores an array of options to determine if the raw field output is used.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $rawOutputOptions = array();
|
||||
|
||||
/**
|
||||
* Overrides \Drupal\views\Plugin\views\row\RowPluginBase::init().
|
||||
*/
|
||||
public function init(ViewExecutable $view, DisplayPluginBase $display, array &$options = NULL) {
|
||||
parent::init($view, $display, $options);
|
||||
|
||||
if (!empty($this->options['field_options'])) {
|
||||
$options = (array) $this->options['field_options'];
|
||||
// Prepare a trimmed version of replacement aliases.
|
||||
$aliases = static::extractFromOptionsArray('alias', $options);
|
||||
$this->replacementAliases = array_filter(array_map('trim', $aliases));
|
||||
// Prepare an array of raw output field options.
|
||||
$this->rawOutputOptions = static::extractFromOptionsArray('raw_output', $options);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides \Drupal\views\Plugin\views\row\RowPluginBase::buildOptionsForm().
|
||||
*/
|
||||
protected function defineOptions() {
|
||||
$options = parent::defineOptions();
|
||||
$options['field_options'] = array('default' => array());
|
||||
|
||||
return $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides \Drupal\views\Plugin\views\row\RowPluginBase::buildOptionsForm().
|
||||
*/
|
||||
public function buildOptionsForm(&$form, FormStateInterface $form_state) {
|
||||
parent::buildOptionsForm($form, $form_state);
|
||||
|
||||
$form['field_options'] = array(
|
||||
'#type' => 'table',
|
||||
'#header' => array($this->t('Field'), $this->t('Alias'), $this->t('Raw output')),
|
||||
'#empty' => $this->t('You have no fields. Add some to your view.'),
|
||||
'#tree' => TRUE,
|
||||
);
|
||||
|
||||
$options = $this->options['field_options'];
|
||||
|
||||
if ($fields = $this->view->display_handler->getOption('fields')) {
|
||||
foreach ($fields as $id => $field) {
|
||||
$form['field_options'][$id]['field'] = array(
|
||||
'#markup' => $id,
|
||||
);
|
||||
$form['field_options'][$id]['alias'] = array(
|
||||
'#title' => $this->t('Alias for @id', array('@id' => $id)),
|
||||
'#title_display' => 'invisible',
|
||||
'#type' => 'textfield',
|
||||
'#default_value' => isset($options[$id]['alias']) ? $options[$id]['alias'] : '',
|
||||
'#element_validate' => array(array($this, 'validateAliasName')),
|
||||
);
|
||||
$form['field_options'][$id]['raw_output'] = array(
|
||||
'#title' => $this->t('Raw output for @id', array('@id' => $id)),
|
||||
'#title_display' => 'invisible',
|
||||
'#type' => 'checkbox',
|
||||
'#default_value' => isset($options[$id]['raw_output']) ? $options[$id]['raw_output'] : '',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Form element validation handler for \Drupal\rest\Plugin\views\row\DataFieldRow::buildOptionsForm().
|
||||
*/
|
||||
public function validateAliasName($element, FormStateInterface $form_state) {
|
||||
if (preg_match('@[^A-Za-z0-9_-]+@', $element['#value'])) {
|
||||
$form_state->setError($element, $this->t('The machine-readable name must contain only letters, numbers, dashes and underscores.'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides \Drupal\views\Plugin\views\row\RowPluginBase::validateOptionsForm().
|
||||
*/
|
||||
public function validateOptionsForm(&$form, FormStateInterface $form_state) {
|
||||
// Collect an array of aliases to validate.
|
||||
$aliases = static::extractFromOptionsArray('alias', $form_state->getValue(array('row_options', 'field_options')));
|
||||
|
||||
// If array filter returns empty, no values have been entered. Unique keys
|
||||
// should only be validated if we have some.
|
||||
if (($filtered = array_filter($aliases)) && (array_unique($filtered) !== $filtered)) {
|
||||
$form_state->setErrorByName('aliases', $this->t('All field aliases must be unique'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides \Drupal\views\Plugin\views\row\RowPluginBase::render().
|
||||
*/
|
||||
public function render($row) {
|
||||
$output = array();
|
||||
|
||||
foreach ($this->view->field as $id => $field) {
|
||||
// If this is not unknown and the raw output option has been set, just get
|
||||
// the raw value.
|
||||
if (($field->field_alias != 'unknown') && !empty($this->rawOutputOptions[$id])) {
|
||||
$value = $field->sanitizeValue($field->getValue($row), 'xss_admin');
|
||||
}
|
||||
// Otherwise, pass this through the field advancedRender() method.
|
||||
else {
|
||||
$value = $field->advancedRender($row);
|
||||
}
|
||||
|
||||
$output[$this->getFieldKeyAlias($id)] = $value;
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an alias for a field ID, as set in the options form.
|
||||
*
|
||||
* @param string $id
|
||||
* The field id to lookup an alias for.
|
||||
*
|
||||
* @return string
|
||||
* The matches user entered alias, or the original ID if nothing is found.
|
||||
*/
|
||||
public function getFieldKeyAlias($id) {
|
||||
if (isset($this->replacementAliases[$id])) {
|
||||
return $this->replacementAliases[$id];
|
||||
}
|
||||
|
||||
return $id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts a set of option values from a nested options array.
|
||||
*
|
||||
* @param string $key
|
||||
* The key to extract from each array item.
|
||||
* @param array $options
|
||||
* The options array to return values from.
|
||||
*
|
||||
* @return array
|
||||
* A regular one dimensional array of values.
|
||||
*/
|
||||
protected static function extractFromOptionsArray($key, $options) {
|
||||
return array_map(function($item) use ($key) {
|
||||
return isset($item[$key]) ? $item[$key] : NULL;
|
||||
}, $options);
|
||||
}
|
||||
|
||||
}
|
167
core/modules/rest/src/Plugin/views/style/Serializer.php
Normal file
167
core/modules/rest/src/Plugin/views/style/Serializer.php
Normal file
|
@ -0,0 +1,167 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\rest\Plugin\views\style\Serializer.
|
||||
*/
|
||||
|
||||
namespace Drupal\rest\Plugin\views\style;
|
||||
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\views\Plugin\CacheablePluginInterface;
|
||||
use Drupal\views\ViewExecutable;
|
||||
use Drupal\views\Plugin\views\display\DisplayPluginBase;
|
||||
use Drupal\views\Plugin\views\style\StylePluginBase;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
use Symfony\Component\Serializer\SerializerInterface;
|
||||
|
||||
/**
|
||||
* The style plugin for serialized output formats.
|
||||
*
|
||||
* @ingroup views_style_plugins
|
||||
*
|
||||
* @ViewsStyle(
|
||||
* id = "serializer",
|
||||
* title = @Translation("Serializer"),
|
||||
* help = @Translation("Serializes views row data using the Serializer component."),
|
||||
* display_types = {"data"}
|
||||
* )
|
||||
*/
|
||||
class Serializer extends StylePluginBase implements CacheablePluginInterface {
|
||||
|
||||
/**
|
||||
* Overrides \Drupal\views\Plugin\views\style\StylePluginBase::$usesRowPlugin.
|
||||
*/
|
||||
protected $usesRowPlugin = TRUE;
|
||||
|
||||
/**
|
||||
* Overrides Drupal\views\Plugin\views\style\StylePluginBase::$usesFields.
|
||||
*/
|
||||
protected $usesGrouping = FALSE;
|
||||
|
||||
/**
|
||||
* The serializer which serializes the views result.
|
||||
*
|
||||
* @var \Symfony\Component\Serializer\Serializer
|
||||
*/
|
||||
protected $serializer;
|
||||
|
||||
/**
|
||||
* The available serialization formats.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $formats = array();
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
|
||||
return new static(
|
||||
$configuration,
|
||||
$plugin_id,
|
||||
$plugin_definition,
|
||||
$container->get('serializer'),
|
||||
$container->getParameter('serializer.formats')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a Plugin object.
|
||||
*/
|
||||
public function __construct(array $configuration, $plugin_id, $plugin_definition, SerializerInterface $serializer, array $serializer_formats) {
|
||||
parent::__construct($configuration, $plugin_id, $plugin_definition);
|
||||
|
||||
$this->definition = $plugin_definition + $configuration;
|
||||
$this->serializer = $serializer;
|
||||
$this->formats = $serializer_formats;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function defineOptions() {
|
||||
$options = parent::defineOptions();
|
||||
$options['formats'] = array('default' => array());
|
||||
|
||||
return $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildOptionsForm(&$form, FormStateInterface $form_state) {
|
||||
parent::buildOptionsForm($form, $form_state);
|
||||
|
||||
$form['formats'] = array(
|
||||
'#type' => 'checkboxes',
|
||||
'#title' => $this->t('Accepted request formats'),
|
||||
'#description' => $this->t('Request formats that will be allowed in responses. If none are selected all formats will be allowed.'),
|
||||
'#options' => array_combine($this->formats, $this->formats),
|
||||
'#default_value' => $this->options['formats'],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function submitOptionsForm(&$form, FormStateInterface $form_state) {
|
||||
parent::submitOptionsForm($form, $form_state);
|
||||
|
||||
$formats = $form_state->getValue(array('style_options', 'formats'));
|
||||
$form_state->setValue(array('style_options', 'formats'), array_filter($formats));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function render() {
|
||||
$rows = array();
|
||||
// If the Data Entity row plugin is used, this will be an array of entities
|
||||
// which will pass through Serializer to one of the registered Normalizers,
|
||||
// which will transform it to arrays/scalars. If the Data field row plugin
|
||||
// is used, $rows will not contain objects and will pass directly to the
|
||||
// Encoder.
|
||||
foreach ($this->view->result as $row) {
|
||||
$rows[] = $this->view->rowPlugin->render($row);
|
||||
}
|
||||
|
||||
// Get the content type configured in the display or fallback to the
|
||||
// default.
|
||||
if ((empty($this->view->live_preview))) {
|
||||
$content_type = $this->displayHandler->getContentType();
|
||||
}
|
||||
else {
|
||||
$content_type = !empty($this->options['formats']) ? reset($this->options['formats']) : 'json';
|
||||
}
|
||||
return $this->serializer->serialize($rows, $content_type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a list of all available formats that can be requested.
|
||||
*
|
||||
* This will return the configured formats, or all formats if none have been
|
||||
* selected.
|
||||
*
|
||||
* @return array
|
||||
* An array of formats.
|
||||
*/
|
||||
public function getFormats() {
|
||||
return $this->options['formats'];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function isCacheable() {
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getCacheContexts() {
|
||||
return ['request_format'];
|
||||
}
|
||||
|
||||
}
|
126
core/modules/rest/src/RequestHandler.php
Normal file
126
core/modules/rest/src/RequestHandler.php
Normal file
|
@ -0,0 +1,126 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\rest\RequestHandler.
|
||||
*/
|
||||
|
||||
namespace Drupal\rest;
|
||||
|
||||
use Drupal\Core\Routing\RouteMatchInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerAwareTrait;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\UnsupportedMediaTypeHttpException;
|
||||
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
|
||||
|
||||
/**
|
||||
* Acts as intermediate request forwarder for resource plugins.
|
||||
*/
|
||||
class RequestHandler implements ContainerAwareInterface {
|
||||
|
||||
use ContainerAwareTrait;
|
||||
|
||||
/**
|
||||
* Handles a web API request.
|
||||
*
|
||||
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
|
||||
* The route match.
|
||||
* @param \Symfony\Component\HttpFoundation\Request $request
|
||||
* The HTTP request object.
|
||||
*
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
* The response object.
|
||||
*/
|
||||
public function handle(RouteMatchInterface $route_match, Request $request) {
|
||||
|
||||
$plugin = $route_match->getRouteObject()->getDefault('_plugin');
|
||||
$method = strtolower($request->getMethod());
|
||||
|
||||
$resource = $this->container
|
||||
->get('plugin.manager.rest')
|
||||
->getInstance(array('id' => $plugin));
|
||||
|
||||
// Deserialize incoming data if available.
|
||||
$serializer = $this->container->get('serializer');
|
||||
$received = $request->getContent();
|
||||
$unserialized = NULL;
|
||||
if (!empty($received)) {
|
||||
$format = $request->getContentType();
|
||||
|
||||
// Only allow serialization formats that are explicitly configured. If no
|
||||
// formats are configured allow all and hope that the serializer knows the
|
||||
// format. If the serializer cannot handle it an exception will be thrown
|
||||
// that bubbles up to the client.
|
||||
$config = $this->container->get('config.factory')->get('rest.settings')->get('resources');
|
||||
$method_settings = $config[$plugin][$request->getMethod()];
|
||||
if (empty($method_settings['supported_formats']) || in_array($format, $method_settings['supported_formats'])) {
|
||||
$definition = $resource->getPluginDefinition();
|
||||
$class = $definition['serialization_class'];
|
||||
try {
|
||||
$unserialized = $serializer->deserialize($received, $class, $format, array('request_method' => $method));
|
||||
}
|
||||
catch (UnexpectedValueException $e) {
|
||||
$error['error'] = $e->getMessage();
|
||||
$content = $serializer->serialize($error, $format);
|
||||
return new Response($content, 400, array('Content-Type' => $request->getMimeType($format)));
|
||||
}
|
||||
}
|
||||
else {
|
||||
throw new UnsupportedMediaTypeHttpException();
|
||||
}
|
||||
}
|
||||
|
||||
// Determine the request parameters that should be passed to the resource
|
||||
// plugin.
|
||||
$route_parameters = $route_match->getParameters();
|
||||
$parameters = array();
|
||||
// Filter out all internal parameters starting with "_".
|
||||
foreach ($route_parameters as $key => $parameter) {
|
||||
if ($key{0} !== '_') {
|
||||
$parameters[] = $parameter;
|
||||
}
|
||||
}
|
||||
|
||||
// Invoke the operation on the resource plugin.
|
||||
// All REST routes are restricted to exactly one format, so instead of
|
||||
// parsing it out of the Accept headers again, we can simply retrieve the
|
||||
// format requirement. If there is no format associated, just pick JSON.
|
||||
$format = $route_match->getRouteObject()->getRequirement('_format') ?: 'json';
|
||||
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);
|
||||
}
|
||||
|
||||
// Serialize the outgoing data for the response, if available.
|
||||
$data = $response->getResponseData();
|
||||
if ($data != NULL) {
|
||||
$output = $serializer->serialize($data, $format);
|
||||
$response->setContent($output);
|
||||
$response->headers->set('Content-Type', $request->getMimeType($format));
|
||||
// Add rest settings config's cache tags.
|
||||
$response->addCacheableDependency($this->container->get('config.factory')->get('rest.settings'));
|
||||
}
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a CSRF protecting session token.
|
||||
*
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
* The response object.
|
||||
*/
|
||||
public function csrfToken() {
|
||||
return new Response(\Drupal::csrfToken()->get('rest'), 200, array('Content-Type' => 'text/plain'));
|
||||
}
|
||||
}
|
57
core/modules/rest/src/ResourceResponse.php
Normal file
57
core/modules/rest/src/ResourceResponse.php
Normal file
|
@ -0,0 +1,57 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\rest\ResourceResponse.
|
||||
*/
|
||||
|
||||
namespace Drupal\rest;
|
||||
|
||||
use Drupal\Core\Cache\CacheableResponseInterface;
|
||||
use Drupal\Core\Cache\CacheableResponseTrait;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
/**
|
||||
* Contains data for serialization before sending the response.
|
||||
*
|
||||
* We do not want to abuse the $content property on the Response class to store
|
||||
* our response data. $content implies that the provided data must either be a
|
||||
* string or an object with a __toString() method, which is not a requirement
|
||||
* for data used here.
|
||||
*/
|
||||
class ResourceResponse extends Response implements CacheableResponseInterface {
|
||||
|
||||
use CacheableResponseTrait;
|
||||
|
||||
/**
|
||||
* Response data that should be serialized.
|
||||
*
|
||||
* @var mixed
|
||||
*/
|
||||
protected $responseData;
|
||||
|
||||
/**
|
||||
* Constructor for ResourceResponse objects.
|
||||
*
|
||||
* @param mixed $data
|
||||
* Response data that should be serialized.
|
||||
* @param int $status
|
||||
* The response status code.
|
||||
* @param array $headers
|
||||
* An array of response headers.
|
||||
*/
|
||||
public function __construct($data = NULL, $status = 200, $headers = array()) {
|
||||
$this->responseData = $data;
|
||||
parent::__construct('', $status, $headers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns response data that should be serialized.
|
||||
*
|
||||
* @return mixed
|
||||
* Response data that should be serialized.
|
||||
*/
|
||||
public function getResponseData() {
|
||||
return $this->responseData;
|
||||
}
|
||||
}
|
71
core/modules/rest/src/RestPermissions.php
Normal file
71
core/modules/rest/src/RestPermissions.php
Normal file
|
@ -0,0 +1,71 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\rest\RestPermissions.
|
||||
*/
|
||||
|
||||
namespace Drupal\rest;
|
||||
|
||||
use Drupal\Core\Config\ConfigFactoryInterface;
|
||||
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
|
||||
use Drupal\rest\Plugin\Type\ResourcePluginManager;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Provides rest module permissions.
|
||||
*/
|
||||
class RestPermissions implements ContainerInjectionInterface {
|
||||
|
||||
/**
|
||||
* The rest resource plugin manager.
|
||||
*
|
||||
* @var \Drupal\rest\Plugin\Type\ResourcePluginManager
|
||||
*/
|
||||
protected $restPluginManager;
|
||||
|
||||
/**
|
||||
* The config factory.
|
||||
*
|
||||
* @var \Drupal\Core\Config\ConfigFactoryInterface
|
||||
*/
|
||||
protected $configFactory;
|
||||
|
||||
/**
|
||||
* Constructs a new RestPermissions instance.
|
||||
*
|
||||
* @param \Drupal\rest\Plugin\Type\ResourcePluginManager $rest_plugin_manager
|
||||
* The rest resource plugin manager.
|
||||
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
|
||||
* The config factory.
|
||||
*/
|
||||
public function __construct(ResourcePluginManager $rest_plugin_manager, ConfigFactoryInterface $config_factory) {
|
||||
$this->restPluginManager = $rest_plugin_manager;
|
||||
$this->configFactory = $config_factory;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container) {
|
||||
return new static($container->get('plugin.manager.rest'), $container->get('config.factory'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of REST permissions.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function permissions() {
|
||||
$permissions = [];
|
||||
$resources = $this->configFactory->get('rest.settings')->get('resources');
|
||||
if ($resources && $enabled = array_intersect_key($this->restPluginManager->getDefinitions(), $resources)) {
|
||||
foreach ($enabled as $key => $resource) {
|
||||
$plugin = $this->restPluginManager->getInstance(['id' => $key]);
|
||||
$permissions = array_merge($permissions, $plugin->permissions());
|
||||
}
|
||||
}
|
||||
return $permissions;
|
||||
}
|
||||
|
||||
}
|
112
core/modules/rest/src/Routing/ResourceRoutes.php
Normal file
112
core/modules/rest/src/Routing/ResourceRoutes.php
Normal file
|
@ -0,0 +1,112 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\rest\Routing\ResourceRoutes.
|
||||
*/
|
||||
|
||||
namespace Drupal\rest\Routing;
|
||||
|
||||
use Drupal\Core\Config\ConfigFactoryInterface;
|
||||
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
|
||||
use Drupal\Core\Routing\RouteSubscriberBase;
|
||||
use Drupal\rest\Plugin\Type\ResourcePluginManager;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\Routing\RouteCollection;
|
||||
|
||||
/**
|
||||
* Subscriber for REST-style routes.
|
||||
*/
|
||||
class ResourceRoutes extends RouteSubscriberBase {
|
||||
|
||||
/**
|
||||
* The plugin manager for REST plugins.
|
||||
*
|
||||
* @var \Drupal\rest\Plugin\Type\ResourcePluginManager
|
||||
*/
|
||||
protected $manager;
|
||||
|
||||
/**
|
||||
* The Drupal configuration factory.
|
||||
*
|
||||
* @var \Drupal\Core\Config\ConfigFactoryInterface
|
||||
*/
|
||||
protected $config;
|
||||
|
||||
/**
|
||||
* A logger instance.
|
||||
*
|
||||
* @var \Psr\Log\LoggerInterface
|
||||
*/
|
||||
protected $logger;
|
||||
|
||||
/**
|
||||
* Constructs a RouteSubscriber object.
|
||||
*
|
||||
* @param \Drupal\rest\Plugin\Type\ResourcePluginManager $manager
|
||||
* The resource plugin manager.
|
||||
* @param \Drupal\Core\Config\ConfigFactoryInterface $config
|
||||
* The configuration factory holding resource settings.
|
||||
* @param \Psr\Log\LoggerInterface $logger
|
||||
* A logger instance.
|
||||
*/
|
||||
public function __construct(ResourcePluginManager $manager, ConfigFactoryInterface $config, LoggerInterface $logger) {
|
||||
$this->manager = $manager;
|
||||
$this->config = $config;
|
||||
$this->logger = $logger;
|
||||
}
|
||||
|
||||
/**
|
||||
* Alters existing routes for a specific collection.
|
||||
*
|
||||
* @param \Symfony\Component\Routing\RouteCollection $collection
|
||||
* The route collection for adding routes.
|
||||
* @return array
|
||||
*/
|
||||
protected function alterRoutes(RouteCollection $collection) {
|
||||
$routes = array();
|
||||
$enabled_resources = $this->config->get('rest.settings')->get('resources') ?: array();
|
||||
|
||||
// Iterate over all enabled resource plugins.
|
||||
foreach ($enabled_resources as $id => $enabled_methods) {
|
||||
$plugin = $this->manager->getInstance(array('id' => $id));
|
||||
|
||||
foreach ($plugin->routes() as $name => $route) {
|
||||
// @todo: Are multiple methods possible here?
|
||||
$methods = $route->getMethods();
|
||||
// Only expose routes where the method is enabled in the configuration.
|
||||
if ($methods && ($method = $methods[0]) && $method && isset($enabled_methods[$method])) {
|
||||
$route->setRequirement('_access_rest_csrf', 'TRUE');
|
||||
|
||||
// Check that authentication providers are defined.
|
||||
if (empty($enabled_methods[$method]['supported_auth']) || !is_array($enabled_methods[$method]['supported_auth'])) {
|
||||
$this->logger->error('At least one authentication provider must be defined for resource @id', array(':id' => $id));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check that formats are defined.
|
||||
if (empty($enabled_methods[$method]['supported_formats']) || !is_array($enabled_methods[$method]['supported_formats'])) {
|
||||
$this->logger->error('At least one format must be defined for resource @id', array(':id' => $id));
|
||||
continue;
|
||||
}
|
||||
|
||||
// If the route has a format requirement, then verify that the
|
||||
// resource has it.
|
||||
$format_requirement = $route->getRequirement('_format');
|
||||
if ($format_requirement && !in_array($format_requirement, $enabled_methods[$method]['supported_formats'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// The configuration seems legit at this point, so we set the
|
||||
// authentication provider and add the route.
|
||||
$route->setOption('_auth', $enabled_methods[$method]['supported_auth']);
|
||||
$routes["rest.$name"] = $route;
|
||||
$collection->add("rest.$name", $route);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
110
core/modules/rest/src/Tests/AuthTest.php
Normal file
110
core/modules/rest/src/Tests/AuthTest.php
Normal file
|
@ -0,0 +1,110 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\rest\Tests\AuthTest.
|
||||
*/
|
||||
|
||||
namespace Drupal\rest\Tests;
|
||||
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\rest\Tests\RESTTestBase;
|
||||
|
||||
/**
|
||||
* Tests authentication provider restrictions.
|
||||
*
|
||||
* @group rest
|
||||
*/
|
||||
class AuthTest extends RESTTestBase {
|
||||
|
||||
/**
|
||||
* Modules to install.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = array('basic_auth', 'hal', 'rest', 'entity_test', 'comment');
|
||||
|
||||
/**
|
||||
* Tests reading from an authenticated resource.
|
||||
*/
|
||||
public function testRead() {
|
||||
$entity_type = 'entity_test';
|
||||
|
||||
// Enable a test resource through GET method and basic HTTP authentication.
|
||||
$this->enableService('entity:' . $entity_type, 'GET', NULL, array('basic_auth'));
|
||||
|
||||
// Create an entity programmatically.
|
||||
$entity = $this->entityCreate($entity_type);
|
||||
$entity->save();
|
||||
|
||||
// Try to read the resource as an anonymous user, which should not work.
|
||||
$this->httpRequest($entity->urlInfo()->setRouteParameter('_format', $this->defaultFormat), 'GET');
|
||||
$this->assertResponse('401', 'HTTP response code is 401 when the request is not authenticated and the user is anonymous.');
|
||||
$this->assertRaw(json_encode(['message' => 'A fatal error occurred: No authentication credentials provided.']));
|
||||
|
||||
// Ensure that cURL settings/headers aren't carried over to next request.
|
||||
unset($this->curlHandle);
|
||||
|
||||
// Create a user account that has the required permissions to read
|
||||
// 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);
|
||||
|
||||
// Try to read the resource with session cookie authentication, which is
|
||||
// not enabled and should not work.
|
||||
$this->httpRequest($entity->urlInfo()->setRouteParameter('_format', $this->defaultFormat), 'GET');
|
||||
$this->assertResponse('403', 'HTTP response code is 403 when the request was authenticated by the wrong authentication provider.');
|
||||
|
||||
// Ensure that cURL settings/headers aren't carried over to next request.
|
||||
unset($this->curlHandle);
|
||||
|
||||
// Now read it with the Basic authentication which is enabled and should
|
||||
// work.
|
||||
$this->basicAuthGet($entity->urlInfo()->setRouteParameter('_format', $this->defaultFormat), $account->getUsername(), $account->pass_raw);
|
||||
$this->assertResponse('200', 'HTTP response code is 200 for successfully authenticated requests.');
|
||||
$this->curlClose();
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a HTTP request with Basic authentication.
|
||||
*
|
||||
* We do not use \Drupal\simpletest\WebTestBase::drupalGet because we need to
|
||||
* set curl settings for basic authentication.
|
||||
*
|
||||
* @param \Drupal\Core\Url $url
|
||||
* An Url object.
|
||||
* @param string $username
|
||||
* The user name to authenticate with.
|
||||
* @param string $password
|
||||
* The password.
|
||||
* @param string $mime_type
|
||||
* The MIME type for the Accept header.
|
||||
*
|
||||
* @return string
|
||||
* Curl output.
|
||||
*/
|
||||
protected function basicAuthGet(Url $url, $username, $password, $mime_type = NULL) {
|
||||
if (!isset($mime_type)) {
|
||||
$mime_type = $this->defaultMimeType;
|
||||
}
|
||||
$out = $this->curlExec(
|
||||
array(
|
||||
CURLOPT_HTTPGET => TRUE,
|
||||
CURLOPT_URL => $url->setAbsolute()->toString(),
|
||||
CURLOPT_NOBODY => FALSE,
|
||||
CURLOPT_HTTPAUTH => CURLAUTH_BASIC,
|
||||
CURLOPT_USERPWD => $username . ':' . $password,
|
||||
CURLOPT_HTTPHEADER => array('Accept: ' . $mime_type),
|
||||
)
|
||||
);
|
||||
|
||||
$this->verbose('GET request to: ' . $url->toString() .
|
||||
'<hr />' . $out);
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
}
|
413
core/modules/rest/src/Tests/CreateTest.php
Normal file
413
core/modules/rest/src/Tests/CreateTest.php
Normal file
|
@ -0,0 +1,413 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\rest\Tests\CreateTest.
|
||||
*/
|
||||
|
||||
namespace Drupal\rest\Tests;
|
||||
|
||||
use Drupal\Component\Serialization\Json;
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
use Drupal\entity_test\Entity\EntityTest;
|
||||
use Drupal\node\Entity\Node;
|
||||
use Drupal\user\Entity\User;
|
||||
|
||||
/**
|
||||
* Tests the creation of resources.
|
||||
*
|
||||
* @group rest
|
||||
*/
|
||||
class CreateTest extends RESTTestBase {
|
||||
|
||||
/**
|
||||
* Modules to install.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = array('hal', 'rest', 'entity_test');
|
||||
|
||||
/**
|
||||
* The 'serializer' service.
|
||||
*
|
||||
* @var \Symfony\Component\Serializer\Serializer
|
||||
*/
|
||||
protected $serializer;
|
||||
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
// Get the 'serializer' service.
|
||||
$this->serializer = $this->container->get('serializer');
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to create a resource which is not REST API enabled.
|
||||
*/
|
||||
public function testCreateResourceRestApiNotEnabled() {
|
||||
$entity_type = 'entity_test';
|
||||
// Enables the REST service for a specific entity type.
|
||||
$this->enableService('entity:' . $entity_type, 'POST');
|
||||
|
||||
// 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);
|
||||
// Populate some entity properties before create the entity.
|
||||
$entity_values = $this->entityValues($entity_type);
|
||||
$entity = EntityTest::create($entity_values);
|
||||
|
||||
// Serialize the entity before the POST request.
|
||||
$serialized = $this->serializer->serialize($entity, $this->defaultFormat, ['account' => $account]);
|
||||
|
||||
// Disable all resource types.
|
||||
$this->enableService(FALSE);
|
||||
$this->drupalLogin($account);
|
||||
|
||||
// POST request to create the current entity. GET request for CSRF token
|
||||
// is included into the httpRequest() method.
|
||||
$this->httpRequest('entity/entity_test', 'POST', $serialized, $this->defaultMimeType);
|
||||
|
||||
// The resource is not enabled. So, we receive a 'not found' response.
|
||||
$this->assertResponse(404);
|
||||
$this->assertFalse(EntityTest::loadMultiple(), 'No entity has been created in the database.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that an entity cannot be created without the restful permission.
|
||||
*/
|
||||
public function testCreateWithoutPermission() {
|
||||
$entity_type = 'entity_test';
|
||||
// Enables the REST service for 'entity_test' entity type.
|
||||
$this->enableService('entity:' . $entity_type, 'POST');
|
||||
$permissions = $this->entityPermissions($entity_type, 'create');
|
||||
// Create a user without the 'restful post entity:entity_test permission.
|
||||
$account = $this->drupalCreateUser($permissions);
|
||||
$this->drupalLogin($account);
|
||||
// Populate some entity properties before create the entity.
|
||||
$entity_values = $this->entityValues($entity_type);
|
||||
$entity = EntityTest::create($entity_values);
|
||||
|
||||
// Serialize the entity before the POST request.
|
||||
$serialized = $this->serializer->serialize($entity, $this->defaultFormat, ['account' => $account]);
|
||||
|
||||
// Create the entity over the REST API.
|
||||
$this->httpRequest('entity/' . $entity_type, 'POST', $serialized, $this->defaultMimeType);
|
||||
$this->assertResponse(403);
|
||||
$this->assertFalse(EntityTest::loadMultiple(), 'No entity has been created in the database.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests valid and invalid create requests for 'entity_test' entity type.
|
||||
*/
|
||||
public function testCreateEntityTest() {
|
||||
$entity_type = 'entity_test';
|
||||
// Enables the REST service for 'entity_test' entity type.
|
||||
$this->enableService('entity:' . $entity_type, 'POST');
|
||||
// Create two accounts with the required permissions to create resources.
|
||||
// The second one has administrative permissions.
|
||||
$accounts = $this->createAccountPerEntity($entity_type);
|
||||
|
||||
// Verify create requests per user.
|
||||
foreach ($accounts as $key => $account) {
|
||||
$this->drupalLogin($account);
|
||||
// Populate some entity properties before create the entity.
|
||||
$entity_values = $this->entityValues($entity_type);
|
||||
$entity = EntityTest::create($entity_values);
|
||||
|
||||
// Serialize the entity before the POST request.
|
||||
$serialized = $this->serializer->serialize($entity, $this->defaultFormat, ['account' => $account]);
|
||||
|
||||
// Create the entity over the REST API.
|
||||
$this->assertCreateEntityOverRestApi($entity_type, $serialized);
|
||||
// Get the entity ID from the location header and try to read it from the
|
||||
// database.
|
||||
$this->assertReadEntityIdFromHeaderAndDb($entity_type, $entity, $entity_values);
|
||||
|
||||
// Try to create an entity with an access protected field.
|
||||
// @see entity_test_entity_field_access()
|
||||
$normalized = $this->serializer->normalize($entity, $this->defaultFormat, ['account' => $account]);
|
||||
$normalized['field_test_text'][0]['value'] = 'no access value';
|
||||
$this->httpRequest('entity/' . $entity_type, 'POST', $this->serializer->serialize($normalized, $this->defaultFormat, ['account' => $account]), $this->defaultMimeType);
|
||||
$this->assertResponse(403);
|
||||
$this->assertFalse(EntityTest::loadMultiple(), 'No entity has been created in the database.');
|
||||
|
||||
// Try to create a field with a text format this user has no access to.
|
||||
$entity->field_test_text->value = $entity_values['field_test_text'][0]['value'];
|
||||
$entity->field_test_text->format = 'full_html';
|
||||
|
||||
$serialized = $this->serializer->serialize($entity, $this->defaultFormat, ['account' => $account]);
|
||||
$this->httpRequest('entity/' . $entity_type, 'POST', $serialized, $this->defaultMimeType);
|
||||
// The value selected is not a valid choice because the format must be
|
||||
// 'plain_txt'.
|
||||
$this->assertResponse(422);
|
||||
$this->assertFalse(EntityTest::loadMultiple(), 'No entity has been created in the database.');
|
||||
|
||||
// Restore the valid test value.
|
||||
$entity->field_test_text->format = 'plain_text';
|
||||
$serialized = $this->serializer->serialize($entity, $this->defaultFormat, ['account' => $account]);
|
||||
|
||||
// Try to send invalid data that cannot be correctly deserialized.
|
||||
$this->assertCreateEntityInvalidData($entity_type);
|
||||
|
||||
// Try to send no data at all, which does not make sense on POST requests.
|
||||
$this->assertCreateEntityNoData($entity_type);
|
||||
|
||||
// Try to send invalid data to trigger the entity validation constraints.
|
||||
// Send a UUID that is too long.
|
||||
$this->assertCreateEntityInvalidSerialized($entity, $entity_type);
|
||||
|
||||
// Try to create an entity without proper permissions.
|
||||
$this->assertCreateEntityWithoutProperPermissions($entity_type, $serialized, ['account' => $account]);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests several valid and invalid create requests for 'node' entity type.
|
||||
*/
|
||||
public function testCreateNode() {
|
||||
$entity_type = 'node';
|
||||
// Enables the REST service for 'node' entity type.
|
||||
$this->enableService('entity:' . $entity_type, 'POST');
|
||||
// Create two accounts that have the required permissions to create
|
||||
// resources. The second one has administrative permissions.
|
||||
$accounts = $this->createAccountPerEntity($entity_type);
|
||||
|
||||
// Verify create requests per user.
|
||||
foreach ($accounts as $key => $account) {
|
||||
$this->drupalLogin($account);
|
||||
// Populate some entity properties before create the entity.
|
||||
$entity_values = $this->entityValues($entity_type);
|
||||
$entity = Node::create($entity_values);
|
||||
|
||||
// Verify that user cannot create content when trying to write to fields
|
||||
// where it is not possible.
|
||||
if (!$account->hasPermission('administer nodes')) {
|
||||
$serialized = $this->serializer->serialize($entity, $this->defaultFormat, ['account' => $account]);
|
||||
$this->httpRequest('entity/' . $entity_type, 'POST', $serialized, $this->defaultMimeType);
|
||||
$this->assertResponse(403);
|
||||
// Remove fields where non-administrative users cannot write.
|
||||
$entity = $this->removeNodeFieldsForNonAdminUsers($entity);
|
||||
}
|
||||
else {
|
||||
// Changed and revision_timestamp fields can never be added.
|
||||
unset($entity->changed);
|
||||
unset($entity->revision_timestamp);
|
||||
}
|
||||
|
||||
$serialized = $this->serializer->serialize($entity, $this->defaultFormat, ['account' => $account]);
|
||||
|
||||
// Create the entity over the REST API.
|
||||
$this->assertCreateEntityOverRestApi($entity_type, $serialized);
|
||||
|
||||
// Get the new entity ID from the location header and try to read it from
|
||||
// the database.
|
||||
$this->assertReadEntityIdFromHeaderAndDb($entity_type, $entity, $entity_values);
|
||||
|
||||
// Try to send invalid data that cannot be correctly deserialized.
|
||||
$this->assertCreateEntityInvalidData($entity_type);
|
||||
|
||||
// Try to send no data at all, which does not make sense on POST requests.
|
||||
$this->assertCreateEntityNoData($entity_type);
|
||||
|
||||
// Try to send invalid data to trigger the entity validation constraints. Send a UUID that is too long.
|
||||
$this->assertCreateEntityInvalidSerialized($entity, $entity_type);
|
||||
|
||||
// Try to create an entity without proper permissions.
|
||||
$this->assertCreateEntityWithoutProperPermissions($entity_type, $serialized);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests several valid and invalid create requests for 'user' entity type.
|
||||
*/
|
||||
public function testCreateUser() {
|
||||
$entity_type = 'user';
|
||||
// Enables the REST service for 'user' entity type.
|
||||
$this->enableService('entity:' . $entity_type, 'POST');
|
||||
// Create two accounts that have the required permissions to create
|
||||
// resources. The second one has administrative permissions.
|
||||
$accounts = $this->createAccountPerEntity($entity_type);
|
||||
|
||||
foreach ($accounts as $key => $account) {
|
||||
$this->drupalLogin($account);
|
||||
$entity_values = $this->entityValues($entity_type);
|
||||
$entity = User::create($entity_values);
|
||||
|
||||
// Verify that only administrative users can create users.
|
||||
if (!$account->hasPermission('administer users')) {
|
||||
$serialized = $this->serializer->serialize($entity, $this->defaultFormat, ['account' => $account]);
|
||||
$this->httpRequest('entity/' . $entity_type, 'POST', $serialized, $this->defaultMimeType);
|
||||
$this->assertResponse(403);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Changed field can never be added.
|
||||
unset($entity->changed);
|
||||
|
||||
$serialized = $this->serializer->serialize($entity, $this->defaultFormat, ['account' => $account]);
|
||||
|
||||
// Create the entity over the REST API.
|
||||
$this->assertCreateEntityOverRestApi($entity_type, $serialized);
|
||||
|
||||
// Get the new entity ID from the location header and try to read it from
|
||||
// the database.
|
||||
$this->assertReadEntityIdFromHeaderAndDb($entity_type, $entity, $entity_values);
|
||||
|
||||
// Try to send invalid data that cannot be correctly deserialized.
|
||||
$this->assertCreateEntityInvalidData($entity_type);
|
||||
|
||||
// Try to send no data at all, which does not make sense on POST requests.
|
||||
$this->assertCreateEntityNoData($entity_type);
|
||||
|
||||
// Try to send invalid data to trigger the entity validation constraints.
|
||||
// Send a UUID that is too long.
|
||||
$this->assertCreateEntityInvalidSerialized($entity, $entity_type);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates user accounts that have the required permissions to create
|
||||
* resources via the REST API. The second one has administrative permissions.
|
||||
*
|
||||
* @param string $entity_type
|
||||
* Entity type needed to apply user permissions.
|
||||
* @return array
|
||||
* An array that contains user accounts.
|
||||
*/
|
||||
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.
|
||||
$permissions[] = 'administer nodes';
|
||||
$permissions[] = 'administer users';
|
||||
// Create an administrative user.
|
||||
$accounts[] = $this->drupalCreateUser($permissions);
|
||||
|
||||
return $accounts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the entity over the REST API.
|
||||
*
|
||||
* @param string $entity_type
|
||||
* The type of the entity that should be created.
|
||||
* @param string $serialized
|
||||
* The body for the POST request.
|
||||
*/
|
||||
public function assertCreateEntityOverRestApi($entity_type, $serialized = NULL) {
|
||||
// Note: this will fail with PHP 5.6 when always_populate_raw_post_data is
|
||||
// set to something other than -1. See https://www.drupal.org/node/2456025.
|
||||
$this->httpRequest('entity/' . $entity_type, 'POST', $serialized, $this->defaultMimeType);
|
||||
$this->assertResponse(201);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the new entity ID from the location header and tries to read it from
|
||||
* the database.
|
||||
*
|
||||
* @param string $entity_type
|
||||
* Entity type we need to load the entity from DB.
|
||||
* @param \Drupal\Core\Entity\EntityInterface $entity
|
||||
* The entity we want to check that was inserted correctly.
|
||||
* @param array $entity_values
|
||||
* The values of $entity.
|
||||
*/
|
||||
public function assertReadEntityIdFromHeaderAndDb($entity_type, EntityInterface $entity, array $entity_values = array()) {
|
||||
// Get the location from the HTTP response header.
|
||||
$location_url = $this->drupalGetHeader('location');
|
||||
$url_parts = explode('/', $location_url);
|
||||
$id = end($url_parts);
|
||||
|
||||
// Get the entity using the ID found.
|
||||
$loaded_entity = \Drupal::entityManager()->getStorage($entity_type)->load($id);
|
||||
$this->assertNotIdentical(FALSE, $loaded_entity, 'The new ' . $entity_type . ' was found in the database.');
|
||||
$this->assertEqual($entity->uuid(), $loaded_entity->uuid(), 'UUID of created entity is correct.');
|
||||
|
||||
// Verify that the field values sent and received from DB are the same.
|
||||
foreach ($entity_values as $property => $value) {
|
||||
$actual_value = $loaded_entity->get($property)->value;
|
||||
$send_value = $entity->get($property)->value;
|
||||
$this->assertEqual($send_value, $actual_value, 'Created property ' . $property . ' expected: ' . $send_value . ', actual: ' . $actual_value);
|
||||
}
|
||||
|
||||
// Delete the entity loaded from DB.
|
||||
$loaded_entity->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to send invalid data that cannot be correctly deserialized.
|
||||
*
|
||||
* @param string $entity_type
|
||||
* The type of the entity that should be created.
|
||||
*/
|
||||
public function assertCreateEntityInvalidData($entity_type) {
|
||||
$this->httpRequest('entity/' . $entity_type, 'POST', 'kaboom!', $this->defaultMimeType);
|
||||
$this->assertResponse(400);
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to send no data at all, which does not make sense on POST requests.
|
||||
*
|
||||
* @param string $entity_type
|
||||
* The type of the entity that should be created.
|
||||
*/
|
||||
public function assertCreateEntityNoData($entity_type) {
|
||||
$this->httpRequest('entity/' . $entity_type, 'POST', NULL, $this->defaultMimeType);
|
||||
$this->assertResponse(400);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send an invalid UUID to trigger the entity validation.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityInterface $entity
|
||||
* The entity we want to check that was inserted correctly.
|
||||
* @param string $entity_type
|
||||
* The type of the entity that should be created.
|
||||
* @param array $context
|
||||
* Options normalizers/encoders have access to.
|
||||
*/
|
||||
public function assertCreateEntityInvalidSerialized(EntityInterface $entity, $entity_type, array $context = array()) {
|
||||
// Add a UUID that is too long.
|
||||
$entity->set('uuid', $this->randomMachineName(129));
|
||||
$invalid_serialized = $this->serializer->serialize($entity, $this->defaultFormat, $context);
|
||||
|
||||
$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['error'], "Unprocessable Entity: validation failed.\nuuid.0.value: <em class=\"placeholder\">UUID</em>: may not be longer than 128 characters.\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to create an entity without proper permissions.
|
||||
*
|
||||
* @param string $entity_type
|
||||
* The type of the entity that should be created.
|
||||
* @param string $serialized
|
||||
* The body for the POST request.
|
||||
*/
|
||||
public function assertCreateEntityWithoutProperPermissions($entity_type, $serialized = NULL) {
|
||||
$this->drupalLogout();
|
||||
$this->httpRequest('entity/' . $entity_type, 'POST', $serialized, $this->defaultMimeType);
|
||||
// Forbidden Error as response.
|
||||
$this->assertResponse(403);
|
||||
$this->assertFalse(\Drupal::entityManager()->getStorage($entity_type)->loadMultiple(), 'No entity has been created in the database.');
|
||||
}
|
||||
|
||||
}
|
119
core/modules/rest/src/Tests/CsrfTest.php
Normal file
119
core/modules/rest/src/Tests/CsrfTest.php
Normal file
|
@ -0,0 +1,119 @@
|
|||
<?php
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\rest\Tests\CsrfTest.
|
||||
*/
|
||||
|
||||
namespace Drupal\rest\Tests;
|
||||
|
||||
use Drupal\Core\Url;
|
||||
|
||||
/**
|
||||
* Tests the CSRF protection.
|
||||
*
|
||||
* @group rest
|
||||
*/
|
||||
class CsrfTest extends RESTTestBase {
|
||||
|
||||
/**
|
||||
* Modules to install.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = array('hal', 'rest', 'entity_test', 'basic_auth');
|
||||
|
||||
/**
|
||||
* A testing user account.
|
||||
*
|
||||
* @var \Drupal\user\Entity\User
|
||||
*/
|
||||
protected $account;
|
||||
|
||||
/**
|
||||
* The serialized entity.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $serialized;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
$this->enableService('entity:' . $this->testEntityType, 'POST', 'hal_json', array('basic_auth', 'cookie'));
|
||||
|
||||
// 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
|
||||
// request.
|
||||
$serializer = $this->container->get('serializer');
|
||||
$entity_values = $this->entityValues($this->testEntityType);
|
||||
$entity = entity_create($this->testEntityType, $entity_values);
|
||||
$this->serialized = $serializer->serialize($entity, $this->defaultFormat);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that CSRF check is not triggered for Basic Auth requests.
|
||||
*/
|
||||
public function testBasicAuth() {
|
||||
$curl_options = $this->getCurlOptions();
|
||||
$curl_options[CURLOPT_HTTPAUTH] = CURLAUTH_BASIC;
|
||||
$curl_options[CURLOPT_USERPWD] = $this->account->getUsername() . ':' . $this->account->pass_raw;
|
||||
$this->curlExec($curl_options);
|
||||
$this->assertResponse(201);
|
||||
// Ensure that the entity was created.
|
||||
$loaded_entity = $this->loadEntityFromLocationHeader($this->drupalGetHeader('location'));
|
||||
$this->assertTrue($loaded_entity, 'An entity was created in the database');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that CSRF check is triggered for Cookie Auth requests.
|
||||
*/
|
||||
public function testCookieAuth() {
|
||||
$this->drupalLogin($this->account);
|
||||
|
||||
$curl_options = $this->getCurlOptions();
|
||||
|
||||
// Try to create an entity without the CSRF token.
|
||||
// Note: this will fail with PHP 5.6 when always_populate_raw_post_data is
|
||||
// set to something other than -1. See https://www.drupal.org/node/2456025.
|
||||
$this->curlExec($curl_options);
|
||||
$this->assertResponse(403);
|
||||
// Ensure that the entity was not created.
|
||||
$this->assertFalse(entity_load_multiple($this->testEntityType, NULL, TRUE), 'No entity has been created in the database.');
|
||||
|
||||
// Create an entity with the CSRF token.
|
||||
$token = $this->drupalGet('rest/session/token');
|
||||
$curl_options[CURLOPT_HTTPHEADER][] = "X-CSRF-Token: $token";
|
||||
$this->curlExec($curl_options);
|
||||
$this->assertResponse(201);
|
||||
// Ensure that the entity was created.
|
||||
$loaded_entity = $this->loadEntityFromLocationHeader($this->drupalGetHeader('location'));
|
||||
$this->assertTrue($loaded_entity, 'An entity was created in the database');
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the cURL options to create an entity with POST.
|
||||
*
|
||||
* @return array
|
||||
* The array of cURL options.
|
||||
*/
|
||||
protected function getCurlOptions() {
|
||||
return array(
|
||||
CURLOPT_HTTPGET => FALSE,
|
||||
CURLOPT_POST => TRUE,
|
||||
CURLOPT_POSTFIELDS => $this->serialized,
|
||||
CURLOPT_URL => Url::fromRoute('rest.entity.' . $this->testEntityType . '.POST')->setAbsolute()->toString(),
|
||||
CURLOPT_NOBODY => FALSE,
|
||||
CURLOPT_HTTPHEADER => array(
|
||||
"Content-Type: {$this->defaultMimeType}",
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
80
core/modules/rest/src/Tests/DeleteTest.php
Normal file
80
core/modules/rest/src/Tests/DeleteTest.php
Normal file
|
@ -0,0 +1,80 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\rest\Tests\DeleteTest.
|
||||
*/
|
||||
|
||||
namespace Drupal\rest\Tests;
|
||||
|
||||
use Drupal\Core\Url;
|
||||
|
||||
/**
|
||||
* Tests the deletion of resources.
|
||||
*
|
||||
* @group rest
|
||||
*/
|
||||
class DeleteTest extends RESTTestBase {
|
||||
|
||||
/**
|
||||
* Modules to install.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = array('hal', 'rest', 'entity_test');
|
||||
|
||||
/**
|
||||
* Tests several valid and invalid delete requests on all entity types.
|
||||
*/
|
||||
public function testDelete() {
|
||||
// Define the entity types we want to test.
|
||||
// @todo expand this test to at least users once their access
|
||||
// controllers are implemented.
|
||||
$entity_types = array('entity_test', 'node');
|
||||
foreach ($entity_types as $entity_type) {
|
||||
$this->enableService('entity:' . $entity_type, 'DELETE');
|
||||
// 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);
|
||||
|
||||
// Create an entity programmatically.
|
||||
$entity = $this->entityCreate($entity_type);
|
||||
$entity->save();
|
||||
// Delete it over the REST API.
|
||||
$response = $this->httpRequest($entity->urlInfo(), 'DELETE');
|
||||
// Clear the static cache with entity_load(), otherwise we won't see the
|
||||
// update.
|
||||
$entity = entity_load($entity_type, $entity->id(), TRUE);
|
||||
$this->assertFalse($entity, $entity_type . ' entity is not in the DB anymore.');
|
||||
$this->assertResponse('204', 'HTTP response code is correct.');
|
||||
$this->assertEqual($response, '', 'Response body is empty.');
|
||||
|
||||
// Try to delete an entity that does not exist.
|
||||
$response = $this->httpRequest(Url::fromRoute('entity.' . $entity_type . '.canonical', [$entity_type => 9999]), 'DELETE');
|
||||
$this->assertResponse(404);
|
||||
$this->assertText('The requested page could not be found.');
|
||||
|
||||
// Try to delete an entity without proper permissions.
|
||||
$this->drupalLogout();
|
||||
// Re-save entity to the database.
|
||||
$entity = $this->entityCreate($entity_type);
|
||||
$entity->save();
|
||||
$this->httpRequest($entity->urlInfo(), 'DELETE');
|
||||
$this->assertResponse(403);
|
||||
$this->assertNotIdentical(FALSE, entity_load($entity_type, $entity->id(), TRUE), 'The ' . $entity_type . ' entity is still in the database.');
|
||||
}
|
||||
// Try to delete a resource which is not REST API enabled.
|
||||
$this->enableService(FALSE);
|
||||
$account = $this->drupalCreateUser();
|
||||
$this->drupalLogin($account);
|
||||
$this->httpRequest($account->urlInfo(), 'DELETE');
|
||||
$user_storage = $this->container->get('entity.manager')->getStorage('user');
|
||||
$user_storage->resetCache(array($account->id()));
|
||||
$user = $user_storage->load($account->id());
|
||||
$this->assertEqual($account->id(), $user->id(), 'User still exists in the database.');
|
||||
$this->assertResponse(405);
|
||||
}
|
||||
}
|
93
core/modules/rest/src/Tests/NodeTest.php
Normal file
93
core/modules/rest/src/Tests/NodeTest.php
Normal file
|
@ -0,0 +1,93 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\rest\Tests\NodeTest.
|
||||
*/
|
||||
|
||||
namespace Drupal\rest\Tests;
|
||||
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\rest\Tests\RESTTestBase;
|
||||
|
||||
/**
|
||||
* Tests special cases for node entities.
|
||||
*
|
||||
* @group rest
|
||||
*/
|
||||
class NodeTest extends RESTTestBase {
|
||||
|
||||
/**
|
||||
* Modules to install.
|
||||
*
|
||||
* Ensure that the node resource works with comment module enabled.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = array('hal', 'rest', 'comment');
|
||||
|
||||
/**
|
||||
* Enables node specific REST API configuration and authentication.
|
||||
*
|
||||
* @param string $method
|
||||
* The HTTP method to be tested.
|
||||
* @param string $operation
|
||||
* The operation, one of 'view', 'create', 'update' or 'delete'.
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs various tests on nodes and their REST API.
|
||||
*/
|
||||
public function testNodes() {
|
||||
$node_storage = $this->container->get('entity.manager')->getStorage('node');
|
||||
$this->enableNodeConfiguration('GET', 'view');
|
||||
|
||||
$node = $this->entityCreate('node');
|
||||
$node->save();
|
||||
$this->httpRequest($node->urlInfo()->setRouteParameter('_format', $this->defaultFormat), 'GET');
|
||||
$this->assertResponse(200);
|
||||
$this->assertHeader('Content-type', $this->defaultMimeType);
|
||||
|
||||
// Also check that JSON works and the routing system selects the correct
|
||||
// REST route.
|
||||
$this->enableService('entity:node', 'GET', 'json');
|
||||
$this->httpRequest($node->urlInfo()->setRouteParameter('_format', 'json'), 'GET');
|
||||
$this->assertResponse(200);
|
||||
$this->assertHeader('Content-type', 'application/json');
|
||||
|
||||
// Check that a simple PATCH update to the node title works as expected.
|
||||
$this->enableNodeConfiguration('PATCH', 'update');
|
||||
|
||||
// Create a PATCH request body that only updates the title field.
|
||||
$new_title = $this->randomString();
|
||||
$data = array(
|
||||
'_links' => array(
|
||||
'type' => array(
|
||||
'href' => Url::fromUri('base:rest/type/node/resttest', array('absolute' => TRUE))->toString(),
|
||||
),
|
||||
),
|
||||
'title' => array(
|
||||
array(
|
||||
'value' => $new_title,
|
||||
),
|
||||
),
|
||||
);
|
||||
$serialized = $this->container->get('serializer')->serialize($data, $this->defaultFormat);
|
||||
$this->httpRequest($node->urlInfo(), 'PATCH', $serialized, $this->defaultMimeType);
|
||||
$this->assertResponse(204);
|
||||
|
||||
// Reload the node from the DB and check if the title was correctly updated.
|
||||
$node_storage->resetCache(array($node->id()));
|
||||
$updated_node = $node_storage->load($node->id());
|
||||
$this->assertEqual($updated_node->getTitle(), $new_title);
|
||||
// Make sure that the UUID of the node has not changed.
|
||||
$this->assertEqual($node->get('uuid')->getValue(), $updated_node->get('uuid')->getValue(), 'UUID was not changed.');
|
||||
}
|
||||
}
|
61
core/modules/rest/src/Tests/PageCacheTest.php
Normal file
61
core/modules/rest/src/Tests/PageCacheTest.php
Normal file
|
@ -0,0 +1,61 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\rest\Tests\PageCacheTest.
|
||||
*/
|
||||
|
||||
namespace Drupal\rest\Tests;
|
||||
|
||||
/**
|
||||
* Tests page caching for REST GET requests.
|
||||
*
|
||||
* @group rest
|
||||
*/
|
||||
class PageCacheTest extends RESTTestBase {
|
||||
|
||||
/**
|
||||
* Modules to install.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = array('hal', 'rest', 'entity_test');
|
||||
|
||||
/**
|
||||
* Tests that configuration changes also clear the page cache.
|
||||
*/
|
||||
public function testConfigChangePageCache() {
|
||||
$this->enableService('entity:entity_test', 'GET');
|
||||
// Allow anonymous users to issue GET requests.
|
||||
$permissions = $this->entityPermissions('entity_test', 'view');
|
||||
$permissions[] = 'restful get entity:entity_test';
|
||||
user_role_grant_permissions('anonymous', $permissions);
|
||||
|
||||
// Create an entity programmatically.
|
||||
$entity = $this->entityCreate('entity_test');
|
||||
$entity->save();
|
||||
// Read it over the REST API.
|
||||
$this->httpRequest($entity->urlInfo()->setRouteParameter('_format', $this->defaultFormat), 'GET', NULL, $this->defaultMimeType);
|
||||
$this->assertResponse(200, 'HTTP response code is correct.');
|
||||
$this->assertHeader('x-drupal-cache', 'MISS');
|
||||
$this->assertCacheTag('config:rest.settings');
|
||||
$this->assertCacheTag('entity_test:1');
|
||||
|
||||
// Read it again, should be page-cached now.
|
||||
$this->httpRequest($entity->urlInfo()->setRouteParameter('_format', $this->defaultFormat), 'GET', NULL, $this->defaultMimeType);
|
||||
$this->assertResponse(200, 'HTTP response code is correct.');
|
||||
$this->assertHeader('x-drupal-cache', 'HIT');
|
||||
$this->assertCacheTag('config:rest.settings');
|
||||
$this->assertCacheTag('entity_test:1');
|
||||
|
||||
// Trigger a config save which should clear the page cache, so we should get
|
||||
// a cache miss now for the same request.
|
||||
$this->config('rest.settings')->save();
|
||||
$this->httpRequest($entity->urlInfo()->setRouteParameter('_format', $this->defaultFormat), 'GET', NULL, $this->defaultMimeType);
|
||||
$this->assertResponse(200, 'HTTP response code is correct.');
|
||||
$this->assertHeader('x-drupal-cache', 'MISS');
|
||||
$this->assertCacheTag('config:rest.settings');
|
||||
$this->assertCacheTag('entity_test:1');
|
||||
}
|
||||
|
||||
}
|
364
core/modules/rest/src/Tests/RESTTestBase.php
Normal file
364
core/modules/rest/src/Tests/RESTTestBase.php
Normal file
|
@ -0,0 +1,364 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\rest\Tests\RESTTestBase.
|
||||
*/
|
||||
|
||||
namespace Drupal\rest\Tests;
|
||||
|
||||
use Drupal\Core\Session\AccountInterface;
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\node\NodeInterface;
|
||||
use Drupal\simpletest\WebTestBase;
|
||||
use Drupal\user\UserInterface;
|
||||
|
||||
/**
|
||||
* Test helper class that provides a REST client method to send HTTP requests.
|
||||
*/
|
||||
abstract class RESTTestBase extends WebTestBase {
|
||||
|
||||
/**
|
||||
* The default serialization format to use for testing REST operations.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $defaultFormat;
|
||||
|
||||
/**
|
||||
* The default MIME type to use for testing REST operations.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $defaultMimeType;
|
||||
|
||||
/**
|
||||
* The entity type to use for testing.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $testEntityType = 'entity_test';
|
||||
|
||||
/**
|
||||
* The default authentication provider to use for testing REST operations.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $defaultAuth;
|
||||
|
||||
/**
|
||||
* Modules to install.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = array('rest', 'entity_test', 'node');
|
||||
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
$this->defaultFormat = 'hal_json';
|
||||
$this->defaultMimeType = 'application/hal+json';
|
||||
$this->defaultAuth = array('cookie');
|
||||
// Create a test content type for node testing.
|
||||
$this->drupalCreateContentType(array('name' => 'resttest', 'type' => 'resttest'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to issue a HTTP request with simpletest's cURL.
|
||||
*
|
||||
* @param string|\Drupal\Core\Url $url
|
||||
* A Url object or system path.
|
||||
* @param string $method
|
||||
* HTTP method, one of GET, POST, PUT or DELETE.
|
||||
* @param string $body
|
||||
* The body for POST and PUT.
|
||||
* @param string $mime_type
|
||||
* The MIME type of the transmitted content.
|
||||
*
|
||||
* @return string
|
||||
* The content returned from the request.
|
||||
*/
|
||||
protected function httpRequest($url, $method, $body = NULL, $mime_type = NULL) {
|
||||
if (!isset($mime_type)) {
|
||||
$mime_type = $this->defaultMimeType;
|
||||
}
|
||||
if (!in_array($method, array('GET', 'HEAD', 'OPTIONS', 'TRACE'))) {
|
||||
// GET the CSRF token first for writing requests.
|
||||
$token = $this->drupalGet('rest/session/token');
|
||||
}
|
||||
|
||||
$url = $this->buildUrl($url);
|
||||
|
||||
switch ($method) {
|
||||
case 'GET':
|
||||
// Set query if there are additional GET parameters.
|
||||
$curl_options = array(
|
||||
CURLOPT_HTTPGET => TRUE,
|
||||
CURLOPT_CUSTOMREQUEST => 'GET',
|
||||
CURLOPT_URL => $url,
|
||||
CURLOPT_NOBODY => FALSE,
|
||||
CURLOPT_HTTPHEADER => array('Accept: ' . $mime_type),
|
||||
);
|
||||
break;
|
||||
|
||||
case 'POST':
|
||||
$curl_options = array(
|
||||
CURLOPT_HTTPGET => FALSE,
|
||||
CURLOPT_POST => TRUE,
|
||||
CURLOPT_POSTFIELDS => $body,
|
||||
CURLOPT_URL => $url,
|
||||
CURLOPT_NOBODY => FALSE,
|
||||
CURLOPT_HTTPHEADER => array(
|
||||
'Content-Type: ' . $mime_type,
|
||||
'X-CSRF-Token: ' . $token,
|
||||
),
|
||||
);
|
||||
break;
|
||||
|
||||
case 'PUT':
|
||||
$curl_options = array(
|
||||
CURLOPT_HTTPGET => FALSE,
|
||||
CURLOPT_CUSTOMREQUEST => 'PUT',
|
||||
CURLOPT_POSTFIELDS => $body,
|
||||
CURLOPT_URL => $url,
|
||||
CURLOPT_NOBODY => FALSE,
|
||||
CURLOPT_HTTPHEADER => array(
|
||||
'Content-Type: ' . $mime_type,
|
||||
'X-CSRF-Token: ' . $token,
|
||||
),
|
||||
);
|
||||
break;
|
||||
|
||||
case 'PATCH':
|
||||
$curl_options = array(
|
||||
CURLOPT_HTTPGET => FALSE,
|
||||
CURLOPT_CUSTOMREQUEST => 'PATCH',
|
||||
CURLOPT_POSTFIELDS => $body,
|
||||
CURLOPT_URL => $url,
|
||||
CURLOPT_NOBODY => FALSE,
|
||||
CURLOPT_HTTPHEADER => array(
|
||||
'Content-Type: ' . $mime_type,
|
||||
'X-CSRF-Token: ' . $token,
|
||||
),
|
||||
);
|
||||
break;
|
||||
|
||||
case 'DELETE':
|
||||
$curl_options = array(
|
||||
CURLOPT_HTTPGET => FALSE,
|
||||
CURLOPT_CUSTOMREQUEST => 'DELETE',
|
||||
CURLOPT_URL => $url,
|
||||
CURLOPT_NOBODY => FALSE,
|
||||
CURLOPT_HTTPHEADER => array('X-CSRF-Token: ' . $token),
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
$response = $this->curlExec($curl_options);
|
||||
|
||||
// Ensure that any changes to variables in the other thread are picked up.
|
||||
$this->refreshVariables();
|
||||
|
||||
$headers = $this->drupalGetHeaders();
|
||||
|
||||
$this->verbose($method . ' request to: ' . $url .
|
||||
'<hr />Code: ' . curl_getinfo($this->curlHandle, CURLINFO_HTTP_CODE) .
|
||||
'<hr />Response headers: ' . nl2br(print_r($headers, TRUE)) .
|
||||
'<hr />Response body: ' . $response);
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates entity objects based on their types.
|
||||
*
|
||||
* @param string $entity_type
|
||||
* The type of the entity that should be created.
|
||||
*
|
||||
* @return \Drupal\Core\Entity\EntityInterface
|
||||
* The new entity object.
|
||||
*/
|
||||
protected function entityCreate($entity_type) {
|
||||
return entity_create($entity_type, $this->entityValues($entity_type));
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides an array of suitable property values for an entity type.
|
||||
*
|
||||
* Required properties differ from entity type to entity type, so we keep a
|
||||
* minimum mapping here.
|
||||
*
|
||||
* @param string $entity_type
|
||||
* The type of the entity that should be created.
|
||||
*
|
||||
* @return array
|
||||
* An array of values keyed by property name.
|
||||
*/
|
||||
protected function entityValues($entity_type) {
|
||||
switch ($entity_type) {
|
||||
case 'entity_test':
|
||||
return array(
|
||||
'name' => $this->randomMachineName(),
|
||||
'user_id' => 1,
|
||||
'field_test_text' => array(0 => array(
|
||||
'value' => $this->randomString(),
|
||||
'format' => 'plain_text',
|
||||
)),
|
||||
);
|
||||
case 'node':
|
||||
return array('title' => $this->randomString(), 'type' => 'resttest');
|
||||
case 'node_type':
|
||||
return array(
|
||||
'type' => 'article',
|
||||
'name' => $this->randomMachineName(),
|
||||
);
|
||||
case 'user':
|
||||
return array('name' => $this->randomMachineName());
|
||||
default:
|
||||
return array();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables the REST service interface for a specific entity type.
|
||||
*
|
||||
* @param string|FALSE $resource_type
|
||||
* The resource type that should get REST API enabled or FALSE to disable all
|
||||
* resource types.
|
||||
* @param string $method
|
||||
* The HTTP method to enable, e.g. GET, POST etc.
|
||||
* @param string $format
|
||||
* (Optional) The serialization format, e.g. hal_json.
|
||||
* @param array $auth
|
||||
* (Optional) The list of valid authentication methods.
|
||||
*/
|
||||
protected function enableService($resource_type, $method = 'GET', $format = NULL, $auth = NULL) {
|
||||
// Enable REST API for this entity type.
|
||||
$config = $this->config('rest.settings');
|
||||
$settings = array();
|
||||
|
||||
if ($resource_type) {
|
||||
if ($format == NULL) {
|
||||
$format = $this->defaultFormat;
|
||||
}
|
||||
$settings[$resource_type][$method]['supported_formats'][] = $format;
|
||||
|
||||
if ($auth == NULL) {
|
||||
$auth = $this->defaultAuth;
|
||||
}
|
||||
$settings[$resource_type][$method]['supported_auth'] = $auth;
|
||||
}
|
||||
$config->set('resources', $settings);
|
||||
$config->save();
|
||||
$this->rebuildCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebuilds routing caches.
|
||||
*/
|
||||
protected function rebuildCache() {
|
||||
// Rebuild routing cache, so that the REST API paths are available.
|
||||
$this->container->get('router.builder')->rebuild();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* This method is overridden to deal with a cURL quirk: the usage of
|
||||
* CURLOPT_CUSTOMREQUEST cannot be unset on the cURL handle, so we need to
|
||||
* override it every time it is omitted.
|
||||
*/
|
||||
protected function curlExec($curl_options, $redirect = FALSE) {
|
||||
if (!isset($curl_options[CURLOPT_CUSTOMREQUEST])) {
|
||||
if (!empty($curl_options[CURLOPT_HTTPGET])) {
|
||||
$curl_options[CURLOPT_CUSTOMREQUEST] = 'GET';
|
||||
}
|
||||
if (!empty($curl_options[CURLOPT_POST])) {
|
||||
$curl_options[CURLOPT_CUSTOMREQUEST] = 'POST';
|
||||
}
|
||||
}
|
||||
return parent::curlExec($curl_options, $redirect);
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides the necessary user permissions for entity operations.
|
||||
*
|
||||
* @param string $entity_type
|
||||
* The entity type.
|
||||
* @param string $operation
|
||||
* The operation, one of 'view', 'create', 'update' or 'delete'.
|
||||
*
|
||||
* @return array
|
||||
* The set of user permission strings.
|
||||
*/
|
||||
protected function entityPermissions($entity_type, $operation) {
|
||||
switch ($entity_type) {
|
||||
case 'entity_test':
|
||||
switch ($operation) {
|
||||
case 'view':
|
||||
return array('view test entity');
|
||||
case 'create':
|
||||
case 'update':
|
||||
case 'delete':
|
||||
return array('administer entity_test content');
|
||||
}
|
||||
case 'node':
|
||||
switch ($operation) {
|
||||
case 'view':
|
||||
return array('access content');
|
||||
case 'create':
|
||||
return array('create resttest content');
|
||||
case 'update':
|
||||
return array('edit any resttest content');
|
||||
case 'delete':
|
||||
return array('delete any resttest content');
|
||||
}
|
||||
|
||||
case 'user':
|
||||
switch ($operation) {
|
||||
case 'view':
|
||||
return ['access user profiles'];
|
||||
|
||||
default:
|
||||
return ['administer users'];
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads an entity based on the location URL returned in the location header.
|
||||
*
|
||||
* @param string $location_url
|
||||
* The URL returned in the Location header.
|
||||
*
|
||||
* @return \Drupal\Core\Entity\Entity|FALSE.
|
||||
* The entity or FALSE if there is no matching entity.
|
||||
*/
|
||||
protected function loadEntityFromLocationHeader($location_url) {
|
||||
$url_parts = explode('/', $location_url);
|
||||
$id = end($url_parts);
|
||||
return entity_load($this->testEntityType, $id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove node fields that can only be written by an admin user.
|
||||
*
|
||||
* @param \Drupal\node\NodeInterface $node
|
||||
* The node to remove fields where non-administrative users cannot write.
|
||||
*
|
||||
* @return \Drupal\node\NodeInterface
|
||||
* The node with removed fields.
|
||||
*/
|
||||
protected function removeNodeFieldsForNonAdminUsers(NodeInterface $node) {
|
||||
$node->set('status', NULL);
|
||||
$node->set('created', NULL);
|
||||
$node->set('changed', NULL);
|
||||
$node->set('promote', NULL);
|
||||
$node->set('sticky', NULL);
|
||||
$node->set('revision_timestamp', NULL);
|
||||
$node->set('uid', NULL);
|
||||
|
||||
return $node;
|
||||
}
|
||||
|
||||
}
|
124
core/modules/rest/src/Tests/ReadTest.php
Normal file
124
core/modules/rest/src/Tests/ReadTest.php
Normal file
|
@ -0,0 +1,124 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\rest\Tests\ReadTest.
|
||||
*/
|
||||
|
||||
namespace Drupal\rest\Tests;
|
||||
|
||||
use Drupal\Component\Serialization\Json;
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\rest\Tests\RESTTestBase;
|
||||
|
||||
/**
|
||||
* Tests the retrieval of resources.
|
||||
*
|
||||
* @group rest
|
||||
*/
|
||||
class ReadTest extends RESTTestBase {
|
||||
|
||||
/**
|
||||
* Modules to install.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = array('hal', 'rest', 'entity_test');
|
||||
|
||||
/**
|
||||
* Tests several valid and invalid read requests on all entity types.
|
||||
*/
|
||||
public function testRead() {
|
||||
// @todo Expand this at least to users.
|
||||
// Define the entity types we want to test.
|
||||
$entity_types = array('entity_test', 'node');
|
||||
foreach ($entity_types as $entity_type) {
|
||||
$this->enableService('entity:' . $entity_type, 'GET');
|
||||
// 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);
|
||||
|
||||
// Create an entity programmatically.
|
||||
$entity = $this->entityCreate($entity_type);
|
||||
$entity->save();
|
||||
// Read it over the REST API.
|
||||
$response = $this->httpRequest($entity->urlInfo()->setRouteParameter('_format', $this->defaultFormat), 'GET');
|
||||
$this->assertResponse('200', 'HTTP response code is correct.');
|
||||
$this->assertHeader('content-type', $this->defaultMimeType);
|
||||
$data = Json::decode($response);
|
||||
// Only assert one example property here, other properties should be
|
||||
// checked in serialization tests.
|
||||
$this->assertEqual($data['uuid'][0]['value'], $entity->uuid(), 'Entity UUID is correct');
|
||||
|
||||
// Try to read the entity with an unsupported mime format.
|
||||
$response = $this->httpRequest($entity->urlInfo()->setRouteParameter('_format', 'wrongformat'), 'GET');
|
||||
$this->assertResponse(406);
|
||||
$this->assertHeader('Content-type', 'application/json');
|
||||
|
||||
// Try to read an entity that does not exist.
|
||||
$response = $this->httpRequest(Url::fromUri('base://' . $entity_type . '/9999', ['query' => ['_format' => $this->defaultFormat]]), 'GET');
|
||||
$this->assertResponse(404);
|
||||
$path = $entity_type == 'node' ? '/node/{node}' : '/entity_test/{entity_test}';
|
||||
$expected_message = Json::encode(['message' => 'The "' . $entity_type . '" parameter was not converted for the path "' . $path . '" (route name: "rest.entity.' . $entity_type . '.GET.hal_json")']);
|
||||
$this->assertIdentical($expected_message, $response, 'Response message is correct.');
|
||||
|
||||
// Make sure that field level access works and that the according field is
|
||||
// not available in the response. Only applies to entity_test.
|
||||
// @see entity_test_entity_field_access()
|
||||
if ($entity_type == 'entity_test') {
|
||||
$entity->field_test_text->value = 'no access value';
|
||||
$entity->save();
|
||||
$response = $this->httpRequest($entity->urlInfo()->setRouteParameter('_format', $this->defaultFormat), 'GET');
|
||||
$this->assertResponse(200);
|
||||
$this->assertHeader('content-type', $this->defaultMimeType);
|
||||
$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($entity->urlInfo()->setRouteParameter('_format', $this->defaultFormat), 'GET');
|
||||
$this->assertResponse(403);
|
||||
$this->assertIdentical('{"message":""}', $response);
|
||||
}
|
||||
// Try to read a resource which is not REST API enabled.
|
||||
$account = $this->drupalCreateUser();
|
||||
$this->drupalLogin($account);
|
||||
$response = $this->httpRequest($account->urlInfo()->setRouteParameter('_format', $this->defaultFormat), 'GET');
|
||||
// \Drupal\Core\Routing\RequestFormatRouteFilter considers the canonical,
|
||||
// non-REST route a match, but a lower quality one: no format restrictions
|
||||
// means there's always a match and hence when there is no matching REST
|
||||
// route, the non-REST route is used, but can't render into
|
||||
// application/hal+json, so it returns a 406.
|
||||
$this->assertResponse('406', 'HTTP response code is 406 when the resource does not define formats, because it falls back to the canonical, non-REST route.');
|
||||
$this->assertEqual($response, Json::encode([
|
||||
'message' => 'Not acceptable',
|
||||
]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the resource structure.
|
||||
*/
|
||||
public function testResourceStructure() {
|
||||
// Enable a service with a format restriction but no authentication.
|
||||
$this->enableService('entity:node', 'GET', 'json');
|
||||
// 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);
|
||||
|
||||
// Create an entity programmatically.
|
||||
$entity = $this->entityCreate('node');
|
||||
$entity->save();
|
||||
|
||||
// Read it over the REST API.
|
||||
$response = $this->httpRequest($entity->urlInfo()->setRouteParameter('_format', 'json'), 'GET');
|
||||
$this->assertResponse('200', 'HTTP response code is correct.');
|
||||
}
|
||||
|
||||
}
|
103
core/modules/rest/src/Tests/ResourceTest.php
Normal file
103
core/modules/rest/src/Tests/ResourceTest.php
Normal file
|
@ -0,0 +1,103 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\rest\Tests\ResourceTest.
|
||||
*/
|
||||
|
||||
namespace Drupal\rest\Tests;
|
||||
|
||||
/**
|
||||
* Tests the structure of a REST resource.
|
||||
*
|
||||
* @group rest
|
||||
*/
|
||||
class ResourceTest extends RESTTestBase {
|
||||
|
||||
/**
|
||||
* Modules to install.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = array('hal', 'rest', 'entity_test');
|
||||
|
||||
/**
|
||||
* The entity.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityInterface
|
||||
*/
|
||||
protected $entity;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
$this->config = $this->config('rest.settings');
|
||||
|
||||
// Create an entity programmatically.
|
||||
$this->entity = $this->entityCreate('entity_test');
|
||||
$this->entity->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that a resource without formats cannot be enabled.
|
||||
*/
|
||||
public function testFormats() {
|
||||
$settings = array(
|
||||
'entity:entity_test' => array(
|
||||
'GET' => array(
|
||||
'supported_auth' => array(
|
||||
'basic_auth',
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Attempt to enable the resource.
|
||||
$this->config->set('resources', $settings);
|
||||
$this->config->save();
|
||||
$this->rebuildCache();
|
||||
|
||||
// Verify that accessing the resource returns 406.
|
||||
$response = $this->httpRequest($this->entity->urlInfo()->setRouteParameter('_format', $this->defaultFormat), 'GET');
|
||||
// \Drupal\Core\Routing\RequestFormatRouteFilter considers the canonical,
|
||||
// non-REST route a match, but a lower quality one: no format restrictions
|
||||
// means there's always a match and hence when there is no matching REST
|
||||
// route, the non-REST route is used, but can't render into
|
||||
// application/hal+json, so it returns a 406.
|
||||
$this->assertResponse('406', 'HTTP response code is 406 when the resource does not define formats, because it falls back to the canonical, non-REST route.');
|
||||
$this->curlClose();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that a resource without authentication cannot be enabled.
|
||||
*/
|
||||
public function testAuthentication() {
|
||||
$settings = array(
|
||||
'entity:entity_test' => array(
|
||||
'GET' => array(
|
||||
'supported_formats' => array(
|
||||
'hal_json',
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Attempt to enable the resource.
|
||||
$this->config->set('resources', $settings);
|
||||
$this->config->save();
|
||||
$this->rebuildCache();
|
||||
|
||||
// Verify that accessing the resource returns 401.
|
||||
$response = $this->httpRequest($this->entity->urlInfo()->setRouteParameter('_format', $this->defaultFormat), 'GET');
|
||||
// \Drupal\Core\Routing\RequestFormatRouteFilter considers the canonical,
|
||||
// non-REST route a match, but a lower quality one: no format restrictions
|
||||
// means there's always a match and hence when there is no matching REST
|
||||
// route, the non-REST route is used, but can't render into
|
||||
// application/hal+json, so it returns a 406.
|
||||
$this->assertResponse('406', 'HTTP response code is 406 when the resource does not define formats, because it falls back to the canonical, non-REST route.');
|
||||
$this->curlClose();
|
||||
}
|
||||
|
||||
}
|
90
core/modules/rest/src/Tests/RestLinkManagerTest.php
Normal file
90
core/modules/rest/src/Tests/RestLinkManagerTest.php
Normal file
|
@ -0,0 +1,90 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\rest\Tests\RestLinkManagerTest.
|
||||
*/
|
||||
|
||||
namespace Drupal\rest\Tests;
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\simpletest\KernelTestBase;
|
||||
|
||||
/**
|
||||
* Tests that REST type and relation link managers work as expected
|
||||
* @group rest
|
||||
*/
|
||||
class RestLinkManagerTest extends KernelTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static $modules = ['rest', 'rest_test', 'system'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
$this->installSchema('system', ['router']);
|
||||
\Drupal::service('router.builder')->rebuild();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that type hooks work as expected.
|
||||
*/
|
||||
public function testRestLinkManagers() {
|
||||
\Drupal::moduleHandler()->invoke('rest', 'install');
|
||||
/* @var \Drupal\rest\LinkManager\TypeLinkManagerInterface $type_manager */
|
||||
$type_manager = \Drupal::service('rest.link_manager.type');
|
||||
$base = Url::fromRoute('<front>', [], ['absolute' => TRUE])->toString();
|
||||
$link = $type_manager->getTypeUri('node', 'page');
|
||||
$this->assertEqual($link, $base . 'rest/type/node/page');
|
||||
// Now with optional context.
|
||||
$link = $type_manager->getTypeUri('node', 'page', ['rest_test' => TRUE]);
|
||||
$this->assertEqual($link, 'rest_test_type');
|
||||
|
||||
/* @var \Drupal\rest\LinkManager\RelationLinkManagerInterface $relation_manager */
|
||||
$relation_manager = \Drupal::service('rest.link_manager.relation');
|
||||
$link = $relation_manager->getRelationUri('node', 'page', 'field_ref');
|
||||
$this->assertEqual($link, $base . 'rest/relation/node/page/field_ref');
|
||||
// Now with optional context.
|
||||
$link = $relation_manager->getRelationUri('node', 'page', 'foobar', ['rest_test' => TRUE]);
|
||||
$this->assertEqual($link, 'rest_test_relation');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that type hooks work as expected even without install hook.
|
||||
*/
|
||||
public function testRestLinkManagersNoInstallHook() {
|
||||
/* @var \Drupal\rest\LinkManager\TypeLinkManagerInterface $type_manager */
|
||||
$type_manager = \Drupal::service('rest.link_manager.type');
|
||||
$base = Url::fromRoute('<front>', [], ['absolute' => TRUE])->toString();
|
||||
$link = $type_manager->getTypeUri('node', 'page');
|
||||
$this->assertEqual($link, $base . 'rest/type/node/page');
|
||||
// Now with optional context.
|
||||
$link = $type_manager->getTypeUri('node', 'page', ['rest_test' => TRUE]);
|
||||
$this->assertEqual($link, 'rest_test_type');
|
||||
|
||||
/* @var \Drupal\rest\LinkManager\RelationLinkManagerInterface $relation_manager */
|
||||
$relation_manager = \Drupal::service('rest.link_manager.relation');
|
||||
$link = $relation_manager->getRelationUri('node', 'page', 'field_ref');
|
||||
$this->assertEqual($link, $base . 'rest/relation/node/page/field_ref');
|
||||
// Now with optional context.
|
||||
$link = $relation_manager->getRelationUri('node', 'page', 'foobar', ['rest_test' => TRUE]);
|
||||
$this->assertEqual($link, 'rest_test_relation');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests \Drupal\rest\LinkManager\LinkManager::setLinkDomain().
|
||||
*/
|
||||
public function testRestLinkManagersSetLinkDomain() {
|
||||
/* @var \Drupal\rest\LinkManager\LinkManager $link_manager */
|
||||
$link_manager = \Drupal::service('rest.link_manager');
|
||||
$link_manager->setLinkDomain('http://example.com/');
|
||||
$link = $link_manager->getTypeUri('node', 'page');
|
||||
$this->assertEqual($link, 'http://example.com/rest/type/node/page');
|
||||
$link = $link_manager->getRelationUri('node', 'page', 'field_ref');
|
||||
$this->assertEqual($link, 'http://example.com/rest/relation/node/page/field_ref');
|
||||
}
|
||||
|
||||
}
|
230
core/modules/rest/src/Tests/UpdateTest.php
Normal file
230
core/modules/rest/src/Tests/UpdateTest.php
Normal file
|
@ -0,0 +1,230 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\rest\Tests\UpdateTest.
|
||||
*/
|
||||
|
||||
namespace Drupal\rest\Tests;
|
||||
|
||||
use Drupal\Component\Serialization\Json;
|
||||
use Drupal\rest\Tests\RESTTestBase;
|
||||
|
||||
/**
|
||||
* Tests the update of resources.
|
||||
*
|
||||
* @group rest
|
||||
*/
|
||||
class UpdateTest extends RESTTestBase {
|
||||
|
||||
/**
|
||||
* Modules to install.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = array('hal', 'rest', 'entity_test');
|
||||
|
||||
/**
|
||||
* Tests several valid and invalid partial update requests on test entities.
|
||||
*/
|
||||
public function testPatchUpdate() {
|
||||
$serializer = $this->container->get('serializer');
|
||||
// @todo Test all other entity types here as well.
|
||||
$entity_type = 'entity_test';
|
||||
|
||||
$this->enableService('entity:' . $entity_type, 'PATCH');
|
||||
// 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);
|
||||
|
||||
$context = ['account' => $account];
|
||||
|
||||
// Create an entity and save it to the database.
|
||||
$entity = $this->entityCreate($entity_type);
|
||||
$entity->save();
|
||||
|
||||
// Create a second stub entity for overwriting a field.
|
||||
$patch_values['field_test_text'] = array(0 => array(
|
||||
'value' => $this->randomString(),
|
||||
'format' => 'plain_text',
|
||||
));
|
||||
$patch_entity = entity_create($entity_type, $patch_values);
|
||||
// We don't want to overwrite the UUID.
|
||||
unset($patch_entity->uuid);
|
||||
$serialized = $serializer->serialize($patch_entity, $this->defaultFormat, $context);
|
||||
|
||||
// Update the entity over the REST API.
|
||||
$this->httpRequest($entity->urlInfo(), 'PATCH', $serialized, $this->defaultMimeType);
|
||||
$this->assertResponse(204);
|
||||
|
||||
// Re-load updated entity from the database.
|
||||
$entity = entity_load($entity_type, $entity->id(), TRUE);
|
||||
$this->assertEqual($entity->field_test_text->value, $patch_entity->field_test_text->value, 'Field was successfully updated.');
|
||||
|
||||
// Make sure that the field does not get deleted if it is not present in the
|
||||
// PATCH request.
|
||||
$normalized = $serializer->normalize($patch_entity, $this->defaultFormat, $context);
|
||||
unset($normalized['field_test_text']);
|
||||
$serialized = $serializer->encode($normalized, $this->defaultFormat);
|
||||
$this->httpRequest($entity->urlInfo(), 'PATCH', $serialized, $this->defaultMimeType);
|
||||
$this->assertResponse(204);
|
||||
|
||||
$entity = entity_load($entity_type, $entity->id(), TRUE);
|
||||
$this->assertNotNull($entity->field_test_text->value. 'Test field has not been deleted.');
|
||||
|
||||
// Try to empty a field.
|
||||
$normalized['field_test_text'] = array();
|
||||
$serialized = $serializer->encode($normalized, $this->defaultFormat);
|
||||
|
||||
// Update the entity over the REST API.
|
||||
$this->httpRequest($entity->urlInfo(), 'PATCH', $serialized, $this->defaultMimeType);
|
||||
$this->assertResponse(204);
|
||||
|
||||
// Re-load updated entity from the database.
|
||||
$entity = entity_load($entity_type, $entity->id(), TRUE);
|
||||
$this->assertNull($entity->field_test_text->value, 'Test field has been cleared.');
|
||||
|
||||
// Enable access protection for the text field.
|
||||
// @see entity_test_entity_field_access()
|
||||
$entity->field_test_text->value = 'no edit access value';
|
||||
$entity->field_test_text->format = 'plain_text';
|
||||
$entity->save();
|
||||
|
||||
// Try to empty a field that is access protected.
|
||||
$this->httpRequest($entity->urlInfo(), 'PATCH', $serialized, $this->defaultMimeType);
|
||||
$this->assertResponse(403);
|
||||
|
||||
// Re-load the entity from the database.
|
||||
$entity = entity_load($entity_type, $entity->id(), TRUE);
|
||||
$this->assertEqual($entity->field_test_text->value, 'no edit access value', 'Text field was not deleted.');
|
||||
|
||||
// Try to update an access protected field.
|
||||
$normalized = $serializer->normalize($patch_entity, $this->defaultFormat, $context);
|
||||
$normalized['field_test_text'][0]['value'] = 'no access value';
|
||||
$serialized = $serializer->serialize($normalized, $this->defaultFormat, $context);
|
||||
$this->httpRequest($entity->urlInfo(), 'PATCH', $serialized, $this->defaultMimeType);
|
||||
$this->assertResponse(403);
|
||||
|
||||
// Re-load the entity from the database.
|
||||
$entity = entity_load($entity_type, $entity->id(), TRUE);
|
||||
$this->assertEqual($entity->field_test_text->value, 'no edit access value', 'Text field was not updated.');
|
||||
|
||||
// Try to update the field with a text format this user has no access to.
|
||||
// First change the original field value so we're allowed to edit it again.
|
||||
$entity->field_test_text->value = 'test';
|
||||
$entity->save();
|
||||
$patch_entity->set('field_test_text', array(
|
||||
'value' => 'test',
|
||||
'format' => 'full_html',
|
||||
));
|
||||
$serialized = $serializer->serialize($patch_entity, $this->defaultFormat, $context);
|
||||
$this->httpRequest($entity->urlInfo(), 'PATCH', $serialized, $this->defaultMimeType);
|
||||
$this->assertResponse(422);
|
||||
|
||||
// Re-load the entity from the database.
|
||||
$entity = entity_load($entity_type, $entity->id(), TRUE);
|
||||
$this->assertEqual($entity->field_test_text->format, 'plain_text', 'Text format was not updated.');
|
||||
|
||||
// Restore the valid test value.
|
||||
$entity->field_test_text->value = $this->randomString();
|
||||
$entity->save();
|
||||
|
||||
// Try to send no data at all, which does not make sense on PATCH requests.
|
||||
$this->httpRequest($entity->urlInfo(), 'PATCH', NULL, $this->defaultMimeType);
|
||||
$this->assertResponse(400);
|
||||
|
||||
// Try to update a non-existing entity with ID 9999.
|
||||
$this->httpRequest($entity_type . '/9999', 'PATCH', $serialized, $this->defaultMimeType);
|
||||
$this->assertResponse(404);
|
||||
$loaded_entity = entity_load($entity_type, 9999, TRUE);
|
||||
$this->assertFalse($loaded_entity, 'Entity 9999 was not created.');
|
||||
|
||||
// Try to send invalid data to trigger the entity validation constraints.
|
||||
// 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->urlInfo(), 'PATCH', $invalid_serialized, $this->defaultMimeType);
|
||||
$this->assertResponse(422);
|
||||
$error = Json::decode($response);
|
||||
$this->assertEqual($error['error'], "Unprocessable Entity: validation failed.\nuuid.0.value: <em class=\"placeholder\">UUID</em>: may not be longer than 128 characters.\n");
|
||||
|
||||
// Try to update an entity without proper permissions.
|
||||
$this->drupalLogout();
|
||||
$this->httpRequest($entity->urlInfo(), 'PATCH', $serialized, $this->defaultMimeType);
|
||||
$this->assertResponse(403);
|
||||
|
||||
// Try to update a resource which is not REST API enabled.
|
||||
$this->enableService(FALSE);
|
||||
$this->drupalLogin($account);
|
||||
$this->httpRequest($entity->urlInfo(), 'PATCH', $serialized, $this->defaultMimeType);
|
||||
$this->assertResponse(405);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests several valid and invalid update requests for the 'user' entity type.
|
||||
*/
|
||||
public function testUpdateUser() {
|
||||
$serializer = $this->container->get('serializer');
|
||||
$entity_type = 'user';
|
||||
// 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);
|
||||
|
||||
// Create an entity and save it to the database.
|
||||
$account->save();
|
||||
$account->set('changed', NULL);
|
||||
|
||||
// Try and set a new email without providing the password.
|
||||
$account->set('mail', 'new-email@example.com');
|
||||
$context = ['account' => $account];
|
||||
$normalized = $serializer->normalize($account, $this->defaultFormat, $context);
|
||||
$serialized = $serializer->serialize($normalized, $this->defaultFormat, $context);
|
||||
$response = $this->httpRequest($account->urlInfo(), 'PATCH', $serialized, $this->defaultMimeType);
|
||||
$this->assertResponse(422);
|
||||
$error = Json::decode($response);
|
||||
$this->assertEqual($error['error'], "Unprocessable Entity: validation failed.\nmail: Your current password is missing or incorrect; it's required to change the <em class=\"placeholder\">Email</em>.\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->urlInfo(), 'PATCH', $serialized, $this->defaultMimeType);
|
||||
$this->assertResponse(422);
|
||||
$error = Json::decode($response);
|
||||
$this->assertEqual($error['error'], "Unprocessable Entity: validation failed.\nmail: Your current password is missing or incorrect; it's required to change the <em class=\"placeholder\">Email</em>.\n");
|
||||
|
||||
// Try again with the password.
|
||||
$normalized['pass'][0]['existing'] = $account->pass_raw;
|
||||
$serialized = $serializer->serialize($normalized, $this->defaultFormat, $context);
|
||||
$this->httpRequest($account->urlInfo(), 'PATCH', $serialized, $this->defaultMimeType);
|
||||
$this->assertResponse(204);
|
||||
|
||||
// Try to change the password without providing the current password.
|
||||
$new_password = $this->randomString();
|
||||
$normalized = $serializer->normalize($account, $this->defaultFormat, $context);
|
||||
$normalized['pass'][0]['value'] = $new_password;
|
||||
$serialized = $serializer->serialize($normalized, $this->defaultFormat, $context);
|
||||
$response = $this->httpRequest($account->urlInfo(), 'PATCH', $serialized, $this->defaultMimeType);
|
||||
$this->assertResponse(422);
|
||||
$error = Json::decode($response);
|
||||
$this->assertEqual($error['error'], "Unprocessable Entity: validation failed.\npass: Your current password is missing or incorrect; it's required to change the <em class=\"placeholder\">Password</em>.\n");
|
||||
|
||||
// Try again with the password.
|
||||
$normalized['pass'][0]['existing'] = $account->pass_raw;
|
||||
$serialized = $serializer->serialize($normalized, $this->defaultFormat, $context);
|
||||
$this->httpRequest($account->urlInfo(), 'PATCH', $serialized, $this->defaultMimeType);
|
||||
$this->assertResponse(204);
|
||||
|
||||
// Verify that we can log in with the new password.
|
||||
$account->pass_raw = $new_password;
|
||||
$this->drupalLogin($account);
|
||||
|
||||
}
|
||||
|
||||
}
|
547
core/modules/rest/src/Tests/Views/StyleSerializerTest.php
Normal file
547
core/modules/rest/src/Tests/Views/StyleSerializerTest.php
Normal file
|
@ -0,0 +1,547 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\rest\Tests\Views\StyleSerializerTest.
|
||||
*/
|
||||
|
||||
namespace Drupal\rest\Tests\Views;
|
||||
|
||||
use Drupal\Component\Utility\SafeMarkup;
|
||||
use Drupal\Core\Cache\Cache;
|
||||
use Drupal\entity_test\Entity\EntityTest;
|
||||
use Drupal\system\Tests\Cache\AssertPageCacheContextsAndTagsTrait;
|
||||
use Drupal\views\Entity\View;
|
||||
use Drupal\views\Plugin\views\display\DisplayPluginBase;
|
||||
use Drupal\views\Views;
|
||||
use Drupal\views\Tests\Plugin\PluginTestBase;
|
||||
use Drupal\views\Tests\ViewTestData;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
/**
|
||||
* Tests the serializer style plugin.
|
||||
*
|
||||
* @group rest
|
||||
* @see \Drupal\rest\Plugin\views\display\RestExport
|
||||
* @see \Drupal\rest\Plugin\views\style\Serializer
|
||||
* @see \Drupal\rest\Plugin\views\row\DataEntityRow
|
||||
* @see \Drupal\rest\Plugin\views\row\DataFieldRow
|
||||
*/
|
||||
class StyleSerializerTest extends PluginTestBase {
|
||||
|
||||
use AssertPageCacheContextsAndTagsTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $dumpHeaders = TRUE;
|
||||
|
||||
/**
|
||||
* Modules to install.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = array('views_ui', 'entity_test', 'hal', 'rest_test_views', 'node', 'text', 'field');
|
||||
|
||||
/**
|
||||
* Views used by this test.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $testViews = array('test_serializer_display_field', 'test_serializer_display_entity', 'test_serializer_node_display_field');
|
||||
|
||||
/**
|
||||
* A user with administrative privileges to look at test entity and configure views.
|
||||
*/
|
||||
protected $adminUser;
|
||||
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
ViewTestData::createTestViews(get_class($this), array('rest_test_views'));
|
||||
|
||||
$this->adminUser = $this->drupalCreateUser(array('administer views', 'administer entity_test content', 'access user profiles', 'view test entity'));
|
||||
|
||||
// Save some entity_test entities.
|
||||
for ($i = 1; $i <= 10; $i++) {
|
||||
entity_create('entity_test', array('name' => 'test_' . $i, 'user_id' => $this->adminUser->id()))->save();
|
||||
}
|
||||
|
||||
$this->enableViewsTestModule();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the behavior of the Serializer callback paths and row plugins.
|
||||
*/
|
||||
public function testSerializerResponses() {
|
||||
// Test the serialize callback.
|
||||
$view = Views::getView('test_serializer_display_field');
|
||||
$view->initDisplay();
|
||||
$this->executeView($view);
|
||||
|
||||
$actual_json = $this->drupalGetWithFormat('test/serialize/field', 'json');
|
||||
$this->assertResponse(200);
|
||||
$this->assertCacheTags($view->getCacheTags());
|
||||
$this->assertCacheContexts(['languages:language_interface', 'theme', 'request_format']);
|
||||
// @todo Due to https://www.drupal.org/node/2352009 we can't yet test the
|
||||
// propagation of cache max-age.
|
||||
|
||||
// Test the http Content-type.
|
||||
$headers = $this->drupalGetHeaders();
|
||||
$this->assertEqual($headers['content-type'], 'application/json', 'The header Content-type is correct.');
|
||||
|
||||
$expected = array();
|
||||
foreach ($view->result as $row) {
|
||||
$expected_row = array();
|
||||
foreach ($view->field as $id => $field) {
|
||||
$expected_row[$id] = $field->render($row);
|
||||
}
|
||||
$expected[] = $expected_row;
|
||||
}
|
||||
|
||||
$this->assertIdentical($actual_json, json_encode($expected), 'The expected JSON output was found.');
|
||||
|
||||
|
||||
// Test that the rendered output and the preview output are the same.
|
||||
$view->destroy();
|
||||
$view->setDisplay('rest_export_1');
|
||||
// Mock the request content type by setting it on the display handler.
|
||||
$view->display_handler->setContentType('json');
|
||||
$output = $view->preview();
|
||||
$this->assertIdentical($actual_json, drupal_render_root($output), 'The expected JSON preview output was found.');
|
||||
|
||||
// Test a 403 callback.
|
||||
$this->drupalGet('test/serialize/denied');
|
||||
$this->assertResponse(403);
|
||||
|
||||
// Test the entity rows.
|
||||
$view = Views::getView('test_serializer_display_entity');
|
||||
$view->initDisplay();
|
||||
$this->executeView($view);
|
||||
|
||||
// Get the serializer service.
|
||||
$serializer = $this->container->get('serializer');
|
||||
|
||||
$entities = array();
|
||||
foreach ($view->result as $row) {
|
||||
$entities[] = $row->_entity;
|
||||
}
|
||||
|
||||
$expected = $serializer->serialize($entities, 'json');
|
||||
|
||||
$actual_json = $this->drupalGetWithFormat('test/serialize/entity', 'json');
|
||||
$this->assertResponse(200);
|
||||
$this->assertIdentical($actual_json, $expected, 'The expected JSON output was found.');
|
||||
$expected_cache_tags = $view->getCacheTags();
|
||||
$expected_cache_tags[] = 'entity_test_list';
|
||||
/** @var \Drupal\Core\Entity\EntityInterface $entity */
|
||||
foreach ($entities as $entity) {
|
||||
$expected_cache_tags = Cache::mergeTags($expected_cache_tags, $entity->getCacheTags());
|
||||
}
|
||||
$this->assertCacheTags($expected_cache_tags);
|
||||
$this->assertCacheContexts(['languages:language_interface', 'theme', 'entity_test_view_grants', 'request_format']);
|
||||
|
||||
$expected = $serializer->serialize($entities, 'hal_json');
|
||||
$actual_json = $this->drupalGetWithFormat('test/serialize/entity', 'hal_json');
|
||||
$this->assertIdentical($actual_json, $expected, 'The expected HAL output was found.');
|
||||
$this->assertCacheTags($expected_cache_tags);
|
||||
|
||||
// Change the default format to xml.
|
||||
$view->setDisplay('rest_export_1');
|
||||
$view->getDisplay()->setOption('style', array(
|
||||
'type' => 'serializer',
|
||||
'options' => array(
|
||||
'uses_fields' => FALSE,
|
||||
'formats' => array(
|
||||
'xml' => 'xml',
|
||||
),
|
||||
),
|
||||
));
|
||||
$view->save();
|
||||
$expected = $serializer->serialize($entities, 'xml');
|
||||
$actual_xml = $this->drupalGet('test/serialize/entity');
|
||||
$this->assertIdentical($actual_xml, $expected, 'The expected XML output was found.');
|
||||
$this->assertCacheContexts(['languages:language_interface', 'theme', 'entity_test_view_grants', 'request_format']);
|
||||
|
||||
// Allow multiple formats.
|
||||
$view->setDisplay('rest_export_1');
|
||||
$view->getDisplay()->setOption('style', array(
|
||||
'type' => 'serializer',
|
||||
'options' => array(
|
||||
'uses_fields' => FALSE,
|
||||
'formats' => array(
|
||||
'xml' => 'xml',
|
||||
'json' => 'json',
|
||||
),
|
||||
),
|
||||
));
|
||||
$view->save();
|
||||
$expected = $serializer->serialize($entities, 'json');
|
||||
$actual_json = $this->drupalGetWithFormat('test/serialize/entity', 'json');
|
||||
$this->assertIdentical($actual_json, $expected, 'The expected JSON output was found.');
|
||||
$expected = $serializer->serialize($entities, 'xml');
|
||||
$actual_xml = $this->drupalGetWithFormat('test/serialize/entity', 'xml');
|
||||
$this->assertIdentical($actual_xml, $expected, 'The expected XML output was found.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up a request on the request stack with a specified format.
|
||||
*
|
||||
* @param string $format
|
||||
* The new request format.
|
||||
*/
|
||||
protected function addRequestWithFormat($format) {
|
||||
$request = \Drupal::request();
|
||||
$request = clone $request;
|
||||
$request->setRequestFormat($format);
|
||||
|
||||
\Drupal::requestStack()->push($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests REST export with views render caching enabled.
|
||||
*/
|
||||
public function testRestRenderCaching() {
|
||||
$this->drupalLogin($this->adminUser);
|
||||
/** @var \Drupal\Core\Render\RenderCacheInterface $render_cache */
|
||||
$render_cache = \Drupal::service('render_cache');
|
||||
|
||||
// Enable render caching for the views.
|
||||
/** @var \Drupal\views\ViewEntityInterface $storage */
|
||||
$storage = View::load('test_serializer_display_entity');
|
||||
$options = &$storage->getDisplay('default');
|
||||
$options['display_options']['cache'] = [
|
||||
'type' => 'tag',
|
||||
];
|
||||
$storage->save();
|
||||
|
||||
$original = DisplayPluginBase::buildBasicRenderable('test_serializer_display_entity', 'rest_export_1');
|
||||
|
||||
// Ensure that there is no corresponding render cache item yet.
|
||||
$original['#cache'] += ['contexts' => []];
|
||||
$original['#cache']['contexts'] = Cache::mergeContexts($original['#cache']['contexts'], $this->container->getParameter('renderer.config')['required_cache_contexts']);
|
||||
|
||||
$cache_tags = [
|
||||
'config:views.view.test_serializer_display_entity',
|
||||
'entity_test:1',
|
||||
'entity_test:10',
|
||||
'entity_test:2',
|
||||
'entity_test:3',
|
||||
'entity_test:4',
|
||||
'entity_test:5',
|
||||
'entity_test:6',
|
||||
'entity_test:7',
|
||||
'entity_test:8',
|
||||
'entity_test:9',
|
||||
'entity_test_list'
|
||||
];
|
||||
$cache_contexts = [
|
||||
'entity_test_view_grants',
|
||||
'languages:language_interface',
|
||||
'theme',
|
||||
'request_format',
|
||||
];
|
||||
|
||||
$this->assertFalse($render_cache->get($original));
|
||||
|
||||
// Request the page, once in XML and once in JSON to ensure that the caching
|
||||
// varies by it.
|
||||
$result1 = $this->drupalGetJSON('test/serialize/entity');
|
||||
$this->addRequestWithFormat('json');
|
||||
$this->assertHeader('content-type', 'application/json');
|
||||
$this->assertCacheContexts($cache_contexts);
|
||||
$this->assertCacheTags($cache_tags);
|
||||
$this->assertTrue($render_cache->get($original));
|
||||
|
||||
$result_xml = $this->drupalGetWithFormat('test/serialize/entity', 'xml');
|
||||
$this->addRequestWithFormat('xml');
|
||||
$this->assertHeader('content-type', 'text/xml; charset=UTF-8');
|
||||
$this->assertCacheContexts($cache_contexts);
|
||||
$this->assertCacheTags($cache_tags);
|
||||
$this->assertTrue($render_cache->get($original));
|
||||
|
||||
// Ensure that the XML output is different from the JSON one.
|
||||
$this->assertNotEqual($result1, $result_xml);
|
||||
|
||||
// Ensure that the cached page works.
|
||||
$result2 = $this->drupalGetJSON('test/serialize/entity');
|
||||
$this->addRequestWithFormat('json');
|
||||
$this->assertHeader('content-type', 'application/json');
|
||||
$this->assertEqual($result2, $result1);
|
||||
$this->assertCacheContexts($cache_contexts);
|
||||
$this->assertCacheTags($cache_tags);
|
||||
$this->assertTrue($render_cache->get($original));
|
||||
|
||||
// Create a new entity and ensure that the cache tags are taken over.
|
||||
EntityTest::create(['name' => 'test_11', 'user_id' => $this->adminUser->id()])->save();
|
||||
$result3 = $this->drupalGetJSON('test/serialize/entity');
|
||||
$this->addRequestWithFormat('json');
|
||||
$this->assertHeader('content-type', 'application/json');
|
||||
$this->assertNotEqual($result3, $result2);
|
||||
|
||||
// Add the new entity cache tag and remove the first one, because we just
|
||||
// show 10 items in total.
|
||||
$cache_tags[] = 'entity_test:11';
|
||||
unset($cache_tags[array_search('entity_test:1', $cache_tags)]);
|
||||
|
||||
$this->assertCacheContexts($cache_contexts);
|
||||
$this->assertCacheTags($cache_tags);
|
||||
$this->assertTrue($render_cache->get($original));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the response format configuration.
|
||||
*/
|
||||
public function testResponseFormatConfiguration() {
|
||||
$this->drupalLogin($this->adminUser);
|
||||
|
||||
$style_options = 'admin/structure/views/nojs/display/test_serializer_display_field/rest_export_1/style_options';
|
||||
|
||||
// Select only 'xml' as an accepted format.
|
||||
$this->drupalPostForm($style_options, array('style_options[formats][xml]' => 'xml'), t('Apply'));
|
||||
$this->drupalPostForm(NULL, array(), t('Save'));
|
||||
|
||||
// Should return a 406.
|
||||
$this->drupalGetWithFormat('test/serialize/field', 'json');
|
||||
$this->assertHeader('content-type', 'application/json');
|
||||
$this->assertResponse(406, 'A 406 response was returned when JSON was requested.');
|
||||
// Should return a 200.
|
||||
$this->drupalGetWithFormat('test/serialize/field', 'xml');
|
||||
$this->assertHeader('content-type', 'text/xml; charset=UTF-8');
|
||||
$this->assertResponse(200, 'A 200 response was returned when XML was requested.');
|
||||
|
||||
// Add 'json' as an accepted format, so we have multiple.
|
||||
$this->drupalPostForm($style_options, array('style_options[formats][json]' => 'json'), t('Apply'));
|
||||
$this->drupalPostForm(NULL, array(), t('Save'));
|
||||
|
||||
// Should return a 200.
|
||||
// @todo This should be fixed when we have better content negotiation.
|
||||
$this->drupalGet('test/serialize/field');
|
||||
$this->assertHeader('content-type', 'application/json');
|
||||
$this->assertResponse(200, 'A 200 response was returned when any format was requested.');
|
||||
|
||||
// Should return a 200. Emulates a sample Firefox header.
|
||||
$this->drupalGet('test/serialize/field', array(), array('Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'));
|
||||
$this->assertHeader('content-type', 'application/json');
|
||||
$this->assertResponse(200, 'A 200 response was returned when a browser accept header was requested.');
|
||||
|
||||
// Should return a 200.
|
||||
$this->drupalGetWithFormat('test/serialize/field', 'json');
|
||||
$this->assertHeader('content-type', 'application/json');
|
||||
$this->assertResponse(200, 'A 200 response was returned when JSON was requested.');
|
||||
$headers = $this->drupalGetHeaders();
|
||||
$this->assertEqual($headers['content-type'], 'application/json', 'The header Content-type is correct.');
|
||||
// Should return a 200.
|
||||
$this->drupalGetWithFormat('test/serialize/field', 'xml');
|
||||
$this->assertHeader('content-type', 'text/xml; charset=UTF-8');
|
||||
$this->assertResponse(200, 'A 200 response was returned when XML was requested');
|
||||
$headers = $this->drupalGetHeaders();
|
||||
$this->assertTrue(strpos($headers['content-type'], 'text/xml') !== FALSE, 'The header Content-type is correct.');
|
||||
// Should return a 406.
|
||||
$this->drupalGetWithFormat('test/serialize/field', 'html');
|
||||
// We want to show the first format by default, see
|
||||
// \Drupal\rest\Plugin\views\style\Serializer::render.
|
||||
$this->assertHeader('content-type', 'application/json');
|
||||
$this->assertResponse(200, 'A 200 response was returned when HTML was requested.');
|
||||
|
||||
// Now configure now format, so all of them should be allowed.
|
||||
$this->drupalPostForm($style_options, array('style_options[formats][json]' => '0', 'style_options[formats][xml]' => '0'), t('Apply'));
|
||||
|
||||
// Should return a 200.
|
||||
$this->drupalGetWithFormat('test/serialize/field', 'json');
|
||||
$this->assertHeader('content-type', 'application/json');
|
||||
$this->assertResponse(200, 'A 200 response was returned when JSON was requested.');
|
||||
// Should return a 200.
|
||||
$this->drupalGetWithFormat('test/serialize/field', 'xml');
|
||||
$this->assertHeader('content-type', 'text/xml; charset=UTF-8');
|
||||
$this->assertResponse(200, 'A 200 response was returned when XML was requested');
|
||||
// Should return a 200.
|
||||
$this->drupalGetWithFormat('test/serialize/field', 'html');
|
||||
// We want to show the first format by default, see
|
||||
// \Drupal\rest\Plugin\views\style\Serializer::render.
|
||||
$this->assertHeader('content-type', 'application/json');
|
||||
$this->assertResponse(200, 'A 200 response was returned when HTML was requested.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the field ID alias functionality of the DataFieldRow plugin.
|
||||
*/
|
||||
public function testUIFieldAlias() {
|
||||
$this->drupalLogin($this->adminUser);
|
||||
|
||||
// Test the UI settings for adding field ID aliases.
|
||||
$this->drupalGet('admin/structure/views/view/test_serializer_display_field/edit/rest_export_1');
|
||||
$row_options = 'admin/structure/views/nojs/display/test_serializer_display_field/rest_export_1/row_options';
|
||||
$this->assertLinkByHref($row_options);
|
||||
|
||||
// Test an empty string for an alias, this should not be used. This also
|
||||
// tests that the form can be submitted with no aliases.
|
||||
$this->drupalPostForm($row_options, array('row_options[field_options][name][alias]' => ''), t('Apply'));
|
||||
$this->drupalPostForm(NULL, array(), t('Save'));
|
||||
|
||||
$view = Views::getView('test_serializer_display_field');
|
||||
$view->setDisplay('rest_export_1');
|
||||
$this->executeView($view);
|
||||
|
||||
$expected = array();
|
||||
foreach ($view->result as $row) {
|
||||
$expected_row = array();
|
||||
foreach ($view->field as $id => $field) {
|
||||
$expected_row[$id] = $field->render($row);
|
||||
}
|
||||
$expected[] = $expected_row;
|
||||
}
|
||||
|
||||
$this->assertIdentical($this->drupalGetJSON('test/serialize/field'), $expected);
|
||||
|
||||
// Test a random aliases for fields, they should be replaced.
|
||||
$alias_map = array(
|
||||
'name' => $this->randomMachineName(),
|
||||
// Use # to produce an invalid character for the validation.
|
||||
'nothing' => '#' . $this->randomMachineName(),
|
||||
'created' => 'created',
|
||||
);
|
||||
|
||||
$edit = array('row_options[field_options][name][alias]' => $alias_map['name'], 'row_options[field_options][nothing][alias]' => $alias_map['nothing']);
|
||||
$this->drupalPostForm($row_options, $edit, t('Apply'));
|
||||
$this->assertText(t('The machine-readable name must contain only letters, numbers, dashes and underscores.'));
|
||||
|
||||
// Change the map alias value to a valid one.
|
||||
$alias_map['nothing'] = $this->randomMachineName();
|
||||
|
||||
$edit = array('row_options[field_options][name][alias]' => $alias_map['name'], 'row_options[field_options][nothing][alias]' => $alias_map['nothing']);
|
||||
$this->drupalPostForm($row_options, $edit, t('Apply'));
|
||||
|
||||
$this->drupalPostForm(NULL, array(), t('Save'));
|
||||
|
||||
$view = Views::getView('test_serializer_display_field');
|
||||
$view->setDisplay('rest_export_1');
|
||||
$this->executeView($view);
|
||||
|
||||
$expected = array();
|
||||
foreach ($view->result as $row) {
|
||||
$expected_row = array();
|
||||
foreach ($view->field as $id => $field) {
|
||||
$expected_row[$alias_map[$id]] = $field->render($row);
|
||||
}
|
||||
$expected[] = $expected_row;
|
||||
}
|
||||
|
||||
$this->assertIdentical($this->drupalGetJSON('test/serialize/field'), $expected);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the raw output options for row field rendering.
|
||||
*/
|
||||
public function testFieldRawOutput() {
|
||||
$this->drupalLogin($this->adminUser);
|
||||
|
||||
// Test the UI settings for adding field ID aliases.
|
||||
$this->drupalGet('admin/structure/views/view/test_serializer_display_field/edit/rest_export_1');
|
||||
$row_options = 'admin/structure/views/nojs/display/test_serializer_display_field/rest_export_1/row_options';
|
||||
$this->assertLinkByHref($row_options);
|
||||
|
||||
// Test an empty string for an alias, this should not be used. This also
|
||||
// tests that the form can be submitted with no aliases.
|
||||
$this->drupalPostForm($row_options, array('row_options[field_options][created][raw_output]' => '1'), t('Apply'));
|
||||
$this->drupalPostForm(NULL, array(), t('Save'));
|
||||
|
||||
$view = Views::getView('test_serializer_display_field');
|
||||
$view->setDisplay('rest_export_1');
|
||||
$this->executeView($view);
|
||||
|
||||
// Just test the raw 'created' value against each row.
|
||||
foreach ($this->drupalGetJSON('test/serialize/field') as $index => $values) {
|
||||
$this->assertIdentical($values['created'], $view->result[$index]->views_test_data_created, 'Expected raw created value found.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the live preview output for json output.
|
||||
*/
|
||||
public function testLivePreview() {
|
||||
// We set up a request so it looks like an request in the live preview.
|
||||
$request = new Request();
|
||||
$request->setFormat('drupal_ajax', 'application/vnd.drupal-ajax');
|
||||
$request->headers->set('Accept', 'application/vnd.drupal-ajax');
|
||||
/** @var \Symfony\Component\HttpFoundation\RequestStack $request_stack */
|
||||
$request_stack = \Drupal::service('request_stack');
|
||||
$request_stack->push($request);
|
||||
|
||||
$view = Views::getView('test_serializer_display_entity');
|
||||
$view->setDisplay('rest_export_1');
|
||||
$this->executeView($view);
|
||||
|
||||
// Get the serializer service.
|
||||
$serializer = $this->container->get('serializer');
|
||||
|
||||
$entities = array();
|
||||
foreach ($view->result as $row) {
|
||||
$entities[] = $row->_entity;
|
||||
}
|
||||
|
||||
$expected = SafeMarkup::checkPlain($serializer->serialize($entities, 'json'));
|
||||
|
||||
$view->live_preview = TRUE;
|
||||
|
||||
$build = $view->preview();
|
||||
$rendered_json = $build['#markup'];
|
||||
$this->assertEqual($rendered_json, $expected, 'Ensure the previewed json is escaped.');
|
||||
$view->destroy();
|
||||
|
||||
$expected = SafeMarkup::checkPlain($serializer->serialize($entities, 'xml'));
|
||||
|
||||
// Change the request format to xml.
|
||||
$view->setDisplay('rest_export_1');
|
||||
$view->getDisplay()->setOption('style', array(
|
||||
'type' => 'serializer',
|
||||
'options' => array(
|
||||
'uses_fields' => FALSE,
|
||||
'formats' => array(
|
||||
'xml' => 'xml',
|
||||
),
|
||||
),
|
||||
));
|
||||
|
||||
$this->executeView($view);
|
||||
$build = $view->preview();
|
||||
$rendered_xml = $build['#markup'];
|
||||
$this->assertEqual($rendered_xml, $expected, 'Ensure we preview xml when we change the request format.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the views interface for REST export displays.
|
||||
*/
|
||||
public function testSerializerViewsUI() {
|
||||
$this->drupalLogin($this->adminUser);
|
||||
// Click the "Update preview button".
|
||||
$this->drupalPostForm('admin/structure/views/view/test_serializer_display_field/edit/rest_export_1', $edit = array(), t('Update preview'));
|
||||
$this->assertResponse(200);
|
||||
// Check if we receive the expected result.
|
||||
$result = $this->xpath('//div[@id="views-live-preview"]/pre');
|
||||
$this->assertIdentical($this->drupalGet('test/serialize/field'), (string) $result[0], 'The expected JSON preview output was found.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the field row style using fieldapi fields.
|
||||
*/
|
||||
public function testFieldapiField() {
|
||||
$this->drupalCreateContentType(array('type' => 'page'));
|
||||
$node = $this->drupalCreateNode();
|
||||
|
||||
$result = $this->drupalGetJSON('test/serialize/node-field');
|
||||
$this->assertEqual($result[0]['nid'], $node->id());
|
||||
$this->assertEqual($result[0]['body'], $node->body->processed);
|
||||
|
||||
// Make sure that serialized fields are not exposed to XSS.
|
||||
$node = $this->drupalCreateNode();
|
||||
$node->body = [
|
||||
'value' => '<script type="text/javascript">alert("node-body");</script>' . $this->randomMachineName(32),
|
||||
'format' => filter_default_format(),
|
||||
];
|
||||
$node->save();
|
||||
$result = $this->drupalGetJSON('test/serialize/node-field');
|
||||
$this->assertEqual($result[1]['nid'], $node->id());
|
||||
$this->assertTrue(strpos($this->getRawContent(), "<script") === FALSE, "No script tag is present in the raw page contents.");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
name: 'REST test'
|
||||
type: module
|
||||
description: 'Provides test hooks for REST module.'
|
||||
package: Testing
|
||||
version: VERSION
|
||||
core: 8.x
|
||||
dependencies:
|
||||
- rest
|
24
core/modules/rest/tests/modules/rest_test/rest_test.module
Normal file
24
core/modules/rest/tests/modules/rest_test/rest_test.module
Normal file
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains hook implementations for testing REST module.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Implements hook_rest_type_uri_alter().
|
||||
*/
|
||||
function rest_test_rest_type_uri_alter(&$uri, $context = array()) {
|
||||
if (!empty($context['rest_test'])) {
|
||||
$uri = 'rest_test_type';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_rest_relation_uri_alter().
|
||||
*/
|
||||
function rest_test_rest_relation_uri_alter(&$uri, $context = array()) {
|
||||
if (!empty($context['rest_test'])) {
|
||||
$uri = 'rest_test_relation';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
name: 'REST test views'
|
||||
type: module
|
||||
description: 'Provides default views for views REST tests.'
|
||||
package: Testing
|
||||
version: VERSION
|
||||
core: 8.x
|
||||
dependencies:
|
||||
- rest
|
||||
- views
|
|
@ -0,0 +1,55 @@
|
|||
langcode: en
|
||||
status: true
|
||||
dependencies:
|
||||
module:
|
||||
- rest
|
||||
- user
|
||||
id: test_serializer_display_entity
|
||||
label: 'Test serialize display entity rows'
|
||||
module: rest
|
||||
description: ''
|
||||
tag: ''
|
||||
base_table: entity_test
|
||||
base_field: id
|
||||
core: 8.x
|
||||
display:
|
||||
default:
|
||||
display_plugin: default
|
||||
id: default
|
||||
display_title: Master
|
||||
position: null
|
||||
display_options:
|
||||
access:
|
||||
type: perm
|
||||
options:
|
||||
perm: 'access content'
|
||||
cache:
|
||||
type: tag
|
||||
query:
|
||||
type: views_query
|
||||
exposed_form:
|
||||
type: basic
|
||||
style:
|
||||
type: serializer
|
||||
row:
|
||||
type: data_entity
|
||||
sorts:
|
||||
id:
|
||||
id: standard
|
||||
table: entity_test
|
||||
field: id
|
||||
order: DESC
|
||||
plugin_id: date
|
||||
entity_type: entity_test
|
||||
entity_field: id
|
||||
title: 'Test serialize'
|
||||
arguments: { }
|
||||
rest_export_1:
|
||||
display_plugin: rest_export
|
||||
id: rest_export_1
|
||||
display_title: serializer
|
||||
position: null
|
||||
display_options:
|
||||
defaults:
|
||||
access: false
|
||||
path: test/serialize/entity
|
|
@ -0,0 +1,105 @@
|
|||
langcode: en
|
||||
status: true
|
||||
dependencies:
|
||||
module:
|
||||
- rest
|
||||
- user
|
||||
id: test_serializer_display_field
|
||||
label: 'Test serializer display field rows'
|
||||
module: rest
|
||||
description: ''
|
||||
tag: ''
|
||||
base_table: views_test_data
|
||||
base_field: id
|
||||
core: 8.x
|
||||
display:
|
||||
default:
|
||||
display_plugin: default
|
||||
id: default
|
||||
display_title: Master
|
||||
position: null
|
||||
display_options:
|
||||
access:
|
||||
type: perm
|
||||
options:
|
||||
perm: 'access content'
|
||||
cache:
|
||||
type: tag
|
||||
query:
|
||||
type: views_query
|
||||
exposed_form:
|
||||
type: basic
|
||||
style:
|
||||
type: serializer
|
||||
row:
|
||||
type: data_field
|
||||
fields:
|
||||
name:
|
||||
id: name
|
||||
table: views_test_data
|
||||
field: name
|
||||
label: ''
|
||||
plugin_id: string
|
||||
nothing:
|
||||
id: nothing
|
||||
table: views
|
||||
field: nothing
|
||||
relationship: none
|
||||
group_type: group
|
||||
admin_label: ''
|
||||
label: 'Custom text'
|
||||
exclude: false
|
||||
alter:
|
||||
alter_text: true
|
||||
text: TEST
|
||||
plugin_id: custom
|
||||
created:
|
||||
id: created
|
||||
table: views_test_data
|
||||
field: created
|
||||
plugin_id: date
|
||||
sorts:
|
||||
created:
|
||||
id: created
|
||||
table: views_test_data
|
||||
field: created
|
||||
order: DESC
|
||||
plugin_id: date
|
||||
title: 'Test serialize'
|
||||
arguments: { }
|
||||
rest_export_1:
|
||||
display_plugin: rest_export
|
||||
id: rest_export_1
|
||||
display_title: serializer
|
||||
position: null
|
||||
display_options:
|
||||
defaults:
|
||||
access: false
|
||||
style: false
|
||||
row: false
|
||||
path: test/serialize/field
|
||||
access:
|
||||
type: none
|
||||
style:
|
||||
type: serializer
|
||||
row:
|
||||
type: data_field
|
||||
rest_export_2:
|
||||
display_plugin: rest_export
|
||||
id: rest_export_2
|
||||
display_title: 'serialize - access denied'
|
||||
position: null
|
||||
display_options:
|
||||
defaults:
|
||||
access: false
|
||||
style: false
|
||||
row: false
|
||||
path: test/serialize/denied
|
||||
access:
|
||||
type: perm
|
||||
options:
|
||||
perm: 'administer views'
|
||||
style:
|
||||
type: serializer
|
||||
row:
|
||||
type: data_field
|
|
@ -0,0 +1,131 @@
|
|||
langcode: en
|
||||
status: true
|
||||
dependencies:
|
||||
config:
|
||||
- field.storage.node.body
|
||||
module:
|
||||
- field
|
||||
- node
|
||||
- rest
|
||||
- rest_test_views
|
||||
- user
|
||||
id: test_serializer_node_display_field
|
||||
label: 'Test serializer display field rows for entity fields'
|
||||
module: rest_test_views
|
||||
description: ''
|
||||
tag: ''
|
||||
base_table: node_field_data
|
||||
base_field: nid
|
||||
core: 8.x
|
||||
display:
|
||||
default:
|
||||
display_plugin: default
|
||||
id: default
|
||||
display_title: Master
|
||||
position: null
|
||||
display_options:
|
||||
access:
|
||||
type: perm
|
||||
options:
|
||||
perm: 'access content'
|
||||
cache:
|
||||
type: tag
|
||||
query:
|
||||
type: views_query
|
||||
exposed_form:
|
||||
type: basic
|
||||
style:
|
||||
type: serializer
|
||||
row:
|
||||
type: data_field
|
||||
fields:
|
||||
nid:
|
||||
id: nid
|
||||
table: node
|
||||
field: nid
|
||||
plugin_id: field
|
||||
entity_type: node
|
||||
entity_field: nid
|
||||
body:
|
||||
id: body
|
||||
table: node__body
|
||||
field: body
|
||||
relationship: none
|
||||
group_type: group
|
||||
admin_label: ''
|
||||
label: Body
|
||||
exclude: false
|
||||
alter:
|
||||
alter_text: false
|
||||
text: ''
|
||||
make_link: false
|
||||
path: ''
|
||||
absolute: false
|
||||
external: false
|
||||
replace_spaces: false
|
||||
path_case: none
|
||||
trim_whitespace: false
|
||||
alt: ''
|
||||
rel: ''
|
||||
link_class: ''
|
||||
prefix: ''
|
||||
suffix: ''
|
||||
target: ''
|
||||
nl2br: false
|
||||
max_length: 0
|
||||
word_boundary: true
|
||||
ellipsis: true
|
||||
more_link: false
|
||||
more_link_text: ''
|
||||
more_link_path: ''
|
||||
strip_tags: false
|
||||
trim: false
|
||||
preserve_tags: ''
|
||||
html: false
|
||||
element_type: ''
|
||||
element_class: ''
|
||||
element_label_type: ''
|
||||
element_label_class: ''
|
||||
element_label_colon: true
|
||||
element_wrapper_type: ''
|
||||
element_wrapper_class: ''
|
||||
element_default_classes: true
|
||||
empty: ''
|
||||
hide_empty: false
|
||||
empty_zero: false
|
||||
hide_alter_empty: true
|
||||
click_sort_column: value
|
||||
type: text_default
|
||||
settings: { }
|
||||
group_column: value
|
||||
group_columns: { }
|
||||
group_rows: true
|
||||
delta_limit: 0
|
||||
delta_offset: 0
|
||||
delta_reversed: false
|
||||
delta_first_last: false
|
||||
multi_type: separator
|
||||
separator: ', '
|
||||
field_api_classes: false
|
||||
plugin_id: field
|
||||
entity_type: node
|
||||
entity_field: body
|
||||
title: 'Test serialize'
|
||||
arguments: { }
|
||||
rest_export_1:
|
||||
display_plugin: rest_export
|
||||
id: rest_export_1
|
||||
display_title: serializer
|
||||
position: null
|
||||
display_options:
|
||||
defaults:
|
||||
access: false
|
||||
style: false
|
||||
row: false
|
||||
path: test/serialize/node-field
|
||||
access:
|
||||
type: none
|
||||
style:
|
||||
type: serializer
|
||||
row:
|
||||
type: data_field
|
138
core/modules/rest/tests/src/Unit/CollectRoutesTest.php
Normal file
138
core/modules/rest/tests/src/Unit/CollectRoutesTest.php
Normal file
|
@ -0,0 +1,138 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\Tests\rest\Unit\CollectRoutesTest.
|
||||
*/
|
||||
|
||||
namespace Drupal\Tests\rest\Unit;
|
||||
|
||||
use Drupal\Tests\UnitTestCase;
|
||||
use Drupal\Core\DependencyInjection\ContainerBuilder;
|
||||
use Drupal\rest\Plugin\views\display\RestExport;
|
||||
use Symfony\Component\Routing\Route;
|
||||
use Symfony\Component\Routing\RouteCollection;
|
||||
|
||||
/**
|
||||
* Tests the REST export view plugin.
|
||||
*
|
||||
* @group rest
|
||||
*/
|
||||
class CollectRoutesTest extends UnitTestCase {
|
||||
|
||||
/**
|
||||
* The REST export instance.
|
||||
*
|
||||
* @var \Drupal\rest\Plugin\views\display\RestExport
|
||||
*/
|
||||
protected $restExport;
|
||||
|
||||
/**
|
||||
* The RouteCollection.
|
||||
*/
|
||||
protected $routes;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
$container = new ContainerBuilder();
|
||||
|
||||
$request = $this->getMockBuilder('\Symfony\Component\HttpFoundation\Request')
|
||||
->disableOriginalConstructor()
|
||||
->getMock();
|
||||
|
||||
$this->view = $this->getMock('\Drupal\views\Entity\View', array('initHandlers'), array(
|
||||
array('id' => 'test_view'),
|
||||
'view',
|
||||
));
|
||||
|
||||
$view_executable = $this->getMock('\Drupal\views\ViewExecutable', array('initHandlers'), array(), '', FALSE);
|
||||
|
||||
$view_executable->storage = $this->view;
|
||||
$view_executable->argument = array();
|
||||
|
||||
$display_manager = $this->getMockBuilder('\Drupal\views\Plugin\ViewsPluginManager')
|
||||
->disableOriginalConstructor()
|
||||
->getMock();
|
||||
$container->set('plugin.manager.views.display', $display_manager);
|
||||
|
||||
$access_manager = $this->getMockBuilder('\Drupal\views\Plugin\ViewsPluginManager')
|
||||
->disableOriginalConstructor()
|
||||
->getMock();
|
||||
$container->set('plugin.manager.views.access', $access_manager);
|
||||
|
||||
$route_provider = $this->getMockBuilder('\Drupal\Core\Routing\RouteProviderInterface')
|
||||
->disableOriginalConstructor()
|
||||
->getMock();
|
||||
$container->set('router.route_provider', $route_provider);
|
||||
|
||||
$state = $this->getMock('\Drupal\Core\State\StateInterface');
|
||||
$container->set('state', $state);
|
||||
|
||||
$style_manager = $this->getMockBuilder('\Drupal\views\Plugin\ViewsPluginManager')
|
||||
->disableOriginalConstructor()
|
||||
->getMock();
|
||||
$container->set('plugin.manager.views.style', $style_manager);
|
||||
$container->set('renderer', $this->getMock('Drupal\Core\Render\RendererInterface'));
|
||||
|
||||
\Drupal::setContainer($container);
|
||||
|
||||
$this->restExport = RestExport::create($container, array(), "test_routes", array());
|
||||
$this->restExport->view = $view_executable;
|
||||
|
||||
// Initialize a display.
|
||||
$this->restExport->display = array('id' => 'page_1');
|
||||
|
||||
// Set the style option.
|
||||
$this->restExport->setOption('style', array('type' => 'serializer'));
|
||||
|
||||
$display_manager->expects($this->once())
|
||||
->method('getDefinition')
|
||||
->will($this->returnValue(array('id' => 'test', 'provider' => 'test')));
|
||||
|
||||
$none = $this->getMockBuilder('\Drupal\views\Plugin\views\access\None')
|
||||
->disableOriginalConstructor()
|
||||
->getMock();
|
||||
|
||||
$access_manager->expects($this->once())
|
||||
->method('createInstance')
|
||||
->will($this->returnValue($none));
|
||||
|
||||
$style_plugin = $this->getMock('\Drupal\rest\Plugin\views\style\Serializer', array('getFormats', 'init'), array(), '', FALSE);
|
||||
|
||||
$style_plugin->expects($this->once())
|
||||
->method('getFormats')
|
||||
->will($this->returnValue(array('json')));
|
||||
|
||||
$style_plugin->expects($this->once())
|
||||
->method('init')
|
||||
->with($view_executable)
|
||||
->will($this->returnValue(TRUE));
|
||||
|
||||
$style_manager->expects($this->once())
|
||||
->method('createInstance')
|
||||
->will($this->returnValue($style_plugin));
|
||||
|
||||
$this->routes = new RouteCollection();
|
||||
$this->routes->add('test_1', new Route('/test/1'));
|
||||
$this->routes->add('view.test_view.page_1', new Route('/test/2'));
|
||||
|
||||
$this->view->addDisplay('page', NULL, 'page_1');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests if adding a requirement to a route only modify one route.
|
||||
*/
|
||||
public function testRoutesRequirements() {
|
||||
$this->restExport->collectRoutes($this->routes);
|
||||
|
||||
$requirements_1 = $this->routes->get('test_1')->getRequirements();
|
||||
$requirements_2 = $this->routes->get('view.test_view.page_1')->getRequirements();
|
||||
|
||||
$this->assertEquals(count($requirements_1), 0, 'First route has no requirement.');
|
||||
$this->assertEquals(count($requirements_2), 2, 'Views route with rest export had the format and method requirements added.');
|
||||
}
|
||||
}
|
Reference in a new issue