<?php
/**
* FireEagle OAuth+API PHP bindings
*
* Copyright (C) 2007-08 Yahoo! Inc
*
* See http://fireeagle.yahoo.net/developer/documentation/walkthru_php
* for usage instructions.
*
*/
require_once('class.Oauth.php');
if (!function_exists("hash_hmac")) {
// Earlier versions of PHP5 are missing hash_hmac(). Here's a
// pure-PHP version in case you're using one of them.
function hash_hmac($algo, $data, $key) {
// Thanks, Kellan: http://laughingmeme.org/code/hmacsha1.php.txt
if ($algo != 'sha1') throw new Exception("fireeagle.php's hash_hmac() can only do sha1, sorry");
$blocksize = 64;
$hashfunc = 'sha1';
if (strlen($key)>$blocksize)
$key = pack('H*', $hashfunc($key));
$key = str_pad($key,$blocksize,chr(0x00));
$ipad = str_repeat(chr(0x36),$blocksize);
$opad = str_repeat(chr(0x5c),$blocksize);
$hmac = pack(
'H*',$hashfunc(
($key^$opad).pack(
'H*',$hashfunc(
($key^$ipad).$data
)
)
)
);
return $hmac;
}
}
// Various things that can go wrong
class FireEagleException extends Exception {
const TOKEN_REQUIRED = 1; // call missing an oauth request/access token
const LOCATION_REQUIRED = 2; // call to update() without a location
const REMOTE_ERROR = 3; // FE sent an error
const REQUEST_FAILED = 4; // empty or malformed response from FE
const CONNECT_FAILED = 5; // totally failed to make an HTTP request
const INTERNAL_ERROR = 6; // totally failed to make an HTTP request
const CONFIG_READ_ERROR = 7; // can't find or parse fireeaglerc
const REMOTE_SUCCESS = 0; // Request succeeded.
const REMOTE_UPDATE_PROHIBITED = 1; // Update not permitted for that user.
const REMOTE_UPDATE_ONLY = 2; // Update successful, but read access prohibited.
const REMOTE_QUERY_PROHIBITED = 3; // Query not permitted for that user.
const REMOTE_SUSPENDED = 4; // User account is suspended.
const REMOTE_PLACE_NOT_FOUND = 6; // Place can't be identified.
const REMOTE_USER_NOT_FOUND = 7; // Authentication token can't be matched to a user.
const REMOTE_INVALID_QUERY = 8; // Invalid location query.
const REMOTE_IS_FROB = 10; // Token provided is a request token, not an auth token.
const REMOTE_NOT_VALIDATED = 11; // Request token has not been validated.
const REMOTE_REQUEST_TOKEN_REQUIRED = 12; // Token provided must be an access token.
const REMOTE_EXPIRED = 13; // Token has expired.
const REMOTE_GENERAL_TOKEN_REQUIRED = 14; // Token provided must be an general purpose token.
const REMOTE_UNKNOWN_CONSUMER = 15; // Unknown consumer key.
const REMOTE_UNKNOWN_TOKEN = 16; // Token not found.
const REMOTE_BAD_IP_ADDRESS = 17; // Request made from non-blessed ip address.
const REMOTE_OAUTH_CONSUMER_KEY_REQUIRED = 20; // oauth_consumer_key parameter required.
const REMOTE_OAUTH_TOKEN_REQUIRED = 21; // oauth_token parameter required.
const REMOTE_BAD_SIGNATURE_METHOD = 22; // Unsupported signature method.
const REMOTE_INVALID_SIGNATURE = 23; // Invalid OAuth signature.
const REMOTE_REPEATED_NONCE = 24; // Provided nonce has been seen before.
const REMOTE_YAHOOAPIS_REQUIRED = 30; // All api methods should use fireeagle.yahooapis.com.
const REMOTE_SSL_REQUIRED = 31; // SSL / https is required.
const REMOTE_RATE_LIMITING = 32; // Rate limit/IP Block due to excessive requests.
const REMOTE_INTERNAL_ERROR = 50; // Internal error occurred; try again later.
public $response; // for REMOTE_ERROR codes, this is the response from FireEagle (useful: $response->code and $response->message)
function __construct($msg, $code, $response=null) {
parent::__construct($msg, $code);
$this->response = $response;
}
}
/**
* FireEagle API access helper class.
*/
class FireEagle {
public static $FE_ROOT = "http://fireeagle.yahoo.net";
public static $FE_API_ROOT = "https://fireeagle.yahooapis.com";
public static $FE_DEBUG = false; // set to true to print out debugging info
public static $FE_DUMP_REQUESTS = false; // set to a pathname to dump out http requests to a log
// Set FireEagle::$FE_PROXY_HOST and $FE_PROXY_PORT to use an HTTP proxy
public static $FE_PROXY_HOST = NULL;
public static $FE_PROXY_PORT = 0;
// Proxy authentication
public static $FE_PROXY_AUTH_METHOD = CURLAUTH_BASIC;
// No authentication by default.
public static $FE_PROXY_AUTH_TOKEN = NULL; //[user]:[passwd] for auth.
// OAuth URLs
function requestTokenURL() { return self::$FE_API_ROOT.'/oauth/request_token'; }
function authorizeURL() { return self::$FE_ROOT.'/oauth/authorize'; }
function accessTokenURL() { return self::$FE_API_ROOT.'/oauth/access_token'; }
// API URLs
function methodURL($method) { return self::$FE_API_ROOT.'/api/0.1/'.$method.'.json'; }
function __construct($consumerKey,
$consumerSecret,
$oAuthToken = null,
$oAuthTokenSecret = null) {
$this->sha1_method = new OAuthSignatureMethod_HMAC_SHA1();
$this->consumer = new OAuthConsumer($consumerKey, $consumerSecret, NULL);
if (!empty($oAuthToken) && !empty($oAuthTokenSecret)) {
$this->token = new OAuthConsumer($oAuthToken, $oAuthTokenSecret);
} else {
$this->token = NULL;
}
}
// read consumer key and secret, and optionally fireeagle auth and api urls, from a .fireeaglerc file
public static function from_fireeaglerc($fn, $token=null, $secret=null) {
$text = @file_get_contents($fn);
if ($text === false) throw new FireEagleException("Could not read $fn", FireEagleException::CONFIG_READ_ERROR);
$info = array();
foreach (preg_split("/\n/", $text) as $line) {
$line = trim(preg_replace("/#.*/", "", $line));
if (empty($line)) continue;
if (!preg_match("/^([^\s=]+)\s*\=\s*(.*)$/", $line, $m)) throw new FireEagleException("Failed to parse line '$line' in $fn", FireEagleException::CONFIG_READ_ERROR);
list(, $k, $v) = $m;
$info[$k] = $v;
}
if (empty($info['consumer_key'])) throw new FireEagleException("Missing consumer_key in $fn", FireEagleException::CONFIG_READ_ERROR);
if (empty($info['consumer_secret'])) throw new FireEagleException("Missing consumer_secret in $fn", FireEagleException::CONFIG_READ_ERROR);
if (isset($info['api_server'])) self::$FE_API_ROOT = self::build_server_url($info, 'api');
if (isset($info['auth_server'])) self::$FE_ROOT = self::build_server_url($info, 'auth');
return new FireEagle($info['consumer_key'], $info['consumer_secret'], $token, $secret);
}
private static function build_server_url($info, $role) {
$proto = isset($info["${role}_protocol"]) ? $info["${role}_protocol"] : 'https';
$default_port = ($proto == 'https' ? 443 : 80);
$port = isset($info["${role}_port"]) ? $info["${role}_port"] : $default_port;
$url = $proto . "://" . $info["${role}_server"];
if ($port != $default_port) $url .= ":" . $port;
return $url;
}
/**
* Get a request token for authenticating your application with FE.
*
* @returns a key/value pair array containing: oauth_token and
* oauth_token_secret.
*/
public function getRequestToken() {
$r = $this->oAuthRequest($this->requestTokenURL());
$token = $this->oAuthParseResponse($r);
$this->token = new OAuthConsumer($token['oauth_token'], $token['oauth_token_secret']); // use this token from now on
if (self::$FE_DUMP_REQUESTS) self::dump("Now the user is redirected to ".$this->getAuthorizeURL($token['oauth_token'])."\nOnce the user returns, via the callback URL for web authentication or manually for desktop authentication, we can get their access token and secret by calling /oauth/access_token.\n\n");
return $token;
}
public function request_token() { return $this->getRequestToken(); }
/**
* Get the URL to redirect to to authorize the user and validate a
* request token.
*
* @returns a string containing the URL to redirect to.
*/
public function getAuthorizeURL($token) {
// $token can be a string, or an array in the format returned by getRequestToken().
if (is_array($token)) $token = $token['oauth_token'];
return $this->authorizeURL() . '?oauth_token=' . $token;
}
public function authorize($token) { return $this->getAuthorizeURL($token); }
/**
* Exchange the request token and secret for an access token and
* secret, to sign API calls.
*
*
* @returns array("oauth_token" => the access token,
* "oauth_token_secret" => the access secret)
*/
public function getAccessToken($token=NULL) {
$this->requireToken();
$r = $this->oAuthRequest($this->accessTokenURL());
$token = $this->oAuthParseResponse($r);
$this->token = new OAuthConsumer($token['oauth_token'], $token['oauth_token_secret']); // use this token from now on
return $token;
}
public function access_token() { return $this->getAccessToken(); }
/**
* Generic method call function. You can use this to get the raw
* output from an API method, or to call future API methods.
*
* e.g.
* Get a user's location: $fe->call("user")
* or $fe->user()
* Set a user's location: $fe->call("update", array("q" => "new york, new york"))
* or $fe->update(array("q" => "new york, new york"))
*/
public function call($method, $params=array(), $request_method=NULL) {
$this->requireToken();
$r = $this->oAuthRequest($this->methodURL($method), $params, $request_method);
return $this->parseJSON($r);
}
// --- Wrappers for individual methods ---
/**
* Wrapper for 'user' API method, which fetches the current location
* for a user.
*/
public function user() {
$r = $this->call("user");
// add latitudes and longitudes, and extract best guess
if (isset($r->user->location_hierarchy)) {
$r->user->best_guess = NULL;
foreach ($r->user->location_hierarchy as &$loc) {
$c = $loc->geometry->coordinates;
switch ($loc->geometry->type) {
case 'Box': // DEPRECATED
$loc->bbox = $c;
$loc->longitude = ($c[0][0] + $c[1][0]) / 2;
$loc->latitude = ($c[0][1] + $c[1][1]) / 2;
$loc->geotype = 'box';
break;
case 'Polygon':
$loc->bbox = $bbox = $loc->geometry->bbox;
$loc->longitude = ($bbox[0][0] + $bbox[1][0]) / 2;
$loc->latitude = ($bbox[0][1] + $bbox[1][1]) / 2;
$loc->geotype = 'box';
break;
case 'Point':
list($loc->longitude, $loc->latitude) = $c;
$loc->geotype = 'point';
break;
}
if ($loc->best_guess) $r->user->best_guess = $loc; // add shortcut to get 'best guess' loc
unset($loc);
}
}
return $r;
}
/**
* Wrapper for 'update' API method, to set a user's location.
*/
public function update($args=array()) {
if (empty($args)) throw new FireEagleException("FireEagle::update() needs a location", FireEagleException::LOCATION_REQUIRED);
return $this->call("update", $args);
}
/**
* Wrapper for 'lookup' API method, to run a location query without
* setting the user's location (so an application can show a list of
* possibilities that match a user-supplied query -- not to be used
* as a generic geocoder).
*/
public function lookup($args=array()) {
if (!is_array($args)) throw new FireEagleException("\$args parameter to FireEagle::lookup() should be an array", FireEagleException::LOCATION_REQUIRED);
if (empty($args)) throw new FireEagleException("FireEagle::lookup() needs a location", FireEagleException::LOCATION_REQUIRED);
return $this->call("lookup", $args, "GET");
}
/**
* Wrapper for 'recent' API method
*/
public function recent($since=NULL, $per_page=NULL, $page=NULL) {
$params = array(
"per_page" => ($per_page === NULL) ? 10 : $per_page,
"page" => ($page === NULL) ? 1 : $page,
);
if (!empty($since)) $params['time'] = $since;
return $this->call("recent", $params, "GET");
}
/**
* Wrapper for 'within' API method
*/
public function within($params=array()) {
return $this->call("within", $params, "GET");
}
// --- Internal bits and pieces ---
protected function parseJSON($json) {
$r = json_decode($json);
if (empty($r)) throw new FireEagleException("Empty JSON response", FireEagleException::REQUEST_FAILED);
if (isset($r->rsp) && $r->rsp->stat != 'ok') {
throw new FireEagleException($r->rsp->code.": ".$r->rsp->message, FireEagleException::REMOTE_ERROR, $r->rsp);
}
return $r;
}
protected function requireToken() {
if (!isset($this->token)) {
throw new FireEagleException("This function requires an OAuth token", FireEagleException::TOKEN_REQUIRED);
}
}
// Parse a URL-encoded OAuth response
protected function oAuthParseResponse($responseString) {
$r = array();
foreach (explode('&', $responseString) as $param) {
$pair = explode('=', $param, 2);
if (count($pair) != 2) continue;
$r[urldecode($pair[0])] = urldecode($pair[1]);
}
return $r;
}
// Format and sign an OAuth / API request
function oAuthRequest($url, $args=array(), $method=NULL) {
if (empty($method)) $method = empty($args) ? "GET" : "POST";
$req = OAuthRequest::from_consumer_and_token($this->consumer, $this->token, $method, $url, $args);
$req->sign_request($this->sha1_method, $this->consumer, $this->token);
if (self::$FE_DEBUG) {
echo "<div>[OAuth request: <blockquote><code>".nl2br(htmlspecialchars(var_export($req, TRUE)))."</code><br>base string: ".htmlspecialchars($req->base_string)."</blockquote>]</div>";
}
if (self::$FE_DUMP_REQUESTS) {
$k = $this->consumer->secret . "&";
if ($this->token) $k .= $this->token->secret;
self::dump("---\n\nOAUTH REQUEST TO $url");
if (!empty($args)) self::dump(" WITH PARAMS ".json_encode($args));
self::dump("\n\nBase string: ".$req->base_string."\nSignature string: $k\n");
}
switch ($method) {
case 'GET': return $this->http($req->to_url());
case 'POST': return $this->http($req->get_normalized_http_url(), $req->to_postdata());
}
}
// Make an HTTP request, throwing an exception if we get anything other than a 200 response
public function http($url, $postData=null) {
if (self::$FE_DEBUG) {
echo "[FE HTTP request: url: ".htmlspecialchars($url).", post data: ".htmlspecialchars(var_export($postData, TRUE))."]";
}
if (self::$FE_DUMP_REQUESTS) {
self::dump("Final URL: $url\n\n");
$url_bits = parse_url($url);
if (isset($postData)) {
self::dump("POST ".$url_bits['path']." HTTP/1.0\nHost: ".$url_bits['host']."\nContent-Type: application/x-www-urlencoded\nContent-Length: ".strlen($postData)."\n\n$postData\n");
} else {
$get_url = $url_bits['path'];
if ($url_bits['query']) $get_url .= '?' . $url_bits['query'];
self::dump("GET $get_url HTTP/1.0\nHost: ".$url_bits['host']."\n\n");
}
}
$ch = curl_init();
if (defined("CURL_CA_BUNDLE_PATH")) curl_setopt($ch, CURLOPT_CAINFO, CURL_CA_BUNDLE_PATH);
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 30);
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
if (isset($postData)) {
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $postData);
}
if (self::$FE_PROXY_HOST) {
curl_setopt($ch, CURLOPT_PROXY, self::$FE_PROXY_HOST);
curl_setopt($ch, CURLOPT_PROXYPORT, self::$FE_PROXY_PORT);
if (self::$FE_PROXY_AUTH_TOKEN) {
curl_setopt($ch, CURLOPT_PROXYUSERPWD, self::$FE_PROXY_AUTH_TOKEN);
curl_setopt($ch, CURLOPT_PROXYAUTH, self::$FE_PROXY_AUTH_METHOD);
}
}
$response = curl_exec($ch);
$status = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
$ct = curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
if ($ct) $ct = preg_replace("/;.*/", "", $ct); // strip off charset
if (!$status) {
$e_msg = "Connection to $url failed.";
if (!self::$FE_PROXY_HOST) {
$e_msg .= " You may need to enable proxy access by setting the FireEagle::\$FE_PROXY_* variables.";
}
throw new FireEagleException($e_msg, FireEagleException::CONNECT_FAILED);
}
if ($status != 200) {
if ($ct == "application/json") {
$r = json_decode($response);
if ($r && isset($r->rsp) && $r->rsp->stat != 'ok') {
throw new FireEagleException($r->rsp->code.": ".$r->rsp->message, FireEagleException::REMOTE_ERROR, $r->rsp);
}
}
throw new FireEagleException("Request to $url failed: HTTP error $status ($response)", FireEagleException::REQUEST_FAILED);
}
if (self::$FE_DUMP_REQUESTS) {
self::dump("HTTP/1.0 $status OK\n");
if ($ct) self::dump("Content-Type: $ct\n");
$cl = curl_getinfo($ch, CURLINFO_CONTENT_LENGTH_DOWNLOAD);
if ($cl) self::dump("Content-Length: $cl\n");
self::dump("\n$response\n\n");
}
curl_close ($ch);
if (self::$FE_DEBUG) {
echo "[HTTP response: <code>".nl2br(htmlspecialchars($response))."</code>]";
}
return $response;
}
private function dump($text) {
if (!self::$FE_DUMP_REQUESTS) throw new Exception('FireEagle::$FE_DUMP_REQUESTS must be set to enable request trace dumping');
file_put_contents(self::$FE_DUMP_REQUESTS, $text, FILE_APPEND);
}
}