You are here

restws.entity.inc in RESTful Web Services 7.2

Same filename and directory in other branches
  1. 7 restws.entity.inc

RESTful web services module integration for entities.

File

restws.entity.inc
View source
<?php

/**
 * @file
 * RESTful web services module integration for entities.
 */

/**
 * Specifies CRUD and access methods for resources.
 */
interface RestWSResourceControllerInterface {

  /**
   * Returns the property info for the given resource.
   *
   * @return array
   *   An array structured as hook_entity_property_info() is structured for an
   *   entity type.
   */
  public function propertyInfo();

  /**
   * Returns a metadata wrapper for the resource with the given id.
   *
   * @return EntityStructureWrapper
   *   Metadata wrapper of the resource.
   */
  public function wrapper($id);

  /**
   * Create a new resource.
   *
   * @param array $values
   *   Array of values for properties of the resource, keyed by property
   *   name. At least for all required properties values have to be given.
   *
   * @return int|string
   *   The id of the newly created resource.
   */
  public function create(array $values);

  /**
   * Returns an existing resource.
   *
   * @param int|string $id
   *   The id of the resource that should be returned.
   *
   * @return
   *   The internal representation of the resource.
   */
  public function read($id);

  /**
   * Update an existing resource.
   *
   * @param int|string $id
   *   The id of the resource that should be updated.
   * @param array $values
   *   An array of values for the properties to be updated, keyed by property
   *   name.
   */
  public function update($id, array $values);

  /**
   * Delete an existing resource.
   *
   * @param int|string $id
   *   The id of the resource that should be deleted.
   */
  public function delete($id);

  /**
   * Determines access for a given operation and resource.
   *
   * @param string $op
   *   Either 'create', 'view' (= read), 'update' or 'delete'.
   * @param int|string $id
   *   The id of the resource.
   *
   * @see entity_access()
   */
  public function access($op, $id);

  /**
   * Returns the name of the resource.
   */
  public function resource();

}

/**
 * Specifies query methods for resources.
 */
interface RestWSQueryResourceControllerInterface extends RestWSResourceControllerInterface {

  /**
   * Query for a list of resources.
   *
   * @param array $filters
   *   A list of properties to query for, or an empty array if all resources
   *   should be counted.
   * @param array $meta_controls
   *   See restws_meta_controls()
   *
   * @return array
   *   An array containing the ids of the matching resources.
   */
  public function query($filters = array(), $meta_controls = array());

  /**
   * Returns the number of resources available with the given filters.
   *
   * @param array $filters
   *   A list of properties to query for, or an empty array if all resources
   *   should be returned.
   *
   * @return int
   *   The number of resources available.
   */
  public function count($filters = array());

  /**
   * Returns the limit for the current query.
   *
   * @param int $client_limit
   *   The limit specified in the meta controls or NULL if not set.
   *
   * @return int
   *   The limit of the current limit.
   */
  public function limit($client_limit = NULL);

}

/**
 * Controller for entity-bases resources.
 */
class RestWSEntityResourceController implements RestWSQueryResourceControllerInterface {
  protected $entityType, $entityInfo;
  public function __construct($name, $info) {
    $this->entityType = $name;
    $this->entityInfo = entity_get_info($name);
  }
  public function propertyInfo() {
    return entity_get_all_property_info($this->entityType);
  }
  public function wrapper($id) {
    return entity_metadata_wrapper($this->entityType, $id);
  }
  public function read($id) {
    return $this
      ->wrapper($id)
      ->value();
  }
  public function create(array $values) {

    // Make sure that bundle information is present on entities that have
    // bundles.
    $entity_info = entity_get_info($this->entityType);
    if (isset($entity_info['bundle keys'])) {
      foreach ($entity_info['bundle keys'] as $bundle_key) {
        if (!array_key_exists($bundle_key, $values)) {
          throw new RestWSException('Missing bundle: ' . $bundle_key, 406);
        }
      }
    }
    try {
      $wrapper = entity_property_values_create_entity($this->entityType, $values);

      // Get the ID and bundle property names.
      $entity_keys = array_intersect_key($entity_info['entity keys'], array(
        'id' => 1,
        'bundle' => 1,
      ));
      foreach (array_keys($values) as $name) {

        // Don't check access on entity keys for new entities. Otherwise,
        // property access checks will fail for, e.g., node type, which
        // requires the 'administer nodes' permission to set.
        // @see entity_metadata_node_entity_property_info().
        if (!in_array($name, $entity_keys)) {
          if (!$this
            ->checkPropertyAccess($wrapper, $name, $wrapper->{$name})) {
            throw new RestWSException(t('Not authorized to set property @p', array(
              '@p' => $name,
            )), 403);
          }
        }
      }
    } catch (EntityMetadataWrapperException $e) {
      throw new RestWSException($e
        ->getMessage(), 406);
    }
    $properties = $wrapper
      ->getPropertyInfo();
    $diff = array_diff_key($values, $properties);
    if (!empty($diff)) {
      throw new RestWSException('Unknown data properties: ' . implode(' ', array_keys($diff)) . '.', 406);
    }
    $this
      ->validateFields($wrapper);
    $wrapper
      ->save();
    return $wrapper
      ->getIdentifier();
  }
  public function update($id, array $values) {
    $wrapper = $this
      ->wrapper($id);
    $entity_info = $wrapper
      ->entityInfo();

    // Get the ID and bundle property names.
    $entity_keys = array_intersect_key($entity_info['entity keys'], array(
      'id' => 1,
      'bundle' => 1,
    ));
    try {
      foreach ($values as $name => $value) {
        if (in_array($name, $entity_keys)) {

          // We don't allow changing the entity ID or bundle.
          if ($wrapper->{$name}
            ->value() != $value) {
            throw new RestWSException('Unable to change ' . $name, 422);
          }
        }
        else {
          $wrapper->{$name}
            ->set($value);
          if (!$this
            ->checkPropertyAccess($wrapper, $name, $wrapper->{$name})) {
            throw new RestWSException(t('Not authorized to set property @p', array(
              '@p' => $name,
            )), 403);
          }
        }
      }
    } catch (EntityMetadataWrapperException $e) {
      throw new RestWSException($e
        ->getMessage(), 406);
    }
    $this
      ->validateFields($wrapper);
    $wrapper
      ->save();
  }
  public function delete($id) {
    entity_delete($this->entityType, $id);
  }

  /**
   * Implements RestWSQueryResourceControllerInterface::query().
   */
  public function query($filters = array(), $meta_controls = array()) {
    $limit = variable_get('restws_query_max_limit', 100);
    $offset = 0;
    $query = new EntityFieldQuery();
    $query
      ->entityCondition('entity_type', $this->entityType);
    foreach ($filters as $filter => $value) {
      $this
        ->propertyQueryOperation($query, 'Condition', $filter, $value);
    }
    $rest_controls = restws_meta_controls();
    foreach ($meta_controls as $control_name => $value) {
      switch ($control_name) {
        case $rest_controls['sort']:
          if (isset($meta_controls[$rest_controls['direction']]) && strtolower($meta_controls[$rest_controls['direction']]) == 'desc') {
            $direction = 'DESC';
          }
          else {
            $direction = 'ASC';
          }
          $this
            ->propertyQueryOperation($query, 'OrderBy', $value, $direction);
          break;
        case $rest_controls['limit']:
          $limit = $this
            ->limit($value);
          break;
        case $rest_controls['page']:
          $offset = $value > 0 ? $value : $offset;
          break;
      }
    }

    // Calculate the offset.
    $offset *= $limit;
    $query
      ->range($offset, $limit);
    $this
      ->nodeAccess($query);

    // Catch any errors, like wrong keywords or properties.
    try {
      $query_result = $query
        ->execute();
    } catch (PDOException $exception) {
      throw new RestWSException('Query failed.', 400);
    }
    $query_result = isset($query_result[$this->entityType]) ? $query_result[$this->entityType] : array();
    $result = array_keys($query_result);
    return $result;
  }

  /**
   * Implements RestWSQueryResourceControllerInterface::count().
   */
  public function count($filters = array()) {
    $query = new EntityFieldQuery();
    $query
      ->entityCondition('entity_type', $this->entityType);
    foreach ($filters as $filter => $value) {
      $this
        ->propertyQueryOperation($query, 'Condition', $filter, $value);
    }
    $query
      ->count();
    $this
      ->nodeAccess($query);
    return $query
      ->execute();
  }

  /**
   * Helper function to respect node permissions while querying.
   *
   * @param EntityFieldQuery $query
   *   The query object.
   */
  protected function nodeAccess(EntityFieldQuery $query) {

    // Respect node access and filter out unpublished nodes if user lacks
    // the right permission.
    if ($this
      ->resource() == 'node') {
      $query
        ->addTag('node_access');
      if (!user_access('bypass node access')) {
        $this
          ->propertyQueryOperation($query, 'Condition', 'status', 1);
      }
    }
  }

  /**
   * Implements RestWSQueryResourceControllerInterface::limit().
   */
  public function limit($client_limit = NULL) {
    $limit = variable_get('restws_query_max_limit', 100);

    // Only allow user provided limits smaller than the system hard limit.
    if (!empty($client_limit) && $client_limit < $limit) {
      return $client_limit;
    }
    else {
      return $limit;
    }
  }
  public function access($op, $id) {
    return entity_access($op, $this->entityType, isset($id) ? $this
      ->wrapper($id)
      ->value() : NULL);
  }
  public function resource() {
    return $this->entityType;
  }

  /**
   * Helper function which takes care of distinguishing between fields and
   * entity properties and executes the right EntityFieldQuery function for it.
   *
   * @param EntityFieldQuery $query
   *   The EntityFieldQuery pointer which should be used.
   *
   * @param string $operation
   *   The general function name, without the words 'property' or 'field'.
   *
   * @param string $property
   *   The property or field which should be used.
   *
   * @param string|array $value
   *   The value for the function.
   */
  protected function propertyQueryOperation(EntityFieldQuery $query, $operation, $property, $value) {
    $properties = $this
      ->propertyInfo();

    // Check property access.
    if (!empty($properties[$property]['access callback'])) {
      if (!call_user_func($properties[$property]['access callback'], 'view', $property, NULL, NULL, $this
        ->resource())) {
        throw new RestWSException(t('Not authorized to query property @p', array(
          '@p' => $property,
        )), 403);
      }
    }

    // If field is not set, then the filter is a property and we can extract
    // the schema field from the property array.
    if (empty($properties[$property]['field'])) {
      $column = $properties[$property]['schema field'];
      $operation = 'property' . $operation;
      $query
        ->{$operation}($column, $value);
    }
    else {

      // For fields we need the field info to get the right column for the
      // query.
      $field_info = field_info_field($property);
      $operation = 'field' . $operation;
      if (is_array($value)) {

        // Specific column filters are given, so add a query condition for each
        // one of them.
        foreach ($value as $column => $val) {
          $query
            ->{$operation}($field_info, $column, $val);
        }
      }
      else {

        // Just pick the first field column for the operation.
        $columns = array_keys($field_info['columns']);
        $column = $columns[0];
        $query
          ->{$operation}($field_info, $column, $value);
      }
    }
  }

  /**
   * Helper method to check access on a property.
   *
   * @todo Remove this once Entity API properly handles text format access.
   *
   * @param EntityMetadataWrapper $entity
   *   The parent entity.
   * @param string $property_name
   *   The property name on the entity.
   * @param EntityMetadataWrapper $property
   *   The property whose access is to be checked.
   *
   * @return bool
   *   TRUE if the current user has access to set the property, FALSE otherwise.
   */
  protected function checkPropertyAccess($entity, $property_name, $property) {
    global $user;

    // Special case node author: we allow access if set to the current user.
    if ($entity
      ->type() == 'node' && $property_name == 'author' && $property
      ->raw() == $GLOBALS['user']->uid) {
      return TRUE;
    }
    elseif ($property
      ->type() == 'text_formatted' && $property->format
      ->value()) {
      $format = (object) array(
        'format' => $property->format
          ->value(),
      );
      if (!filter_access($format)) {
        return FALSE;
      }
    }

    // We don't want the property wrapper to check access again on the parent
    // entity so we directly check access for the property. That way only the
    // pure property/field access is taken into account.
    $info = $property
      ->info();
    if (!empty($info['access callback'])) {
      global $user;
      $data = $entity
        ->value();
      return call_user_func($info['access callback'], 'edit', $property_name, $data, $user, $entity
        ->type());
    }
    elseif (isset($info['setter permission'])) {
      return user_access($info['setter permission']);
    }
    return TRUE;
  }

  /**
   * Validates an entity's fields before they are saved.
   *
   * @param EntityDrupalWrapper $wrapper
   *   A metadata wrapper for the entity.
   *
   * @throws RestWSException
   */
  protected function validateFields($wrapper) {
    try {
      field_attach_validate($wrapper
        ->type(), $wrapper
        ->value());
    } catch (FieldValidationException $e) {
      throw new RestWSException($e
        ->getMessage(), 422);
    }
  }

}

Classes

Namesort descending Description
RestWSEntityResourceController Controller for entity-bases resources.

Interfaces

Namesort descending Description
RestWSQueryResourceControllerInterface Specifies query methods for resources.
RestWSResourceControllerInterface Specifies CRUD and access methods for resources.