#!/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 '' . "\n"; print '' . "\n"; print '' . "\n"; print '
Perl Demo: Google Base API
Before you get started, please sign in to your personal Google Base account.
' . "\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 '' . "\n"; getItems($token); if (@parsedEntries) { print '' . "\n"; print '' . "\n"; print '' . "\n"; print '' . "\n"; print '' . "\n"; print '' . "\n"; foreach my $entry (@parsedEntries) { print '' . "\n"; print '' . "\n"; print '' . "\n"; print '' . "\n"; # Create an Edit button for each existing recipe. print '' . "\n"; # Create a Delete button for each existing recipe. print '' . "\n"; print '' . "\n"; } } else { print '' . "\n"; } # Create a "Delete all" button to demonstrate batch requests. print '' . "\n"; print '
Recipes you have added
NameCuisineServesActions
' . $entry->{'title'} . '' . $entry->{'g:cuisine'} . '' . $entry->{'g:serving_count'} . '' . "\n"; print '
' . "\n"; print '' . "\n"; print '' . "\n"; foreach my $key (keys %{$entry}) { print '' . "\n"; } print '' . "\n"; print '
' . "\n"; print '
' . "\n"; print '
' . "\n"; print '' . "\n"; print '' . "\n"; print '' . "\n"; print '' . "\n"; print '
' . "\n"; print '
(none)
' . "\n"; print '
' . "\n"; print '' . "\n"; print '' . "\n"; for(my $i = 0; $i < @parsedEntries; $i++) { print '' . "\n"; } print '
' . "\n"; print '' . "\n"; } sub showRecipeInsertPane { my $token = shift; print '' . "\n"; print '' . "\n"; print '' . "\n"; print '' . "\n"; print '' . "\n"; print '' . "\n"; print '' . "\n"; print '' . "\n"; print '' . '' . "\n"; print '' . "\n"; print '' . "\n"; print '' . "\n"; print '
Insert a new recipe:
Title:' . '
Main ingredient:' . '
Cuisine:' . '
Cooking Time:' . ' 
Serves:
Recipe:
 
' . "\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 '' . "\n"; print '' . "\n"; print '' . "\n"; print '' . "\n"; print '' . "\n"; print '' . "\n"; print '' . "\n"; print '' . "\n"; print '' . "\n"; print '' . "\n"; print '' . "\n"; print '' . '' . "\n"; print '' . "\n"; print '' . "\n"; print '' . "\n"; print '' . "\n"; print '' . "\n"; print '
Edit recipe:
Title:
Main ingredient:
Cuisine:
Cooking Time: ' . "\n"; print '
Serves:
Recipe:
 
' . "\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 '' . "\n"; print '' . "\n"; print '' . "\n"; # Create the two sub-tables. showRecipeListPane($sessionToken); showRecipeInsertPane($sessionToken); # Add a "Sign out" link. print '' . "\n"; # Close the master table. print '
Perl Demo: Google Base API' . "\n"; print '
' . $tableTitle . '
Or click here to ' . ' ' . 'sign out of your Google account.
' . "\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(); }