Your browser is very old. You might enjoy surfing the web more if you used something newer like:

Google Chrome

Even Firefox would be OK.

If you're being forced at gunpoint to use Internet Explorer, you should at least upgrade it. Version 8 is tolerable and 9 will be OK when it comes out.

Posts tagged “php”

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. The GoogleClientLogin class can be found here:

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);

Coding for Doctors in Haiti

A colleague of mine who does a lot of programming work in the medical community came to me with an interesting project.

A number of doctors were heading to work at a medical camp in Haiti treating earthquake victims and they needed an easy way to track patient information. Almost all of the doctors there have iPhones so the initial thought was to write a quick iPhone app using Appcelerator. That way, patient information could be stored locally on the iPhone until a network connection was available to sync the data up to a server somewhere.

Unfortunately the turn-around time was too tight to navigate the process of getting an app into the Apple App Store. This is a perfect example of why I dislike the Apple model of only allowing approved apps onto the iPhone. We would had a much wider range of solutions had we been able to easily distribute an app ourselves.

In the end, we had to go with a simple PHP/MySQL web application that used jQtouch to simulate the iPhone look-and-feel (with some tweaks so it would display properly in Firefox and IE). The basic web app was fairly simple. Just a couple of pages with lists of existing patients and a form for entering patient information.

Initially the web app was hosted on a server here in the U.S. but the satellite internet connection in the medical camp in Haiti proved too unreliable. In the end, one of the doctors was able to get a hold of a Windows 2003 which we installed WAMP on. With some minor tweaking, we were able to move the web app from the US-based server over to the WAMP server in Haiti. A team of Google & Apple engineers setup a wireless LAN that covered the medical camp and the doctors were able to move freely about the camp with their mobile devices entering patient information.

Additional features were added over the next several days. The camp is organized into rows of tents so the app has a mechanism to easily list patients in a specific tent or an entire row. A native iPhone app would have been nice because we could’ve used the GPS to do that sort of thing automatically. We also added the ability to upload images (unfortunately not from the iPhone itself) so the doctors now have access to x-rays and other patient photos directly from their devices.

Most of the difficulty with the whole process was related to the lack of infrastructure in Haiti. The lack of reliable Internet meant that most communication and file transfer had to happen via email and we had to make an effort to keep things small. Cell phone/satellite phone service is spotty at best. Being on the phone with these doctors and hearing the chaos in the background and hearing about the condition of the people they’re treating is unreal. I have an enormous amount of respect for these medical professionals who have traveled to Haiti and are doing their best to help.

All in all, an interesting experience with a different set of challenges and I feel privileged to have been able to contribute, albeit in a very small way. Score 1 for the power of technology.

Localizing PHP with gettext

Today, we’re going to talk about the basics of localizing (creating versions in multiple languages) a PHP site using the Gettext extension.

In theory, the steps are pretty simple.

  1. Wrap each block of text that’s going to be translated in gettext(“This is my string”) (You can use _() as a shortcut for gettext())
  2. Create translation tables for each additional language. There’s a one-to-one correspondence in the translation tables so “This is my string” will be mapped to a different string in each language.
  3. Setup which language your site is using at any given time.
  4. PHP handles the rest!

In practice, it’s a little more confusing than that.

Step 1: Create your translation tables

There are two parts to the translation files:

  1. The PO (.po) file. This is a plain text file which contains some project & charset info, then a list of PHP files, the strings in those files, and the translations of those strings.
  2. The MO (.mo) file. This is a binary file that is essentially a “compiled” version of the PO file. This is the file that actually gets read by PHP to figure out which string to display.

It’s possible to create the PO and MO files using the xgettext and msgfmt command-line tools.

However there’s a very nice GUI app called POEdit which runs on Windows, Linux, and OSX. It’s free and makes working with these files much easier so that’s the method I’m going to cover.

The first thing you want to do is create an area for your locale files to be stored. I recommend something like the following where folders are named according to their standard locale names. In fact, on Ubuntu, this didn’t work unless the name of folder containing the PO and MO files exactly matched the locale name listed in /etc/locale.aliases (minus the charset info). On Ubuntu, you may have to install the language packs for the languages you’re going to be using.

I had to manually install the language packs for German, Spanish, and Chinese for this example:

sudo apt-get install language-pack-de
sudo apt-get install language-pack-es
sudo apt-get install language-pack-zh

Then let’s create the following directory layout. I’ve been putting the locale folder in the same path as my PHP project but it technically doesn’t matter.

locale
  |
   de_DE
     |
      LC_MESSAGES
  |
   es_US
     |
     LC_MESSAGES
  |
   zh_CN
     |
     LC_MESSAGES

Now fire up POEdit and create a new catalog.
New catalog menu

Enter some relevant information about your project. The most important things here are the language and country this translation is going to be for.
Project Settings

Set the Base path to the directory where your PHP files live, then add “.” to the list of paths to scan for files.
Project Path Settings

Then save the file as messages.po (you’ll see why the name is important down the road) to the LC_MESSAGES directory you created for this particular language.
Save .po file

Now click the “Update Catalog” button to tell POEdit to scan your PHP files for strings wrapped in gettext(), _(), or any other functions you may have added to the Keywords section of the project settings.
Scan PHP Files

This will display a list of strings that will be added to the PO file.
Update Summary

Once the strings have been loaded, it’s simply a matter of entering the translations and saving to generate the MO file.
Define translation tables

Step 2: Tell PHP how to load the translation information

There are several things happening here.

First we check to see if the locale is specified as the query string. If not, we default to English. Then we specify the path to the locale directory and set up the translation “domain” ($domain = ‘messages’; tells PHP to look for MO files named messages.po).

//Make sure we specify a charset of utf-8 or lots of foreign characters (Chinese in particular) won't show up properly
header('Content-type: text/html; charset=utf-8');

$locale = ($_GET['locale']) ? $_GET['locale'] : 'en_US';

$localePath = DOCROOT . '/locale';
$domain = 'messages';

//Set the language to whichever locale we're using
putenv("LC_ALL=$locale.utf8");
setlocale(LC_ALL, "$locale.utf8");

//Specify the location and charset of the translation tables
bindtextdomain($domain, $localePath) ;
bind_textdomain_codeset($domain, 'utf8');

//Select the translation domain
textdomain($domain);

echo _("Page Title");

As long as “Page Title” exists in the translation table, the translated string should be output instead of “Page Title”.

For simple cases where English is the default, I think it’s reasonable to just put the English text in the _(“”) call (like _(“Welcome to my page!”)). In the product case where I’m going to be using this, there’s going to be a huge amount of content in big chunks so I opted to treat each string as a named indentifier (like _(“Page TItle”)) and then create a translation table for english.

Caveats

Here’s a short list of things that threw me a bit, some of which were mentioned previously:

  • I’ve only tried this with PHP 5.2+ and Apache. However the code worked perfectly on Ubuntu+Apache as well as Windows+Apache
  • You need to restart Apache anytime your PO and MO files change. This because gettext caches the translation tables and won’t reload them without a webserver restart.
  • Ubuntu needed to have language-packs installed for each language I implemented.
  • Ubuntu also needed to have the locale names match the names in /etc/locale.aliases