Permalink
Find file
651 lines (591 sloc) 20.6 KB
<?php
// Allow OAuthSimple to be autoloaded instead of always including directly.
// The class_exists() call fires the autoloader.
if (! class_exists ( 'OAuthSimple' )) {
require_once __DIR__ . DIRECTORY_SEPARATOR . 'OAuthSimple.php';
}
/**
* Define a custom Exception for easy trap and detection
*/
class XeroOAuthException extends Exception {
}
class XeroOAuth {
var $_xero_defaults;
var $_xero_consumer_options;
var $_action;
var $_nonce_chars;
/**
* Creates a new XeroOAuth object
*
* @param string $config,
* the configuration settings
*/
function __construct($config) {
$this->params = array ();
$this->headers = array ();
$this->auto_fixed_time = false;
$this->buffer = null;
$this->request_params = array();
if (! empty ( $config ['application_type'] )) {
switch ($config ['application_type']) {
case "Public" :
$this->_xero_defaults = array (
'xero_url' => 'https://api.xero.com/',
'site' => 'https://api.xero.com',
'authorize_url' => 'https://api.xero.com/oauth/Authorize',
'signature_method' => 'HMAC-SHA1'
);
break;
case "Private" :
$this->_xero_defaults = array (
'xero_url' => 'https://api.xero.com/',
'site' => 'https://api.xero.com',
'authorize_url' => 'https://api.xero.com/oauth/Authorize',
'signature_method' => 'RSA-SHA1'
);
break;
case "Partner" :
$this->_xero_defaults = array (
'xero_url' => 'https://api.xero.com/',
'site' => 'https://api.xero.com',
'authorize_url' => 'https://api.xero.com/oauth/Authorize',
'signature_method' => 'RSA-SHA1'
);
break;
}
}
$this->_xero_consumer_options = array (
'request_token_path' => 'oauth/RequestToken',
'access_token_path' => 'oauth/AccessToken',
'authorize_path' => 'oauth/Authorize'
);
// Remove forced dependency on BASE_PATH constant.
// Note that __DIR__ is PHP 5.3 and above only.
$base_path = defined ( 'BASE_PATH' ) ? BASE_PATH : dirname ( __DIR__ );
$this->_xero_curl_options = array ( // you probably don't want to change any of these curl values
'curl_connecttimeout' => 30,
'curl_timeout' => 20,
// for security you may want to set this to TRUE. If you do you need
// to install the servers certificate in your local certificate store.
'curl_ssl_verifypeer' => 2,
// include ca-bundle.crt from http://curl.haxx.se/ca/cacert.pem
'curl_cainfo' => $base_path . '/certs/ca-bundle.crt',
'curl_followlocation' => false, // whether to follow redirects or not
// TRUE/1 is not a valid ssl verifyhost value with curl >= 7.28.1 and 2 is more secure as well.
// More details here: http://php.net/manual/en/function.curl-setopt.php
'curl_ssl_verifyhost' => 2,
// support for proxy servers
'curl_proxy' => false, // really you don't want to use this if you are using streaming
'curl_proxyuserpwd' => false, // format username:password for proxy, if required
'curl_encoding' => '', // leave blank for all supported formats, else use gzip, deflate, identity
'curl_verbose' => true
);
$this->config = array_merge ( $this->_xero_defaults, $this->_xero_consumer_options, $this->_xero_curl_options, $config );
}
/**
* Utility function to parse the returned curl headers and store them in the
* class array variable.
*
* @param object $ch
* curl handle
* @param string $header
* the response headers
* @return the string length of the header
*/
private function curlHeader($ch, $header) {
$i = strpos ( $header, ':' );
if (! empty ( $i )) {
$key = str_replace ( '-', '_', strtolower ( substr ( $header, 0, $i ) ) );
$value = trim ( substr ( $header, $i + 2 ) );
$this->response ['headers'] [$key] = $value;
}
return strlen ( $header );
}
/**
* Utility function to parse the returned curl buffer and store them until
* an EOL is found.
* The buffer for curl is an undefined size so we need
* to collect the content until an EOL is found.
*
* This function calls the previously defined streaming callback method.
*
* @param object $ch
* curl handle
* @param string $data
* the current curl buffer
*/
private function curlWrite($ch, $data) {
$l = strlen ( $data );
if (strpos ( $data, $this->config ['streaming_eol'] ) === false) {
$this->buffer .= $data;
return $l;
}
$buffered = explode ( $this->config ['streaming_eol'], $data );
$content = $this->buffer . $buffered [0];
$this->metrics ['tweets'] ++;
$this->metrics ['bytes'] += strlen ( $content );
if (! function_exists ( $this->config ['streaming_callback'] ))
return 0;
$metrics = $this->update_metrics ();
$stop = call_user_func ( $this->config ['streaming_callback'], $content, strlen ( $content ), $metrics );
$this->buffer = $buffered [1];
if ($stop)
return 0;
return $l;
}
/**
* Extracts and decodes OAuth parameters from the passed string
*
* @param string $body
* the response body from an OAuth flow method
* @return array the response body safely decoded to an array of key => values
*/
function extract_params($body) {
$kvs = explode ( '&', $body );
$decoded = array ();
foreach ( $kvs as $kv ) {
$kv = explode ( '=', $kv, 2 );
$kv [0] = $this->safe_decode ( $kv [0] );
$kv [1] = $this->safe_decode ( $kv [1] );
$decoded [$kv [0]] = $kv [1];
}
return $decoded;
}
/**
* Encodes the string or array passed in a way compatible with OAuth.
* If an array is passed each array value will will be encoded.
*
* @param mixed $data
* the scalar or array to encode
* @return $data encoded in a way compatible with OAuth
*/
private function safe_encode($data) {
if (is_array ( $data )) {
return array_map ( array (
$this,
'safe_encode'
), $data );
} else if (is_scalar ( $data )) {
return str_ireplace ( array (
'+',
'%7E'
), array (
' ',
'~'
), rawurlencode ( $data ) );
} else {
return '';
}
}
/**
* Decodes the string or array from it's URL encoded form
* If an array is passed each array value will will be decoded.
*
* @param mixed $data
* the scalar or array to decode
* @return $data decoded from the URL encoded form
*/
private function safe_decode($data) {
if (is_array ( $data )) {
return array_map ( array (
$this,
'safe_decode'
), $data );
} else if (is_scalar ( $data )) {
return rawurldecode ( $data );
} else {
return '';
}
}
/**
* Prepares the HTTP method for use in the base string by converting it to
* uppercase.
*
* @param string $method
* an HTTP method such as GET or POST
* @return void value is stored to a class variable
* @author themattharris
*/
private function prepare_method($method) {
$this->method = strtoupper ( $method );
}
/**
* Makes a curl request.
* Takes no parameters as all should have been prepared
* by the request method
*
* @return void response data is stored in the class variable 'response'
*/
private function curlit() {
$this->request_params = array();
// configure curl
$c = curl_init ();
$useragent = (isset ( $this->config ['user_agent'] )) ? (empty ( $this->config ['user_agent'] ) ? 'XeroOAuth-PHP' : $this->config ['user_agent']) : 'XeroOAuth-PHP';
curl_setopt_array ( $c, array (
CURLOPT_USERAGENT => $useragent,
CURLOPT_CONNECTTIMEOUT => $this->config ['curl_connecttimeout'],
CURLOPT_TIMEOUT => $this->config ['curl_timeout'],
CURLOPT_RETURNTRANSFER => TRUE,
CURLOPT_SSL_VERIFYPEER => $this->config ['curl_ssl_verifypeer'],
CURLOPT_CAINFO => $this->config ['curl_cainfo'],
CURLOPT_SSL_VERIFYHOST => $this->config ['curl_ssl_verifyhost'],
CURLOPT_FOLLOWLOCATION => $this->config ['curl_followlocation'],
CURLOPT_PROXY => $this->config ['curl_proxy'],
CURLOPT_ENCODING => $this->config ['curl_encoding'],
CURLOPT_URL => $this->sign ['signed_url'],
CURLOPT_VERBOSE => $this->config ['curl_verbose'],
// process the headers
CURLOPT_HEADERFUNCTION => array (
$this,
'curlHeader'
),
CURLOPT_HEADER => FALSE,
CURLINFO_HEADER_OUT => TRUE
) );
/* ENTRUST CERTIFICATE DEPRECATED
if ($this->config ['application_type'] == "Partner") {
curl_setopt_array ( $c, array (
// ssl client cert options for partner apps
CURLOPT_SSLCERT => $this->config ['curl_ssl_cert'],
CURLOPT_SSLKEYPASSWD => $this->config ['curl_ssl_password'],
CURLOPT_SSLKEY => $this->config ['curl_ssl_key']
) );
}
*/
if ($this->config ['curl_proxyuserpwd'] !== false)
curl_setopt ( $c, CURLOPT_PROXYUSERPWD, $this->config ['curl_proxyuserpwd'] );
if (isset ( $this->config ['is_streaming'] )) {
// process the body
$this->response ['content-length'] = 0;
curl_setopt ( $c, CURLOPT_TIMEOUT, 0 );
curl_setopt ( $c, CURLOPT_WRITEFUNCTION, array (
$this,
'curlWrite'
) );
}
switch ($this->method) {
case 'GET' :
$contentLength = 0;
break;
case 'POST' :
curl_setopt ( $c, CURLOPT_POST, TRUE );
$post_body = $this->safe_encode ( $this->xml );
curl_setopt ( $c, CURLOPT_POSTFIELDS, $post_body );
$this->request_params ['xml'] = $post_body;
$contentLength = strlen ( $post_body );
$this->headers ['Content-Type'] = 'application/x-www-form-urlencoded';
break;
case 'PUT' :
$fh = tmpfile();
if ($this->format == "file") {
$put_body = $this->xml;
} else {
$put_body = $this->safe_encode ( $this->xml );
$this->headers ['Content-Type'] = 'application/x-www-form-urlencoded';
}
fwrite ( $fh, $put_body );
rewind ( $fh );
curl_setopt ( $c, CURLOPT_PUT, true );
curl_setopt ( $c, CURLOPT_INFILE, $fh );
curl_setopt ( $c, CURLOPT_INFILESIZE, strlen ( $put_body ) );
$contentLength = strlen ( $put_body );
break;
default :
curl_setopt ( $c, CURLOPT_CUSTOMREQUEST, $this->method );
}
if (! empty ( $this->request_params )) {
// if not doing multipart we need to implode the parameters
if (! $this->config ['multipart']) {
foreach ( $this->request_params as $k => $v ) {
$ps [] = "{$k}={$v}";
}
$this->request_payload = implode ( '&', $ps );
}
curl_setopt ( $c, CURLOPT_POSTFIELDS, $this->request_payload);
} else {
// CURL will set length to -1 when there is no data
$this->headers ['Content-Length'] = $contentLength;
}
$this->headers ['Expect'] = '';
if (! empty ( $this->headers )) {
foreach ( $this->headers as $k => $v ) {
$headers [] = trim ( $k . ': ' . $v );
}
curl_setopt ( $c, CURLOPT_HTTPHEADER, $headers );
}
if (isset ( $this->config ['prevent_request'] ) && false == $this->config ['prevent_request'])
return;
// do it!
$response = curl_exec ( $c );
if ($response === false) {
$response = 'Curl error: ' . curl_error ( $c );
$code = 1;
} else {
$code = curl_getinfo ( $c, CURLINFO_HTTP_CODE );
}
$info = curl_getinfo ( $c );
curl_close ( $c );
if (isset ( $fh )) {
fclose( $fh );
}
// store the response
$this->response ['code'] = $code;
$this->response ['response'] = $response;
$this->response ['info'] = $info;
$this->response ['format'] = $this->format;
return $code;
}
/**
* Make an HTTP request using this library.
* This method doesn't return anything.
* Instead the response should be inspected directly.
*
* @param string $method
* the HTTP method being used. e.g. POST, GET, HEAD etc
* @param string $url
* the request URL without query string parameters
* @param array $params
* the request parameters as an array of key=value pairs
* @param string $format
* the format of the response. Default json. Set to an empty string to exclude the format
*
*/
function request($method, $url, $params = array(), $xml = "", $format = 'xml') {
// removed these as function parameters for now
$useauth = true;
$multipart = false;
$this->headers = array ();
if (isset ( $format )) {
switch ($format) {
case "pdf" :
$this->headers ['Accept'] = 'application/pdf';
break;
case "json" :
$this->headers ['Accept'] = 'application/json';
break;
case "xml" :
default :
$this->headers ['Accept'] = 'application/xml';
break;
}
}
if (isset ( $params ['If-Modified-Since'] )) {
$modDate = "If-Modified-Since: " . $params ['If-Modified-Since'];
$this->headers ['If-Modified-Since'] = $params ['If-Modified-Since'];
}
if ($xml !== "") {
$xml = trim($xml);
$this->xml = $xml;
}
if ($method == "POST")
$params ['xml'] = $xml;
$this->prepare_method ( $method );
$this->config ['multipart'] = $multipart;
$this->url = $url;
$oauthObject = new OAuthSimple ();
try {
$this->sign = $oauthObject->sign ( array (
'path' => $url,
'action' => $method,
'parameters' => array_merge ( $params, array (
'oauth_signature_method' => $this->config ['signature_method']
) ),
'signatures' => $this->config
) );
}
catch ( Exception $e ) {
$errorMessage = 'XeroOAuth::request() ' . $e->getMessage ();
$this->response['response'] = $errorMessage;
$this->response['helper'] = $url;
return $this->response;
}
$this->format = $format;
$curlRequest = $this->curlit ();
if ($this->response ['code'] == 401 && isset ( $this->config ['session_handle'] )) {
if ((strpos ( $this->response ['response'], "oauth_problem=token_expired" ) !== false)) {
$this->response ['helper'] = "TokenExpired";
} else {
$this->response ['helper'] = "TokenFatal";
}
}
if ($this->response ['code'] == 403) {
$errorMessage = "It looks like your Xero Entrust cert issued by Xero is either invalid or has expired. See http://developer.xero.com/api-overview/http-response-codes/#403 for more";
// default IIS page isn't informative, a little swap
$this->response ['response'] = $errorMessage;
$this->response ['helper'] = "SetupIssue";
}
if ($this->response ['code'] == 0) {
$errorMessage = "It looks like your Xero Entrust cert issued by Xero is either invalid or has expired. See http://developer.xero.com/api-overview/http-response-codes/#403 for more";
$this->response ['response'] = $errorMessage;
$this->response ['helper'] = "SetupIssue";
}
return $this->response;
}
/**
* Convert the response into usable data
*
* @param string $response
* the raw response from the API
* @param string $format
* the format of the response
* @return string the concatenation of the host, API version and API method
*/
function parseResponse($response, $format) {
if (isset ( $format )) {
switch ($format) {
case "pdf" :
$theResponse = $response;
break;
case "json" :
$theResponse = json_decode ( $response );
break;
default :
$theResponse = simplexml_load_string ( $response );
break;
}
}
return $theResponse;
}
/**
* Utility function to create the request URL in the requested format
*
* @param string $request
* the API method without extension
* @return string the concatenation of the host, API version and API method
*/
function url($request, $api = "core") {
if ($request == "RequestToken") {
$this->config ['host'] = $this->config ['site'] . '/oauth/';
} elseif ($request == "Authorize") {
$this->config ['host'] = $this->config ['authorize_url'];
$request = "";
} elseif ($request == "AccessToken") {
$this->config ['host'] = $this->config ['site'] . '/oauth/';
} else {
if (isset ( $api )) {
if ($api == "core") {
$api_stem = "api.xro";
$api_version = $this->config ['core_version'];
}
if ($api == "payroll") {
$api_stem = "payroll.xro";
$api_version = $this->config ['payroll_version'];
}
if ($api == "file") {
$api_stem = "files.xro";
$api_version = $this->config ['file_version'];
}
}
$this->config ['host'] = $this->config ['xero_url'] . $api_stem . '/' . $api_version . '/';
}
return implode ( array (
$this->config ['host'],
$request
) );
}
/**
* Refreshes the access token for partner API type applications
*
* @param string $accessToken
* the current access token for the session
* @param string $sessionHandle
* the current session handle for the session
* @return array response array from request
*/
function refreshToken($accessToken, $sessionHandle) {
$code = $this->request ( 'GET', $this->url ( 'AccessToken', '' ), array (
'oauth_token' => $accessToken,
'oauth_session_handle' => $sessionHandle
) );
if ($this->response ['code'] == 200) {
$response = $this->extract_params ( $this->response ['response'] );
return $response;
} else {
$this->response ['helper'] = "TokenFatal";
return $this->response;
}
}
/**
* Returns the current URL.
* This is instead of PHP_SELF which is unsafe
*
* @param bool $dropqs
* whether to drop the querystring or not. Default true
* @return string the current URL
*/
public static function php_self($dropqs = true)
{
$protocol = 'http';
if (isset($_SERVER['HTTPS']) && strtolower($_SERVER['HTTPS']) == 'on')
{
$protocol = 'https';
}
elseif (isset($_SERVER['SERVER_PORT']) && ($_SERVER['SERVER_PORT'] == '443'))
{
$protocol = 'https';
}
$url = sprintf('%s://%s%s', $protocol, $_SERVER['SERVER_NAME'], $_SERVER['REQUEST_URI']);
$parts = parse_url($url);
$port = $_SERVER['SERVER_PORT'];
$scheme = $parts['scheme'];
$host = $parts['host'];
$path = @$parts['path'];
$qs = @$parts['query'];
$port or $port = ($scheme == 'https') ? '443' : '80';
if (($scheme == 'https' && $port != '443') || ($scheme == 'http' && $port != '80'))
{
$host = "$host:$port";
}
$url = "$scheme://$host$path";
if (!$dropqs) return "{$url}?{$qs}";
else return $url;
}
/*
* Run some basic checks on our config options etc to make sure all is ok
*/
function diagnostics() {
$testOutput = array ();
/* ENTRUST CERTIFICATE DEPRECATED
if ($this->config ['application_type'] == 'Partner') {
if (! file_get_contents ( $this->config ['curl_ssl_cert'] )) {
$testOutput ['ssl_cert_error'] = "Can't read the Xero Entrust cert. You need one for partner API applications. http://developer.xero.com/documentation/getting-started/partner-applications/ \n";
} else {
$data = openssl_x509_parse ( file_get_contents ( $this->config ['curl_ssl_cert'] ) );
$validFrom = date ( 'Y-m-d H:i:s', $data ['validFrom_time_t'] );
if (time () < $data ['validFrom_time_t']) {
$testOutput ['ssl_cert_error'] = "Xero Entrust cert not yet valid - cert valid from " . $validFrom . "\n";
}
$validTo = date ( 'Y-m-d H:i:s', $data ['validTo_time_t'] );
if (time () > $data ['validTo_time_t']) {
$testOutput ['ssl_cert_error'] = "Xero Entrust cert expired - cert valid to " . $validFrom . "\n";
}
}
}
*/
if ($this->config ['application_type'] == 'Partner' || $this->config ['application_type'] == 'Private') {
if (! file_exists ( $this->config ['rsa_public_key'] ))
$testOutput ['rsa_cert_error'] = "Can't read the self-signed SSL cert. Private and Partner API applications require a self-signed X509 cert http://developer.xero.com/documentation/advanced-docs/public-private-keypair/ \n";
if (file_exists ( $this->config ['rsa_public_key'] )) {
$data = openssl_x509_parse ( file_get_contents ( $this->config ['rsa_public_key'] ) );
$validFrom = date ( 'Y-m-d H:i:s', $data ['validFrom_time_t'] );
if (time () < $data ['validFrom_time_t']) {
$testOutput ['ssl_cert_error'] = "Application cert not yet valid - cert valid from " . $validFrom . "\n";
}
$validTo = date ( 'Y-m-d H:i:s', $data ['validTo_time_t'] );
if (time () > $data ['validTo_time_t']) {
$testOutput ['ssl_cert_error'] = "Application cert cert expired - cert valid to " . $validFrom . "\n";
}
}
if (! file_exists ( $this->config ['rsa_private_key'] ))
$testOutput ['rsa_cert_error'] = "Can't read the self-signed cert key. Check your rsa_private_key config variable. Private and Partner API applications require a self-signed X509 cert http://developer.xero.com/documentation/advanced-docs/public-private-keypair/ \n";
if (file_exists ( $this->config ['rsa_private_key'] )) {
$cert_content = file_get_contents ( $this->config ['rsa_public_key'] );
$priv_key_content = file_get_contents ( $this->config ['rsa_private_key'] );
if (! openssl_x509_check_private_key ( $cert_content, $priv_key_content ))
$testOutput ['rsa_cert_error'] = "Application certificate and key do not match \n";
;
}
}
return $testOutput;
}
}