Commit 5f61e266 authored by catch's avatar catch

Issue #2019123 by klausi, ygerasimov, Crell: Use the same canonical URI paths as for HTML routes.

parent 743957c4
......@@ -352,8 +352,13 @@ services:
password:
class: Drupal\Core\Password\PhpassHashedPassword
arguments: [16]
mime_type_matcher:
class: Drupal\Core\Routing\MimeTypeMatcher
accept_header_matcher:
class: Drupal\Core\Routing\AcceptHeaderMatcher
arguments: ['@content_negotiation']
tags:
- { name: route_filter }
content_type_header_matcher:
class: Drupal\Core\Routing\ContentTypeHeaderMatcher
tags:
- { name: route_filter }
paramconverter_manager:
......@@ -427,6 +432,10 @@ services:
class: Drupal\Core\EventSubscriber\SpecialAttributesRouteSubscriber
tags:
- { name: event_subscriber }
route_http_method_subscriber:
class: Drupal\Core\EventSubscriber\RouteMethodSubscriber
tags:
- { name: event_subscriber }
controller.page:
class: Drupal\Core\Controller\HtmlPageController
arguments: ['@controller_resolver', '@string_translation', '@title_resolver']
......
<?php
/**
* @file
* Contains \Drupal\Core\EventSubscriber\RouteMethodSubscriber.
*/
namespace Drupal\Core\EventSubscriber;
use Drupal\Core\Routing\RouteBuildEvent;
use Drupal\Core\Routing\RoutingEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* Provides a default value for the HTTP method restriction on routes.
*
* Most routes will only deal with GET and POST requests, so we restrict them to
* those two if nothing else is specified. This is necessary to give other
* routes a chance during the route matching process when they are listening
* for example to DELETE requests on the same path. A typical use case are REST
* web service routes that use the full spectrum of HTTP methods.
*/
class RouteMethodSubscriber implements EventSubscriberInterface {
/**
* Sets a default value of GET|POST for the _method route property.
*
* @param \Drupal\Core\Routing\RouteBuildEvent $event
* The event containing the build routes.
*/
public function onRouteBuilding(RouteBuildEvent $event) {
foreach ($event->getRouteCollection() as $route) {
$methods = $route->getMethods();
if (empty($methods)) {
$route->setMethods(array('GET', 'POST'));
}
}
}
/**
* {@inheritdoc}
*/
static function getSubscribedEvents() {
// Set a higher priority to ensure that routes get the default HTTP methods
// as early as possible.
$events[RoutingEvents::ALTER][] = array('onRouteBuilding', 5000);
return $events;
}
}
......@@ -2,49 +2,84 @@
/**
* @file
* Contains Drupal\Core\Routing\MimeTypeMatcher.
* Contains Drupal\Core\Routing\AcceptHeaderMatcher.
*/
namespace Drupal\Core\Routing;
use Drupal\Component\Utility\String;
use Drupal\Core\ContentNegotiation;
use Symfony\Cmf\Component\Routing\NestedMatcher\RouteFilterInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\NotAcceptableHttpException;
use Symfony\Component\Routing\RouteCollection;
use Symfony\Cmf\Component\Routing\NestedMatcher\RouteFilterInterface;
/**
* This class filters routes based on the media type in HTTP Accept headers.
* Filters routes based on the media type specified in the HTTP Accept headers.
*/
class MimeTypeMatcher implements RouteFilterInterface {
class AcceptHeaderMatcher implements RouteFilterInterface {
/**
* The content negotiation library.
*
* @var \Drupal\Core\ContentNegotiation
*/
protected $contentNegotiation;
/**
* Implements \Symfony\Cmf\Component\Routing\NestedMatcher\RouteFilterInterface::filter()
* Constructs a new AcceptHeaderMatcher.
*
* @param \Drupal\Core\ContentNegotiation $cotent_negotiation
* The content negotiation library.
*/
public function __construct(ContentNegotiation $content_negotiation) {
$this->contentNegotiation = $content_negotiation;
}
/**
* {@inheritdoc}
*/
public function filter(RouteCollection $collection, Request $request) {
// Generates a list of Symfony formats matching the acceptable MIME types.
// @todo replace by proper content negotiation library.
$acceptable_mime_types = $request->getAcceptableContentTypes();
$acceptable_formats = array_map(array($request, 'getFormat'), $acceptable_mime_types);
$filtered_collection = new RouteCollection();
$acceptable_formats = array_filter(array_map(array($request, 'getFormat'), $acceptable_mime_types));
$primary_format = $this->contentNegotiation->getContentType($request);
foreach ($collection as $name => $route) {
// _format could be a |-delimited list of supported formats.
$supported_formats = array_filter(explode('|', $route->getRequirement('_format')));
if (empty($supported_formats)) {
// No format restriction on the route, so it always matches. Move it to
// the end of the collection by re-adding it.
$collection->add($name, $route);
}
elseif (in_array($primary_format, $supported_formats)) {
// Perfect match, which will get a higher priority by leaving the route
// on top of the list.
}
// The route partially matches if it doesn't care about format, if it
// explicitly allows any format, or if one of its allowed formats is
// in the request's list of acceptable formats.
if (empty($supported_formats) || in_array('*/*', $acceptable_mime_types) || array_intersect($acceptable_formats, $supported_formats)) {
$filtered_collection->add($name, $route);
elseif (in_array('*/*', $acceptable_mime_types) || array_intersect($acceptable_formats, $supported_formats)) {
// Move it to the end of the list.
$collection->add($name, $route);
}
else {
// Remove the route if it does not match at all.
$collection->remove($name);
}
}
if (!count($filtered_collection)) {
throw new NotAcceptableHttpException();
if (count($collection)) {
return $collection;
}
return $filtered_collection;
// We do not throw a
// \Symfony\Component\Routing\Exception\ResourceNotFoundException here
// because we don't want to return a 404 status code, but rather a 406.
throw new NotAcceptableHttpException(String::format('No route found for the specified formats @formats.', array('@formats' => implode(' ', $acceptable_mime_types))));
}
}
<?php
/**
* @file
* Contains Drupal\Core\Routing\ContentTypeHeaderMatcher.
*/
namespace Drupal\Core\Routing;
use Symfony\Cmf\Component\Routing\NestedMatcher\RouteFilterInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\UnsupportedMediaTypeHttpException;
use Symfony\Component\Routing\RouteCollection;
/**
* Filters routes based on the HTTP Content-type header.
*/
class ContentTypeHeaderMatcher implements RouteFilterInterface {
/**
* {@inheritdoc}
*/
public function filter(RouteCollection $collection, Request $request) {
// The Content-type header does not make sense on GET requests, because GET
// requests do not carry any content. Nothing to filter in this case.
if ($request->isMethod('GET')) {
return $collection;
}
$format = $request->getContentType();
foreach ($collection as $name => $route) {
$supported_formats = array_filter(explode('|', $route->getRequirement('_content_type_format')));
if (empty($supported_formats)) {
// No restriction on the route, so we move the route to the end of the
// collection by re-adding it. That way generic routes sink down in the
// list and exact matching routes stay on top.
$collection->add($name, $route);
}
elseif (!in_array($format, $supported_formats)) {
$collection->remove($name);
}
}
if (count($collection)) {
return $collection;
}
// We do not throw a
// \Symfony\Component\Routing\Exception\ResourceNotFoundException here
// because we don't want to return a 404 status code, but rather a 415.
throw new UnsupportedMediaTypeHttpException('No route found that matches the Content-Type header.');
}
}
......@@ -9,7 +9,9 @@
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Plugin\Discovery\ContainerDerivativeInterface;
use Drupal\Core\Routing\RouteProviderInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Routing\Exception\RouteNotFoundException;
/**
* Provides a resource plugin definition for every entity type.
......@@ -30,14 +32,24 @@ class EntityDerivative implements ContainerDerivativeInterface {
*/
protected $entityManager;
/**
* The route provider.
*
* @var \Drupal\Core\Routing\RouteProviderInterface
*/
protected $routeProvider;
/**
* Constructs an EntityDerivative object.
*
* @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
* The entity manager.
* @param \Drupal\Core\Routing\RouteProviderInterface $route_provider
* The route provider.
*/
public function __construct(EntityManagerInterface $entity_manager) {
public function __construct(EntityManagerInterface $entity_manager, RouteProviderInterface $route_provider) {
$this->entityManager = $entity_manager;
$this->routeProvider = $route_provider;
}
/**
......@@ -45,7 +57,8 @@ public function __construct(EntityManagerInterface $entity_manager) {
*/
public static function create(ContainerInterface $container, $base_plugin_id) {
return new static(
$container->get('entity.manager')
$container->get('entity.manager'),
$container->get('router.route_provider')
);
}
......@@ -74,6 +87,36 @@ public function getDerivativeDefinitions($base_plugin_definition) {
'serialization_class' => $entity_type->getClass(),
'label' => $entity_type->getLabel(),
);
$default_uris = array(
'canonical' => "/entity/$entity_type_id/" . '{' . $entity_type_id . '}',
'http://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 ($route_name = $entity_type->getLinkTemplate($link_relation)) {
// @todo remove the try/catch as part of
// http://drupal.org/node/2158571
try {
$route = $this->routeProvider->getRouteByName($route_name);
$this->derivatives[$entity_type_id]['uri_paths'][$link_relation] = $route->getPath();
}
catch (RouteNotFoundException $e) {
// If the route does not exist it means we are in a brittle state
// of module enabling/disabling, so we simply exclude this entity
// type.
unset($this->derivatives[$entity_type_id]);
// Continue with the next entity type;
continue 2;
}
}
else {
$this->derivatives[$entity_type_id]['uri_paths'][$link_relation] = $default_uri;
}
}
$this->derivatives[$entity_type_id] += $base_plugin_definition;
}
}
......
......@@ -78,13 +78,17 @@ public function permissions() {
*/
public function routes() {
$collection = new RouteCollection();
$path_prefix = strtr($this->pluginId, ':', '/');
$definition = $this->getPluginDefinition();
$canonical_path = isset($definition['uri_paths']['canonical']) ? $definition['uri_paths']['canonical'] : '/' . strtr($this->pluginId, ':', '/') . '/{id}';
$create_path = isset($definition['uri_paths']['http://drupal.org/link-relations/create']) ? $definition['uri_paths']['http://drupal.org/link-relations/create'] : '/' . strtr($this->pluginId, ':', '/');
$route_name = strtr($this->pluginId, ':', '.');
$methods = $this->availableMethods();
foreach ($methods as $method) {
$lower_method = strtolower($method);
$route = new Route("/$path_prefix/{id}", array(
$route = new Route($canonical_path, array(
'_controller' => 'Drupal\rest\RequestHandler::handle',
// Pass the resource plugin ID along as default property.
'_plugin' => $this->pluginId,
......@@ -98,9 +102,17 @@ public function routes() {
switch ($method) {
case 'POST':
// POST routes do not require an ID in the URL path.
$route->setPattern("/$path_prefix");
$route->addDefaults(array('id' => NULL));
$route->setPattern($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;
......@@ -110,7 +122,6 @@ public function routes() {
// HTTP Accept headers.
foreach ($this->serializerFormats as $format_name) {
// Expose one route per available format.
//$format_route = new Route($route->getPath(), $route->getDefaults(), $route->getRequirements());
$format_route = clone $route;
$format_route->addRequirements(array('_format' => $format_name));
$collection->add("$route_name.$method.$format_name", $format_route);
......
......@@ -17,7 +17,10 @@
*
* @RestResource(
* id = "dblog",
* label = @Translation("Watchdog database log")
* label = @Translation("Watchdog database log"),
* uri_paths = {
* "canonical" = "/dblog/{id}"
* }
* )
*/
class DBLogResource extends ResourceBase {
......
......@@ -14,7 +14,6 @@
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/**
* Represents entities as resources.
......@@ -23,7 +22,11 @@
* id = "entity",
* label = @Translation("Entity"),
* serialization_class = "Drupal\Core\Entity\Entity",
* derivative = "Drupal\rest\Plugin\Derivative\EntityDerivative"
* derivative = "Drupal\rest\Plugin\Derivative\EntityDerivative",
* uri_paths = {
* "canonical" = "/entity/{entity_type}/{entity}",
* "http://drupal.org/link-relations/create" = "/entity/{entity_type}"
* }
* )
*/
class EntityResource extends ResourceBase {
......@@ -31,36 +34,29 @@ class EntityResource extends ResourceBase {
/**
* Responds to entity GET requests.
*
* @param mixed $id
* The entity ID.
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity object.
*
* @return \Drupal\rest\ResourceResponse
* The response containing the loaded entity.
* The response containing the entity with its accessible fields.
*
* @throws \Symfony\Component\HttpKernel\Exception\HttpException
*/
public function get($id) {
$definition = $this->getPluginDefinition();
$entity = entity_load($definition['entity_type'], $id);
if ($entity) {
if (!$entity->access('view')) {
throw new AccessDeniedHttpException();
}
foreach ($entity as $field_name => $field) {
if (!$field->access('view')) {
unset($entity->{$field_name});
}
public function get(EntityInterface $entity) {
if (!$entity->access('view')) {
throw new AccessDeniedHttpException();
}
foreach ($entity as $field_name => $field) {
if (!$field->access('view')) {
unset($entity->{$field_name});
}
return new ResourceResponse($entity);
}
throw new NotFoundHttpException(t('Entity with ID @id not found', array('@id' => $id)));
return new ResourceResponse($entity);
}
/**
* Responds to entity POST requests and saves the new entity.
*
* @param mixed $id
* Ignored. A new entity is created with a new ID.
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity.
*
......@@ -69,7 +65,7 @@ public function get($id) {
*
* @throws \Symfony\Component\HttpKernel\Exception\HttpException
*/
public function post($id, EntityInterface $entity = NULL) {
public function post(EntityInterface $entity = NULL) {
if ($entity == NULL) {
throw new BadRequestHttpException(t('No entity content received.'));
}
......@@ -112,8 +108,8 @@ public function post($id, EntityInterface $entity = NULL) {
/**
* Responds to entity PATCH requests.
*
* @param mixed $id
* The entity ID.
* @param \Drupal\Core\Entity\EntityInterface $original_entity
* The original entity object.
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity.
*
......@@ -122,24 +118,14 @@ public function post($id, EntityInterface $entity = NULL) {
*
* @throws \Symfony\Component\HttpKernel\Exception\HttpException
*/
public function patch($id, EntityInterface $entity = NULL) {
public function patch(EntityInterface $original_entity, EntityInterface $entity = NULL) {
if ($entity == NULL) {
throw new BadRequestHttpException(t('No entity content received.'));
}
if (empty($id)) {
throw new NotFoundHttpException();
}
$definition = $this->getPluginDefinition();
if ($entity->getEntityTypeId() != $definition['entity_type']) {
throw new BadRequestHttpException(t('Invalid entity type'));
}
$original_entity = entity_load($definition['entity_type'], $id);
// We don't support creating entities with PATCH, so we throw an error if
// there is no existing entity.
if ($original_entity == FALSE) {
throw new NotFoundHttpException();
}
if (!$original_entity->access('update')) {
throw new AccessDeniedHttpException();
}
......@@ -174,33 +160,28 @@ public function patch($id, EntityInterface $entity = NULL) {
/**
* Responds to entity DELETE requests.
*
* @param mixed $id
* The entity ID.
* @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($id) {
$definition = $this->getPluginDefinition();
$entity = entity_load($definition['entity_type'], $id);
if ($entity) {
if (!$entity->access('delete')) {
throw new AccessDeniedHttpException();
}
try {
$entity->delete();
watchdog('rest', 'Deleted entity %type with ID %id.', array('%type' => $entity->getEntityTypeId(), '%id' => $entity->id()));
public function delete(EntityInterface $entity) {
if (!$entity->access('delete')) {
throw new AccessDeniedHttpException();
}
try {
$entity->delete();
watchdog('rest', '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, t('Internal Server Error'), $e);
}
// Delete responses have an empty body.
return new ResourceResponse(NULL, 204);
}
catch (EntityStorageException $e) {
throw new HttpException(500, t('Internal Server Error'), $e);
}
throw new NotFoundHttpException(t('Entity with ID @id not found', array('@id' => $id)));
}
/**
......
......@@ -25,13 +25,12 @@ class RequestHandler extends ContainerAware {
*
* @param Symfony\Component\HttpFoundation\Request $request
* The HTTP request object.
* @param mixed $id
* The resource ID.
*
* @return \Symfony\Component\HttpFoundation\Response
* The response object.
*/
public function handle(Request $request, $id = NULL) {
public function handle(Request $request) {
$plugin = $request->attributes->get(RouteObjectInterface::ROUTE_OBJECT)->getDefault('_plugin');
$method = strtolower($request->getMethod());
......@@ -69,13 +68,24 @@ public function handle(Request $request, $id = NULL) {
}
}
// Determine the request parameters that should be passed to the resource
// plugin.
$route_parameters = $request->attributes->get('_route_params');
$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 = $request->attributes->get(RouteObjectInterface::ROUTE_OBJECT)->getRequirement('_format') ?: 'json';
try {
$response = $resource->{$method}($id, $unserialized, $request);
$response = call_user_func_array(array($resource, $method), array_merge($parameters, array($unserialized, $request)));
}
catch (HttpException $e) {
$error['error'] = $e->getMessage();
......
......@@ -46,7 +46,7 @@ public function testRead() {
$entity->save();
// Try to read the resource as an anonymous user, which should not work.
$response = $this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'GET', NULL, $this->defaultMimeType);
$this->httpRequest($entity->getSystemPath(), 'GET', NULL, $this->defaultMimeType);
$this->assertResponse('401', 'HTTP response code is 401 when the request is not authenticated and the user is anonymous.');
$this->assertText('A fatal error occurred: No authentication credentials provided.');
......@@ -63,7 +63,7 @@ public function testRead() {
// Try to read the resource with session cookie authentication, which is
// not enabled and should not work.
$response = $this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'GET', NULL, $this->defaultMimeType);
$this->httpRequest($entity->getSystemPath(), 'GET', NULL, $this->defaultMimeType);
$this->assertResponse('401', 'HTTP response code is 401 when the request is authenticated but not authorized.');
// Ensure that cURL settings/headers aren't carried over to next request.
......@@ -71,7 +71,7 @@ public function testRead() {
// Now read it with the Basic authentication which is enabled and should
// work.
$response = $this->basicAuthGet('entity/' . $entity_type . '/' . $entity->id(), $account->getUsername(), $account->pass_raw);
$this->basicAuthGet($entity->getSystemPath(), $account->getUsername(), $account->pass_raw);
$this->assertResponse('200', 'HTTP response code is 200 for successfully authorized requests.');
$this->curlClose();
}
......
......@@ -7,7 +7,6 @@
namespace Drupal\rest\Tests;
use Drupal\Component\Utility\Json;
use Drupal\rest\Tests\RESTTestBase;
/**
......@@ -51,7 +50,7 @@ public function testDelete() {
$entity = $this->entityCreate($entity_type);
$entity->save();
// Delete it over the REST API.
$response = $this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'DELETE');
$response = $this->httpRequest($entity->getSystemPath(), 'DELETE');
// Clear the static cache with entity_load(), otherwise we won't see the
// update.
$entity = entity_load($entity_type, $entity->id(), TRUE);
......@@ -60,17 +59,16 @@ public function testDelete() {
$this->assertEqual($response, '', 'Response body is empty.');
// Try to delete an entity that does not exist.
$response = $this->httpRequest('entity/' . $entity_type . '/9999', 'DELETE');
$response = $this->httpRequest($entity_type . '/9999', 'DELETE');
$this->assertResponse(404);
$decoded = Json::decode($response);
$this->assertEqual($decoded['error'], 'Entity with ID 9999 not found', 'Response message is correct.');
$this->assertText('The requested page "/' . $entity_type . '/9999" 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/' . $entity_type . '/' . $entity->id(), 'DELETE');
$this->httpRequest($entity->getSystemPath(), 'DELETE');
$this->assertResponse(403);
$this->assertNotIdentical(FALSE, entity_load($entity_type, $entity->id(), TRUE), 'The ' . $entity_type . ' entity is still in the database.');
}
......
......@@ -55,8 +55,16 @@ public function testNodes() {
$node = $this->entityCreate('node');
$node->save();
$this->httpRequest('entity/node/' . $node->id(), 'GET', NULL, $this->defaultMimeType);
$this->httpRequest('node/' . $node->id(), 'GET', NULL, $this->defaultMimeType);
$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/' . $node->id(), 'GET', NULL, 'application/json');
$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');
......@@ -76,7 +84,7 @@ public function testNodes() {
),
);
$serialized = $this->container->get('serializer')->serialize($data, $this->defaultFormat);
$this->httpRequest('entity/node/' . $node->id(), 'PATCH', $serialized, $this->defaultMimeType);
$this->httpRequest('node/' . $node->id(), 'PATCH', $serialized, $this->defaultMimeType);
$this->assertResponse(204);
// Reload the node from the DB and check if the title was correctly updated.
......
......@@ -276,16 +276,22 @@ protected function assertHeader($header, $value, $message = '', $group = 'Browse
}
/**
* Overrides WebTestBase::drupalLogin().
* {@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 drupalLogin(AccountInterface $user) {
if (isset($this->curlHandle)) {
// cURL quirk: when setting CURLOPT_CUSTOMREQUEST to anything other than
// POST in httpRequest() it has to be restored to POST here. Otherwise the
// POST request to login a user will not work.
curl_setopt($this->curlHandle, CURLOPT_CUSTOMREQUEST, 'POST');
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';
}
}
parent::drupalLogin($user);
return parent::curlExec($curl_options, $redirect);