You are here

restws.module in RESTful Web Services 7

Same filename and directory in other branches
  1. 7.2 restws.module

RESTful web services module.

File

restws.module
View source
<?php

/**
 * @file
 * RESTful web services module.
 */

/**
 * Returns info about all defined resources.
 */
function restws_get_resource_info() {
  $info =& drupal_static(__FUNCTION__);
  if (!isset($info)) {
    $info = module_invoke_all('restws_resource_info');
    drupal_alter('restws_resource_info', $info);
  }
  return $info;
}

/**
 * Returns info about all defined formats.
 */
function restws_get_format_info() {
  $info =& drupal_static(__FUNCTION__);
  if (!isset($info)) {
    $info = module_invoke_all('restws_format_info');
    drupal_alter('restws_format_info', $info);
  }
  return $info;
}

/**
 * Implements hook_restws_resource_info().
 *
 * Provides resources for all entity types.
 */
function restws_restws_resource_info() {
  foreach (entity_get_info() as $entity_type => $info) {
    $result[$entity_type] = array(
      'label' => $info['label'],
      'class' => 'RestWSEntityResourceController',
    );
  }
  return $result;
}

/**
 * Returns a instance of a resource controller.
 *
 * @return RestWSResourceControllerInterface
 *   A resource controller object.
 */
function restws_resource_controller($name) {
  $static =& drupal_static(__FUNCTION__);
  if (!isset($static[$name])) {
    $info = restws_get_resource_info();
    $static[$name] = isset($info[$name]) ? new $info[$name]['class']($name, $info[$name]) : FALSE;
  }
  return $static[$name];
}

/**
 * Implements hook_restws_format_info().
 *
 * Provides basic formats.
 */
function restws_restws_format_info() {
  $result = array(
    'json' => array(
      'label' => t('JSON'),
      'class' => 'RestWSFormatJSON',
      'mime type' => 'application/json',
    ),
    'xml' => array(
      'label' => t('XML'),
      'class' => 'RestWSFormatXML',
      'mime type' => 'application/xml',
    ),
  );
  if (module_exists('rdf')) {
    $result['rdf'] = array(
      'label' => t('RDF'),
      'class' => 'RestWSFormatRDF',
      'mime type' => 'application/rdf+xml',
    );
  }
  return $result;
}

/**
 * Returns an instance of a format.
 *
 * @return RestWSFormatInterface
 *   A resource format object.
 */
function restws_format($name) {
  $static =& drupal_static(__FUNCTION__);
  if (!isset($static[$name])) {
    $info = restws_get_format_info();
    $static[$name] = isset($info[$name]) ? new $info[$name]['class']($name, $info[$name]) : FALSE;
  }
  return $static[$name];
}

/**
 * Handles a request.
 *
 * @param string $op
 *   One of 'create', 'update', 'delete' or 'view'.
 */
function restws_handle_request($op, $format, $resource_name, $id = NULL, $payload = NULL) {
  if ($resource = restws_resource_controller($resource_name)) {

    // Allow other modules to change the web service request or react upon it.
    $request = array(
      'op' => &$op,
      'format' => &$format,
      'resource' => &$resource,
      'id' => &$id,
      'payload' => &$payload,
    );
    drupal_alter('restws_request', $request);
    if (user_access('access resource ' . $resource_name) && $resource
      ->access($op, $id)) {
      try {
        $method = $op . 'Resource';
        if ($op == 'create') {
          print $format
            ->{$method}($resource, $payload);
          drupal_add_http_header('Status', '201 Created');
        }
        else {
          print $format
            ->{$method}($resource, $id, $payload);
        }
        drupal_add_http_header('Content-Type', $format
          ->mimeType());
      } catch (RestWSException $e) {
        echo check_plain($e
          ->getHTTPError()) . ': ' . check_plain($e
          ->getMessage());
        drupal_add_http_header('Status', $e
          ->getHTTPError());
      }
    }
    else {
      echo '403 Forbidden';
      drupal_add_http_header('Status', '403 Forbidden');
      watchdog('access denied', check_plain($_GET['q']), NULL, WATCHDOG_WARNING);
    }
  }
  else {
    echo '404 Not Found';
    drupal_add_http_header('Status', '404 Not Found');
  }
  drupal_page_footer();
  exit;
}

/**
 * An exception defining the HTTP error code and message.
 */
class RestWSException extends Exception {
  public function getHTTPError() {
    $code = $this
      ->getCode();
    switch ($code) {
      case 403:
        return '403 Forbidden';
      case 404:
        return '404 Not Found';
      case 406:
        return '406 Not Acceptable';
      case 422:
        return '422 Unprocessable Entity';
      default:
        return '500 Internal Server Error';
    }
  }

}

/**
 * Implements hook_menu_alter().
 */
function restws_menu_alter(&$items) {
  foreach (restws_get_resource_info() as $resource => $info) {

    // @todo document 'menu path'
    $menu_path = isset($info['menu_path']) ? $info['menu_path'] . '/%' : $resource . '/%';

    // Replace existing page callbacks with our own (e.g. node/%)
    if (isset($items[$menu_path])) {

      // Prepend the page callback and the resource to the page arguments.
      // So we can re-use it on standard HTML page requests.
      array_unshift($items[$menu_path]['page arguments'], $resource, $items[$menu_path]['page callback']);
      $items[$menu_path]['page callback'] = 'restws_page_callback';
    }
    elseif (isset($items[$menu_path . $resource])) {
      $menu_path = $menu_path . $resource;
      array_unshift($items[$menu_path]['page arguments'], $resource, $items[$menu_path]['page callback']);
      $items[$menu_path]['page callback'] = 'restws_page_callback';
    }
    else {
      $items[$menu_path] = array(
        'page callback' => 'restws_page_callback',
        'page arguments' => array(
          $resource,
          'drupal_not_found',
        ),
        'access callback' => TRUE,
        'type' => MENU_CALLBACK,
      );
    }

    // Resource base path (e.g. /node or /user) for creating resources.
    // @todo fix 'menu path'
    if (!isset($info['menu_path'])) {
      if (isset($items[$resource])) {

        // Prepend the page callback and the resource to the page arguments.
        if (!isset($items[$resource]['page arguments'])) {
          $items[$resource]['page arguments'] = array();
        }
        array_unshift($items[$resource]['page arguments'], $resource, $items[$resource]['page callback']);
        $items[$resource]['page callback'] = 'restws_page_callback';
      }
      else {
        $items[$resource] = array(
          'page callback' => 'restws_page_callback',
          'page arguments' => array(
            $resource,
            'drupal_not_found',
          ),
          'access callback' => TRUE,
          'type' => MENU_CALLBACK,
        );
      }
    }
  }
}

/**
 * Menu page callback.
 *
 * @param string $resource
 *   The name of the resource.
 * @param string $page_callback
 *   The page callback to pass through when the request is not handled by this
 *   module. If no other pre-existing callback is used, 'drupal_not_found'
 *   should be passed explicitly.
 * @param mixed $arg1,...
 *   Further arguments that are passed through to the given page callback.
 */
function restws_page_callback($resource, $page_callback) {
  $id_arg = arg(1);
  $format = FALSE;
  if (($pos = strpos($id_arg, '.')) && ($format_name = substr($id_arg, $pos + 1))) {
    $id = substr($id_arg, 0, $pos);
    $format = restws_format($format_name);
  }
  else {
    $id = $id_arg;
    switch ($_SERVER['REQUEST_METHOD']) {
      case 'PUT':
      case 'POST':

        // Get format MIME type form HTTP Content type header.
        $parts = explode(';', $_SERVER['CONTENT_TYPE'], 2);
        $format = restws_format_mimetype($parts[0]);
        break;
      case 'DELETE':
        if (isset($_SERVER['HTTP_ACCEPT'])) {
          $parts = explode(',', $_SERVER['HTTP_ACCEPT'], 2);
          $format = restws_format_mimetype($parts[0]);
        }
        if (!$format) {

          // We don't care about the format, just pick JSON.
          $format = restws_format('json');
        }
        break;
      default:

        // Get the format MIME type form the HTTP Accept header.
        // Ignore requests from web browsers that accept HTML.
        if (isset($_SERVER['HTTP_ACCEPT']) && strpos($_SERVER['HTTP_ACCEPT'], 'html') === FALSE) {

          // Use the first MIME type.
          $parts = explode(',', $_SERVER['HTTP_ACCEPT'], 2);
          $format = restws_format_mimetype($parts[0]);
        }

        // Consumers should not use this URL if page caching is enabled.
        // Drupal's page cache IDs are only determined by URL path, so this
        // could poison the HTML page cache. A browser request to /node/1 could
        // suddenly return JSON if the cache was primed with this RESTWS
        // response.
        if ($format && !isset($_COOKIE[session_name()]) && variable_get('cache')) {

          // Redirect to the URL path containing the format name instead.
          drupal_goto($_GET['q'] . '.' . $format
            ->getName(), array(), 301);
        }
    }
  }
  if ($format) {
    switch ($_SERVER['REQUEST_METHOD']) {
      case 'PUT':
        $op = 'create';
        break;
      case 'POST':
        $op = 'update';
        break;
      case 'DELETE':
        $op = 'delete';
        break;
      default:
        $op = 'view';
    }

    // CSRF protection on write operations.
    if (!in_array($_SERVER['REQUEST_METHOD'], array(
      'GET',
      'HEAD',
      'OPTIONS',
      'TRACE',
    )) && !restws_csrf_validation()) {
      echo '403 Access Denied: CSRF validation failed';
      drupal_add_http_header('Status', '403 Forbidden');
      drupal_page_footer();
      exit;
    }
    $payload = file_get_contents('php://input');
    if ($file = variable_get('restws_debug_log')) {
      $log = date(DATE_ISO8601) . "\n";
      $log .= 'Resource: ' . $resource . "\n";
      $log .= 'Operation: ' . $op . "\n";
      $log .= 'Format: ' . $format
        ->mimeType() . "\n";
      $log .= 'Id: ' . $id . "\n";
      $log .= 'Payload: ' . $payload . "\n";
      $log .= "----------------------------------------------------------------\n";
      file_put_contents($file, $log, FILE_APPEND);
    }
    restws_handle_request($op, $format, $resource, $id, $payload);
  }

  // @todo: Determine human readable URIs and redirect, if there is no
  // page callback.
  // Fall back to the passed $page_callback and pass through more arguments.
  $args = func_get_args();
  return call_user_func_array($page_callback, array_slice($args, 2));
}

/**
 * Ensures that a request with cookies has the required CSRF header set.
 *
 * @return bool
 *   TRUE if the request passed the CSRF protection, FALSE otherwise.
 */
function restws_csrf_validation() {

  // This check only applies if the user was successfully authenticated and the
  // request comes with a session cookie.
  if (user_is_logged_in() && !empty($_COOKIE[session_name()])) {
    return isset($_SERVER['HTTP_X_CSRF_TOKEN']) && drupal_valid_token($_SERVER['HTTP_X_CSRF_TOKEN'], 'restws');
  }
  return TRUE;
}

/**
 * Returns the URI used for the given resource.
 */
function restws_resource_uri($resource, $id) {

  // Avoid having the URLs aliased.
  return url($resource . '/' . $id, array(
    'absolute' => TRUE,
    'alias' => TRUE,
  ));
}

/**
 * Returns the format instance for a given MIME type.
 *
 * @param string $mime
 *   The MIME type, e.g. 'application/json' or 'application/xml'.
 *
 * @return bool|RestWSFormatInterface
 *   The format controller or FALSE if the format was not found.
 */
function restws_format_mimetype($mime) {
  foreach (restws_get_format_info() as $format_name => $info) {
    if ($info['mime type'] == $mime) {
      return restws_format($format_name);
    }
  }
  return FALSE;
}

/**
 * Implements hook_permission().
 */
function restws_permission() {
  $permissions = array();

  // Create service access permissions per resource type.
  foreach (restws_get_resource_info() as $type => $info) {
    $permissions['access resource ' . $type] = array(
      'title' => t('Access the resource %resource', array(
        '%resource' => $type,
      )),
    );
  }
  return $permissions;
}

/**
 * Implements hook_module_implements_alter().
 */
function restws_module_implements_alter(&$implementations, $hook) {

  // Make sure that restws runs last.
  // @todo remove entity_info_alter once https://drupal.org/node/1780646 is fixed.
  if ($hook == 'menu_alter' || $hook == 'entity_info_alter') {
    $group = $implementations['restws'];
    unset($implementations['restws']);
    $implementations['restws'] = $group;
  }
}

/**
 * Implements hook_menu().
 */
function restws_menu() {
  $items['restws/session/token'] = array(
    'page callback' => 'restws_session_token',
    // Only authenticated users are allowed to retrieve a session token.
    'access callback' => 'user_is_logged_in',
    'type' => MENU_CALLBACK,
  );
  return $items;
}

/**
 * Page callback: returns a session token for the currently active user.
 */
function restws_session_token() {
  drupal_add_http_header('Content-Type', 'text/plain');
  print drupal_get_token('restws');
  drupal_exit();
}

/**
 * Access callback for the node entity.
 *
 * Replacement for entity_metadata_no_hook_node_access() because it does not
 * work with the create operation.
 *
 * @todo Remove this once https://drupal.org/node/1780646 is fixed.
 *
 * @see restws_entity_info_alter()
 * @see entity_metadata_no_hook_node_access()
 */
function restws_entity_node_access($op, $node = NULL, $account = NULL) {

  // First deal with the case where a $node is provided.
  if (isset($node)) {

    // Ugly hack to handle field access, because entity_api does not distinguish
    // between 'create' and 'update' permissions for fields. This should rather
    // be fixed in EntityStructureWrapper::propertyAccess() (entity.wrapper.inc).
    if ($op == 'update' && empty($node->nid)) {
      $op = 'create';
    }
    if ($op == 'create') {
      if (isset($node->type)) {
        return node_access($op, $node->type, $account);
      }
      else {
        throw new EntityMalformedException('Permission to create a node was requested but no node type was given.');
      }
    }

    // If a non-default revision is given, incorporate revision access.
    $default_revision = node_load($node->nid);
    if ($node->vid !== $default_revision->vid) {
      return _node_revision_access($node, $op, $account);
    }
    else {
      return node_access($op, $node, $account);
    }
  }

  // No node is provided. Check for access to all nodes.
  if (user_access('bypass node access', $account)) {
    return TRUE;
  }
  if (!user_access('access content', $account)) {
    return FALSE;
  }
  if ($op == 'view' && node_access_view_all_nodes($account)) {
    return TRUE;
  }
  return FALSE;
}

/**
 * Implements hook_entity_info_alter().
 *
 * @todo Remove this once https://drupal.org/node/1780646 is fixed.
 */
function restws_entity_info_alter(&$info) {

  // In order for this to work we have to make sure we run after entity_api.
  // @see restws_module_implements_alter().
  $info['node']['access callback'] = 'restws_entity_node_access';
}

Functions

Namesort descending Description
restws_csrf_validation Ensures that a request with cookies has the required CSRF header set.
restws_entity_info_alter Implements hook_entity_info_alter().
restws_entity_node_access Access callback for the node entity.
restws_format Returns an instance of a format.
restws_format_mimetype Returns the format instance for a given MIME type.
restws_get_format_info Returns info about all defined formats.
restws_get_resource_info Returns info about all defined resources.
restws_handle_request Handles a request.
restws_menu Implements hook_menu().
restws_menu_alter Implements hook_menu_alter().
restws_module_implements_alter Implements hook_module_implements_alter().
restws_page_callback Menu page callback.
restws_permission Implements hook_permission().
restws_resource_controller Returns a instance of a resource controller.
restws_resource_uri Returns the URI used for the given resource.
restws_restws_format_info Implements hook_restws_format_info().
restws_restws_resource_info Implements hook_restws_resource_info().
restws_session_token Page callback: returns a session token for the currently active user.

Classes

Namesort descending Description
RestWSException An exception defining the HTTP error code and message.