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
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.");
|
||||
}
|
||||
}
|
Reference in a new issue