#!/usr/bin/perl -w
use strict;
use WWW::Curl::Easy; # Needed for HTTP transactions.
use URI::Escape; # Needed for uri_escape().
use CGI qw(:standard); # Needed for parsing GET/POST params.
use XML::Parser; # Needed for parsing XML.
# Our Google Base API developer key.
my $developerKey = "REPLACE WITH YOUR DEVELOPER KEY";
# Parsed recipe entries from a query.
my @parsedEntries;
# Are we currently parsing an XML ENTRY tag?
my $foundEntry;
# Current XML element being processed.
my $curElement;
# Types of cuisine the user may select when inserting a recipe.
my @cuisines = ('African', 'American', 'Asian', 'Caribbean', 'Chinese',
'French', 'Greek', 'Indian', 'Italian', 'Japanese', 'Jewish',
'Mediterranean', 'Mexican', 'Middle Eastern', 'Moroccan',
'North American', 'Spanish', 'Thai', 'Vietnamese', 'Other'
);
# Returns the URI of this script.
sub getSelfURI {
return 'http://' . $ENV{"SERVER_NAME"} . $ENV{"SCRIPT_NAME"};
}
# Removes whitespace from the start and end of a string.
sub trim {
my($string) = @_;
$string =~ s/^\s+//;
$string =~ s/\s+$//;
return $string;
}
# Creates the XML content used to insert a new recipe.
sub buildInsertXML {
my $result = "" . "\n";
$result .= "" . "\n";
$result .= "" . "\n";
$result .= "" . param('recipe_title') . "" . "\n";
$result .= "" . param('cuisine') . "" . "\n";
$result .= "Recipes" . "\n";
$result .= "" . param('cooking_time_val') .
" " . param('cooking_time_units') . "" . "\n";
$result .= "" . param('main_ingredient') .
"" . "\n";
$result .= "" . param('serves') .
"" . "\n";
$result .= "" . param('recipe_text') . "" . "\n";
$result .= "" . "\n";
return $result;
}
# Creates the XML content used to perform a batch delete.
sub buildBatchDeleteXML {
my $counter = 0;
my $result = '' . "\n";
$result .= '' . "\n";
$result .= '' . "\n";
$result .= '' . $counter . '' . "\n";
$result .= '' . "\n";
}
}
$result .= '' . "\n";
return $result;
}
# Callback function that's fired as cURL objects receive chunks of
# content data from the server.
sub writeCallback {
my ($data, $pointer) = @_;
push @{$pointer}, $data;
return length($data);
}
# Callback function that's fired as cURL objects receive chunks of
# header data from the server. Since we don't need to do anything
# with the headers for this application, this callback function
# just returns the length of the data it received.
sub headerCallback {
my($data, $pointer) = @_;
return length($data);
}
# Exchanges the given single-use token for a session
# token using AuthSubSessionToken, and returns the result.
sub exchangeToken {
my $token = shift;
my $curl = new WWW::Curl::Easy;
my @body;
my @authHeader = ("Authorization: AuthSub token=\"" . $token . "\"");
$curl->setopt(CURLOPT_URL,
"https://www.google.com/accounts/AuthSubSessionToken");
$curl->setopt(CURLOPT_FAILONERROR, 1);
$curl->setopt(CURLOPT_WRITEFUNCTION, \&writeCallback );
$curl->setopt(CURLOPT_HEADERFUNCTION, \&headerCallback );
$curl->setopt(CURLOPT_FILE, \@body);
$curl->setopt(CURLOPT_FOLLOWLOCATION, 1);
$curl->setopt(CURLOPT_HTTPHEADER, \@authHeader);
my $result = $curl->perform();
if ($result > 0) {
return 0;
} else {
# Extract everything to the right of the equals sign in
# the response "Token=..."
my @splitStr = split(/=/, $body[0]);
return trim($splitStr[1]);
}
}
# Performs a query for all of the user's items using the
# items feed, then parses the resulting XML with the
# startElement, endElement and characterData functions
# (below).
sub getItems {
my $token = shift;
my $curl = new WWW::Curl::Easy;
my @body;
my @authHeader = (
'Content-Type: application/atom+xml',
'Authorization: AuthSub token="' . trim($token) . '"',
'X-Google-Key: key=' . $developerKey
);
$curl->setopt(CURLOPT_URL, "http://www.google.com/base/feeds/items?");
$curl->setopt(CURLOPT_FAILONERROR, 1);
$curl->setopt(CURLOPT_WRITEFUNCTION, \&writeCallback );
$curl->setopt(CURLOPT_HEADERFUNCTION, \&headerCallback );
$curl->setopt(CURLOPT_FILE, \@body);
$curl->setopt(CURLOPT_FOLLOWLOCATION, 1);
$curl->setopt(CURLOPT_HTTPHEADER, \@authHeader);
my $result = $curl->perform();
if (!$result) {
my $parser = new XML::Parser(Handlers => {Start => \&handleXMLstart,
End => \&handleXMLend,
Char => \&handleXMLchar});
$parser->parse(join("", @body));
}
}
# Inserts a new recipe by performing an HTTP POST to the
# items feed.
sub postItem {
my $curl = new WWW::Curl::Easy;
my @body;
my @authHeader = (
'Content-Type: application/atom+xml',
'Authorization: AuthSub token="' . param('session_token') . '"',
'X-Google-Key: key=' . $developerKey
);
$curl->setopt(CURLOPT_URL, "http://www.google.com/base/feeds/items");
$curl->setopt(CURLOPT_READFUNCTION, \&buildInsertXML);
$curl->setopt(CURLOPT_INFILESIZE, length(buildInsertXML()));
$curl->setopt(CURLOPT_UPLOAD, 1);
$curl->setopt(CURLOPT_CUSTOMREQUEST, "POST");
$curl->setopt(CURLOPT_FAILONERROR, 1);
$curl->setopt(CURLOPT_WRITEFUNCTION, \&writeCallback );
$curl->setopt(CURLOPT_HEADERFUNCTION, \&headerCallback );
$curl->setopt(CURLOPT_FILE, \@body);
$curl->setopt(CURLOPT_FOLLOWLOCATION, 1);
$curl->setopt(CURLOPT_HTTPHEADER, \@authHeader);
my $result = $curl->perform();
return $result;
}
# Updates an existing recipe by performing an HTTP
# PUT on its feed URI.
sub updateItem {
my $curl = new WWW::Curl::Easy;
my @body;
my @authHeader = (
'Authorization: AuthSub token="' . param('session_token') . '"',
'X-Google-Key: key=' . $developerKey,
'Content-Type: application/atom+xml'
);
my $feedURL = param('link');
chomp $feedURL;
$curl->setopt(CURLOPT_URL, $feedURL);
$curl->setopt(CURLOPT_READFUNCTION, \&buildInsertXML);
$curl->setopt(CURLOPT_INFILESIZE, length(buildInsertXML()));
$curl->setopt(CURLOPT_UPLOAD, 1);
$curl->setopt(CURLOPT_CUSTOMREQUEST, "PUT");
$curl->setopt(CURLOPT_FAILONERROR, 1);
$curl->setopt(CURLOPT_WRITEFUNCTION, \&writeCallback );
$curl->setopt(CURLOPT_HEADERFUNCTION, \&headerCallback );
$curl->setopt(CURLOPT_FILE, \@body);
$curl->setopt(CURLOPT_VERBOSE, 1);
$curl->setopt(CURLOPT_FOLLOWLOCATION, 1);
$curl->setopt(CURLOPT_HTTPHEADER, \@authHeader);
my $result = $curl->perform();
return $result;
}
# Deletes a recipe by performing an HTTP DELETE
# on its feed URI.
sub deleteItem {
my $curl = new WWW::Curl::Easy;
my $feedURL = param('link');
my @body;
my @authHeader = (
'Authorization: AuthSub token="' . param('session_token') . '"',
'X-Google-Key: key=' . $developerKey
);
chomp $feedURL;
$curl->setopt(CURLOPT_URL, $feedURL);
$curl->setopt(CURLOPT_CUSTOMREQUEST, "DELETE");
$curl->setopt(CURLOPT_FAILONERROR, 1);
$curl->setopt(CURLOPT_WRITEFUNCTION, \&writeCallback );
$curl->setopt(CURLOPT_HEADERFUNCTION, \&headerCallback );
$curl->setopt(CURLOPT_FILE, \@body);
$curl->setopt(CURLOPT_FOLLOWLOCATION, 1);
$curl->setopt(CURLOPT_HTTPHEADER, \@authHeader);
my $result = $curl->perform();
return $result;
}
# Deletes all recipes by performing an HTTP POST to the
# batch URI.
sub batchDelete {
my $curl = new WWW::Curl::Easy;
my @body;
my @authHeader = (
'Content-Type: application/atom+xml',
'Authorization: AuthSub token="' . param('session_token') . '"',
'X-Google-Key: key=' . $developerKey
);
$curl->setopt(CURLOPT_URL, "http://www.google.com/base/feeds/items/batch");
$curl->setopt(CURLOPT_READFUNCTION, \&buildBatchDeleteXML);
$curl->setopt(CURLOPT_INFILESIZE, length(buildBatchDeleteXML()));
$curl->setopt(CURLOPT_UPLOAD, 1);
$curl->setopt(CURLOPT_CUSTOMREQUEST, "POST");
$curl->setopt(CURLOPT_FAILONERROR, 1);
$curl->setopt(CURLOPT_WRITEFUNCTION, \&writeCallback );
$curl->setopt(CURLOPT_HEADERFUNCTION, \&headerCallback );
$curl->setopt(CURLOPT_FILE, \@body);
$curl->setopt(CURLOPT_FOLLOWLOCATION, 1);
$curl->setopt(CURLOPT_HTTPHEADER, \@authHeader);
my $result = $curl->perform();
return $result;
}
# Callback function that's fired by the Expat XML parser on parsing
# a start tag.
sub handleXMLstart {
my($parser, $elem, %attrs) = @_;
$curElement = lc($elem);
if ($curElement eq "entry") {
$foundEntry = 1;
push(@parsedEntries, {});
} elsif ($foundEntry && $curElement eq "link") {
$parsedEntries[$#parsedEntries]{$attrs{"rel"}} = $attrs{"href"};
}
}
# Callback function that's fired by the Expat XML parser on parsing
# an end tag.
sub handleXMLend {
my($parser, $elem) = @_;
if (lc($elem) eq "entry") {
$foundEntry = 0;
}
}
# Callback function that's fired by the Expat XML parser on parsing
# a sequence of characters.
sub handleXMLchar {
my($parser, $chars) = @_;
if ($foundEntry) {
$parsedEntries[$#parsedEntries]{lc($curElement)} = $chars;
}
}
# We arrive here when the user first comes to the form. The first step is
# to have them get a single-use token.
sub showIntroPage {
my $redirect_url =
'https://www.google.com/accounts/AuthSubRequest?session=1';
$redirect_url .= '&next=';
$redirect_url .= uri_escape(getSelfURI());
$redirect_url .= "&scope=";
$redirect_url .= uri_escape("http://www.google.com/base/feeds");
print '' . "\n";
print '
Perl Demo: Google Base API' . "\n";
print '' . "\n";
print '' . "\n";
print '
' . "\n";
print '
' . "\n";
print '
' . "\n";
print '
Perl Demo: Google Base API
' . "\n";
print '
' . "\n";
print '
Before you get started, please sign in to your personal Google Base account.
' . "\n";
print '
' . "\n";
print '
' . "\n";
}
# Prints the table of recipes the user has already entered
# on the left-hand side of the page.
sub showRecipeListPane {
my $token = shift;
print '
' . "\n";
print '
' . "\n";
print '
Recipes you have added
' . "\n";
getItems($token);
if (@parsedEntries) {
print '
' . "\n";
print '
Name
' . "\n";
print '
Cuisine
' . "\n";
print '
Serves
' . "\n";
print '
Actions
' . "\n";
print '
' . "\n";
foreach my $entry (@parsedEntries) {
print '
' . "\n";
# Create an Edit button for each existing recipe.
print '
' . "\n";
print '' . "\n";
print '
' . "\n";
# Create a Delete button for each existing recipe.
print '
' . "\n";
print '' . "\n";
print '
' . "\n";
print '
' . "\n";
}
} else {
print '
(none)
' . "\n";
}
# Create a "Delete all" button to demonstrate batch requests.
print '
' . "\n";
print '
' . "\n";
print '
' . "\n";
print '
' . "\n";
}
sub showRecipeInsertPane {
my $token = shift;
print '
' .
"\n";
print '
Insert a new recipe:
' . "\n";
print '
' . "\n";
print '
' . "\n";
}
sub showEditMenu {
my @splitCookingTime = split(/\s+/, param('g:cooking_time'));
print '' . "\n";
print 'Perl Demo: Google Base API' . "\n";
print '' . "\n";
print '' . "\n";
print '
' . "\n";
print '
' . "\n";
print '
Edit recipe:
' . "\n";
print '
' . "\n";
print '' . "\n";
}
# Displays both the "List of current recipes" and
# "Insert a new recipe" panels in a single table.
sub showMainMenu {
my($tableTitle, $sessionToken) = @_;
print '' . "\n";
print 'Perl Demo: Google Base API' . "\n";
print '' . "\n";
print '' . "\n";
print '
' . "\n";
print '
' . "\n";
print '
' . "\n";
print '
Perl Demo: Google Base API' . "\n";
print '
' . "\n";
print '
' . $tableTitle . '
' .
"\n";
print '
' . "\n";
# Create the two sub-tables.
showRecipeListPane($sessionToken);
showRecipeInsertPane($sessionToken);
# Add a "Sign out" link.
print '
Or click here to ' .
' ' .
'sign out of your Google account.
' . "\n";
# Close the master table.
print '
' . "\n";
print '
' . "\n";
}
# We arrive here after the user first authenticates and we get back
# a single-use token.
sub showFirstAuthScreen {
my $singleUseToken = param('token');
my $sessionToken = exchangeToken($singleUseToken);
if (!$sessionToken) {
showIntroPage();
} else {
my $tableTitle =
'Here\'s your single use token:' . $singleUseToken .
'' . "\n" . ' And here\'s the session token:' .
$sessionToken . '';
showMainMenu($tableTitle, $sessionToken);
}
}
# Main logic. Take action based on the GET and POST
# parameters, which reflect whether the user has
# authenticated and which action they want to perform.
print "Content-type: text/html\n\n";
if (defined param('token')) {
showFirstAuthScreen();
} elsif (defined param('session_token')) {
if (param('action') eq "insert") {
if (postItem()) {
showMainMenu('Recipe inserted!', param('session_token'));
} else {
showMainMenu('Recipe insertion failed.', param('session_token'));
}
} elsif (param('action') eq "delete") {
if (!deleteItem()) {
showMainMenu('Item deleted.', param('session_token'));
} else {
showMainMenu('Item deletion failed.', param('session_token'));
}
} elsif (param('action') eq "delete_all") {
if (batchDelete()) {
showMainMenu('All items deleted.', param('session_token'));
} else {
showMainMenu('Batch deletion failed.', param('session_token'));
}
} elsif (param('action') eq "edit") {
showEditMenu();
} elsif (param('action') eq "update") {
if (updateItem()) {
showMainMenu('Item successfully updated.', param('session_token'));
} else {
showMainMenu('Item update failed.', param('session_token'));
}
} else {
showIntroPage();
}
} else {
showIntroPage();
}