Basics of QR Codes

Ever see one of these and wondered what the hell it is?

http://mark.biek.org/blog/

It’s a QR Code! (One of the more common two-dimensional (or matrix) barcode types. Benefits of the format include fast decoding speed, holds more data than traditional barcodes, and error correction.)

While a variety of software packages exist for generating QR codes, one of the easiest was to generate one is through the Google Charts API.

The QR code at the top of this post is generated using just that, via the following URL:


http://chart.apis.google.com/chart?chs=100x100&cht=qr&chl=http://mark.biek.org/blog&chld=L|1&choe=UTF-8

You can see that the information being encoded is simply a link to this blog (http://mark.biek.org/blog).

QR Codes support encoding of digits 0-9, letters A-Z, and the characters space $ % * + – . / :

There are also different versions of the QR Code standard ranging from 1-40. Each increasing level allows for more data to be encoded but also limits which devices will be able to decode it. Levels 1-4 are the most commonly supported, especially on mobile devices. The Google Charts API documentation has a nice table of how many characters are allowed at each version for the different levels of error correction.

For example, here’s the breakdown for Version 4:

  • EC level L (allows 7% data loss) holds 187 digits or 114 Alphanumeric characters.
  • EC level M (allows 15% data loss) holds 149 digits or 90 Alphanumeric characters.
  • EC level Q (allows 25% data loss) holds 111 digits or 67 Alphanumeric characters.
  • EC level H (allows 30% data loss) holds 82 digits or 50 Alphanumeric characters.

It’s worth mentioning that the Google Charts API will pick the version for you, based on the amount of data you’re encoding so all you have to worry about specifying is pixel size and error correction level.

There are interesting possibilities for using QR Codes (like these Calvin Klein billboards) but, unfortunately, the last-mile is a bit of a problem.

There are barcode scanning programs for most mobile phones (lots of them for iPhone and Android) but you are reliant entirely on the scanning program for what happens when the QR Code is scanned. Double-unfortunately, there are no real standards yet for handling encoded data.

What little, unofficial, standardization of encoded data is described in this wiki article entitled BarcodeContents.

Most/many scanners handle support for the obvious things like http://, mailto:, and even sms:, but there’s no real guarentee. Plus, since it’s not built into the device (requiring a download from whichever App store you’re using), the person with the phone has to take some additional steps to be able to do anything.

The end result is that you can’t control how smooth the end-user experience is, although it will definitely be neat for the people who can use it successfully.

Adding/Deleting Events with the Google Calendar API

I recently had a chance to fool around with the Google Calendar API, specifically adding and deleting events.

I’ve talked in the past about using Google Client Logins to access the Google Data API and we’ll use that same ClientLogin code to authenticate our requests to the Calendar API.

Authenticating

There are different ways to authenticate requests to Calendar depending on the type of application you’re writing. If you’re writing a front-end that a user is going to interact with, it’s a good idea to use AuthSub proxy authentication which will redirect the user, let them login with their Google Account, and send them back to your application.

In our case, we’re writing PHP code that will modify the calendar behind the scenes. For that scenario, we’ll use ClientLogin authentication and use the class we wrote last time

In additional to needing a Google Account for authenticating, you’ll also need the Calendar ID if the calendar being used is not the default calendar. The Calendar ID can be found near the bottom of the page in the Settings for the calendar and is just a special, randomly generated email address.

Adding an Event

Adding an event is pretty straightforward. It’s just a matter of sending an authenticated <entry> XML packet via HTTP POST to the calendar URL which is the following:


http://www.google.com/calendar/feeds/userId/private/full

The userId is either your Google Account email address (if using the default calendar) or the Calendar ID mentioned above (if using any other calendar). You also need to make sure the Content-Type of the POST request is application/atom+xml.

The event XML looks like this:

$xml = "
          
          {$params["title"]}
          {$params["content"]}
          
          
          
          
          
          
        ";

A request is authenticated by including the Authorization: GoogleLogin auth=”authToken” header in the request. The authToken value comes back with the initial ClientLogin request. If you’re using my GoogleClientLogin class, the authToken can be retrieved with the getAuth() method.

One thing to watch for is the initial POST request may come back with an HTTP 302 redirect. If it does, the redirect url will contain a gsessionid. In that case, append the gesessionid to the calendar URL and resend the exact same POST request.

If the entry is successfully added, you’ll get back an XML response containing the entry as well as HTTP headers with the ETag (more on this in a minute) and the entry’s edit link (more on that too).

Deleting an Event

Deleting an event is even easier. Just send an HTTP DELETE request to the edit link you got back when you added the event. This can be pulled from either the response XML when you add the event or from the response “Location” header.

If want to make sure you don’t delete an event that’s been modified by someone else, include the If-Match: etag header where etag is the ETag returned when you added the event. If you don’t care, just send If-Match: *

Miscellaneous

I didn’t know how to send an HTTP DELETE with PHP but it turns out to be pretty simple. Just set the curl option CURLOPT_CUSTOMREQUEST to ‘DELETE’ and off you go.

Also, there are currently two versions of the Google Data Protocol. The current is Version 2 and Version 1 is slowly being phased out. All of the above refers to Version 2 and you want to make sure Google Calendar knows that’s the version you’re using. To that end, make sure to include the header GData-Version: 2 with every request.

Code!

And, to illustrate all of the above, here’s a class for encapsulating some of this stuff.

class GoogleCalendar {
    public $data;
    public $xml;

    public function __construct($login=null, $magicCookie="") {
        $this->data = array();

        if(!is_null($login)) {
            $this->login = $login;
        }
        $this->magicCookie = $magicCookie;
    }

    //Return the authorization header used to authenticate all requests after the first one
    protected function getAuthHeader() {
        return 'Authorization:  GoogleLogin auth="' . $this->login->getAuth() . '"';
    }

    //If the calendar we are accessing is the default, the email address is the same as the email used to login.
    //If the calendar is NOT the default, the email address can be found in the Calendar Settings
    //and should be used as the altEmail
    protected function getFeedEmail() {
        return $this->altEmail ? $this->altEmail : $this->login->email;
    }

    //Adding an entry returns the ETag value as part of the HTTP header
    //This function parses the header and attempts to find the ETag and return it
    //$retFields should be the response exploded using \n as the delimeter
    protected function getETagFromHeader($retFields) {
        return $this->getHeaderFromRegex($retFields, "/^ETag:\s*(.*?)$/");
    }

    protected function getEditLinkFromHeader($retFields) {
        return $this->getHeaderFromRegex($retFields, "/^Location:\s*(.*?)$/");
    }

    protected function getHeaderFromRegex($retFields, $regex) {
        if(is_array($retFields)) {
            foreach($retFields as $header) {
                $matches = array();

                if(preg_match("$regex", $header, $matches)) {
                    return $matches[1];
                }
            }
        }else {
            throw new Exception("The header could not be found because the header array was invalid.");
        }

    }

    //Adds an event to the calendar
    public function addEvent($params) {
        $url = "http://www.google.com/calendar/feeds/{$this->getFeedEmail()}/private/full";

        //startTime should be a time() value so we can convert it into the correct format
        $params["startTime"] = date("c", $params["startTime"]);

        //If no end-time is specified, set the end-time to 1 hour after the start-time
        if(!array_key_exists("endTime", $params)) {
            $params["endTime"] = date("c", strtotime($params["startTime"])+60*60*1);
        }

        $xml = "
                  
                  {$params["title"]}
                  {$params["content"]}
                  
                  
                  
                  
                  
                  
                ";

        //Do the initial POST to Google
        $ret = $this->calPostRequest($url, $xml);

        //If Google sends back a gsessionid, we need to make the request again
        $matches = array();
        if(preg_match('/gsessionid=(.*?)\s+/', $ret, $matches)) {
            $url .= "?gsessionid={$matches[1]}";
            $ret = $this->calPostRequest($url, $xml);
        }

        //Parse the XML response (which contains the newly added entry)
        $retFields = explode("\n", $ret);
        //print_r($retFields);
        $entryXML = simplexml_load_string($retFields[count($retFields)-1]);

        //Return an array containing the entry id (url) and the etag
        return array(
                "id"=> (string)$entryXML->id,
                "etag"=> $this->getETagFromHeader($retFields),
                "link"=> $this->getEditLinkFromHeader($retFields)
                );
    }

    public function deleteEvent($url) {
        return $this->calDeleteRequest($url);
    }

    public function calGetRequest($url) {
        $curlOpts = array();
        return $this->calCurlRequest($url, $curlOpts);
    }

    public function calPostRequest($url, $data) {
        $curlOpts = array(
            CURLOPT_POST=> true,
            CURLOPT_POSTFIELDS=> $data,
            CURLOPT_HEADER=> true,
            CURLOPT_HTTPHEADER=> array('GData-Version:  2', $this->getAuthHeader(), 'Content-Type:  application/atom+xml')
        );
        return $this->calCurlRequest($url, $curlOpts);
    }

    public function calDeleteRequest($url) {
        $curlOpts = array(
            CURLOPT_CUSTOMREQUEST=> "DELETE",
            CURLOPT_HTTPHEADER=> array('GData-Version:  2', $this->getAuthHeader(), 'If-Match:  *')
        );
        return $this->calCurlRequest($url, $curlOpts);
    }

    //This is a generic function for doing curl requests
    //It expects a url and an array of CURLOPT values.  Certain defaults are set if not provided
    private function calCurlRequest($url, $curlOpts) {
        if(!array_key_exists(CURLOPT_FOLLOWLOCATION, $curlOpts)) {
            $curlOpts[CURLOPT_FOLLOWLOCATION] = true;
        }
        if(!array_key_exists(CURLOPT_RETURNTRANSFER, $curlOpts)) {
            $curlOpts[CURLOPT_RETURNTRANSFER] = true;
        }
        if(!array_key_exists(CURLOPT_HEADER, $curlOpts)) {
            $curlOpts[CURLOPT_HEADER] = false;
        }
        if(!array_key_exists(CURLOPT_HTTPHEADER, $curlOpts)) {
            $curlOpts[CURLOPT_HTTPHEADER] = array('GData-Version:  2', $this->getAuthHeader());
        }

        $ch = curl_init($url);
        curl_setopt_array($ch, $curlOpts);
        $ret = curl_exec($ch);
        curl_close($ch);

        return $ret;
    }

    public function __get($name) {
        return $this->data[$name];
    }

    public function __set($name, $val) {
        $this->data[$name] = $val;
    }
}

Here’s a simple example that adds an entry:

define("APP_NAME", "MY APP");
$email = "my.account@gmail.com";
$password = "mypassword";
$altEmail = "this_is_the_random_email_from_the_calendar_settings";
$login = new GoogleClientLogin($email, $password, GoogleClientLogin::$CALENDAR_SERVICE, APP_NAME);

$cal = new GoogleCalendar($login);
$cal->altEmail = $altEmail;

$entryData = $cal->addEvent(array(
                "title"=> "Auto Test event",
                "content"=> "This is a test event",
                "where"=> "Test location",
                "startTime"=> time()+60*60*24*1
            ));
print_r($entryData);

Basic Authentication with Titanium.Network.HTTPClient

I’ve been trying, over the last couple of days, to figure out how to do an HTTP request with Basic Authentication using Appcelerator Titanium.

It seemed like it should be pretty easy. Just create an HTTPClient object, pass it the username and password via setBasicCredentials(), and off you go.

But it didn’t work. I got 401 – Unauthorized on every single site I tried.

Finally, after way to much research, I discovered that setBasicCredentials just doesn’t work and you have to do Basic Authentication by setting the headers yourself like this:

authstr = 'Basic ' +Titanium.Utils.base64encode(username+':'+password);
xhr.setRequestHeader('Authorization', authstr);

It’s important to note that setRequestHeader() has to be called after you’ve called open() but before you call send()

And here’s a complete function for making HTTP requests.

var httpRequest = function(params) {
    var xhr = Titanium.Network.createHTTPClient();

    xhr.onload = function() {
        if(params.hasOwnProperty('callback')) {
            if(typeof params.callback == 'function') {
                params.callback(this.responseText);
            }else {
                Ti.API.error('getXML:  Invalid callback function');
            }
        }
    };
    xhr.onerror = function() {
        Ti.API.error(this.status + ' - ' + this.statusText);
    };

    xhr.open( params.hasOwnProperty('method') ? params.method : 'GET', params.url);

    try {
        if(params.hasOwnProperty('username') && params.hasOwnProperty('password')) {
            authstr = 'Basic ' +Titanium.Utils.base64encode(params.username+':'+params.password);
            xhr.setRequestHeader('Authorization', authstr);
        }
    }catch(e) {
        Ti.API.error('Error in getXML:  ' + e.message);
    }

    xhr.send();
};

//Call it like this
httpRequest( {
        url:  'https://api.del.icio.us/v1/tags/get',
        username: 'username',
        password:  'password',
        callback:  function(resp) {
            //TODO
        }
    });