Blog : Example Drive App: DrEdit for PHP

Example Drive App: DrEdit for PHP


Get Started Documentation Examples Support Blog GDG Live
DrEdit PHP is a sample Google Drive app for the Google Drive platform written in PHP. It is a text editor capable of editing files with the MIME types text/* and extensions such as .txt, .html, .csv, .xml, etc. that are stored in a user's Google Drive. This article describes the complete application to help you in your integrations.

Important: You can download the complete source code from the DrEdit repository.
Setting up this sample application
Features
Authorization
Setting up the client ID, client secret, and other OAuth 2.0 parameters
Authorizing a code passed from the Google Drive UI
Loading credentials from the session
Failing to authorize
Putting together the pieces: getting a complete set of credentials for every request
Handling HTTP requests
Requests to the user interface
Loading the Drive state
Rendering the user interface
Requests to the Files service
Fetching file metadata and content from Google Drive
Saving files
Saving new files
Updating files
Responding with the file ID
Personalizing the user interface
About service
User service
Additional resources
Setting up this sample application

Setting up DrEdit requires performing some configuration. Follow the instructions in the repository's README to set up the application.

Features

The app uses these two types of endpoints:

User interface at /
The user interface is rendered including HTML, JavaScript, and CSS.

AJAX service to communicate with Drive at /svc, /about and /user
Drive API requests are made in response to GET, PUT, and POST requests and return JSON data.

DrEdit implements two Google Drive use cases:

Create New file from the Google Drive UI
Open With file from the Google Drive UI
The flow for both use cases is similar. A user is redirected to DrEdit after selecting DrEdit PHP from the Create menu or Open with context menu of a file with a registered MIME type or registered file extension.

This redirect takes place with two important parts of data as query parameters:

An authorization code, capable of being exchanged for an access token to access the user's data.
A state object describing the user's action, i.e. which file(s) and which action (open, create) to perform.
DrEdit responds by performing the required action on the passed files using the authorization credentials. The authorization credentials are stored in a session for future use by Ajax requests from the user interface and are also persisted to a server-side database.

Authorization

Important: For additional information about Authorizing Drive apps, please see the authorization documentation
DrEdit requires an authorized API client to make calls to the Drive API. The first step is to obtain the user's credentials to authorize the client.

These credentials can be loaded from:

A code passed from the Google Drive UI
The user's session
The choice of credential source is determined by the action that DrEdit is performing.

In cases where the user has been redirected from the Drive user interface, an authorization code is always provided and DrEdit always uses it, potentially in combination with a refresh token stored in a server-side database.

Google Drive applications only receive a long-lived refresh token during the very first exchange of an authorization code. This refresh token is used to request new access tokens after a short-lived access token expires, and it must be stored in the application's database to be retrieved every time the user returns to the app.

In cases where the user is making Ajax calls from the user interface, the OAuth credentials established when loading the user interface are retrieved from a server-side session.

Setting up the client ID, client secret, and other OAuth 2.0 parameters

Google Drive applications need these three values for authorization:

Client ID
Client secret
List of valid redirect URIs
The CLIENT_ID and CLIENT_SECRET for an application are created when an application is registered in the Google Developers Console.

To find your project's client ID and client secret, do the following:

Go to the Google Developers Console.
Select a project, or create a new one.
In the sidebar on the left, expand APIs & auth. Next, click APIs. In the list of APIs, make sure the status is ON for the Drive API.
In the sidebar on the left, select Credentials.
If you haven't done so already, create your project's OAuth 2.0 credentials by clicking Create new Client ID, and providing the information needed to create the credentials.
Look for the Client ID and Client secret in the table associated with each of your credentials.
Note that not all types of credentials use both a client ID and client secret and won't be listed in the table if they are not used.

In DrEdit for PHP, these values are stored in the credentials.php file.

Authorizing a code passed from the Google Drive UI

After a user chooses DrEdit to open or create a file, the user's browser is redirected to DrEdit's registered redirect URI (set in the Google APIs Console), with an OAuth 2.0 authorization code attached as the code query parameter in the URI. The given authorization code is scoped specifically to open the single file (Open With) or to create any file (Create New), and must be exchanged for an access token with the OAuth 2.0 web server flow.

Once an access token is obtained, it is used to fetch the user profile information from the Userinfo service. The OAuth 2.0 credentials are then stored in the session for later use by the AJAX endpoints.

$client = new Google_Client();
$client->setClientId(CLIENT_ID);
$client->setClientSecret(CLIENT_SECRET);
$client->setRedirectUri(REDIRECT_URI);
$client->setScopes(array(
  'https://www.googleapis.com/auth/drive',
  'email',
  'profile'));
$client->setUseObjects(true);

//...

// if the request is a callback from Google, exchange
// token to retrieve access and refresh token
if ($code = $app->request()->get('code')) {
  // handle code, retrieve credentials.
  $client->authenticate();
  $tokens = $client->getAccessToken();
  set_user($tokens);
}
Once tokens are retrieved, the user is redirected to the home page.

Loading credentials from the session

At the beginning of each request, DrEdit attempts to load credentials from the current user's session. On each request, we're going to check whether there is a user in the seesion and set the tokens stored in the session to authenticate the client.

// if there is an existing session, set the access token
if ($user = get_user()) {
  $client->setAccessToken($user->tokens);
}
Failing to authorize

There are a number of points at which the authorization process can fail for AJAX endpoints. If a request fails due to an authorization problem, a 401 Unauthorized response is returned. If the operation was an Open action, the user is redirected to the OAuth 2.0 authorization endpoint URL configured in credentials.php.

dredit.Page.prototype.OpenError = function(xhr, text_status, error) {
  console.log(error);
  window.location.href = dredit.OAUTH_AUTH_URL;
}
However, if the error occurred during a save, the error is simply logged to the console and displayed instead of redirecting the user. This allows the user to copy/paste the document elsewhere.

Putting together the pieces: getting a complete set of credentials for every request

Once an access token has been retrieved, it is used to authenticate the Google_DriveService instance that is used for making API requests.

$client = new Google_Client();
$client->setClientId(CLIENT_ID);
$client->setClientSecret(CLIENT_SECRET);
$client->setRedirectUri(REDIRECT_URI);
$client->setScopes(array(
  'https://www.googleapis.com/auth/drive',
  'email',
  'profile'));
$client->setUseObjects(true);

// if there is an existing session, set the access token
if ($user = get_user()) {
  $client->setAccessToken($user->tokens);
}

// initialize the drive service with the client.
$service = new Google_DriveService($client);
Handling HTTP requests

This section describes how DrEdit handles HTTP requests for operations such as creating and opening new files and rendering the user interface.

Requests to the user interface

The user interface is provided as a single HTML template which is used for both opening (Open With) and creating new files. Once loaded, the user interface loads any data required.

The text editing component uses the Ace Editor, a text-editing component with syntax highlighting and configurable key bindings. It is used in DrEdit as a form field with advanced editing features.

Users may arrive at the / endpoint from two separate sources:

Drive User interface, along with Drive State
By directly accessing the URL, with no Drive State
Loading the Drive state

Before loading the editor, / parses the Drive state. The state parameter is a JSON string containing two important properties:

The action to be performed, create or open in the action field
The file IDs (if any) to perform the action on in the ids field
The parent folder ID to place created files in (for a create action)
DrEdit uses this state to determine how it should behave. If file ids are provided they are loaded immediately after the user interface is loaded.

Note: If no Drive state is provided, DrEdit performs exactly as it would if the action was create with no file ids, i.e., it creates a new file.
Rendering the user interface

The user interface is rendered in a response to GET / requests. The Drive state is loaded and the template is rendered. There is no communication with the Drive API at this stage, but credentials are checked.

if ($user) { // if there is a user in the session
  $app->render('index.html');
} else {
  // redirect to the auth page
  $app->redirect($client->createAuthUrl());
}
Requests to the Files service

The /svc endpoint allows three HTTP methods.

GET: Fetches file metadata and content from Google Drive.
POST: Creates new files in Google Drive.
PUT: Updates existing files in Google Drive.
Data is returned as JSON with the application/json content-type and when data is required (in POST and PUT requests) it is provided from the user interface as JSON with the application/json content-type.

Fetching file metadata and content from Google Drive

The file is retrieved in two steps. First, the metadata is fetched using the files.get method, passing the file_id that was sent in the initial redirect. Second, the file content is fetched by making a simple authorized GET request to the file's downloadUrl. Both metadata and file content are serialized into JSON and returned to the user.

/**
 * Gets the metadata and contents for the given file_id.
 */
$app->get('/svc', function() use ($app, $client, $service) {
  checkUserAuthentication($app);
  checkRequiredQueryParams($app, array('file_id'));
  $fileId = $app->request()->get('file_id');
  try {
  // Retrieve metadata for the file specified by $fileId.
  $file = $service->files->get($fileId);

  // Get the contents of the file.
  $request = new Google_HttpRequest($file->downloadUrl);
  $response = $client->getIo()->authenticatedRequest($request);
  $file->content = $response->getResponseBody();

  renderJson($app, $file);
  } catch (Exception $ex) {
  renderEx($app, $ex);
  }
});
The file content is retrieved using the $client->getIo()->authenticatedRequest helper method of the PHP client library.

Saving files

When the user clicks the "Save" button" from the DrEdit user interface, an AJAX request is made from the user interface to the server. This request is either:

POST: indicating that a new file is to be created.
PUT: indicating that an existing file is to be updated, or
These methods match standard REST semantics, and are convenient because both can be represented as a single URL which handles different HTTP methods.

The data is sent as JSON containing the following fields:

title: Title of the file.
description: Description of the file.
content: Content of the file, i.e., the content from the text editor.
file_id: File ID of the file, or an empty string if this is a new file.
parentId: Parent folder ID of the file.
parents: Collection containing the ID of the folder to save new files to.
labels: Map containing the boolean indicating if the file has been starred.
Both PUT and POST methods return a JSON response that contains the file ID of the saved or created file. The file ID is stored in the user interface, and is sent along with future requests.

Saving new files

The POST method is used to save newly created files. A files.insert call is made to create the new file and set the metadata.

/**
 * Creates a new file with the metadata and contents
 * in the request body. Requires login.
 */
$app->post('/svc', function() use ($app, $client, $service) {
  checkUserAuthentication($app);
  $inputFile = json_decode($app->request()->getBody());
  try {
  $file = new Google_DriveFile();
  $file->setTitle($inputFile->title);
  $file->setDescription($inputFile->description);
  $file->setMimeType($mimeType);
  // Set the parent folder.
  if ($inputFile->parentId != null) {
  $parentsCollectionData = new Google_DriveFileParentsCollection();
  $parentsCollectionData->setId($inputFile->parentId);
  $file->setParentsCollection(array($parentsCollectionData));
  }
  $createdFile = $service->files->insert($file, array(
  'data' => $inputFile->content,
  'mimeType' => $mimeType,
  ));
  renderJson($app, $createdFile->id);
  } catch (Exception $ex) {
  renderEx($app, $ex);
  }
});
Updating files

The process for updating a file is similar to that of creating one. File updates are made using an HTTP PUT instead of an HTTP POST.

Unlike when creating files, the files.update method of the API is used, and this requires the file ID to be passed as the file_id parameter.

/**
 * Modifies an existing file given in the request body and responds
 * with the file id. Requires login.
 */
$app->put('/svc', function() use ($app, $client, $service) {
  checkUserAuthentication($app);
  $inputFile = json_decode($app->request()->getBody());
  $fileId = $inputFile->id;
  try {
  // Retrieve metadata for the file specified by $fileId and modify it with
  // the new changes.
  $file = $service->files->get($fileId);
  $file->setTitle($inputFile->title);
  $file->setDescription($inputFile->description);
  $file->getLabels()->setStarred($inputFile->labels->starred);

  // Update the existing file.
  $output = $service->files->update(
  $fileId, $file, array('data' => $inputFile->content)
  );

  renderJson($app, $output->id);
  } catch (Exception $ex) {
  renderEx($app, $ex);
  }
});
Responding with the file ID

Both creating new files and updating existing files returns a JSON response containing the file ID of the file that has been created or modified. The user interface can use this file ID to ensure that future saves are to the same file, and that a new file is not created each time.

Personalizing the user interface

Additional services are provided to customize the user interface for the authorized user. This has the benefit of providing a familiar and personalized experience for the user.

About service

The /about endpoint allows one HTTP method.

GET: Fetches information about the authorized user and their account.
This service returns information about the currently authorized user and settings for their account. This service fetches and returns the JSON resource representation described in about.get.

/**
 * Gets the information about the current user along with Drive API settings.
 * Requires login.
 */
$app->get('/about', function() use ($app, $client, $service) {
  checkUserAuthentication($app);
  try {
  $about = $service->about->get();
  renderJson($app, $about);
  } catch (Exception $ex) {
  renderEx($app, $ex);
  }
});
User service

The /user endpoint allows one HTTP method.

GET: Fetches information about the user
This service returns information about the user that was cached during authorization.

/**
 * Gets user profile. Requires login.
 */
$app->get('/user', function() use ($app, $client, $service) {
  checkUserAuthentication($app);
  $userinfoService = new Google_OAuth2Service($client);
  try {
  $user = $userinfoService->userinfo->get();
  renderJson($app, $user);
  } catch (Exception $ex) {
  renderEx($app, $ex);
  }
});
Additional resources

Integrate with the Drive UI
API Reference