caore d8 files
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
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\dblog\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,20 @@
|
||||
<?php
|
||||
|
||||
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);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getTypeUri($entity_type, $bundle, $context = array()) {
|
||||
return $this->typeLinkManager->getTypeUri($entity_type, $bundle, $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getTypeInternalIds($type_uri, $context = array()) {
|
||||
return $this->typeLinkManager->getTypeInternalIds($type_uri, $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getRelationUri($entity_type, $bundle, $field_name, $context = array()) {
|
||||
return $this->relationLinkManager->getRelationUri($entity_type, $bundle, $field_name, $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\rest\LinkManager;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
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 {
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
<?php
|
||||
|
||||
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()) {
|
||||
// Per the interface documention of this method, the returned URI may
|
||||
// optionally also serve as the URL of a documentation page about this
|
||||
// field. However, the REST module does not currently implement such
|
||||
// a documentation page. Therefore, we return a URI assembled relative to
|
||||
// the site's base URL, which is sufficient to uniquely identify the site's
|
||||
// entity type + bundle + field for use in hypermedia formats, but we do
|
||||
// not take into account unclean URLs, language prefixing, or anything else
|
||||
// that would be required for Drupal to be able to respond with content
|
||||
// at this URL. If a module is installed that adds such content, but
|
||||
// requires this URL to be different (e.g., include a language prefix),
|
||||
// then the module must also override the RelationLinkManager class/service
|
||||
// to return the desired URL.
|
||||
$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,39 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\rest\LinkManager;
|
||||
|
||||
interface RelationLinkManagerInterface extends ConfigurableLinkManagerInterface {
|
||||
|
||||
/**
|
||||
* Gets the URI that corresponds to a field.
|
||||
*
|
||||
* When using hypermedia formats, this URI can be used to indicate which
|
||||
* field the data represents. Documentation about this field can also be
|
||||
* provided at this URI.
|
||||
*
|
||||
* @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);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
<?php
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getTypeUri($entity_type, $bundle, $context = array()) {
|
||||
// Per the interface documention of this method, the returned URI may
|
||||
// optionally also serve as the URL of a documentation page about this
|
||||
// bundle. However, the REST module does not currently implement such
|
||||
// a documentation page. Therefore, we return a URI assembled relative to
|
||||
// the site's base URL, which is sufficient to uniquely identify the site's
|
||||
// entity type and bundle for use in hypermedia formats, but we do not
|
||||
// take into account unclean URLs, language prefixing, or anything else
|
||||
// that would be required for Drupal to be able to respond with content
|
||||
// at this URL. If a module is installed that adds such content, but
|
||||
// requires this URL to be different (e.g., include a language prefix),
|
||||
// then the module must also override the TypeLinkManager class/service to
|
||||
// return the desired URL.
|
||||
$uri = $this->getLinkDomain() . "/rest/type/$entity_type/$bundle";
|
||||
$this->moduleHandler->alter('rest_type_uri', $uri, $context);
|
||||
return $uri;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
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,40 @@
|
||||
<?php
|
||||
|
||||
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());
|
||||
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\rest\Plugin\Deriver;
|
||||
|
||||
use Drupal\Core\Entity\EntityManagerInterface;
|
||||
use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* 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 EntityDeriver 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')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
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];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\rest\Plugin;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
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',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
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();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getInstance(array $options){
|
||||
if (isset($options['id'])) {
|
||||
return $this->createInstance($options['id']);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
<?php
|
||||
|
||||
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 Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||
|
||||
/**
|
||||
* Represents entities as resources.
|
||||
*
|
||||
* @see \Drupal\rest\Plugin\Deriver\EntityDeriver
|
||||
*
|
||||
* @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}"
|
||||
* }
|
||||
* )
|
||||
*/
|
||||
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) {
|
||||
$entity_access = $entity->access('view', NULL, TRUE);
|
||||
if (!$entity_access->isAllowed()) {
|
||||
throw new AccessDeniedHttpException();
|
||||
}
|
||||
|
||||
$response = new ResourceResponse($entity, 200);
|
||||
$response->addCacheableDependency($entity);
|
||||
$response->addCacheableDependency($entity_access);
|
||||
foreach ($entity as $field_name => $field) {
|
||||
/** @var \Drupal\Core\Field\FieldItemListInterface $field */
|
||||
$field_access = $field->access('view', NULL, TRUE);
|
||||
$response->addCacheableDependency($field_access);
|
||||
|
||||
if (!$field_access->isAllowed()) {
|
||||
$entity->set($field_name, NULL);
|
||||
}
|
||||
}
|
||||
|
||||
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("Access denied on creating 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 return the newly created entity in the response
|
||||
// body.
|
||||
$url = $entity->urlInfo('canonical', ['absolute' => TRUE])->toString(TRUE);
|
||||
$response = new ResourceResponse($entity, 201, ['Location' => $url->getGeneratedUrl()]);
|
||||
// Responses after creating an entity are not cacheable, so we add no
|
||||
// cacheability metadata here.
|
||||
return $response;
|
||||
}
|
||||
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.
|
||||
$entity_keys = $entity->getEntityType()->getKeys();
|
||||
foreach ($entity->_restSubmittedFields as $field_name) {
|
||||
$field = $entity->get($field_name);
|
||||
|
||||
// Entity key fields need special treatment: together they uniquely
|
||||
// identify the entity. Therefore it does not make sense to modify any of
|
||||
// them. However, rather than throwing an error, we just ignore them as
|
||||
// long as their specified values match their current values.
|
||||
if (in_array($field_name, $entity_keys, TRUE)) {
|
||||
// Unchanged values for entity keys don't need access checking.
|
||||
if ($original_entity->get($field_name)->getValue() === $entity->get($field_name)->getValue()) {
|
||||
continue;
|
||||
}
|
||||
// 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.
|
||||
elseif (isset($entity_keys['langcode']) && $field_name === $entity_keys['langcode'] && $field->isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$original_entity->get($field_name)->access('edit')) {
|
||||
throw new AccessDeniedHttpException("Access denied on updating 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' => $original_entity->getEntityTypeId(), '%id' => $original_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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,351 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\rest\Plugin\views\display;
|
||||
|
||||
use Drupal\Core\Cache\CacheableMetadata;
|
||||
use Drupal\Core\Cache\CacheableResponse;
|
||||
use Drupal\Core\Render\RenderContext;
|
||||
use Drupal\Core\Render\RendererInterface;
|
||||
use Drupal\Core\Routing\RouteProviderInterface;
|
||||
use Drupal\Core\State\StateInterface;
|
||||
use Drupal\views\Plugin\views\display\ResponseDisplayPluginInterface;
|
||||
use Drupal\views\Render\ViewsRenderPipelineMarkup;
|
||||
use Drupal\views\ViewExecutable;
|
||||
use Drupal\views\Plugin\views\display\PathPluginBase;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
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 {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $usesAJAX = FALSE;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $usesPager = FALSE;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $usesMore = FALSE;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $usesAreas = FALSE;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
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;
|
||||
|
||||
/**
|
||||
* The renderer.
|
||||
*
|
||||
* @var \Drupal\Core\Render\RendererInterface
|
||||
*/
|
||||
protected $renderer;
|
||||
|
||||
/**
|
||||
* Constructs a RestExport 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 \Drupal\Core\Routing\RouteProviderInterface $route_provider
|
||||
* The route provider.
|
||||
* @param \Drupal\Core\State\StateInterface $state
|
||||
* The state key value store.
|
||||
* @param \Drupal\Core\Render\RendererInterface $renderer
|
||||
* The renderer.
|
||||
*/
|
||||
public function __construct(array $configuration, $plugin_id, $plugin_definition, RouteProviderInterface $route_provider, StateInterface $state, RendererInterface $renderer) {
|
||||
parent::__construct($configuration, $plugin_id, $plugin_definition, $route_provider, $state);
|
||||
|
||||
$this->renderer = $renderer;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
|
||||
return new static(
|
||||
$configuration,
|
||||
$plugin_id,
|
||||
$plugin_definition,
|
||||
$container->get('router.route_provider'),
|
||||
$container->get('state'),
|
||||
$container->get('renderer')
|
||||
);
|
||||
}
|
||||
/**
|
||||
* {@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}
|
||||
*/
|
||||
public function getType() {
|
||||
return 'data';
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function usesExposed() {
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@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->renderer->executeInRenderContext(new RenderContext(), function() {
|
||||
return $this->view->style_plugin->render();
|
||||
});
|
||||
|
||||
$this->view->element['#content_type'] = $this->getMimeType();
|
||||
$this->view->element['#cache_properties'][] = '#content_type';
|
||||
|
||||
// Encode and wrap the output in a pre tag if this is for a live preview.
|
||||
if (!empty($this->view->live_preview)) {
|
||||
$build['#prefix'] = '<pre>';
|
||||
$build['#plain_text'] = $build['#markup'];
|
||||
$build['#suffix'] = '</pre>';
|
||||
unset($build['#markup']);
|
||||
}
|
||||
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'] = ViewsRenderPipelineMarkup::create($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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\rest\Plugin\views\row;
|
||||
|
||||
use Drupal\Core\Entity\EntityManagerInterface;
|
||||
use Drupal\Core\Language\LanguageManagerInterface;
|
||||
use Drupal\views\Entity\Render\EntityTranslationRenderTrait;
|
||||
use Drupal\views\Plugin\views\row\RowPluginBase;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
|
||||
use EntityTranslationRenderTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $usesOptions = FALSE;
|
||||
|
||||
/**
|
||||
* Contains the entity type of this row plugin instance.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityTypeInterface
|
||||
*/
|
||||
protected $entityType;
|
||||
|
||||
/**
|
||||
* The entity manager.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityManagerInterface
|
||||
*/
|
||||
public $entityManager;
|
||||
|
||||
/**
|
||||
* The language manager.
|
||||
*
|
||||
* @var \Drupal\Core\Language\LanguageManagerInterface
|
||||
*/
|
||||
protected $languageManager;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
|
||||
* The entity manager.
|
||||
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
|
||||
* The language manager.
|
||||
*/
|
||||
public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityManagerInterface $entity_manager, LanguageManagerInterface $language_manager) {
|
||||
parent::__construct($configuration, $plugin_id, $plugin_definition);
|
||||
|
||||
$this->entityManager = $entity_manager;
|
||||
$this->languageManager = $language_manager;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
|
||||
return new static($configuration, $plugin_id, $plugin_definition, $container->get('entity.manager'), $container->get('language_manager'));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function render($row) {
|
||||
return $this->getEntityTranslation($row->_entity, $row);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getEntityTypeId() {
|
||||
return $this->view->getBaseEntityType()->id();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getEntityManager() {
|
||||
return $this->entityManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getLanguageManager() {
|
||||
return $this->languageManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getView() {
|
||||
return $this->view;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function query() {
|
||||
parent::query();
|
||||
$this->getEntityTranslationRenderer()->query($this->view->getQuery());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
<?php
|
||||
|
||||
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 {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
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();
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function defineOptions() {
|
||||
$options = parent::defineOptions();
|
||||
$options['field_options'] = array('default' => array());
|
||||
|
||||
return $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
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) {
|
||||
// Don't show the field if it has been excluded.
|
||||
if (!empty($field['exclude'])) {
|
||||
continue;
|
||||
}
|
||||
$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.'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
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'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function render($row) {
|
||||
$output = array();
|
||||
|
||||
foreach ($this->view->field as $id => $field) {
|
||||
// Don't render anything if this field is excluded.
|
||||
if (!empty($field->options['exclude'])) {
|
||||
continue;
|
||||
}
|
||||
// If the raw output option has been set, just get the raw value.
|
||||
if (!empty($this->rawOutputOptions[$id])) {
|
||||
$value = $field->getValue($row);
|
||||
}
|
||||
// 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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\rest\Plugin\views\style;
|
||||
|
||||
use Drupal\Core\Cache\Cache;
|
||||
use Drupal\Core\Cache\CacheableDependencyInterface;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
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 CacheableDependencyInterface {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $usesRowPlugin = TRUE;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
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_index => $row) {
|
||||
$this->view->row_index = $row_index;
|
||||
$rows[] = $this->view->rowPlugin->render($row);
|
||||
}
|
||||
unset($this->view->row_index);
|
||||
|
||||
// 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, ['views_style_plugin' => $this]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 getCacheMaxAge() {
|
||||
return Cache::PERMANENT;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getCacheContexts() {
|
||||
return ['request_format'];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getCacheTags() {
|
||||
return [];
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\rest;
|
||||
|
||||
use Drupal\Core\Render\RenderContext;
|
||||
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\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());
|
||||
|
||||
// Symfony is built to transparently map HEAD requests to a GET request. In
|
||||
// the case of the REST module's RequestHandler though, we essentially have
|
||||
// our own light-weight routing system on top of the Drupal/symfony routing
|
||||
// system. So, we have to do the same as what the UrlMatcher does: map HEAD
|
||||
// requests to the logic for GET. This also guarantees response headers for
|
||||
// HEAD requests are identical to those for GET requests, because we just
|
||||
// return a GET response. Response::prepare() will transform it to a HEAD
|
||||
// response at the very last moment.
|
||||
// @see https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.4
|
||||
// @see \Symfony\Component\Routing\Matcher\UrlMatcher::matchCollection()
|
||||
// @see \Symfony\Component\HttpFoundation\Response::prepare()
|
||||
if ($method === 'head') {
|
||||
$method = 'get';
|
||||
}
|
||||
|
||||
$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);
|
||||
}
|
||||
|
||||
if ($response instanceof ResourceResponse) {
|
||||
$data = $response->getResponseData();
|
||||
// Serialization can invoke rendering (e.g., generating URLs), but the
|
||||
// serialization API does not provide a mechanism to collect the
|
||||
// bubbleable metadata associated with that (e.g., language and other
|
||||
// contexts), so instead, allow those to "leak" and collect them here in
|
||||
// a render context.
|
||||
// @todo Add test coverage for language negotiation contexts in
|
||||
// https://www.drupal.org/node/2135829.
|
||||
$context = new RenderContext();
|
||||
$output = $this->container->get('renderer')->executeInRenderContext($context, function() use ($serializer, $data, $format) {
|
||||
return $serializer->serialize($data, $format);
|
||||
});
|
||||
$response->setContent($output);
|
||||
if (!$context->isEmpty()) {
|
||||
$response->addCacheableDependency($context->pop());
|
||||
}
|
||||
|
||||
$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'));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\rest\Routing;
|
||||
|
||||
use Drupal\Core\Config\ConfigFactoryInterface;
|
||||
use Drupal\Core\Routing\RouteSubscriberBase;
|
||||
use Drupal\rest\Plugin\Type\ResourcePluginManager;
|
||||
use Psr\Log\LoggerInterface;
|
||||
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();
|
||||
|
||||
// Silently ignore resources that are in the settings but are not defined on
|
||||
// the plugin manager currently. That avoids exceptions when REST module is
|
||||
// enabled before another module that provides the resource plugin specified
|
||||
// in the settings.
|
||||
// @todo Remove in https://www.drupal.org/node/2308745
|
||||
$resources = $this->config->get('rest.settings')->get('resources') ?: array();
|
||||
$enabled_resources = array_intersect_key($resources, $this->manager->getDefinitions());
|
||||
if (count($resources) != count($enabled_resources)) {
|
||||
trigger_error('rest.settings lists resources relying on the following missing plugins: ' . implode(', ', array_keys(array_diff_key($resources, $enabled_resources))));
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\rest\Tests;
|
||||
|
||||
use Drupal\Core\Url;
|
||||
|
||||
/**
|
||||
* 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
|
||||
* A 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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,465 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\rest\Tests;
|
||||
|
||||
use Drupal\comment\Tests\CommentTestTrait;
|
||||
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;
|
||||
use Drupal\comment\Entity\Comment;
|
||||
|
||||
/**
|
||||
* Tests the creation of resources.
|
||||
*
|
||||
* @group rest
|
||||
*/
|
||||
class CreateTest extends RESTTestBase {
|
||||
|
||||
use CommentTestTrait;
|
||||
/**
|
||||
* Modules to install.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = array('hal', 'rest', 'entity_test', 'comment');
|
||||
|
||||
/**
|
||||
* The 'serializer' service.
|
||||
*
|
||||
* @var \Symfony\Component\Serializer\Serializer
|
||||
*/
|
||||
protected $serializer;
|
||||
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
$this->addDefaultCommentField('node', 'resttest');
|
||||
// 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.
|
||||
$entity->set('changed', NULL);
|
||||
$entity->set('revision_timestamp', NULL);
|
||||
}
|
||||
|
||||
$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);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Test comment creation.
|
||||
*/
|
||||
protected function testCreateComment() {
|
||||
$node = Node::create([
|
||||
'type' => 'resttest',
|
||||
'title' => 'some node',
|
||||
]);
|
||||
$node->save();
|
||||
$entity_type = 'comment';
|
||||
// Enable the REST service for 'comment' 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);
|
||||
$account = end($accounts);
|
||||
|
||||
$this->drupalLogin($account);
|
||||
$entity_values = $this->entityValues($entity_type);
|
||||
$entity_values['entity_id'] = $node->id();
|
||||
|
||||
$entity = Comment::create($entity_values);
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
$entity->set('changed', NULL);
|
||||
|
||||
$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';
|
||||
$permissions[] = 'administer comments';
|
||||
// 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.
|
||||
$response = $this->httpRequest('entity/' . $entity_type, 'POST', $serialized, $this->defaultMimeType);
|
||||
$this->assertResponse(201);
|
||||
|
||||
// Make sure that the response includes an entity in the body and check the
|
||||
// UUID as an example.
|
||||
$request = Json::decode($serialized);
|
||||
$response = Json::decode($response);
|
||||
$this->assertEqual($request['uuid'][0]['value'], $response['uuid'][0]['value'], 'Got new entity created as response after successful POST over Rest API');
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
|
||||
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 = $this->container->get('entity_type.manager')
|
||||
->getStorage($this->testEntityType)
|
||||
->create($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}",
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\rest\Tests;
|
||||
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\node\Entity\Node;
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes and attempts to create a node via a REST "post" http request.
|
||||
*
|
||||
* @param array $data
|
||||
*/
|
||||
protected function postNode($data) {
|
||||
// Enable node creation via POST.
|
||||
$this->enableNodeConfiguration('POST', 'create');
|
||||
$this->enableService('entity:node', 'POST', 'json');
|
||||
|
||||
// Create a JSON version of a simple node with the title.
|
||||
$serialized = $this->container->get('serializer')->serialize($data, 'json');
|
||||
|
||||
// Post to the REST service to create the node.
|
||||
$this->httpRequest('/entity/node', 'POST', $serialized, 'application/json');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the title on a newly created node.
|
||||
*
|
||||
* @param array $data
|
||||
* @return \Drupal\node\Entity\Node
|
||||
*/
|
||||
protected function assertNodeTitleMatch($data) {
|
||||
/** @var \Drupal\node\Entity\Node $node */
|
||||
// Load the newly created node.
|
||||
$node = Node::load(1);
|
||||
|
||||
// Test that the title is the same as what we posted.
|
||||
$this->assertEqual($node->title->value, $data['title'][0]['value']);
|
||||
|
||||
return $node;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test creating a node using json serialization.
|
||||
*/
|
||||
public function testCreate() {
|
||||
// Data to be used for serialization.
|
||||
$data = [
|
||||
'type' => [['target_id' => 'resttest']],
|
||||
'title' => [['value' => $this->randomString() ]],
|
||||
];
|
||||
|
||||
$this->postNode($data);
|
||||
|
||||
// Make sure the response is "CREATED".
|
||||
$this->assertResponse(201);
|
||||
|
||||
// Make sure the node was created and the title matches.
|
||||
$node = $this->assertNodeTitleMatch($data);
|
||||
|
||||
// Make sure the request returned a redirect header to view the node.
|
||||
$this->assertHeader('Location', $node->url('canonical', ['absolute' => TRUE]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test bundle normalization when posting bundle as a simple string.
|
||||
*/
|
||||
public function testBundleNormalization() {
|
||||
// Data to be used for serialization.
|
||||
$data = [
|
||||
'type' => 'resttest',
|
||||
'title' => [['value' => $this->randomString() ]],
|
||||
];
|
||||
|
||||
$this->postNode($data);
|
||||
|
||||
// Make sure the response is "CREATED".
|
||||
$this->assertResponse(201);
|
||||
|
||||
// Make sure the node was created and the title matches.
|
||||
$this->assertNodeTitleMatch($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test bundle normalization when posting using a simple string.
|
||||
*/
|
||||
public function testInvalidBundle() {
|
||||
// Data to be used for serialization.
|
||||
$data = [
|
||||
'type' => 'bad_bundle_name',
|
||||
'title' => [['value' => $this->randomString() ]],
|
||||
];
|
||||
|
||||
$this->postNode($data);
|
||||
|
||||
// Make sure the response is "Bad Request".
|
||||
$this->assertResponse(400);
|
||||
$this->assertResponseBody('{"error":"\"bad_bundle_name\" is not a valid bundle type for denormalization."}');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test when the bundle is missing.
|
||||
*/
|
||||
public function testMissingBundle() {
|
||||
// Data to be used for serialization.
|
||||
$data = [
|
||||
'title' => [['value' => $this->randomString() ]],
|
||||
];
|
||||
|
||||
// testing
|
||||
$this->postNode($data);
|
||||
|
||||
// Make sure the response is "Bad Request".
|
||||
$this->assertResponse(400);
|
||||
$this->assertResponseBody('{"error":"A string must be provided as a bundle value."}');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\rest\Tests;
|
||||
|
||||
use Drupal\Component\Serialization\Json;
|
||||
use Drupal\Core\Url;
|
||||
|
||||
/**
|
||||
* Tests page caching for REST GET requests.
|
||||
*
|
||||
* @group rest
|
||||
*/
|
||||
class PageCacheTest extends RESTTestBase {
|
||||
|
||||
/**
|
||||
* Modules to install.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = array('hal');
|
||||
|
||||
/**
|
||||
* 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->set('field_test_text', 'custom cache tag value');
|
||||
$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');
|
||||
$this->assertCacheTag('entity_test_access:field_test_text');
|
||||
|
||||
// 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');
|
||||
$this->assertCacheTag('entity_test_access:field_test_text');
|
||||
|
||||
// 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');
|
||||
$this->assertCacheTag('entity_test_access:field_test_text');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests HEAD support when a REST resource supports GET.
|
||||
*/
|
||||
public function testHeadSupport() {
|
||||
user_role_grant_permissions('anonymous', ['view test entity', 'restful get entity:entity_test']);
|
||||
|
||||
// Create an entity programatically.
|
||||
$this->entityCreate('entity_test')->save();
|
||||
|
||||
$url = Url::fromUri('internal:/entity_test/1?_format=' . $this->defaultFormat);
|
||||
|
||||
$this->enableService('entity:entity_test', 'GET');
|
||||
|
||||
$this->httpRequest($url, 'HEAD', NULL, $this->defaultMimeType);
|
||||
$this->assertResponse(200, 'HTTP response code is correct.');
|
||||
$this->assertHeader('X-Drupal-Cache', 'MISS');
|
||||
$this->assertResponseBody('');
|
||||
|
||||
$response = $this->httpRequest($url, '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');
|
||||
$data = Json::decode($response);
|
||||
$this->assertEqual($data['type'][0]['value'], 'entity_test');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,434 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\rest\Tests;
|
||||
|
||||
use Drupal\node\NodeInterface;
|
||||
use Drupal\simpletest\WebTestBase;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
|
||||
/**
|
||||
* The raw response body from http request operations.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $responseBody;
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
$curl_options = array();
|
||||
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 'HEAD':
|
||||
$curl_options = array(
|
||||
CURLOPT_HTTPGET => FALSE,
|
||||
CURLOPT_CUSTOMREQUEST => 'HEAD',
|
||||
CURLOPT_URL => $url,
|
||||
CURLOPT_NOBODY => TRUE,
|
||||
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;
|
||||
}
|
||||
|
||||
$this->responseBody = $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: ' . $this->responseBody);
|
||||
|
||||
return $this->responseBody;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 $this->container->get('entity_type.manager')
|
||||
->getStorage($entity_type)
|
||||
->create($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());
|
||||
|
||||
case 'comment':
|
||||
return [
|
||||
'subject' => $this->randomMachineName(),
|
||||
'entity_type' => 'node',
|
||||
'comment_type' => 'comment',
|
||||
'comment_body' => $this->randomString(),
|
||||
'entity_id' => 'invalid',
|
||||
'field_name' => 'comment',
|
||||
];
|
||||
|
||||
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|array $format
|
||||
* (Optional) The serialization format, e.g. hal_json, or a list of formats.
|
||||
* @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 (is_array($format)) {
|
||||
$settings[$resource_type][$method]['supported_formats'] = $format;
|
||||
}
|
||||
else {
|
||||
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 'comment':
|
||||
switch ($operation) {
|
||||
case 'view':
|
||||
return ['access comments'];
|
||||
|
||||
case 'create':
|
||||
return ['post comments', 'skip comment approval'];
|
||||
|
||||
case 'update':
|
||||
return ['edit own comments'];
|
||||
|
||||
case 'delete':
|
||||
return ['administer comments'];
|
||||
}
|
||||
break;
|
||||
|
||||
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('revision_log', NULL);
|
||||
$node->set('uid', NULL);
|
||||
|
||||
return $node;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check to see if the HTTP request response body is identical to the expected
|
||||
* value.
|
||||
*
|
||||
* @param $expected
|
||||
* The first value to check.
|
||||
* @param $message
|
||||
* (optional) A message to display with the assertion. Do not translate
|
||||
* messages: use \Drupal\Component\Utility\SafeMarkup::format() to embed
|
||||
* variables in the message text, not t(). If left blank, a default message
|
||||
* will be displayed.
|
||||
* @param $group
|
||||
* (optional) The group this message is in, which is displayed in a column
|
||||
* in test output. Use 'Debug' to indicate this is debugging output. Do not
|
||||
* translate this string. Defaults to 'Other'; most tests do not override
|
||||
* this default.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE if the assertion succeeded, FALSE otherwise.
|
||||
*/
|
||||
protected function assertResponseBody($expected, $message = '', $group = 'REST Response') {
|
||||
return $this->assertIdentical($expected, $this->responseBody, $message ? $message : strtr('Response body @expected (expected) is equal to @response (actual).', array('@expected' => var_export($expected, TRUE), '@response' => var_export($this->responseBody, TRUE))), $group);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\rest\Tests;
|
||||
|
||||
use Drupal\Component\Serialization\Json;
|
||||
use Drupal\Core\Url;
|
||||
|
||||
/**
|
||||
* 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();
|
||||
|
||||
// Verify that it exists: use a HEAD request.
|
||||
$response = $this->httpRequest($entity->urlInfo()->setRouteParameter('_format', $this->defaultFormat), 'HEAD');
|
||||
$this->assertResponseBody('');
|
||||
$head_headers = $this->drupalGetHeaders();
|
||||
|
||||
// Read it over the REST API.
|
||||
$response = $this->httpRequest($entity->urlInfo()->setRouteParameter('_format', $this->defaultFormat), 'GET');
|
||||
$get_headers = $this->drupalGetHeaders();
|
||||
$this->assertResponse('200', 'HTTP response code is correct.');
|
||||
|
||||
// Verify that the GET and HEAD responses are the same, that the only
|
||||
// difference is that there's no body.
|
||||
unset($get_headers['date']);
|
||||
unset($head_headers['date']);
|
||||
unset($get_headers['content-length']);
|
||||
unset($head_headers['content-length']);
|
||||
unset($get_headers['x-drupal-dynamic-cache']);
|
||||
unset($head_headers['x-drupal-dynamic-cache']);
|
||||
$this->assertIdentical($get_headers, $head_headers);
|
||||
$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 format: hal_json',
|
||||
]));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\rest\Tests;
|
||||
|
||||
use Drupal\Component\Plugin\Exception\PluginNotFoundException;
|
||||
use Drupal\Core\Session\AccountInterface;
|
||||
use Drupal\user\Entity\Role;
|
||||
|
||||
/**
|
||||
* 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();
|
||||
|
||||
Role::load(AccountInterface::ANONYMOUS_ROLE)
|
||||
->grantPermission('view test 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();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that resource URI paths are formatted properly.
|
||||
*/
|
||||
public function testUriPaths() {
|
||||
$this->enableService('entity:entity_test');
|
||||
/** @var \Drupal\rest\Plugin\Type\ResourcePluginManager $manager */
|
||||
$manager = \Drupal::service('plugin.manager.rest');
|
||||
|
||||
foreach ($manager->getDefinitions() as $resource => $definition) {
|
||||
foreach ($definition['uri_paths'] as $key => $uri_path) {
|
||||
$this->assertFalse(strpos($uri_path, '//'), 'The resource URI path does not have duplicate slashes.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that a resource with a missing plugin does not cause an exception.
|
||||
*/
|
||||
public function testMissingPlugin() {
|
||||
$settings = array(
|
||||
'entity:nonexisting' => array(
|
||||
'GET' => array(
|
||||
'supported_formats' => array(
|
||||
'hal_json',
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
try {
|
||||
// Attempt to enable the resource.
|
||||
$this->config->set('resources', $settings);
|
||||
$this->config->save();
|
||||
$this->rebuildCache();
|
||||
$this->pass('rest.settings referencing a missing REST resource plugin does not cause an exception.');
|
||||
}
|
||||
catch (PluginNotFoundException $e) {
|
||||
$this->fail('rest.settings referencing a missing REST resource plugin caused an exception.');
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,363 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\rest\Tests;
|
||||
|
||||
use Drupal\comment\Entity\Comment;
|
||||
use Drupal\comment\Tests\CommentTestTrait;
|
||||
use Drupal\Component\Serialization\Json;
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
use Drupal\Core\Session\AccountInterface;
|
||||
use Drupal\entity_test\Entity\EntityTest;
|
||||
|
||||
/**
|
||||
* Tests the update of resources.
|
||||
*
|
||||
* @group rest
|
||||
*/
|
||||
class UpdateTest extends RESTTestBase {
|
||||
|
||||
use CommentTestTrait;
|
||||
|
||||
/**
|
||||
* Modules to install.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = ['hal', 'rest', 'entity_test', 'comment'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
$this->addDefaultCommentField('entity_test', '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 = $this->container->get('entity_type.manager')
|
||||
->getStorage($entity_type)
|
||||
->create($patch_values);
|
||||
// We don't want to overwrite the UUID.
|
||||
$patch_entity->set('uuid', NULL);
|
||||
$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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test patching a comment using both HAL+JSON and JSON.
|
||||
*/
|
||||
public function testUpdateComment() {
|
||||
$entity_type = 'comment';
|
||||
// Enables the REST service for 'comment' entity type.
|
||||
$this->enableService('entity:' . $entity_type, 'PATCH', ['hal_json', 'json']);
|
||||
$permissions = $this->entityPermissions($entity_type, 'update');
|
||||
$permissions[] = 'restful patch entity:' . $entity_type;
|
||||
$account = $this->drupalCreateUser($permissions);
|
||||
$account->set('mail', 'old-email@example.com');
|
||||
$this->drupalLogin($account);
|
||||
|
||||
// Create & save an entity to comment on, plus a comment.
|
||||
$entity_test = EntityTest::create();
|
||||
$entity_test->save();
|
||||
$entity_values = $this->entityValues($entity_type);
|
||||
$entity_values['entity_id'] = $entity_test->id();
|
||||
$entity_values['uid'] = $account->id();
|
||||
$comment = Comment::create($entity_values);
|
||||
$comment->save();
|
||||
|
||||
$this->pass('Test case 1: PATCH comment using HAL+JSON.');
|
||||
$comment->setSubject('Initial subject')->save();
|
||||
$read_only_fields = [
|
||||
'name',
|
||||
'created',
|
||||
'changed',
|
||||
'status',
|
||||
'thread',
|
||||
'entity_type',
|
||||
'field_name',
|
||||
'entity_id',
|
||||
'uid',
|
||||
];
|
||||
$this->assertNotEqual('Updated subject', $comment->getSubject());
|
||||
$comment->setSubject('Updated subject');
|
||||
$this->patchEntity($comment, $read_only_fields, $account, 'hal_json', 'application/hal+json');
|
||||
$comment = Comment::load($comment->id());
|
||||
$this->assertEqual('Updated subject', $comment->getSubject());
|
||||
|
||||
$this->pass('Test case 1: PATCH comment using JSON.');
|
||||
$comment->setSubject('Initial subject')->save();
|
||||
$read_only_fields = [
|
||||
'pid', // Extra compared to HAL+JSON.
|
||||
'entity_id',
|
||||
'uid',
|
||||
'name',
|
||||
'homepage', // Extra compared to HAL+JSON.
|
||||
'created',
|
||||
'changed',
|
||||
'status',
|
||||
'thread',
|
||||
'entity_type',
|
||||
'field_name',
|
||||
];
|
||||
$this->assertNotEqual('Updated subject', $comment->getSubject());
|
||||
$comment->setSubject('Updated subject');
|
||||
$this->patchEntity($comment, $read_only_fields, $account, 'json', 'application/json');
|
||||
$comment = Comment::load($comment->id());
|
||||
$this->assertEqual('Updated subject', $comment->getSubject());
|
||||
}
|
||||
|
||||
/**
|
||||
* Patches an existing entity using the passed in (modified) entity.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityInterface $entity
|
||||
* The updated entity to send.
|
||||
* @param string[] $read_only_fields
|
||||
* Names of the fields that are read-only, in validation order.
|
||||
* @param \Drupal\Core\Session\AccountInterface $account
|
||||
* The account to use for serialization.
|
||||
* @param string $format
|
||||
* A serialization format.
|
||||
* @param string $mime_type
|
||||
* The MIME type corresponding to the specified serialization format.
|
||||
*/
|
||||
protected function patchEntity(EntityInterface $entity, array $read_only_fields, AccountInterface $account, $format, $mime_type) {
|
||||
$serializer = $this->container->get('serializer');
|
||||
|
||||
$url = $entity->toUrl();
|
||||
$context = ['account' => $account];
|
||||
// Certain fields are always read-only, others this user simply is not
|
||||
// allowed to modify. For all of them, ensure they are not serialized, else
|
||||
// we'll get a 403 plus an error message.
|
||||
for ($i = 0; $i < count($read_only_fields); $i++) {
|
||||
$field = $read_only_fields[$i];
|
||||
|
||||
$normalized = $serializer->normalize($entity, $format, $context);
|
||||
if ($format !== 'hal_json') {
|
||||
// The default normalizer always keeps fields, even if they are unset
|
||||
// here because they should be omitted during a PATCH request. Therefore
|
||||
// manually strip them
|
||||
// @see \Drupal\Core\Entity\ContentEntityBase::__unset()
|
||||
// @see \Drupal\serialization\Normalizer\EntityNormalizer::normalize()
|
||||
// @see \Drupal\hal\Normalizer\ContentEntityNormalizer::normalize()
|
||||
$read_only_fields_so_far = array_slice($read_only_fields, 0, $i);
|
||||
$normalized = array_diff_key($normalized, array_flip($read_only_fields_so_far));
|
||||
}
|
||||
$serialized = $serializer->serialize($normalized, $format, $context);
|
||||
|
||||
$this->httpRequest($url, 'PATCH', $serialized, $mime_type);
|
||||
$this->assertResponse(403);
|
||||
$this->assertResponseBody('{"error":"Access denied on updating field \'' . $field . '\'."}');
|
||||
|
||||
if ($format === 'hal_json') {
|
||||
// We've just tried with this read-only field, now unset it.
|
||||
$entity->set($field, NULL);
|
||||
}
|
||||
}
|
||||
|
||||
// Finally, with all read-only fields unset, the request should succeed.
|
||||
$normalized = $serializer->normalize($entity, $format, $context);
|
||||
if ($format !== 'hal_json') {
|
||||
$normalized = array_diff_key($normalized, array_combine($read_only_fields, $read_only_fields));
|
||||
}
|
||||
$serialized = $serializer->serialize($normalized, $format, $context);
|
||||
|
||||
$this->httpRequest($url, 'PATCH', $serialized, $mime_type);
|
||||
$this->assertResponse(204);
|
||||
$this->assertResponseBody('');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,805 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\rest\Tests\Views;
|
||||
|
||||
use Drupal\Core\Cache\Cache;
|
||||
use Drupal\Core\Field\FieldStorageDefinitionInterface;
|
||||
use Drupal\entity_test\Entity\EntityTest;
|
||||
use Drupal\field\Entity\FieldConfig;
|
||||
use Drupal\field\Entity\FieldStorageConfig;
|
||||
use Drupal\language\Entity\ConfigurableLanguage;
|
||||
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', 'language');
|
||||
|
||||
/**
|
||||
* Views used by this test.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $testViews = array('test_serializer_display_field', 'test_serializer_display_entity', 'test_serializer_display_entity_translated', 'test_serializer_node_display_field', 'test_serializer_node_exposed_filter');
|
||||
|
||||
/**
|
||||
* 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++) {
|
||||
EntityTest::create(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, (string) 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.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies site maintenance mode functionality.
|
||||
*/
|
||||
protected function testSiteMaintenance() {
|
||||
$view = Views::getView('test_serializer_display_field');
|
||||
$view->initDisplay();
|
||||
$this->executeView($view);
|
||||
|
||||
// Set the site to maintenance mode.
|
||||
$this->container->get('state')->set('system.maintenance_mode', TRUE);
|
||||
|
||||
$this->drupalGetWithFormat('test/serialize/entity', 'json');
|
||||
// Verify that the endpoint is unavailable for anonymous users.
|
||||
$this->assertResponse(503);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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'), $this->castSafeStrings($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'), $this->castSafeStrings($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.
|
||||
$values = array(
|
||||
'row_options[field_options][created][raw_output]' => '1',
|
||||
'row_options[field_options][name][raw_output]' => '1',
|
||||
);
|
||||
$this->drupalPostForm($row_options, $values, t('Apply'));
|
||||
$this->drupalPostForm(NULL, array(), t('Save'));
|
||||
|
||||
$view = Views::getView('test_serializer_display_field');
|
||||
$view->setDisplay('rest_export_1');
|
||||
$this->executeView($view);
|
||||
|
||||
$storage = $this->container->get('entity_type.manager')->getStorage('entity_test');
|
||||
|
||||
// Update the name for each to include a script tag.
|
||||
foreach ($storage->loadMultiple() as $entity_test) {
|
||||
$name = $entity_test->name->value;
|
||||
$entity_test->set('name', "<script>$name</script>");
|
||||
$entity_test->save();
|
||||
}
|
||||
|
||||
// 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.');
|
||||
$this->assertIdentical($values['name'], $view->result[$index]->views_test_data_name, 'Expected raw name value found.');
|
||||
}
|
||||
|
||||
// Test result with an excluded field.
|
||||
$view->setDisplay('rest_export_1');
|
||||
$view->displayHandlers->get('rest_export_1')->overrideOption('fields', [
|
||||
'name' => [
|
||||
'id' => 'name',
|
||||
'table' => 'views_test_data',
|
||||
'field' => 'name',
|
||||
'relationship' => 'none',
|
||||
],
|
||||
'created' => [
|
||||
'id' => 'created',
|
||||
'exclude' => TRUE,
|
||||
'table' => 'views_test_data',
|
||||
'field' => 'created',
|
||||
'relationship' => 'none',
|
||||
],
|
||||
]);
|
||||
$view->save();
|
||||
$this->executeView($view);
|
||||
foreach ($this->drupalGetJSON('test/serialize/field') as $index => $values) {
|
||||
$this->assertTrue(!isset($values['created']), 'Excluded value not found.');
|
||||
}
|
||||
// Test that the excluded field is not shown in the row options.
|
||||
$this->drupalGet('admin/structure/views/nojs/display/test_serializer_display_field/rest_export_1/row_options');
|
||||
$this->assertNoText('created');
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 = $serializer->serialize($entities, 'json');
|
||||
|
||||
$view->live_preview = TRUE;
|
||||
|
||||
$build = $view->preview();
|
||||
$rendered_json = $build['#plain_text'];
|
||||
$this->assertTrue(!isset($build['#markup']) && $rendered_json == $expected, 'Ensure the previewed json is escaped.');
|
||||
$view->destroy();
|
||||
|
||||
$expected = $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['#plain_text'];
|
||||
$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.");
|
||||
|
||||
$this->drupalLogin($this->adminUser);
|
||||
|
||||
// Add an alias and make the output raw.
|
||||
$row_options = 'admin/structure/views/nojs/display/test_serializer_node_display_field/rest_export_1/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, ['row_options[field_options][title][raw_output]' => '1'], t('Apply'));
|
||||
$this->drupalPostForm(NULL, [], t('Save'));
|
||||
|
||||
$view = Views::getView('test_serializer_node_display_field');
|
||||
$view->setDisplay('rest_export_1');
|
||||
$this->executeView($view);
|
||||
|
||||
// Test the raw 'created' value against each row.
|
||||
foreach ($this->drupalGetJSON('test/serialize/node-field') as $index => $values) {
|
||||
$this->assertIdentical($values['title'], $view->result[$index]->_entity->title->value, 'Expected raw title value found.');
|
||||
}
|
||||
|
||||
// Test that multiple raw body fields are shown.
|
||||
// Make the body field unlimited cardinatlity.
|
||||
$storage_definition = $node->getFieldDefinition('body')->getFieldStorageDefinition();
|
||||
$storage_definition->setCardinality(FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED);
|
||||
$storage_definition->save();
|
||||
|
||||
$this->drupalPostForm($row_options, ['row_options[field_options][body][raw_output]' => '1'], t('Apply'));
|
||||
$this->drupalPostForm(NULL, [], t('Save'));
|
||||
|
||||
$node = $this->drupalCreateNode();
|
||||
|
||||
$body = [
|
||||
'value' => '<script type="text/javascript">alert("node-body");</script>' . $this->randomMachineName(32),
|
||||
'format' => filter_default_format(),
|
||||
];
|
||||
// Add two body items.
|
||||
$node->body = [$body, $body];
|
||||
$node->save();
|
||||
|
||||
$view = Views::getView('test_serializer_node_display_field');
|
||||
$view->setDisplay('rest_export_1');
|
||||
$this->executeView($view);
|
||||
|
||||
$result = $this->drupalGetJSON('test/serialize/node-field');
|
||||
$this->assertEqual(count($result[2]['body']), $node->body->count(), 'Expected count of values');
|
||||
$this->assertEqual($result[2]['body'], array_map(function($item) { return $item['value']; }, $node->body->getValue()), 'Expected raw body values found.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the "Grouped rows" functionality.
|
||||
*/
|
||||
public function testGroupRows() {
|
||||
/** @var \Drupal\Core\Render\RendererInterface $renderer */
|
||||
$renderer = $this->container->get('renderer');
|
||||
$this->drupalCreateContentType(['type' => 'page']);
|
||||
// Create a text field with cardinality set to unlimited.
|
||||
$field_name = 'field_group_rows';
|
||||
$field_storage = FieldStorageConfig::create([
|
||||
'field_name' => $field_name,
|
||||
'entity_type' => 'node',
|
||||
'type' => 'string',
|
||||
'cardinality' => FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED,
|
||||
]);
|
||||
$field_storage->save();
|
||||
// Create an instance of the text field on the content type.
|
||||
$field = FieldConfig::create([
|
||||
'field_storage' => $field_storage,
|
||||
'bundle' => 'page',
|
||||
]);
|
||||
$field->save();
|
||||
$grouped_field_values = ['a', 'b', 'c'];
|
||||
$edit = [
|
||||
'title' => $this->randomMachineName(),
|
||||
$field_name => $grouped_field_values,
|
||||
];
|
||||
$this->drupalCreateNode($edit);
|
||||
$view = Views::getView('test_serializer_node_display_field');
|
||||
$view->setDisplay('rest_export_1');
|
||||
// Override the view's fields to include the field_group_rows field, set the
|
||||
// group_rows setting to true.
|
||||
$fields = [
|
||||
$field_name => [
|
||||
'id' => $field_name,
|
||||
'table' => 'node__' . $field_name,
|
||||
'field' => $field_name,
|
||||
'type' => 'string',
|
||||
'group_rows' => TRUE,
|
||||
],
|
||||
];
|
||||
$view->displayHandlers->get('default')->overrideOption('fields', $fields);
|
||||
$build = $view->preview();
|
||||
// Get the serializer service.
|
||||
$serializer = $this->container->get('serializer');
|
||||
// Check if the field_group_rows field is grouped.
|
||||
$expected = [];
|
||||
$expected[] = [$field_name => implode(', ', $grouped_field_values)];
|
||||
$this->assertEqual($serializer->serialize($expected, 'json'), (string) $renderer->renderRoot($build));
|
||||
// Set the group rows setting to false.
|
||||
$view = Views::getView('test_serializer_node_display_field');
|
||||
$view->setDisplay('rest_export_1');
|
||||
$fields[$field_name]['group_rows'] = FALSE;
|
||||
$view->displayHandlers->get('default')->overrideOption('fields', $fields);
|
||||
$build = $view->preview();
|
||||
// Check if the field_group_rows field is ungrouped and displayed per row.
|
||||
$expected = [];
|
||||
foreach ($grouped_field_values as $grouped_field_value) {
|
||||
$expected[] = [$field_name => $grouped_field_value];
|
||||
}
|
||||
$this->assertEqual($serializer->serialize($expected, 'json'), (string) $renderer->renderRoot($build));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the exposed filter works.
|
||||
*
|
||||
* There is an exposed filter on the title field which takes a title query
|
||||
* parameter. This is set to filter nodes by those whose title starts with
|
||||
* the value provided.
|
||||
*/
|
||||
public function testRestViewExposedFilter() {
|
||||
$this->drupalCreateContentType(array('type' => 'page'));
|
||||
$node0 = $this->drupalCreateNode(array('title' => 'Node 1'));
|
||||
$node1 = $this->drupalCreateNode(array('title' => 'Node 11'));
|
||||
$node2 = $this->drupalCreateNode(array('title' => 'Node 111'));
|
||||
|
||||
// Test that no filter brings back all three nodes.
|
||||
$result = $this->drupalGetJSON('test/serialize/node-exposed-filter');
|
||||
|
||||
$expected = array(
|
||||
0 => array(
|
||||
'nid' => $node0->id(),
|
||||
'body' => $node0->body->processed,
|
||||
),
|
||||
1 => array(
|
||||
'nid' => $node1->id(),
|
||||
'body' => $node1->body->processed,
|
||||
),
|
||||
2 => array(
|
||||
'nid' => $node2->id(),
|
||||
'body' => $node2->body->processed,
|
||||
),
|
||||
);
|
||||
|
||||
$this->assertEqual($result, $expected, 'Querying a view with no exposed filter returns all nodes.');
|
||||
|
||||
// Test that title starts with 'Node 11' query finds 2 of the 3 nodes.
|
||||
$result = $this->drupalGetJSON('test/serialize/node-exposed-filter', ['query' => ['title' => 'Node 11']]);
|
||||
|
||||
$expected = array(
|
||||
0 => array(
|
||||
'nid' => $node1->id(),
|
||||
'body' => $node1->body->processed,
|
||||
),
|
||||
1 => array(
|
||||
'nid' => $node2->id(),
|
||||
'body' => $node2->body->processed,
|
||||
),
|
||||
);
|
||||
|
||||
$cache_contexts = [
|
||||
'languages:language_content',
|
||||
'languages:language_interface',
|
||||
'theme',
|
||||
'request_format',
|
||||
'user.node_grants:view',
|
||||
'url',
|
||||
];
|
||||
|
||||
$this->assertEqual($result, $expected, 'Querying a view with a starts with exposed filter on the title returns nodes whose title starts with value provided.');
|
||||
$this->assertCacheContexts($cache_contexts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test multilingual entity rows.
|
||||
*/
|
||||
public function testMulEntityRows() {
|
||||
// Create some languages.
|
||||
ConfigurableLanguage::createFromLangcode('l1')->save();
|
||||
ConfigurableLanguage::createFromLangcode('l2')->save();
|
||||
|
||||
// Create an entity with no translations.
|
||||
$storage = \Drupal::entityTypeManager()->getStorage('entity_test_mul');
|
||||
$storage->create(['langcode' => 'l1', 'name' => 'mul-none'])->save();
|
||||
|
||||
// Create some entities with translations.
|
||||
$entity = $storage->create(['langcode' => 'l1', 'name' => 'mul-l1-orig']);
|
||||
$entity->save();
|
||||
$entity->addTranslation('l2', ['name' => 'mul-l1-l2'])->save();
|
||||
$entity = $storage->create(['langcode' => 'l2', 'name' => 'mul-l2-orig']);
|
||||
$entity->save();
|
||||
$entity->addTranslation('l1', ['name' => 'mul-l2-l1'])->save();
|
||||
|
||||
// Get the names of the output.
|
||||
$json = $this->drupalGetWithFormat('test/serialize/translated_entity', 'json');
|
||||
$decoded = $this->container->get('serializer')->decode($json, 'hal_json');
|
||||
$names = [];
|
||||
foreach ($decoded as $item) {
|
||||
$names[] = $item['name'][0]['value'];
|
||||
}
|
||||
sort($names);
|
||||
|
||||
// Check that the names are correct.
|
||||
$expected = ['mul-l1-l2', 'mul-l1-orig', 'mul-l2-l1', 'mul-l2-orig', 'mul-none'];
|
||||
$this->assertIdentical($names, $expected, 'The translated content was found in the JSON.');
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user