Integrating Drupal with the Box.com API

In a recent project, we needed a public place for Drupal users to upload, store and share a large number of files. The interface had to be simple and integration with Drupal was mandatory, allowing users to upload their files through the site to distinct, per user, locations, making them available to a large number of people in the organization.

There are multiple file sharing options available, but when it comes to enterprise solutions, we found box.com to be the most promising for our purposes. In case you haven’t heard about box.com, it is cloud based file store/sharing service designed with security and easy of use in mind. Although it’s RESTful API is very developer friendly, there no preexisting solution was available for integrating Drupal with v2 of the box.com API.

This blog post describes our custom solution for implementing a file upload system, connecting each user in Drupal with his box.com account.

Prerequisites:

An up to date Drupal installation with curl installed. One or more user accounts on box.com. A box.com API key as well as your client id and secret (obtained through http://developers.box.com/).

Our solution for Box.com integration:

Since each user needed to upload files to his personal box.com folder, we needed a place to store all the id’s of each user’s root folder. To accomplish this, we used a custom form on the user’s settings page that the user would fill out with his root folder’s id, along with a table, created table using hook_schema() in our module’s .install file, designed to hold this id, as well as the user’s access and refresh tokens from box.com.

Getting your first access token

To initiate any requests against box.com’s API you’re going to need an access token. These tokens are valid for 1 hour and can be generated by following box’s excellent in-depth oAuth tutorial here.

To use it with Drupal, you’ll have to:

  • Create a menu entry that will receive requests from box.com, e.g.
     $items['box_connect/authenticate/%'] = array(
                    'page callback' => 'box_connect_authenticate',
                    'page arguments' => array(2),
                    'type' => MENU_NORMAL_ITEM,
                    'access arguments' => array('manage box_connect settings'),
            );
    
  • Add your Drupal installation’s redirect url to box’s oAuth 2 parameters
  • When you manually visit the path from the menu entry it will issue a GET request to box.com with the parameters in the “First leg” section of the aforementioned box.com oAuth tutorial
  • Box.com will then issue a response towards your Drupal path from step 1 along with an authorization code
  • Your callback function, after receiving the authorization code, will extract it and issue a POST request to https://www.box.com/api/oauth2/token, with the required parameters and authorization code
  • It will extract from box.com’s response body the access and refresh tokens and store them in the table created during installation, along with 2 timestamps of time() augmented by 3600 and 1209600 each (1 hour and 14 days). Please note that the response body is in JSON format.

Here’s an example callback function:

function box_connect_authenticate() {
        global $user;
        global $base_url;
        $params = drupal_get_query_parameters();

        // First visit to url
        if (empty($params)) {

                $query = array(
                        'response_type' => 'code',
                        'client_id' => CLIENT_ID,
                        'redirect_uri' => $base_url.'/box_connect/authenticate/'.$user->name,
                );
                drupal_goto('https://www.box.com/api/oauth2/authorize', array('query' => $query));
        } else {

                if (isset($params['error'])) {

                        drupal_set_message('Could not authenticate with box.com. Reason: '.$params['error_description'], 'error');
                        watchdog('box_connect', 'Error while authenticating @username (@error): @error_description', array('@username' => $user->name, '@error' => $params['error'], '@error_description' => $params['error_description']), WATCHDOG_ERROR);
                } else {

                        $data = array(
                                'grant_type' => 'authorization_code',
                                'code' => $params['code'],
                                'client_id' => CLIENT_ID,
                                'client_secret' => CLIENT_SECRET,
                                'redirect_uri' => $base_url.'/box_connect/authenticate/'.$user->name,
                        );

                        $options = array(
                                'method' => 'POST',
                                'data' => http_build_query($data, '', '&'),
                                'headers' => array('Content-Type' => 'application/x-www-form-urlencoded'),
                        );

                        $response = drupal_http_request('https://www.box.com/api/oauth2/token', $options);

                        $data = drupal_json_decode($response->data);

                        if (empty($response->error)) {

                                unset($data['restricted_to']);
                                $data['generated'] = time();
                                $data['username'] = $user->name;

                                //Note: the $data['bearer'] field returned from the API uses a b instead of a capital B like the header has
                                db_merge('box_connect_tokens')
                                ->key(array('username' => $user->name))
                                ->fields($data)
                                ->execute();

                                drupal_set_message('Succesfully authenticated with box.com.');
                        } else {

                                drupal_set_message('Could not authenticate with box.com. Reason: '.$data['error_description'], 'error');
                                watchdog('box_connect', 'Error while fetching access token for @username (@error): @error_description', array('@username' => $user->name, '@error' => $data['error'], '@error_description' => $data['error_description']), WATCHDOG_ERROR);
                        }
                }
        }
}

You are now ready to make your first request towards box.com from Drupal!

Refreshing the tokens

As the the access and refresh tokens from box.com expire, we need to keep track of their expiration dates and refresh them before it is too late. This is why we stored those 2 timestamps in our table during the first tokens’ creation. Every access token is valid for 1 hour and similarly every refresh token is for 14 days or until a new access token is requested.

To overcome this limitation and facilitate making requests to box without user interaction, we are generating a new access token with every request that takes place more than 55 mins after the previous access token was generated. This way, we always have a fresh token to work with for the next hour which should be more than adequate. hook_cron() is also leveraged to update any old refresh tokens every 13 days, ensuring that we can always request a new token, regardless of when the last one was generated. In both cases we replace the previous entries in the database with the new tokens.
function box_connect_refresh_tokens() {

        $result = db_query("SELECT * FROM {box_connect_tokens}");

        while ($row = $result->fetchAssoc()) {

                if ($row['generated'] < strtotime("-13 days")) {

                        $data = array(
                                'refresh_token' => $row['refresh_token'],
                                'client_id' => CLIENT_ID,
                                'grant_type' => 'refresh_token',
                                'client_secret' => CLIENT_SECRET,
                        );

                        $options = array(
                                'method' => 'POST',
                                'data' => http_build_query($data, '', '&'),
                                'headers' => array('Content-Type' => 'application/x-www-form-urlencoded'),
                        );

                        $response = drupal_http_request('https://www.box.com/api/oauth2/token', $options);

                        $data = drupal_json_decode($response->data);

                        if (empty($response->error)) {

                                unset($data['restricted_to']);
                                $data['generated'] = time();

                                db_update('box_connect_tokens')
                                ->condition('refresh_token', $row['refresh_token'], '=')
                                ->fields($data)
                                ->execute();

                        } else {

                                watchdog('box_connect', 'Error while refreshing token @refresh_token (@error): @error_description', array('@refresh_token' => $row['refresh_token'], '@error' => $data['error'], '@error_description' => $data['error_description']), WATCHDOG_ERROR);
                                drupal_access_denied();
                        }

                }
        }
}

Using the API

When it comes to actually using the box.com API, we complied a list of all the actions that our Drupal installation will need to perform and built a series of functions for each one. These function, accessed through a PHP object, work using the typical REST verbs (get, put, post & delete) pointed towards the API’s endpoint that we’re interested in.

Each function takes a series of relevant arguments, e.g. an array of files to upload or a file/folder id and it’s destination folder id for moving documents in box.com, calls the access token provider function and then issues the request to box.com.
public function create_folder($folder_name, $parent_id) {

       $res = $this->post('folders', array('name' => $folder_name, 'parent' => array('id' => intval($parent_id))));

       if (empty($res)) {
              throw new Box_API_Client_Exception('API Error: Folder creation failed. Did not receive any data from box.com.');
       }
       return new Box_API_Client_Folder($this, $res);
}

To increase our module’s reliability, we’re utilizing a custom exception that, if thrown, is caught and passed to a custom function, logging the error to the watchdog, and sends us an email with the details. This way we can act on any potential issues immediately.