HowTo: Use the Zend Framework GData Google Data API on GoDaddy servers

InfoDabble > Tech Notes > HowTo > HowTo: Use the Zend Framework GData Google Data API on GoDaddy servers
Jump to: navigation, search

[edit] How to get the Google Data API to work with PHP on GoDaddy servers

I plan to use the base code from the HtmlToWiki extension to import clippings from Google Notebook into MediaWiki.

As part of its web application framework, Google offer the Google Data APIs, a set of web services for read/write access to applications hosted at google.com such as Calendar, Spreadsheets, Notebook, Blogger, CodeSearch, and Base. Even better, the Zend Google Data Client provides a PHP 5 component to execute queries and commands against the Google Data APIs.

And that's all you need. Unless you're using GoDaddy as a cheap host. You get what you pay for ...

Fatal error: Uncaught exception 'Zend_Gdata_HttpException' with message 'Unable to Connect to sslv2://www.google.com:443. Error #110: Connection timed out' in Zend/Gdata/AuthSub.php:106
This is one of those tech notes that shouldn't be needed in the first place.

[edit] GData Zend Framework authentication problem

This discussion comes from the topic "GData Zend Framework authentication problem" in the Google Calendar Data API newsgroup.

  • Original posts by CrazyAtlantaGuy, Erik Vande, Ryan Boyd (Google) - June 8-15, 2007
  • Edits, reformatting and additional detail by Eric Hartwell - August 20, 2007. I've added the full source for the ProxyWithCurl.php module and the corresponding changes to Gapps.php.

After several emails CrazyAtlantaGuy finally got GoDaddy support to admit, "It appears that our hosting does not support the 'POST' option when using our proxy service". Since the Google Data API's PHP code does all of its communications by POSTs, this means that the Zend Framework just won't work without changes (CrazyAtlantaGuy - June 8, 2007).

Erik Vande pointed out that Godaddy uses a proxy for the google checkout application. He used the following proxy settings and it seemed to be working better, at least to the point where it ran into different problems (Erik Vande - June 8, 2007):

$config = array( 
                'adapter'    => 'Zend_Http_Client_Adapter_Proxy', 
                'proxy_host' => 'proxy.shr.secureserver.net', 
                'proxy_port' => 3128 
        ); 
        // Instantiate a client object 
        $clientp = new Zend_Http_Client($myCalendar, $config); 
        $client = Zend_Gdata_ClientLogin::getHttpClient($user, $pass, 'cl', $clientp);

Later, CrazyAtlantaGuy reported "the latest development from the confused and distorted reality that is GoDaddy support: They say the scripts will need to be modified to make all connections using CURL" (CrazyAtlantaGuy - June 14, 2007).

[edit] Roll-Your-Own Zend Http Client Adapter with Curl for Gdata

By Ryan Boyd (Google) - June 15, 2007

After some investigation, it appears that the GoDaddy proxy servers handle HTTPS connections via tunneling.

Thus, the HTTP exchange looks like:

CONNECT www.google.com:443 HTTP/1.1 
Host: www.google.com 

HTTP/1.1 200 Connection established 
(SSL-encrypted request to www.google.com) 
(SSL-encrypted response from www.google.com) 

This is different from what Zend_Http_Client_Adapter_Proxy is currently doing, which is something like this:

POST https://www.google.com:443 HTTP/1.1 
Host: www.google.com 

(clear-text request) 

The Zend Framework classes currently connect via fsockopen, which, as far as I can tell, doesn't permit connecting via HTTP, sending the CONNECT headers and then 'upgrading' to HTTPS.

I don't want the GoDaddy users out there to have to live without Google data APIs!, so I looked at how this issue can be solved. Basically, it involves writing a Zend_Http_Client_Adapter which uses Curl rather than fsockopen. There is an adapter for curl in the incubator of zend framework, though it's not intended for use with proxies and doesn't support methods other than GET and POST currently.

So, I rolled my own-- there's no guarantees with the following code, but I was able to get it to work-- with a godaddy account and with full CRUD operations-- including authentication over HTTPS via ClientLogin (which only should be used by web applications if the username/password is part of the application configuration-- you should not be prompting users for their credentials to use with ClientLogin).

[edit] Procedure

  1. Make a copy of Zend_Http_Client_Adapter_Proxy and rename it something like Adapter_ProxyWithCurl
  2. Replace the contents of the following methods in your newly created adapter (see complete ProxyWithCurl.php code below)
  3. In your code which is using the Calendar data API, follow the suggestions of Erik above except pass the name of the adapter you just created instead of Zend_Http_Client_Adapter_Proxy (for example, see Changes to Gapps.php below). Also make sure that this newly-created adapter has a corresponding require or loadClass statement.
  4. Make a quick patch to Zend_Gdata_ClientLogin to prevent the proxy configuration from being lost after authentication is performed. You'll want to comment out the creation of the new Zend_Http_Client object in the following lines (~ line 130 of ClientLogin.php)
  5. Done -- test away.

So- this is a quick and dirty hack. However, I will be following up on this problem by:

  • Fixing the two issues in Zend_Gdata_ClientLogin-- the issue where exceptions aren't thrown, and the issue above where the client proxy configuration is lost
  • Trying to get the Curl HTTP adapter pushed into core with support for proxies and all HTTP methods

[edit] Changes to Gapps.php

function getClientLoginHttpClient($user, $pass) 
{
    $service = Zend_Gdata_Gapps::AUTH_SERVICE_NAME;
 
## $client = Zend_Gdata_ClientLogin::getHttpClient($user, $pass, $service);
     $config = array( 
                'adapter'    => 'Zend_Http_Client_Adapter_ProxyWithCurl', 
                'proxy_host' => 'proxy.shr.secureserver.net', 
                'proxy_port' => 3128 
               ); 
    // Instantiate a client object 
    $clientp = new Zend_Http_Client($myCalendar, $config); 
    $client = Zend_Gdata_ClientLogin::getHttpClient($user, $pass, 'cl', $clientp); 
 
    return $client;
}

[edit] Changes to ClientLogin.php

if ($response->getStatus() == 200) { 
            $headers['authorization'] = 'GoogleLogin auth=' . $goog_resp['Auth']; 
            //$client = new Zend_Http_Client();

[edit] ProxyWithCurl.php

<?php
 
### Hack for GData with GoDaddy
### June 15, 2007
### http://groups.google.com/group/google-calendar-help-dataapi/browse_thread/thread/8f13fb00cb518535/6edd8ca8aa4a7ca8?#6edd8ca8aa4a7ca8
 
/**
 * Zend Framework
 *
 * LICENSE
 *
 * This source file is subject to the new BSD license that is bundled
 * with this package in the file LICENSE.txt.
 * It is also available through the world-wide-web at this URL:
 * http://framework.zend.com/license/new-bsd
 * If you did not receive a copy of the license and are unable to
 * obtain it through the world-wide-web, please send an email
 * to license@zend.com so we can send you a copy immediately.
 *
 * @category   Zend
 * @package    Zend_Http
 * @subpackage Client_Adapter
 * @version    $Id: Proxy.php 5771 2007-07-18 22:06:24Z thomas $
 * @copyright  Copyright (c) 2005-2007 Zend Technologies USA Inc. (http://www.zend.com)
 * @license    http://framework.zend.com/license/new-bsd     New BSD License
 */
 
require_once 'Zend/Uri/Http.php';
require_once 'Zend/Http/Client.php';
require_once 'Zend/Http/Client/Adapter/Socket.php';
require_once 'Zend/Http/Client/Adapter/Exception.php';
 
/**
 * HTTP Proxy-supporting Zend_Http_Client adapter class, based on the default
 * socket based adapter.
 *
 * Should be used if proxy HTTP access is required. If no proxy is set, will
 * fall back to Zend_Http_Client_Adapter_Socket behavior. Just like the
 * default Socket adapter, this adapter does not require any special extensions
 * installed.
 *
 * @category   Zend
 * @package    Zend_Http
 * @subpackage Client_Adapter
 * @copyright  Copyright (c) 2005-2007 Zend Technologies USA Inc. (http://www.zend.com)
 * @license    http://framework.zend.com/license/new-bsd     New BSD License
 */
class Zend_Http_Client_Adapter_ProxyWithCurl extends Zend_Http_Client_Adapter_Socket
{
    /**
     * Parameters array
     *
     * @var array
     */
    protected $config = array(
        'ssltransport'  => 'ssl',
        'proxy_host'    => '',
        'proxy_port'    => 8080,
        'proxy_user'    => '',
        'proxy_pass'    => '',
        'proxy_auth'    => Zend_Http_Client::AUTH_BASIC
    );
 
    /**
     * Connect to the remote server
     *
     * Will try to connect to the proxy server. If no proxy was set, will
     * fall back to the target server (behave like regular Socket adapter)
     *
     * @param string  $host
     * @param int     $port
     * @param boolean $secure
     * @param int     $timeout
     */
 
    public function connect($host, $port = 80, $secure = false) 
    { 
        // If no proxy is set, fall back to Socket adapter 
        if (! $this->config['proxy_host']) { 
            return parent::connect($host, $port, $secure); 
        } 
    } 
 
    /**
     * Send request to the proxy server
     *
     * @param string        $method
     * @param Zend_Uri_Http $uri
     * @param string        $http_ver
     * @param array         $headers
     * @param string        $body
     * @return string Request as string
     */
 
    public function write($method, $uri, $http_ver = '1.1', $headers = array(), $body = '') 
    { 
        // If no proxy is set, fall back to default Socket adapter 
       if (! $this->config['proxy_host']) { 
           return parent::write($method, $uri, $http_ver, $headers, $body); 
       } 
 
 
        $host = $this->config['proxy_host']; 
        $port = $this->config['proxy_port']; 
 
        $ch = curl_init(); 
        curl_setopt($ch, CURLOPT_VERBOSE, 0); 
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); 
        curl_setopt($ch, CURLOPT_HEADER, true); 
        curl_setopt($ch, CURLOPT_PROXYTYPE, CURLPROXY_HTTP); 
        curl_setopt($ch, CURLOPT_PROXY,"http://" . $host . ':' . $port); 
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE); 
        curl_setopt($ch, CURLOPT_URL, $uri->__toString()); 
        curl_setopt($ch, CURLOPT_TIMEOUT, 120); 
 
        // Save request method for later 
        $this->method = $method; 
 
        curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method); 
 
        // Add Proxy-Authorization header 
        if ($this->config['proxy_user'] && ! isset($headers['proxy-authorization'])) { 
             $headers['proxy-authorization'] = Zend_Http_Client::encodeAuthHeader( 
                  $this->config['proxy_user'], $this->config['proxy_pass'], $this->config['proxy_auth'] 
             ); 
        } 
 
        $curlHeaders = array(); 
 
        // Add all headers to the curl header array 
        foreach ($headers as $k => $v) { 
            if (is_string($k)) $v = ucfirst($k) . ": $v"; 
            $curlHeaders[] = $v; 
        } 
        curl_setopt($ch, CURLOPT_HTTPHEADER, $curlHeaders); 
 
        if ($body != null) { 
            curl_setopt($ch, CURLOPT_POSTFIELDS, $body); 
        } 
 
        $this->result = curl_exec($ch); 
        // HACK- The server returns chunked transfer encoding at times, and it's indicated in the header 
        // However, curl and ZF both attempt to decode, leading to exceptions being thrown 
        // There has to be a way to avoid the following line (or something like it), but haven't figured it out yet 
        $this->result = str_ireplace('Transfer-encoding:', 'Transfer-encoding-old:', $this->result); 
        return $this->result; 
    } 
 
    public function read() 
    { 
        return $this->result; 
    } 
 
    /**
     * Destructor: make sure the socket is disconnected
     *
     */
    public function __destruct()
    {
        if ($this->socket) $this->close();
    }
}

[edit] References