Advanced Services Tutorial

PRATIK BOTHRA   |  24th January, 2014

Coming from a Rails background, which is almost built to cater to services and REST API, Drupal was always gonna be tough when it came to services. Our hands were tied, as we knew we had to use Drupal because of the the advantages it offered far outweighed the disadvantages, so we put on a brave face and marched on to tackle services. 

In a nutshell, Web Services make it possible for external applications to interact with our application (in this case our Drupal site). These interactions are most commonly reading, creating, updating, and deleting resources. REST is one of the most popular ways of making Web Services work and is the Drupal standard. Making use of these CRUD options is considered making it RESTful. Popular usage of a REST interface is a mobile application that needs to read and write data from your site's database. 

There are plenty of tutorials across the web to get you kickstarted on setting up services, and we found this to be relatively easy. One of the best tutorials, I found on the web which spurred me on the way was http://pingv.com/blog/an-introduction-drupal-7-restful-services.

The major problem, I will deal within this tutorial is creating a custom POST driven service, where we will tackle CSRF Token, Cookies etc. 

At the end of the article, I will also be sharing quick tips on working with UUID and integrating it with Services.

Custom POST Service, using cookies and CSRF Token

Submitting a custom form from an external site (or mobile application) to your site. You have services setup in for the site, but can't handle this custom post request. Here are a list of things which needs to happen:

  • Establish a session between mobile application and website so that you can retrieve a cookie which is required for authentication
  • Establish a token so your request can be authenticated, and it knows its not a cross site forgery request
  • Send the data to the endpoint at the site
  • Receive data at the endpoint of the site

Assuming external site is in Drupal, but it really could be anything. If it's a mobile app, it would obviously be something else....This is just so that you get a idea, and can follow. You can execute the code in another local drupal site using devel execute php so that you have an exact idea of what you need to achieve.

In a system config form, 4 variables are saved -: services_username, services_password, services_url, services_endpoint

a) Retreive session cookie

function retrieve_session_cookie() {
  $login_details = drupal_json_encode(array(
      "username"  => variable_get('services_username'),
      "password" => variable_get('services_password')
    )
  );
  $url = variable_get('services_url') . '/' .  variable_get('services_endpoint') . '/user/login';
  $options = array(
    'method' => 'POST',
    'headers' => array('Content-Type' => 'application/json'),
    'data' => $login_details ,
    );
  $result = drupal_json_decode(drupal_http_request($url,$options)->data);
  if ($result == 'Wrong username or password.') {
    drupal_set_message(t('Services Url/Password is not setup for fetching content'), 'error');
    return FALSE;
  } 
  else {
    $sessid = $result["sessid"];
    $session_name = $result["session_name"];
    $cookie = $session_name . '=' . $sessid;
    return $cookie;
  }
}
$cookie = retrieve_session_cookie();
if ($cookie == FALSE) return;

b) Retrieve CSRF token

$token = drupal_http_request(
  variable_get('services_url') . '/' . variable_get('services_endpoint') .'/session/token',
  array(
   'headers' => array('Cookie' => $cookie)
  )
)->data;

c) Sending data to the site

$params = array(
  "title" => $form_state["values"]["title"],
  "question" => $form_state["values"]["question"],
  "name" => $form_state["values"]["name"],
  "email" => $form_state["values"]["email"],
);
$args = array(
  'method' => 'POST',
  'headers' => array('Content-Type' => 'application/json','Cookie' => $cookie, 'X-CSRF-Token' => $token),
  'data' => drupal_json_encode($params),
  'timeout' => 300.0,
);
$result = drupal_http_request(variable_get('services_url') . '/' .  variable_get('services_endpoint') .'/create_question_form/', $args);
if (isset ($result->data)) {
  $result = drupal_json_decode($result->data);
  if ($result["response"] == "Ok" && $result["status"] == "200")
    drupal_set_message(t("Form was submitted successfully"), 'status', FALSE);
  else if ($result["response"] == "Failed")
    drupal_set_message(t("Form was not submitted successfully. Please try after some time."), 'error', FALSE);
}

d) Receiving data at the server

function hook_services_resources() {
  $items = array(
    'create_form' => array(
      'operations' => array(
        'create' => array(
          'help' => 'Creating Form',
          'callback' => '_create_form',
          'access callback' => 'user_access',
          'access arguments' => array('access content'),
          'access arguments append' => FALSE,
          'args' => array(
            array(
              'name' => 'question_details',
              'type' => 'array',
              'description' => 'Question Details',
              'source' => 'data',
              'optional' => FALSE,
            ),
          ),
        ),
      ),
    ),
   );
  return $items;
 }
 
function _create_form($question_details) {
  $new_node = new stdClass();
  $new_node->type = "question";
  $new_node->language = "en";
  $new_node->title = $question_details["title"];
  $new_node->field_question = array("und"=>array(0 => array("value" => $question_details["question"])));
  $new_node->field_user_email = array("und"=>array(0 => array("value" => $question_details["email"])));
  $new_node->field_user_name = array("und"=>array(0 => array("value" => $question_details["name"])));
  $new_node->status = 0;
  $new_node->uid = 1;
  node_save($new_node);
  if ($new_node->nid) {
    return array("response"=>"Ok","status"=>"200");
  }
  else
    return array("response"=>"Failed");
}

As promised few quick tips when integrating services with UUID. 

a) By default the services module, will give you an index of 20 terms when you do /service-endpoint/node. On this view, you only get basic details of the node, language, title, author, uid etc. What you don't get is custom fields like body, tag, category etc. To see the detailed node view, go /service-endpoint/node/nid where nid is the node nid. As promised few quick tips when integrating services with UUID. 

But the catch is this no longer works with UUID. Then on, you have to do /service-endpoint/node/uuid. This applies to everything, /service-endpoint/user/uuid

b) With UUID enabled, CREATE based methods will no longer be accepted. You instead have to do a PUT call on service-endpoint/node/uuid which will create the content if it doesn't exist. In the params, you need to pre-generate a UUID, and pass it in the PUT call, for content to be created with that UUID. Sucks, doesn't it? That's why we resorted to custom calls in our website, rather doing PUT for node creation

Bonus : UUID is not even unique!!! Yes, Universal Unique Identifier does not guarantee unique values. It's the biggest veil ever. If you see the install file in UUID module, uuid.install, you will see in table alterations it adds a 36 character field to many tables, but doesn't add the condition for it to be unique. The explanation is that the chances of having a 36 character being same in a table is 1/36! which is really rare, and its not worth the overhead to check if its unique. Interesting, isn't it?. :-)

 With Services, now being integrated into the Drupal Core for D8, it is only going to gain traction in popularity and downloads.. Quoting the founder of Drupal, Dries Buytaert, the future is a RESTFUL Drupal. 

Looking for a Drupal partner ?

We are drupal 8 ready