You are here

deploy_services_client.client.inc in Deploy Services Client 7

Defines a Services client class which communicates with Deployment endpoints.

File

deploy_services_client.client.inc
View source
<?php

/**
 * @file
 * Defines a Services client class which communicates with Deployment endpoints.
 */

/**
 * Class which defines a Services client based on a Deployment endpoint.
 *
 * This class is currently hardcoded to use a REST-based JSON communication
 * method, and it makes other assumptions as well (e.g., a session-based
 * authentication method). A significant part of its code is borrowed from the
 * Deployment module, but rewritten so that it can be extended to make other
 * kinds of requests (e.g., GET and DELETE) and accessed outside the execution
 * of a deployment plan.
 *
 * @see DeployAuthenticatorSession::deploy()
 * @see DeployServiceRestJSON::deploy()
 * @see DeployServiceRest::httpRequest()
 */
class DeployServicesClient {

  /**
   * The Deployment endpoint to which Services requests will be made.
   */
  protected $endpoint;

  /**
   * The login cookie for session-based authentication.
   */
  protected $loginCookie;

  /**
   * The CSRF token for session-based authentication.
   */
  protected $tokenResponse;

  /**
   * Initializes the DeployServicesClient class.
   *
   * @param DeployEndpoint $endpoint
   *   The Deployment endpoint object to which Services requests will be made.
   */
  public function __construct(DeployEndpoint $endpoint) {
    $this->endpoint = $endpoint;
  }

  /**
   * Logs in to the deployment endpoint via session authentication.
   *
   * @return
   *   TRUE on successful login, or FALSE if the login was skipped due to an
   *   absence of login credentials.
   *
   * @throws DeployAuthenticationException
   *
   * @see DeployAuthenticatorSession::deploy()
   */
  public function login() {

    // Make sure we are starting a new session.
    $this
      ->logout();
    if (!empty($this->endpoint->authenticator_config['username'])) {
      $login_url = $this->endpoint->service_config['url'] . '/user/login';
      $options = array(
        'method' => 'POST',
        'headers' => array(
          'Content-Type' => 'application/json',
        ),
        'data' => drupal_json_encode(array(
          'username' => $this->endpoint->authenticator_config['username'],
          'password' => $this->endpoint->authenticator_config['password'],
        )),
      );
      if ($this
        ->debugEnabled()) {
        watchdog('deploy_services_client', 'Authentication request: %url <pre>@options</pre>', array(
          '%url' => $login_url,
          '@options' => print_r($options, TRUE),
        ), WATCHDOG_DEBUG);
      }

      // Perform the authentication request.
      $response = drupal_http_request($login_url, $options);
      if ($this
        ->debugEnabled()) {
        watchdog('deploy_services_client', 'Authentication response: <pre>@response</pre>', array(
          '@response' => print_r($response, TRUE),
        ), WATCHDOG_DEBUG);
      }
      if (isset($response->error) || !in_array($response->code, array(
        200,
        304,
      ))) {
        throw new DeployAuthenticationException(t('Authentication error: @code @error', array(
          '@code' => $response->code,
          '@error' => $response->error,
        )));
      }

      // Store the login cookie and CSRF token so we can use them elsewhere.
      $response_data = drupal_json_decode($response->data);
      if (!empty($response_data['session_name']) && !empty($response_data['sessid'])) {
        $this->endpoint->service_config['headers']['Cookie'] = $response_data['session_name'] . "=" . $response_data['sessid'] . ";";

        // We have to get a CSFR Token from the server and pass along with
        // each services call (introduced in Services 7.x-3.4
        // as per SA-CONTRIB-2013-051; https://drupal.org/node/2012982).
        $parts = parse_url($this->endpoint->service_config['url']);
        $port = '';
        $user = '';
        $pass = '';
        if (isset($parts['port'])) {
          $port = ':' . $parts['port'];
        }
        if (isset($parts['user'])) {
          $user = $parts['user'];
        }
        if (isset($parts['pass'])) {
          $pass = $parts['pass'];
        }
        $token_url = $parts['scheme'] . '://' . ($user ? $user . ':' . $pass . '@' : '') . $parts['host'] . $port . '/services/session/token';
        $token_response = drupal_http_request($token_url, array(
          'method' => 'GET',
          'headers' => $this->endpoint->service_config['headers'],
        ));
        if (isset($token_response->error) || !in_array($token_response->code, array(
          200,
          304,
        ))) {
          throw new DeployAuthenticationException(t('Authentication error: @code @error', array(
            '@code' => $token_response->code,
            '@error' => t('Failed to retrieve CSRF token.'),
          )));
        }
        if ($this
          ->debugEnabled()) {
          watchdog('deploy_services_client', 'Session CSRF Token response: @response', array(
            '@response' => print_r($token_response, TRUE),
          ), WATCHDOG_DEBUG);
        }
        $this->loginCookie = $response->headers['set-cookie'];
        $this->tokenResponse = trim($token_response->data);
        $this->endpoint->service_config['headers']['X-CSRF-Token'] = $this->tokenResponse;
        return TRUE;
      }
      else {
        throw new DeployAuthenticationException(t("No session was returned from the authentication request."));
      }
    }
    else {
      watchdog('deploy_services_client', 'Deployment authentication was done without user credentials.', array(), WATCHDOG_WARNING);
      return FALSE;
    }
  }

  /**
   * Logs out of the deployment endpoint.
   *
   * @return
   *   TRUE on successful logout, or FALSE if there was no active session and
   *   therefore no logout could be performed.
   *
   * @see DeployAuthenticatorSession::deploy()
   */
  public function logout() {
    if ($this->loginCookie) {
      $logout_url = $this->endpoint->service_config['url'] . '/user/logout';
      $options = array(
        'method' => 'POST',
        'headers' => array(
          'Cookie' => $this->loginCookie,
          'X-CSRF-Token' => $this->tokenResponse,
        ),
      );
      $response = drupal_http_request($logout_url, $options);
      if ($this
        ->debugEnabled()) {
        watchdog('deploy_services_client', 'Logout response: <pre>@response</pre>', array(
          '@response' => print_r($response, TRUE),
        ), WATCHDOG_DEBUG);
      }
      $this->loginCookie = NULL;
      return TRUE;
    }
    else {
      return FALSE;
    }
  }

  /**
   * Performs a GET request to retrieve data about an entity from the endpoint.
   *
   * @param EntityMetadataWrapper $entity
   *   The entity object to retrieve data about. Construct this by calling
   *   entity_metadata_wrapper() on a normal Drupal entity.
   *
   * @return
   *   The response data from the endpoint. This will be FALSE if the entity is
   *   not found on the endpoint site.
   *
   * @throws DeployServiceException
   */
  public function get(EntityMetadataWrapper $entity) {
    $response = $this
      ->entityRequest($entity, 'GET');
    return empty($response->data) ? FALSE : drupal_json_decode($response->data);
  }

  /**
   * Performs a DELETE request to delete an entity on the endpoint.
   *
   * @param EntityMetadataWrapper $entity
   *   The entity object to delete. Construct this by calling
   *   entity_metadata_wrapper() on a normal Drupal entity.
   *
   * @throws DeployServiceException
   */
  public function delete(EntityMetadataWrapper $entity) {

    // @todo: We'd like to return something here, but there isn't much point
    //   because it seems that $reponse->data is always empty (regardless of
    //   whether the delete request was successful or not).
    $this
      ->entityRequest($entity, 'DELETE');
  }

  /**
   * Performs a request to unpublish an entity on the endpoint.
   *
   * This only works on entities that have a 'status' property indicating the
   * publication status (e.g., nodes). Using this method on other entities can
   * cause problems, and therefore an exception will be thrown in the case
   * where an entity without a 'status' property is provided.
   *
   * @param EntityMetadataWrapper $entity
   *   The entity object to unpublish. Construct this by calling
   *   entity_metadata_wrapper() on a normal Drupal entity.
   *
   * @throws DeployServiceException
   */
  public function unpublish(EntityMetadataWrapper $entity) {

    // Replace IDs with UUIDs before sending. (This also has the effect of
    // getting a fresh copy of the entity, which may not match the provided
    // object. It's not clear if that's what we want, but it shouldn't be too
    // bad since the entity is being unpublished on the endpoint anyway.)
    $entities_to_send = entity_uuid_load($entity
      ->type(), array(
      $entity->uuid
        ->value(),
    ));
    $entity_to_send = reset($entities_to_send);

    // Bail out if we don't have a status property.
    if (!isset($entity_to_send->status)) {
      throw new DeployServiceException(t('Entity of type "@type" with UUID "@uuid" did not have a "status" property and could not be unpublished.', array(
        '@type' => $entity
          ->type(),
        '@uuid' => $entity->uuid
          ->value(),
      )));
    }

    // Set the status to unpublished and send the request.
    // @todo: We'd like to return something here to indicate success or
    //   failure, but it's not clear what to return.
    $entity_to_send->status = 0;
    $json_data = drupal_json_encode($entity_to_send);
    $this
      ->entityRequest($entity, 'PUT', $json_data);
  }

  /**
   * Performs a request to the endpoint to perform an action on an entity.
   *
   * @param EntityMetadataWrapper $entity
   *   The entity object to be used in the request. Construct this by calling
   *   entity_metadata_wrapper() on a normal Drupal entity.
   * @param $method
   *   The HTTP method (e.g., 'GET' or 'DELETE') for the request.
   * @param $data
   *   (Optional) A string containing the data to send (e.g., for a 'PUT'
   *   request). If not provided, no data will be sent.
   *
   * @return
   *   The drupal_http_request() response object returned from the request.
   *
   * @throws DeployServiceException
   *
   * @see drupal_http_request()
   */
  public function entityRequest(EntityMetadataWrapper $entity, $method, $data = NULL) {
    $path = $entity
      ->type() . '/' . $entity->uuid
      ->value() . '.json';
    return $this
      ->request($path, $method, $data);
  }

  /**
   * Performs a request to a REST path on the endpoint.
   *
   * @param $path
   *   The path (relative to the endpoint URL) to request.
   * @param $method
   *   The HTTP method (e.g., 'GET' or 'DELETE') for the request.
   * @param $data
   *   (Optional) A string containing the data to send (e.g., for a 'PUT'
   *   request). If not provided, no data will be sent.
   *
   * @return
   *   The drupal_http_request() response object returned from the request.
   *
   * @throws DeployServiceException
   *
   * @see drupal_http_request()
   * @see DeployServiceRestJSON::deploy()
   * @see DeployServiceRest::httpRequest()
   */
  public function request($path, $method, $data = NULL) {
    $url = $this->endpoint->service_config['url'] . '/' . $path;
    $headers['Content-Type'] = 'application/json';

    // Make the request as an authenticated user if we are already logged in to
    // the endpoint.
    if ($this->loginCookie) {
      $headers['Cookie'] = $this->loginCookie;
      $headers['X-CSRF-Token'] = $this->tokenResponse;
    }
    $options = array(
      'method' => $method,
      'headers' => $headers,
      'data' => $data,
    );
    if ($this
      ->debugEnabled()) {
      watchdog('deploy_services_client', 'Service request: %url <pre>@options</pre>', array(
        '%url' => $url,
        '@options' => print_r($options, TRUE),
      ), WATCHDOG_DEBUG);
    }

    // Perform the request.
    $response = drupal_http_request($url, $options);
    if ($this
      ->debugEnabled()) {
      watchdog('deploy_services_client', 'Service response: <pre>@response</pre>', array(
        '@response' => print_r($response, TRUE),
      ), WATCHDOG_DEBUG);
    }
    if (isset($response->error) || !in_array($response->code, array(
      200,
      304,
    ))) {
      throw new DeployServiceException(t('Service error: @code @error', array(
        '@code' => $response->code,
        '@error' => $response->error,
      )));
    }
    return $response;
  }

  /**
   * Determines if the endpoint has debugging turned on.
   *
   * @return
   *   TRUE if debugging is enabled, or FALSE if it isn't.
   */
  protected function debugEnabled() {
    return !empty($this->endpoint->debug);
  }

}

Classes

Namesort descending Description
DeployServicesClient Class which defines a Services client based on a Deployment endpoint.