From 73f3973425e2d69ba523e869b4f1dcadf7af1be3 Mon Sep 17 00:00:00 2001
From: Dries <dries@buytaert.net>
Date: Wed, 27 Feb 2013 16:58:32 -0500
Subject: [PATCH] Issue #1850734 by klausi: Make serialization formats
 configurable.

---
 .../rest/EventSubscriber/RouteSubscriber.php  | 27 ++++--
 .../lib/Drupal/rest/Plugin/ResourceBase.php   | 91 +++++++++++--------
 .../Drupal/rest/Plugin/ResourceInterface.php  |  8 ++
 .../Plugin/rest/resource/DBLogResource.php    |  7 +-
 .../rest/lib/Drupal/rest/RequestHandler.php   | 49 ++++++----
 .../rest/lib/Drupal/rest/Tests/CreateTest.php |  2 +-
 .../rest/lib/Drupal/rest/Tests/DeleteTest.php |  2 +-
 .../lib/Drupal/rest/Tests/RESTTestBase.php    | 20 ++--
 .../rest/lib/Drupal/rest/Tests/ReadTest.php   |  8 +-
 .../rest/lib/Drupal/rest/Tests/UpdateTest.php |  4 +-
 core/modules/rest/rest.admin.inc              | 19 +++-
 11 files changed, 152 insertions(+), 85 deletions(-)

diff --git a/core/modules/rest/lib/Drupal/rest/EventSubscriber/RouteSubscriber.php b/core/modules/rest/lib/Drupal/rest/EventSubscriber/RouteSubscriber.php
index d4711e00bdfa..965c149b4bda 100644
--- a/core/modules/rest/lib/Drupal/rest/EventSubscriber/RouteSubscriber.php
+++ b/core/modules/rest/lib/Drupal/rest/EventSubscriber/RouteSubscriber.php
@@ -54,15 +54,30 @@ public function __construct(ResourcePluginManager $manager, ConfigFactory $confi
   public function dynamicRoutes(RouteBuildEvent $event) {
 
     $collection = $event->getRouteCollection();
+    $enabled_resources = $this->config->get('rest.settings')->load()->get('resources');
 
-    $resources = $this->config->get('rest.settings')->load()->get('resources');
-    if ($resources && $enabled = array_intersect_key($this->manager->getDefinitions(), $resources)) {
-      foreach ($enabled as $key => $resource) {
-        $plugin = $this->manager->getInstance(array('id' => $key));
+    // 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) {
+      foreach ($plugin->routes() as $name => $route) {
+        $method = $route->getRequirement('_method');
+        // Only expose routes where the method is enabled in the configuration.
+        if ($method && isset($enabled_methods[$method])) {
           $route->setRequirement('_access_rest_csrf',  'TRUE');
-          $collection->add("rest.$name", $route);
+
+          // If the array of configured format restrictions is empty for a
+          // method always add the route.
+          if (empty($enabled_methods[$method])) {
+            $collection->add("rest.$name", $route);
+            continue;
+          }
+          // If there is no format requirement or if it matches the
+          // configuration also add the route.
+          $format_requirement = $route->getRequirement('_format');
+          if (!$format_requirement || isset($enabled_methods[$method][$format_requirement])) {
+            $collection->add("rest.$name", $route);
+          }
         }
       }
     }
diff --git a/core/modules/rest/lib/Drupal/rest/Plugin/ResourceBase.php b/core/modules/rest/lib/Drupal/rest/Plugin/ResourceBase.php
index 41f19fd2a5da..747df459a261 100644
--- a/core/modules/rest/lib/Drupal/rest/Plugin/ResourceBase.php
+++ b/core/modules/rest/lib/Drupal/rest/Plugin/ResourceBase.php
@@ -26,15 +26,11 @@ abstract class ResourceBase extends PluginBase implements ResourceInterface {
   public function permissions() {
     $permissions = array();
     $definition = $this->getDefinition();
-    foreach ($this->requestMethods() as $method) {
+    foreach ($this->availableMethods() as $method) {
       $lowered_method = strtolower($method);
-      // Only expose permissions where the HTTP request method exists on the
-      // plugin.
-      if (method_exists($this, $lowered_method)) {
-        $permissions["restful $lowered_method $this->plugin_id"] = array(
-          'title' => t('Access @method on %label resource', array('@method' => $method, '%label' => $definition['label'])),
-        );
-      }
+      $permissions["restful $lowered_method $this->plugin_id"] = array(
+        'title' => t('Access @method on %label resource', array('@method' => $method, '%label' => $definition['label'])),
+      );
     }
     return $permissions;
   }
@@ -47,38 +43,44 @@ public function routes() {
     $path_prefix = strtr($this->plugin_id, ':', '/');
     $route_name = strtr($this->plugin_id, ':', '.');
 
-    $methods = $this->requestMethods();
+    $methods = $this->availableMethods();
     foreach ($methods as $method) {
       $lower_method = strtolower($method);
-      // Only expose routes where the HTTP request method exists on the plugin.
-      if (method_exists($this, $lower_method)) {
-        $route = new Route("/$path_prefix/{id}", array(
-          '_controller' => 'Drupal\rest\RequestHandler::handle',
-          // Pass the resource plugin ID along as default property.
-          '_plugin' => $this->plugin_id,
-        ), array(
-          // The HTTP method is a requirement for this route.
-          '_method' => $method,
-          '_permission' => "restful $lower_method $this->plugin_id",
-        ));
+      $route = new Route("/$path_prefix/{id}", array(
+        '_controller' => 'Drupal\rest\RequestHandler::handle',
+        // Pass the resource plugin ID along as default property.
+        '_plugin' => $this->plugin_id,
+      ), array(
+        // The HTTP method is a requirement for this route.
+        '_method' => $method,
+        '_permission' => "restful $lower_method $this->plugin_id",
+      ));
 
-        switch ($method) {
-          case 'POST':
-            // POST routes do not require an ID in the URL path.
-            $route->setPattern("/$path_prefix");
-            $route->addDefaults(array('id' => NULL));
-            break;
+      switch ($method) {
+        case 'POST':
+          // POST routes do not require an ID in the URL path.
+          $route->setPattern("/$path_prefix");
+          $route->addDefaults(array('id' => NULL));
+          $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.
-            // @todo Replace hard coded format here with available formats.
-            $route->addRequirements(array('_format' => 'drupal_jsonld'));
-            break;
-        }
+        case 'GET':
+        case 'HEAD':
+          // Restrict GET and HEAD requests to the media type specified in the
+          // HTTP Accept headers.
+          $formats = drupal_container()->getParameter('serializer.formats');
+          foreach ($formats as $format_name => $label) {
+            // Expose one route per available format.
+            //$format_route = new Route($route->getPattern(), $route->getDefaults(), $route->getRequirements());
+            $format_route = clone $route;
+            $format_route->addRequirements(array('_format' => $format_name));
+            $collection->add("$route_name.$method.$format_name", $format_route);
+          }
+          break;
 
-        $collection->add("$route_name.$method", $route);
+        default:
+          $collection->add("$route_name.$method", $route);
+          break;
       }
     }
 
@@ -95,7 +97,7 @@ public function routes() {
    *   The list of allowed HTTP request method strings.
    */
   protected function requestMethods() {
-    return drupal_map_assoc(array(
+    return array(
       'HEAD',
       'GET',
       'POST',
@@ -105,6 +107,21 @@ protected function requestMethods() {
       'OPTIONS',
       'CONNECT',
       'PATCH',
-    ));
+    );
+  }
+
+  /**
+   * Implements ResourceInterface::availableMethods().
+   */
+  public function availableMethods() {
+    $methods = $this->requestMethods();
+    $available = array();
+    foreach ($methods as $method) {
+      // Only expose methods where the HTTP request method exists on the plugin.
+      if (method_exists($this, strtolower($method))) {
+        $available[] = $method;
+      }
+    }
+    return $available;
   }
 }
diff --git a/core/modules/rest/lib/Drupal/rest/Plugin/ResourceInterface.php b/core/modules/rest/lib/Drupal/rest/Plugin/ResourceInterface.php
index 87da39d73a6b..3f21f0d395f5 100644
--- a/core/modules/rest/lib/Drupal/rest/Plugin/ResourceInterface.php
+++ b/core/modules/rest/lib/Drupal/rest/Plugin/ResourceInterface.php
@@ -36,4 +36,12 @@ public function routes();
    */
   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();
+
 }
diff --git a/core/modules/rest/lib/Drupal/rest/Plugin/rest/resource/DBLogResource.php b/core/modules/rest/lib/Drupal/rest/Plugin/rest/resource/DBLogResource.php
index cc06e1a51262..cc4360317acc 100644
--- a/core/modules/rest/lib/Drupal/rest/Plugin/rest/resource/DBLogResource.php
+++ b/core/modules/rest/lib/Drupal/rest/Plugin/rest/resource/DBLogResource.php
@@ -50,12 +50,7 @@ public function get($id = NULL) {
       $record = db_query("SELECT * FROM {watchdog} WHERE wid = :wid", array(':wid' => $id))
         ->fetchObject();
       if (!empty($record)) {
-        // Serialization is done here, so we indicate with NULL that there is no
-        // subsequent serialization necessary.
-        $response = new ResourceResponse(NULL, 200, array('Content-Type' => 'application/vnd.drupal.ld+json'));
-        // @todo remove hard coded format here.
-        $response->setContent(drupal_json_encode($record));
-        return $response;
+        return new ResourceResponse((array) $record);
       }
     }
     throw new NotFoundHttpException(t('Log entry with ID @id was not found', array('@id' => $id)));
diff --git a/core/modules/rest/lib/Drupal/rest/RequestHandler.php b/core/modules/rest/lib/Drupal/rest/RequestHandler.php
index 3922e9e13971..30156de693ec 100644
--- a/core/modules/rest/lib/Drupal/rest/RequestHandler.php
+++ b/core/modules/rest/lib/Drupal/rest/RequestHandler.php
@@ -12,6 +12,7 @@
 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;
 
 /**
@@ -43,17 +44,28 @@ public function handle(Request $request, $id = NULL) {
     $received = $request->getContent();
     $unserialized = NULL;
     if (!empty($received)) {
-      $definition = $resource->getDefinition();
-      $class = $definition['serialization_class'];
-      try {
-        // @todo Replace the format here with something we get from the HTTP
-        //   Content-type header. See http://drupal.org/node/1850704
-        $unserialized = $serializer->deserialize($received, $class, 'drupal_jsonld');
+      $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');
+      $enabled_formats = $config[$plugin][$request->getMethod()];
+      if (empty($enabled_formats) || isset($enabled_formats[$format])) {
+        $definition = $resource->getDefinition();
+        $class = $definition['serialization_class'];
+        try {
+          $unserialized = $serializer->deserialize($received, $class, $format);
+        }
+        catch (UnexpectedValueException $e) {
+          $error['error'] = $e->getMessage();
+          $content = $serializer->serialize($error, $format);
+          return new Response($content, 400, array('Content-Type' => $request->getMimeType($format)));
+        }
       }
-      catch (UnexpectedValueException $e) {
-        $error['error'] = $e->getMessage();
-        $content = $serializer->serialize($error, 'drupal_jsonld');
-        return new Response($content, 400, array('Content-Type' => 'application/vnd.drupal.ld+json'));
+      else {
+        throw new UnsupportedMediaTypeHttpException();
       }
     }
 
@@ -63,21 +75,26 @@ public function handle(Request $request, $id = NULL) {
     }
     catch (HttpException $e) {
       $error['error'] = $e->getMessage();
-      $content = $serializer->serialize($error, 'drupal_jsonld');
+      $format = $request->attributes->get(RouteObjectInterface::ROUTE_OBJECT)->getRequirement('_format') ?: 'drupal_jsonld';
+      $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' => 'application/vnd.drupal.ld+json');
+      $headers = $e->getHeaders() + array('Content-Type' => $request->getMimeType($format));
       return new Response($content, $e->getStatusCode(), $headers);
     }
 
     // Serialize the outgoing data for the response, if available.
     $data = $response->getResponseData();
     if ($data != NULL) {
-      // @todo Replace the format here with something we get from the HTTP
-      //   Accept headers. See http://drupal.org/node/1833440
-      $output = $serializer->serialize($data, 'drupal_jsonld');
+      // 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 Drupal
+      // JSON-LD.
+      $format = $request->attributes->get(RouteObjectInterface::ROUTE_OBJECT)->getRequirement('_format') ?: 'drupal_jsonld';
+
+      $output = $serializer->serialize($data, $format);
       $response->setContent($output);
-      $response->headers->set('Content-Type', 'application/vnd.drupal.ld+json');
+      $response->headers->set('Content-Type', $request->getMimeType($format));
     }
     return $response;
   }
diff --git a/core/modules/rest/lib/Drupal/rest/Tests/CreateTest.php b/core/modules/rest/lib/Drupal/rest/Tests/CreateTest.php
index df77ec3efaa7..a3a5c6028b22 100644
--- a/core/modules/rest/lib/Drupal/rest/Tests/CreateTest.php
+++ b/core/modules/rest/lib/Drupal/rest/Tests/CreateTest.php
@@ -38,7 +38,7 @@ public function testCreate() {
     // entity types here as well.
     $entity_type = 'entity_test';
 
-    $this->enableService('entity:' . $entity_type);
+    $this->enableService('entity:' . $entity_type, 'POST');
     // Create a user account that has the required permissions to create
     // resources via the web API.
     $account = $this->drupalCreateUser(array('restful post entity:' . $entity_type));
diff --git a/core/modules/rest/lib/Drupal/rest/Tests/DeleteTest.php b/core/modules/rest/lib/Drupal/rest/Tests/DeleteTest.php
index e39675e1ef6b..c29e7e65f7a6 100644
--- a/core/modules/rest/lib/Drupal/rest/Tests/DeleteTest.php
+++ b/core/modules/rest/lib/Drupal/rest/Tests/DeleteTest.php
@@ -36,7 +36,7 @@ public function testDelete() {
     // Define the entity types we want to test.
     $entity_types = array('entity_test', 'node', 'user');
     foreach ($entity_types as $entity_type) {
-      $this->enableService('entity:' . $entity_type);
+      $this->enableService('entity:' . $entity_type, 'DELETE');
       // Create a user account that has the required permissions to delete
       // resources via the web API.
       $account = $this->drupalCreateUser(array('restful delete entity:' . $entity_type));
diff --git a/core/modules/rest/lib/Drupal/rest/Tests/RESTTestBase.php b/core/modules/rest/lib/Drupal/rest/Tests/RESTTestBase.php
index f7dbdb65fdd7..c428c86973e5 100644
--- a/core/modules/rest/lib/Drupal/rest/Tests/RESTTestBase.php
+++ b/core/modules/rest/lib/Drupal/rest/Tests/RESTTestBase.php
@@ -156,18 +156,24 @@ protected function entityValues($entity_type) {
    * @param string|FALSE $resource_type
    *   The resource type that should get web API enabled or FALSE to disable all
    *   resource types.
+   * @param string $method
+   *   The HTTP method to enable, e.g. GET, POST etc.
+   * @param string $format
+   *   (Optional) The serialization format, e.g. jsonld.
    */
-  protected function enableService($resource_type) {
+  protected function enableService($resource_type, $method = 'GET', $format = NULL) {
     // Enable web API for this entity type.
     $config = config('rest.settings');
+    $settings = array();
     if ($resource_type) {
-      $config->set('resources', array(
-        $resource_type => $resource_type,
-      ));
-    }
-    else {
-      $config->set('resources', array());
+      if ($format) {
+        $settings[$resource_type][$method][$format] = 'TRUE';
+      }
+      else {
+        $settings[$resource_type][$method] = array();
+      }
     }
+    $config->set('resources', $settings);
     $config->save();
 
     // Rebuild routing cache, so that the web API paths are available.
diff --git a/core/modules/rest/lib/Drupal/rest/Tests/ReadTest.php b/core/modules/rest/lib/Drupal/rest/Tests/ReadTest.php
index 29fb1befc4ef..299bdd49c8a5 100644
--- a/core/modules/rest/lib/Drupal/rest/Tests/ReadTest.php
+++ b/core/modules/rest/lib/Drupal/rest/Tests/ReadTest.php
@@ -38,7 +38,7 @@ public function testRead() {
     // Define the entity types we want to test.
     $entity_types = array('entity_test');
     foreach ($entity_types as $entity_type) {
-      $this->enableService('entity:' . $entity_type);
+      $this->enableService('entity:' . $entity_type, 'GET');
       // Create a user account that has the required permissions to delete
       // resources via the web API.
       $account = $this->drupalCreateUser(array('restful get entity:' . $entity_type));
@@ -57,12 +57,8 @@ public function testRead() {
       $this->assertEqual($data['uuid'][LANGUAGE_DEFAULT][0]['value'], $entity->uuid(), 'Entity UUID is correct');
 
       // Try to read the entity with an unsupported mime format.
-      // Because the matcher checks mime type first, then method, this will hit
-      // zero viable routes on the method.  If the mime matcher wasn't working,
-      // we would still find an existing GET route with the wrong format. That
-      // means this is a valid functional test for mime-matching.
       $response = $this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'GET', NULL, 'application/wrongformat');
-      $this->assertResponse(405);
+      $this->assertResponse(415);
 
       // Try to read an entity that does not exist.
       $response = $this->httpRequest('entity/' . $entity_type . '/9999', 'GET', NULL, 'application/vnd.drupal.ld+json');
diff --git a/core/modules/rest/lib/Drupal/rest/Tests/UpdateTest.php b/core/modules/rest/lib/Drupal/rest/Tests/UpdateTest.php
index fa65a2f30bad..94be2bf58652 100644
--- a/core/modules/rest/lib/Drupal/rest/Tests/UpdateTest.php
+++ b/core/modules/rest/lib/Drupal/rest/Tests/UpdateTest.php
@@ -38,7 +38,7 @@ public function testPatchUpdate() {
     // entity types here as well.
     $entity_type = 'entity_test';
 
-    $this->enableService('entity:' . $entity_type);
+    $this->enableService('entity:' . $entity_type, 'PATCH');
     // Create a user account that has the required permissions to create
     // resources via the web API.
     $account = $this->drupalCreateUser(array('restful patch entity:' . $entity_type));
@@ -103,7 +103,7 @@ public function testPutUpdate() {
     // entity types here as well.
     $entity_type = 'entity_test';
 
-    $this->enableService('entity:' . $entity_type);
+    $this->enableService('entity:' . $entity_type, 'PUT');
     // Create a user account that has the required permissions to create
     // resources via the web API.
     $account = $this->drupalCreateUser(array('restful put entity:' . $entity_type));
diff --git a/core/modules/rest/rest.admin.inc b/core/modules/rest/rest.admin.inc
index ef9d0d608518..95246ea2d7fb 100644
--- a/core/modules/rest/rest.admin.inc
+++ b/core/modules/rest/rest.admin.inc
@@ -34,7 +34,10 @@ function rest_admin_form($form, &$form_state) {
   }
   asort($entity_resources);
   asort($other_resources);
-  $enabled_resources = config('rest.settings')->get('resources') ?: array();
+  $config = config('rest.settings')->get('resources') ?: array();
+  // Strip out the nested method configuration, we are only interested in the
+  // plugin IDs of the resources.
+  $enabled_resources = drupal_map_assoc(array_keys($config));
 
   // Render the output using table_select().
   $header = array(
@@ -79,9 +82,19 @@ function rest_admin_form($form, &$form_state) {
  * Form submission handler for rest_admin_form().
  */
 function rest_admin_form_submit($form, &$form_state) {
-  $resources = array_filter($form_state['values']['entity_resources']);
+  $enabled_resources = array_filter($form_state['values']['entity_resources']);
   if (!empty($form_state['values']['other_resources'])) {
-    $resources += array_filter($form_state['values']['other_resources']);
+    $enabled_resources += array_filter($form_state['values']['other_resources']);
+  }
+  $resources = array();
+  $plugin_manager = drupal_container()->get('plugin.manager.rest');
+
+  // Enable all methods and all formats for each selected resource.
+  foreach ($enabled_resources as $resource) {
+    $plugin = $plugin_manager->getInstance(array('id' => $resource));
+    $methods = $plugin->availableMethods();
+    // An empty array means all formats are allowed for a method.
+    $resources[$resource] = array_fill_keys($methods, array());
   }
 
   $config = config('rest.settings');
-- 
GitLab