JavaScript plugins, like the comment plugin from Chapter 3, allow you to move beyond the official social plugins, but the depth of integration that they provide is limited. As you move on to deeper integration, some processing must be done server-side. To accommodate this, further examples are written in PHP. You will need to an environment capable of running PHP and a SQLite database.
The Google+ platform is not limited to PHP. You can find client libraries and starter projects for many popular languages including Java, Python, .NET, and Ruby. If Google does not supply an official client library for your language of choice, you may still use the REST APIs directly.
Baking Disasters is fun to publish as a static HTML blog, but as time passes visitors have started to express a desire to contribute their baking experiences. Being a social baker with a streak of PHP ability, this seems like the perfect opportunity to transform Baking Disasters into a social web application where everyone can contribute.
After one night of frenzied PHP hacking, Baking Disasters 2.0, as pictured in Figure 4-1, was born. It consists of an administration page for managing recipes and a public page for each recipe where visitors can publish their hilarious baking disasters.
Figure 4-1. The screens of Baking Disasters 2.0. Upper left, index page for recipes; lower left, an administrative console to add new recipes; right, detail pages for each recipe that include user contributed attempts and a form to submit a new attempt.
Consisting of a couple hundred lines of PHP and a SQLite database, Baking Disasters 2.0 may not scale to millions of users, but it is a great starting point for further exploration of the Google+ Platform. Its simple architecture is described by Figure 4-2.
You can see the initial state of this application in action at http://bakingdisasters.com/app-initial. It’s read only, for reasons that will become apparent shortly.
Within hours of launch the site has been overrun with spam. Since visitors can post content without identifying themselves, the application is being abused. You must find a way to lock down Baking Disasters and protect it.
Fortunately, the Google+ platform provides APIs for identifying users. The REST API exposes public profile fields. It also allows visitors to share their identity with us via OAuth 2.0. This provides everything that you need to address the spam problem, without having to build your own user management infrastructure.
The Google+ platform uses OAuth 2.0 to authenticate users, and to authorize your access to their private data. Scopes control which data is accessible. When the user grants access, Google will provide your application with tokens that can be used to access your user’s private data hosted by Google. Baking Disasters can use this to identify users as shown in Figure 4-3.
Once a user’s identity is known, you can use the REST APIs to fetch their public data. This includes their profile photo and their public activity on Google+. Additionally, their user ID makes a great user identifier in your application.
Your application must be configured on the API Console to use OAuth 2.0. Return to the Google API console to generate a client ID and secret. Use the client ID and secret to initiate the OAuth dance. They guarantee to the user that they are authorizing the correct application.
The client ID is publicly exposed during several steps of the authentication dance, but you should keep your client secret secure. If at any time your client secret is compromised you can return to this page to reset it.
Follow these steps to create your OAuth 2.0 credentials:
Navigate to the API console on Google Developers: https://developers.google.com/console.
Select your application from the drop down and click API Access in the menu.
Click on the large blue button shown in Figure 4-4 to create an OAuth 2.0 client ID.
Next, specify branding information for your application as shown in Figure 4-5. The product name and logo that you specify here are presented to your user on the OAuth grant screen.
Now for the tricky part. The OAuth dance requires you to specify the destination page, as shown in Figure 4-6. Google will redirect users there, once they have granted you privileges.
You should now see your client ID and secret, as shown in Figure 4-7.
OAuth 2.0 is designed to solve the general problem of accessing user data that exists on a third-party system. This is a big problem to solve. Baking Disasters uses the basic flow for web applications that are running on a server. This flow is only a tiny sliver of the much larger OAuth 2.0 specification.
Not every application runs in a web browser. Applications that run as native code on a mobile device, or as a command line script, are also valid clients. To accommodate these applications OAuth 2.0 provides a variety of flows, each with their own nuances.
OAuth 2.0 is big enough that any discussion here can’t do it justice. You can learn more about OAuth at Google here: http://code.google.com/apis/accounts/docs/OAuth2.html.
Client libraries make development much faster in the long run. However, just like any other tool, they have a learning curve. It will take you some time to make the most of their time-saving features. To smooth out this potentially sharp learning curve, the Google+ platform provides starter projects for the most popular client libraries. They are listed in Table 4-1.
These aptly named starter projects provide you with a turnkey foundation for whatever you would like to write. All you need to do is download the starter project and add your application identifiers. Starting from working code makes further development much easier.
Baking Disasters is written in PHP, so the PHP starter project is a great place to start. Once it’s working you can merge them together to start your Google+ integration.
Download the starter project from the downloads tab on http://code.google.com/p/google-plus-php-starter/. Unzip the archive to reveal that the project contains three files, including a readme with usage instructions, as shown in Figure 4-8.
The readme opens with a description of the starter project prerequisites. Most PHP installations will include the cURL and JSON extensions. The only thing that you need is the PHP client library.
Download the PHP client library from http://code.google.com/p/google-api-php-client/ and extract it in the starter project’s folder. You should end up with a directory structure that looks like Figure 4-9.
The readme instructs you to set up an application on the API Console to create a Client ID, Client Secret, and API key. Use the values that you set up for your API project in the previous section, as shown in Figure 4-10.
Use your favorite text editor to edit
index.php
. Scroll to about line 30 and paste these identifiers in the appropriate places, as shown in Example 4-1.Next, update the
redirectURI
field to match the value in the API console and the place you’ll be hosting this file. If you plan to run the starter project from a your workstation, this may belocalhost
.
Example 4-1. The fully configured PHP Google+ API starter project
$client
=
new
apiClient
();
$client
->
setApplicationName
(
"Google+ PHP Starter Application"
);
// Visit https://code.google.com/apis/console to generate your
// oauth2_client_id, oauth2_client_secret, and to register your oauth2_redirect_uri.
$client
->
setClientId
(
'116363269786.apps.googleusercontent.com'
);
$client
->
setClientSecret
(
'EJlqDrWkEYmmYznlken2JW-B'
);
$client
->
setRedirectUri
(
'http://bakingdisasters.com/web-app/php-starter'
);
$client
->
setDeveloperKey
(
'AIzaSyDrH_5j2-cPK7EZRANWjA6_g0xCZRrxH-U'
);
$client
->
setScopes
(
array
(
'https://www.googleapis.com/auth/plus.me'
));
$plus
=
new
apiPlusService
(
$client
);
if
(
isset
(
$_REQUEST
[
'logout'
]))
{
unset
(
$_SESSION
[
'access_token'
]);
}
It’s ready to go. Deploy the starter project to your web server, and view it in a web browser. Figure 4-11 shows what happens next. You’ll be greeted by a page that asks you to log in. Clicking the big blue link redirects you to Google’s authentication service where you’ll be asked to grant the starter project permissions to know who you are on Google. Click the Allow access button.
Having been granted access, the starter project now completes the OAuth dance. It exchanges a code for an access token. This token is used to make API calls, which are shown in Figure 4-12.
Figure 4-12. The completed flow of the starter project displaying your name, profile icon, and recent activity
With only a few minutes invested you now have a self contained web application capable of making API calls to the Google+ REST APIs. If you run into issues as you progress you can always return to this code as a sanity check.
You now have two functioning applications: Baking Disasters, and the PHP starter project. Combine them to add sign-in functionality to Baking Disasters. This consists mostly of strategic copy-and-paste from the starter project into the appropriate places in Baking Disasters.
Create new PHP files for the sign-in and sign-out operations and
store the currently signed in state within PHP’s session. Add code
that’s shared across multiple files, such as code to create or manage
API clients, to util.php
.
All of the pages in Baking Disasters require the use of an API
client. Take the code from the top of the starter project and copy it
into util.php
, as shown in Example 4-2. Create a new function that returns a Google+ API
client.
Example 4-2. A PHP function that creates a configured API client
require_once 'google-api-php-client/src/apiClient.php'; require_once 'google-api-php-client/src/contrib/apiPlusService.php'; session_start(); date_default_timezone_set('America/Los_Angeles'); function init_api_client() { global $app_base_path; $client = new apiClient(); $client->setApplicationName("Baking Disasters"); $client->setClientId('116363269786.apps.googleusercontent.com'); $client->setClientSecret('EJlqDrWkEYmmYznlken2JW-B'); $client->setRedirectUri($app_base_path . '/login.php'); $client->setDeveloperKey('AIzaSyDrH_5j2-cPK7EZRANWjA6_g0xCZRrx'); $client->setScopes(array('https://www.googleapis.com/auth/plus.me')); return $client; }
Next, copy the rest of the authentication logic into two PHP
files: login.php
and logout.php
. With a bit of shuffling login.php
looks Example 4-3.
Example 4-3. Initiate an OAuth 2.0 flow from login.php
<?php include_once("util.php"); $client = init_api_client(); $auth_url = $client->createAuthUrl(); if(!isset($_GET['code'])) { header("location: " . $auth_url); } else { //if (isset($_GET['code'])) { $client->authenticate(); $_SESSION['access_token'] = $client->getAccessToken(); header('Location: '.$app_base_path); }
And logout.php
looks like Example 4-4.
Example 4-4. Sign users out by deleting their access and refresh tokens from the session
<?
php
include_once
(
"util.php"
);
unset
(
$_SESSION
[
'access_token'
]);
header
(
'Location: '
.
$app_base_path
);
With these files created add sign-in and sign-out links to the header of every page with the code in Example 4-5.
Example 4-5. Sign-in and sign-out links for the header of each page
<body>
<header
class=
"blog-header"
>
<span
class=
"login"
>
<?php if(isset($_SESSION['access_token'])) { ?>
<a
href=
"logout.php"
>
Logout</a>
<?php } else { ?>
<a
href=
"login.php"
>
Sign in with Google+</a>
<?php } ?>
</span>
<a
href=
"index.php"
><img
id=
"blog-logo"
src=
"images/logo.png"
/></a>
<h1>
Baking Disasters</h1>
This implementation allows users to sign in and out, but it can
already benefit from some refactoring. Currently, signing in and out
redirects users back to the index page. Use the session and referrer
header to return them to the page where they started. Example 4-6 is an updated login.php
.
Example 4-6. Redirecting the user to the page from which they initiated the sign in improves their experience
// If there's a code we need to swap it for an access token
else
{
//if (isset($_GET['code'])) {
$client
->
authenticate
();
$_SESSION
[
'access_token'
]
=
$client
->
getAccessToken
();
if
(
isset
(
$_SESSION
[
'original_referrer'
]))
{
header
(
'Location: '
.
$_SESSION
[
'original_referrer'
]);
unset
(
$_SESSION
[
'original_referrer'
]);
}
else
{
header
(
'Location: '
.
$app_base_path
);
}
}
Example 4-7 shows the updated logout.php
.
Example 4-7. Also redirect the user to the page from which they initiated the sign out
<?
php
include_once
(
"util.php"
);
unset
(
$_SESSION
[
'access_token'
]);
if
(
isset
(
$_SERVER
[
'HTTP_REFERER'
]))
{
header
(
'Location: '
.
$_SERVER
[
'HTTP_REFERER'
]);
}
else
{
header
(
'Location: '
.
$app_base_path
);
}
The page header determines if a user is currently signed in by
checking the existence of an access token in the session. Checking the
signed-in state is something that many parts of the Baking Disasters
needs, so refactor this into a utility function. Add this function to
util.php
, as shown in Example 4-8.
Example 4-8. A function that checks the current user’s sign-in state
function
is_logged_in
()
{
if
(
isset
(
$_SESSION
[
'access_token'
]))
{
return
true
;
}
else
{
return
false
;
}
}
And update the header of each page to use it, as shown in Example 4-9.
Example 4-9. Using the is_logged_in() function abstracts the session implementation out of your PHP pages
<
body
>
<
header
class
=
"blog-header"
>
<
span
class
=
"login"
>
<?
php
if
(
is_logged_in
())
{
?>
<a href="logout.php">Logout</a>
<?php
}
else
{
?>
<a href="login.php">Sign in with Google+</a>
<?php
}
?>
</span>
<a href="index.php"><img id="blog-logo" src="images/logo.png"/></a>
<h1>Baking Disasters</h1>
Now that users can sign in, you can restrict access to sensitive features such as the disaster submission form and the administration console.
Restricting the disaster submission form to currently signed in
users is quite easy. Just add an is_logged_in
check when you render the form in
recipe.php
, as shown in Example 4-10.
Example 4-10. Only display the report attempt form to signed in users
<
section
class
=
"content attempt-form"
>
<?
php
if
(
is_logged_in
())
{
?>
<h2>Report Your Attempt</h2>
<p>Have you attempted this recipe with disastrous results?
Tell us about it!</p>
<form method="post">
<input type="hidden" name="recipe_id" value="
<?
=
$_GET
[
'id'
]
?>
"/>
<label>Your Name: <input name="author_name"></label>
<label>Description: <textarea name="description"></textarea></label>
<label>Photo URL: <input type="text" name="photo_url"/></label>
<input type="submit"/>
</form>
<?php
}
else
{
?>
<p>Log in to tell us about your attempt!</p>
<?php
}
?>
</section>
And when you insert into the database, as shown in Example 4-11.
Example 4-11. Only allow signed-in users to write to the database
<?
php
include_once
(
"util.php"
);
if
(
$_POST
&&
is_logged_in
())
{
insert_attempt
(
$_POST
[
'recipe_id'
],
$_POST
[
'author_name'
],
$_POST
[
'description'
],
$_POST
[
'photo_url'
]);
echo
"<p class='notice'>Attempt inserted!</p>"
;
}
?>
Unauthenticated users are now asked to log in to share their attempts, as shown in Figure 4-13.
This plugs up the biggest opening for spam, but what if someone
discovers admin.php
? They could
insert new recipes. To ensure that only the site administrator, Jenny
Murphy, can add new recipes, check to see if the signed in user has the
correct user ID. To make this comparison fetch the signed in user’s
profile from the API.
The starter project shows us how to make this call. It fetches the current user’s profile just after it validates that the current user has an access token. This call is shown in Example 4-12.
Wrap this behavior into a get_plus_profile
function, as shown in Example 4-13.
Example 4-13. A function that fetches the signed-in user
function
get_plus_profile
()
{
if
(
!
is_logged_in
())
{
die
(
"Expected to be logged in here"
);
}
$client
=
init_api_client
();
$client
->
setAccessToken
(
$_SESSION
[
'access_token'
]);
$plus
=
new
apiPlusService
(
$client
);
$me
=
$plus
->
people
->
get
(
'me'
);
return
$me
;
}
And then use the code in Example 4-14 to verify that
Jenny is the user viewing admin.php
.
Example 4-14. Check the signed-in user’s profile ID to restrict access to admin.php
</
header
>
<?
php
if
(
is_logged_in
())
{
$me
=
get_plus_profile
();
if
(
$me
[
'id'
]
==
"102817283354809142195"
)
{
if
(
$_POST
)
{
insert_recipe
(
$_POST
[
'name'
],
$_POST
[
'description'
],
$_POST
[
'ingredients'
],
$_POST
[
'directions'
],
$_POST
[
'photo_url'
]);
echo
"<p class='notice'>Recipe inserted.</p>"
;
}
?>
<section class="content">
...
</section>
<?php
}
else
{
echo
"Only Jenny Murphy can access this page."
;
}}
?>
<footer>
This prevents interlopers from adding recipes. Instead, they see the error message shown in Figure 4-14.
All writes to Baking Disasters are now protected, but there’s so much more that you can do with Google+.
OAuth 2.0 is an amazing standard. It gives your users a way to share their Google data and identity with your application. Unfortunately, to support OAuth, you had to add quite a bit of code to your application. OAuth is complex, but implementing it does not have to be.
The Google+ sign-in button, shown in Figure 4-15, aims to make implementing OAuth easier. It accomplishes this by providing you with a plugin just like the +1 button. When the user clicks the Sign In button they are taken through an OAuth flow and the code is returned to your application. This replaces the first half of your OAuth code with a single line of markup, shown in Example 4-15.
Example 4-15. Sign-in button markup
<g:plus
action=
"connect"
clientid=
"1234567890.apps.googleusercontent.com"
callback=
"onSignInCallback"
></g:plus>
The sign-in button is currently in developer preview. During this time, preview only works for developers who are enrolled in the preview. You can start experimenting with it, but you can’t release software that uses it until the developer preview has finished. You should also be prepared for breaking changes at any point during this developer preview.
The developer preview is tied to a Google account. When you are logged in to Google with a developer preview account, the sign-in button will render. Use this form to sign up: https://developers.google.com/+/history/preview/.
Google Developers provides theoretical explanation, API reference material, and starter projects that use the Sign In button: https://developers.google.com/+/history/.
Baking Disasters now requires authentication for the creation of data. Authentication is useful, but it’s neither very exciting nor does it add any new features to your site. Let’s knock it up a notch and use the APIs to leverage even more features of Google+.
Baking Disasters would benefit a lot from importing content from Google+. If users could import attempts from their recent activity on Google+ they can leverage the Google+ mobile application. They can live-share their baking attempt on the Google+ mobile application. Once the smoke has cleared and they return to their laptops, they can import content at their convenience. This flow is described in Figure 4-16.
Importing activity is a bit more involved than the previous enhancements. It will require you to add a new page to provide an import interface and update the recipe page to render imported attempts.
To comply with the developer policies, which restrict the storage of Google+ user data, you must store references to the activities on Google+ instead of the whole activity. This requires refactoring of your database. It also means that you must handle activities that have been deleted. However, it pays off in the form of automatically handling updates and edits. The resulting flow is shown in Figure 4-17.
Figure 4-17. A sequence diagram describing what happens during the baking party, when the user imports activity into Baking Disasters, and when Baking Disasters renders the activity on the recipe page
The database schema changes are simple, but they do require a breaking change. Example 4-16 shows the new table creation statement. You can either run this by hand using the SQLite command line interface or delete the database and start from scratch.
Example 4-16. A new table schema for baking attempts
sqlite_exec
(
$db
,
'create table attempts (recipe_id int,
google_plus_activity_id text);'
);
Just as in the previous enhancements most of the heavy lifting will be done by functions in util.php. Add a function that fetches a page of public activities for the currently logged in user. You can model this code from the activity list in the starter project. The resulting code is shown in Example 4-17.
Example 4-17. Fetch the signed in user’s recent public activities from the API
function
get_recent_activities
()
{
if
(
!
is_logged_in
())
{
die
(
"Expected to be logged in here"
);
}
$client
=
init_api_client
();
$client
->
setAccessToken
(
$_SESSION
[
'access_token'
]);
$plus
=
new
apiPlusService
(
$client
);
$optional_parameters
=
array
(
'maxResults'
=>
20
);
$activities
=
$plus
->
activities
->
listActivities
(
'me'
,
'public'
,
$optional_parameters
);
return
$activities
;
}
Having changed the way activities are stored, you also need to update the way that you recall them, as shown in Example 4-18. The database only stores the activity ID. The rest of the fields must come from an API call to fetch each activity.
The fetch is a great time to clean up any activities that were deleted from Google+. If the API returns a 404, you know that the activity is gone and you should clean up the reference to it.
Example 4-18. This code fetches the activities associated with each recorded attempt for a specific recipe ID. If an activity no longer exists, it removes it from the database
function list_attempts($recipe_id) { $db = init_db(); $recipe_id = sqlite_escape_string(strip_tags($recipe_id)); $query = sqlite_query($db, "select * from attempts where recipe_id = '$recipe_id'"); $attempt_stubs = sqlite_fetch_all($query, SQLITE_ASSOC); $client = init_api_client(); $plus = new apiPlusService($client); $attempts = Array(); foreach ($attempt_stubs as $attempt_stub) { $google_plus_activity_id = $attempt_stub['google_plus_activity_id']; try { $activity = $plus->activities->get($google_plus_activity_id); $attempt = Array(); $attempt['url'] = $activity['url']; $attempt['author'] = $activity['actor']; $attempt['description'] = $activity['object']['content']; if (count($activity['object']['attachments']) > 0) { $attempt['photo_url'] = $activity['object']['attachments'][0]['image']['url']; } array_push($attempts, $attempt); } catch (Exception $e) { if ($e->getCode() == 404) { // If it's a 404, it has been deleted by the user. Clean it up. sqlite_exec($db, "delete from attempts where google_plus_activity_id='$google_plus_activity_id';"); } } } return $attempts; }
This method returns a list of activities that is far more rich
than the previous representation. All you need to do is to update the
code that renders it in recipes.php
,
as shown in Example 4-19. You also can also take advantage
of the profile icon that is returned with the attached user
object.
Example 4-19. Display attempts from Google+ on the recipe page
<
section
class
=
"content attempts"
>
<?
php
foreach
(
$attempts
as
$attempt
)
{
?>
<div class="attempt">
<?php
if
(
isset
(
$attempt
[
'photo_url'
]))
{
?>
<img class="attempt-photo" src="
<?
=
$attempt
[
'photo_url'
]
?>
" />
<?php
}
?>
<h3>
<a href="
<?
=
$attempt
[
'author'
][
'url'
]
?>
">
<img src="
<?
=
$attempt
[
'author'
][
'image'
][
'url'
]
?>
"/></a>
<?
=
$attempt
[
'author'
][
'displayName'
]
?>
's Attempt
<a class="import-link" href="
<?
=
$attempt
[
'url'
]
?>
">imported from Google+</a>
</h3>
<p>
<?
=
str_replace
(
"
\n
"
,
"<br/>
\n
"
,
stripslashes
(
$attempt
[
'description'
]))
?>
</p>
<div style="clear:both;"></div>
</div>
<?php
}
?>
</section>
Reload Baking Disasters and use the new import feature to pull in some of your recent baking attempts. The resulting import should look like Figure 4-18.
Your modest PHP web application is better than ever. You have used the Google+ APIs for a simple sign-in solution. Knowing who is signed in has provided the tools that you need to restrict administrative features to the correct users. You’ve also used the activity APIs to enable users import their recent adventures and associate them with a recipe. The way this integration was implemented has the added benefit of automatically updating the imported view when the canonical copy on Google+ is updated or deleted.
Additional endpoints exist for searching activities, listing people who took action on a specific activity and viewing comments. You can use these same techniques to add functionality that pulls data from these endpoints.
The same developer preview for the sign-in button also includes a write API for Google+ called the history API. This API allows your application to write moments that represent your user’s activity to a private place on Google+. Later your user can share the moments that are important to them with the right people on Google+.
Signing up for this developer preview follows the same process as for the sign-in button. Complete the sign-up form and your account will be able to create API projects that use the history API and write moments to your Google history. You can also see these moments in a beta version of the Google+ history user interface at https://plus.google.com/history.
Each write to the history API has two parts: an HTTP POST and a publicly accessible target page with schema.org structured markup. The POST body, shown in Example 4-20, describes the type of activity that has occurred. It includes the URL of the entity that was the target of the activity and a JSON representation of any content created as the result of the activity.
Example 4-20. A CommentActivity
{ "type": "http://schemas.google.com/CommentActivity", "target": { "url": "https://developers.google.com/+/plugins/snippet/examples/blog-entry" }, "result": { "type": "http://schema.org/Comment", "url": "https://developers.google.com/+/plugins/snippet/examples/blog-entry#comment-1", "name": "This is amazing!", "text": "I can't wait to use it on my site :)" } }
The target page, specified in the POST by target.url, is the second part of the moment write. It is fetched when the moment is written. This fetch extracts schema.org markup and includes it into the moment.
If you have already added schema.org markup to your pages for other social plugins like the +1 button, you will not need to make any additional changes to them.
Warning
The history API is currently in developer preview. Expect rapid changes. Expect these changes to break your code.
Please see the documentation to see the latest starter projects, reference docs, and implementation advice.
The developer preview of the history API provides a rare opportunity. Google is very interested in feedback from developers as they experiment with the APIs. For example, there is a pre-populated form for requesting new moment types: https://developers.google.com/+/history/api/moments#request_a_new_type. If you request a moment type or change to the API during the developer preview it is much more likely to be incorporated into the released API.
For the sake of simplicity and ease of understanding the example code presented so far has cut some corners. For example, code has been copied and pasted in many places and it is not as efficient as it could be. Here are some tips to help guide you through efficient usage of the API.
- Cache
The code presented above creates client libraries and often uses them only once. Cache client libraries. They can be reused. Leverage caches for API requests too. Most of the client libraries take care of this for you, but you should still make sure that the client library is configured to cached data appropriately for your project. For example, the PHP client library provides four different cache implementations, one of which may work better for you.
- Be paranoid
Don’t trust anyone when it comes to cross site scripting. Be very cautious when rendering input that comes from a user, even if it’s coming via a Google API. Escape everything. The implementation of these practices will differ a bit on your language and platform.
Get Developing with Google+ now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.