<?php
/**
* Wave Framework <http://github.com/kristovaher/Wave-Framework>
* PHP API Wrapper Class
*
* Main purpose of an API Wrapper is to make it easier to make API requests over HTTP to a system
* built on Wave Framework. API Wrapper class does everything for the developer without requiring
* the developer to learn the ins and outs of technical details about how to build an API request.
* Wave Framework comes with two separate API authentication methods, one more secure than the
* other, both which are handled by this Wrapper class.
*
* @package API
* @author Kristo Vaher <kristo@waher.net>
* @copyright Copyright (c) 2012, Kristo Vaher
* @license GNU Lesser General Public License Version 3
* @tutorial /doc/pages/wrapper_php.htm
* @since 2.0.0
* @version 3.7.2
*/
class WWW_Wrapper {
/**
* This is the address and URL of the API that the Wrapper will connect to. The API address
* must be for Wave Framework API. This value is set either in object creation or when
* setting 'www-address' input variable.
*/
private $apiAddress;
/**
* This holds the current language of the API, it can be useful if the API commands return
* language-specific responses and translations from the API. This variable is set by sending
* 'www-language' input variable.
*/
private $apiLanguage=false;
/**
* This holds information about current API state, such as profile name, secret key and
* various API-related flags for callbacks, asyncrhonous status and more. This variable is
* passed around per each API call.
*/
private $apiState=array(
'apiProfile'=>false,
'apiSecretKey'=>false,
'apiToken'=>false,
'apiPublicToken'=>false,
'apiHashValidation'=>true,
'apiStateKey'=>false,
'apiVersion'=>false,
'headers'=>false,
'returnHash'=>false,
'returnTimestamp'=>false,
'trueCallback'=>false,
'falseCallback'=>false,
'errorCallback'=>false,
'trueCallbackParameters'=>false,
'falseCallbackParameters'=>false,
'requestTimeout'=>10,
'timestampDuration'=>60,
'unserialize'=>true,
'lastModified'=>false
);
/**
* This variable holds the last known error message returned from the API.
*/
public $errorMessage=false;
/**
* This variable holds the last known response code returned from the API.
*/
public $responseCode=false;
/**
* Input data is a variable that stores all the plain-text input sent with the API request,
* it's a key-value pair of variables and their values for the API.
*/
private $inputData=array();
/**
* Crypted input is an array of keys and values that holds data that will be encrypted
* prior to be sent to API. This will be encrypted with the session token of the API in
* serialized form.
*/
private $cryptedData=array();
/**
* This array stores keys and values for files that will be sent to API. Key is the 'input
* file name' and value is the location of the file in filesystem.
*/
private $inputFiles=array();
/**
* This flag holds state about support for cURL. cURL will be used to make requests unless
* it is not enabled on the server.
*/
private $curlEnabled=false;
/**
* This is an array that gathers log information about the requests made through the API
* that can be used for debugging purposes should something go wrong.
*/
private $log=array();
/**
* This is a flag that halts the entire functionality of the Wrapper object, if it is set.
* Once this happens you should check the log to see what went wrong.
*/
private $criticalError=false;
/**
* This variable holds the address for the file that is used as a cookie container in the
* file system. This allows Wrapper to use cookies when making API requests.
*/
private $cookieContainer=false;
/**
* This variable holds the address for the file that is used as a certificate container in the
* file system. When this is set, then certificates are validated when they are used.
*/
private $certificateContainer=false;
/**
* This is the user-agent string of the API Wrapper and it is sent by the Wrapper when making
* cURL requests. It is useful later on to determine where the requests come from. Note that
* when cURL is not supported and file_get_contents() makes the request, then user agent is
* not sent with the request.
*/
private $userAgent='WaveFramework/3.6.9 (PHP)';
/**
* This is the GET string maximum length. Most servers should easily be able to deal with
* 2048 bytes of request string length, but this value can be changed by submitting a
* different length with 'www-get-length' input value.
*/
private $getLimit=2048;
/**
* If this value is set, then API log will be reset after each API request. This value can
* be sent with 'www-reset-log' keyword sent to Wrapper.
*/
private $resetLog=true;
/**
* Wrapper object creation requires an 'address', which is the address that Wrapper will make
* API requests to. If this is not defined, then 'address' assumes that the system it makes
* requests to is the same where the API is loaded from. 'language' is a language keyword from
* the system that API makes a connection with and is used whenever language-specific results
* are returned from API.
*
* @param boolean|string $address API address, default value is current domain presumed API address
* @param boolean|string $language language keyword, default value is current document language
* @return WWW_Wrapper
*/
public function __construct($address=false,$language=false){
// For cases when the API address is not set
if(!$address){
$address=((isset($_SERVER['HTTPS']) && ($_SERVER['HTTPS']==1 || $_SERVER['HTTPS']=='on'))?'https://':'http://').$_SERVER['HTTP_HOST'].'/json.api';
}
// If language is set, then this language is used across API
if($language){
$this->apiLanguage=$language;
}
// This should be URL to API of Wave Framework
$this->apiAddress=$address;
// This checks for cURL support, which is required for making POST requests
// cURL is also faster than file_get_contents() method
if(extension_loaded('curl')){
// Flag is checked during request creation
$this->curlEnabled=true;
} elseif(!function_exists('ini_get') || ini_get('allow_url_fopen')!=1){
// This means that requests cannot be made at all
$this->criticalError=true;
// If cURL is enabled, then file_get_contents() requires PHP setting to make requests to URL's
$this->responseCode=213;
$this->errorMessage='Cannot make URL requests: Cannot detect if PHP can make URL requests, please enable allow_url_fopen setting or ini_get() function';
$this->log[]=$this->errorMessage;
}
// JSON is required
if(!function_exists('json_encode')){
// This means that requests cannot be made at all
$this->criticalError=true;
$this->responseCode=214;
$this->errorMessage='Cannot serialize data: JSON is required for API requests to work properly';
$this->log[]=$this->errorMessage;
}
// Log entry
$this->log[]='Wave API Wrapper object created with API address: '.$address;
}
// SETTINGS
/**
* This method returns current log of the API wrapper. If $implode is set, then the
* value of $implode is used as a character to implode the log with. Otherwise the
* log is returned as an array.
*
* @param boolean|string $implode string to implode the log with
* @return array/string depending if imploded
*/
public function returnLog($implode=false){
// Log entry for log access
$this->log[]='Returning log';
// Imploding, if requested
if(!$implode){
return $this->log;
} else {
return implode($implode,$this->log);
}
}
/**
* This method clears the API log. This method can be called manually or is called
* automatically if log is assigned to be reset with each new API request made by
* the object.
*
* @return boolean
*/
public function clearLog(){
$this->log=array();
$this->log[]='Log cleared';
return true;
}
/**
* This method allows to set cookie container for cURL calls. If this is set to false,
* then cookies are not used at all. $location is the file that is used for cookie
* container, it is automatically created if the file does not exist.
*
* @param boolean|string $location cookie container file location in filesystem
* @return boolean
*/
public function setCookieContainer($location=false){
// If value is anything but false
if($location){
// Testing if file exists or attempting to create that file
if(file_exists($location) && is_writable($location)){
$this->cookieContainer=$location;
$this->log[]='Cookie container set to: '.$location;
return true;
} elseif(file_put_contents($location,'')){
$this->cookieContainer=$location;
$this->log[]='Cookie container set to: '.$location;
return true;
} else {
// Cookie container is not accessible
$this->cookieContainer=false;
$this->log[]='Cannot set cookie container to: '.$location;
return false;
}
} else {
$this->cookieContainer=false;
$this->log[]='Cookies are turned off';
return true;
}
}
/**
* This sets the CA Cert container file. This file can be gotten from various sources,
* like http://curl.haxx.se/docs/caextract.html and can be used to verify host and
* peer data with cURL requests.
*
* @param boolean|string $location cookie container file location in filesystem
* @return boolean
*/
public function setCertificateContainer($location=false){
// If value is anything but false
if($location){
// Testing if file exists or attempting to create that file
if(file_exists($location)){
$this->certificateContainer=$location;
$this->log[]='Certificate container set to: '.$location;
return true;
} else {
// Certificate container is not found
$this->certificateContainer=false;
$this->log[]='Cannot set certificate container to: '.$location;
return false;
}
} else {
$this->certificateContainer=false;
$this->log[]='Certificate container is not used';
return true;
}
}
/**
* This method will clear and delete all cookies stored in cookie container defined
* in $location or in general. Warning, this method technically removes the contents
* of any writable file if set in $location. If $location is not set, then it attempts
* to use the previously defined cookie container.
*
* @param boolean|string $location location of cookies file, if this is not set then uses current one
* @return boolean
*/
public function clearCookieContainer($location=false){
// If location was not sent with the request, then using the existing one
if(!$location){
$location=$this->cookieContainer;
}
if($location){
if(file_exists($location) && is_writable($location) && file_put_contents($location,'')){
$this->log[]='Cookies cleared';
return true;
} else {
$this->log[]='Cannot clear cookies';
return false;
}
} else {
return false;
}
}
/**
* This method returns currently used token, if one exists. This can be stored for
* subsequent requests with Wrapper (or manually over HTTP).
*
* @return string or false if token does not exist
*/
public function getToken(){
// Returning from the state array
return $this->apiState['apiToken'];
}
// INPUT
/**
* This method is used to set an input value in the API Wrapper. $input is the key
* to set and $value is the value of the input key. $input can also be an array,
* in which case multiple input values will be set in the same call. This method
* calls private inputSetter() function that checks the input value for any internal
* flags that might not actually be sent as an input to the API.
*
* @param string|array $input key of the input data, or an array of keys and values
* @param boolean|string $value value of the input data
* @return boolean
*/
public function setInput($input,$value=false){
// If this is an array then it populates input array recursively
if(is_array($input)){
foreach($input as $key=>$val){
// Value is filtered through inputSetter function
$this->inputSetter($key,$val);
}
} else {
// Value is filtered through inputSetter function
$this->inputSetter($input,$value);
}
return true;
}
/**
* This is a helper function that setInput() method uses to actually assign $value
* to the $input keyword. A lot of the keywords set carry additional functionality
* that may entirely be API Wrapper specific. This method also creates a log entry
* for any value that is changed or set.
*
* @param string $input input data key
* @param string $value input data value
* @return boolean
*/
private function inputSetter($input,$value){
// Different input keys carry additional functionality
switch($input){
case 'www-api':
$this->apiAddress=$value;
$this->log[]='API address changed to: '.$value;
break;
case 'www-hash-validation':
$this->apiState['apiHashValidation']=$value;
if($value){
$this->log[]='API hash validation is used';
} else {
$this->log[]='API hash validation is not used';
}
break;
case 'www-secret-key':
$this->apiState['apiSecretKey']=$value;
$this->log[]='API secret key set to: '.$value;
break;
case 'www-token':
$this->apiState['apiToken']=$value;
$this->log[]='API session token set to: '.$value;
break;
case 'www-profile':
$this->apiState['apiProfile']=$value;
$this->log[]='API profile set to: '.$value;
break;
case 'www-version':
$this->apiState['apiVersion']=$value;
$this->log[]='API version set to: '.$value;
break;
case 'www-state':
$this->apiState['apiStateKey']=$value;
$this->log[]='API state check key set to: '.$value;
break;
case 'www-headers':
$this->apiState['headers']=$value;
if($value){
$this->log[]='System specific parameters will be returned as headers';
} else {
$this->log[]='System specific parameters will be returned as part of response';
}
break;
case 'www-return-hash':
$this->apiState['returnHash']=$value;
if($value){
$this->log[]='API request will require hash validation';
} else {
$this->log[]='API request will not require hash validation';
}
break;
case 'www-return-timestamp':
$this->apiState['returnTimestamp']=$value;
if($value){
$this->log[]='API request will require timestamp validation';
} else {
$this->log[]='API request will not require timestamp validation';
}
break;
case 'www-public-token':
$this->apiState['apiPublicToken']=$value;
if($value){
$this->log[]='API public token set to: '.$value;
} else {
$this->log[]='API public token unset';
}
break;
case 'www-return-type':
$this->inputData[$input]=$value;
$this->log[]='Input value of "'.$input.'" set to: '.$value;
if($value!='json' && $value!='serializedarray' && $value!='querystring'){
$this->apiState['unserialize']=false;
$this->log[]='API result cannot be unserialized, setting unserialize flag to false';
}
break;
case 'www-request-timeout':
$this->apiState['requestTimeout']=$value;
$this->log[]='API request timeout set to: '.$value;
break;
case 'www-unserialize':
$this->apiState['unserialize']=$value;
$this->log[]='API serialization value set to: '.$value;
break;
case 'www-true-callback':
$this->apiState['trueCallback']=$value;
if($value){
if(gettype($value)!=='object'){
$this->log[]='API return true/success callback set to: '.$value.'()';
} else {
$this->log[]='API return true/success callback uses an anonymous function';
}
}
break;
case 'www-false-callback':
$this->apiState['falseCallback']=$value;
if($value){
if(gettype($value)!=='object'){
$this->log[]='API return false/failure callback set to: '.$value.'()';
} else {
$this->log[]='API return false/failure callback uses an anonymous function';
}
}
break;
case 'www-error-callback':
$this->apiState['errorCallback']=$value;
if($value){
if(gettype($value)!=='object'){
$this->log[]='API return error callback set to: '.$value.'()';
} else {
$this->log[]='API return error callback uses an anonymous function';
}
}
break;
case 'www-true-callback-parameters':
$this->apiState['trueCallbackParameters']=$value;
if($value){
$this->log[]='API return true/success callback parameters set';
}
break;
case 'www-false-callback-parameters':
$this->apiState['falseCallbackParameters']=$value;
if($value){
$this->log[]='API return false/failure callback parameters set';
}
break;
case 'www-last-modified':
$this->apiState['lastModified']=$value;
$this->log[]='API last-modified request time set to: '.$value;
break;
case 'www-language':
$this->apiLanguage=$value;
if($value){
$this->log[]='API result language set to: '.$value;
} else {
$this->log[]='API result language uninitialized';
}
break;
case 'www-get-limit':
$this->getLimit=$value;
$this->log[]='Maximum GET string length is set to: '.$value;
break;
case 'www-reset-log':
$this->resetLog=$value;
if($value){
$this->log[]='Log is reset after each new request';
} else {
$this->log[]='Log is kept for multiple requests';
}
break;
case 'www-timestamp-duration':
$this->apiState['timestampDuration']=$value;
$this->log[]='API valid timestamp duration set to: '.$value;
break;
case 'www-output':
$this->log[]='Ignoring www-output setting, wrapper always requires output to be set to true';
break;
case 'www-cookie-container':
return $this->setCookieContainer($value);
break;
case 'www-certificate-container':
return $this->setcertificateContainer($value);
break;
default:
// True/false conversions for input strings
if($value===true){
$value=1;
} elseif($value===false){
$value=0;
}
$this->inputData[$input]=$value;
$this->log[]='Input value of "'.$input.'" set to: '.$value;
break;
}
return true;
}
/**
* This method sets a crypted input data that will be encrypted with secret key or a
* token prior to making the HTTP request. This allows to transmit secure data across
* servers. Note that crypted input should not be used when hash validation is not used
* for making a request, since the token or secret key would also be sent with the
* request. $input is the keyword and $value is the value. $input can also be an array
* of keys and values.
*
* @param string|array $input input data key or an array of keys and values
* @param boolean|string $value input data value
* @return boolean
*/
public function setCryptedInput($input,$value=false){
// If this is an array then it populates input array recursively
if(is_array($input)){
foreach($input as $key=>$val){
// Value is converted to string to make sure that json_encode() includes quotes in hash calculations
$this->cryptedData[$key]=$val;
$this->log[]='Crypted input value of "'.$key.'" set to: '.$val;
}
} else {
// Value is simply added to inputData array
$this->cryptedData[$input]=$value;
$this->log[]='Crypted input value of "'.$input.'" set to: '.$value;
}
return true;
}
/**
* This method sets files that will be uploaded with the API request. $file is the name
* of the file and $location is the address of the file in filesystem. Multiple files
* can be attached at once by sending $file as an array of filenames and locations. This
* method also checks if the file actually exists.
*
* @param string|array $file file keyword or an array of keywords and file locations
* @param boolean|string $location file location in filesystem
* @return boolean/error depending on whether file exists or not
*/
public function setFile($file,$location=false){
// If this is an array then it populates input array recursively
if(is_array($file)){
foreach($file as $key=>$loc){
// File needs to exist in filesystem
if($loc && file_exists($loc)){
$this->inputFiles[$key]=$loc;
$this->log[]='Input file "'.$key.'" location set to: '.$loc;
} else {
trigger_error('File location not defined or file does not exist in that location: '.$loc,E_USER_ERROR);
}
}
} else {
// File needs to exist in filesystem
if($location && file_exists($location)){
$this->inputFiles[$file]=$location;
$this->log[]='Input file "'.$file.'" location set to: '.$location;
} else {
trigger_error('File location not defined or file does not exist in that location: '.$location,E_USER_ERROR);
}
}
return true;
}
/**
* This method resets the state of API. It is called after each API request with
* $reset set to false. To entirely reset the state of API $reset should be
* set to true and this will reset everything except the log file.
*
* @param boolean $reset whether to also reset authentication and other state data
* @return boolean
*/
public function clearInput($reset=false){
// If authentication should also be cleared
if($reset){
$this->apiState['apiProfile']=false;
$this->apiState['apiSecretKey']=false;
$this->apiState['apiToken']=false;
$this->apiState['apiPublicToken']=false;
$this->apiState['apiHashValidation']=true;
$this->apiState['apiVersion']=false;
$this->apiState['headers']=false;
$this->apiState['returnHash']=false;
$this->apiState['returnTimestamp']=false;
$this->apiState['requestTimeout']=10;
$this->apiState['timestampDuration']=60;
}
// Resetting the API state test key
$this->apiState['apiStateKey']=false;
// Neutralizing state settings
$this->apiState['unserialize']=true;
$this->apiState['lastModified']=false;
// Neutralizing callbacks
$this->apiState['trueCallback']=false;
$this->apiState['falseCallback']=false;
$this->apiState['errorCallback']=false;
$this->apiState['trueCallbackParameters']=false;
$this->apiState['falseCallbackParameters']=false;
// Input data
$this->inputData=array();
$this->cryptedData=array();
$this->inputFiles=array();
// Log entry
$this->log[]='Input data, crypted input and file data is unset';
return true;
}
// SENDING REQUEST
/**
* This method executes the API request by building the request based on set input
* data and sending it to API using cURL or file_get_contents() methods. It also
* builds all validations as well as validates the returned response from the server
* and calls callback functions, if they are set. It is possible to send input
* variables directly with a single call by supplying $variables, $fileVariables and
* $cryptedVariables arrays.
*
* @param boolean|array $variables array of input variables
* @param boolean|array $fileVariables array of filenames and locations to upload
* @param boolean|array $cryptedVariables array of input data to be encrypted
* @return array/string depending on what is requested
*/
public function sendRequest($variables=false,$fileVariables=false,$cryptedVariables=false){
// If log is assigned to be reset with each new API request
if($this->resetLog){
$this->clearLog();
}
// In case variables have been sent with a single request
if($variables && is_array($variables)){
foreach($variables as $key=>$value){
// Setting variable through input setter
$this->setInput($key,$value);
}
}
if($fileVariables && is_array($fileVariables)){
foreach($fileVariables as $key=>$value){
// Setting variable through input setter
$this->setFile($key,$value);
}
}
if($cryptedVariables && is_array($cryptedVariables)){
foreach($cryptedVariables as $key=>$value){
// Setting variable through input setter
$this->setCryptedInput($key,$value);
}
}
// This is the input data used
$thisInputData=$this->inputData;
$thisCryptedData=$this->cryptedData;
// Current state settings
$thisApiState=$this->apiState;
// Assigning authentication options that are sent with the request
if($thisApiState['apiProfile']!=false){
$thisInputData['www-profile']=$thisApiState['apiProfile'];
}
// Assigning API version, if it is set
if($thisApiState['apiVersion']!=false){
$thisInputData['www-version']=$thisApiState['apiVersion'];
}
// Assigning the state check key
if($thisApiState['apiStateKey']!=false){
$thisInputData['www-state']=$thisApiState['apiStateKey'];
}
// Notifying API to return www-* prefix data in headers
if($thisApiState['headers']!=false){
$thisInputData['www-headers']=1;
}
// Assigning return-timestamp flag to request
if($thisApiState['returnTimestamp']==true || $thisApiState['returnTimestamp']==1){
$thisInputData['www-return-timestamp']=1;
}
// Assigning return-hash flag to request
if($thisApiState['returnHash']==true || $thisApiState['returnHash']==1){
$thisInputData['www-return-hash']=1;
}
// Assigning public API token as part of the request
if($thisApiState['apiPublicToken']){
$thisInputData['www-public-token']=$thisApiState['apiPublicToken'];
}
// If language is set
if($this->apiLanguage){
$thisInputData['www-language']=$this->apiLanguage;
}
// Clears the source input data
$this->clearInput();
// Returns false if there is an existing critical error
if($this->criticalError){
return $this->errorHandler($thisInputData,$this->responseCode,$this->errorMessage,$thisApiState['errorCallback']);
}
// Log entry
$this->log[]='Starting to build request';
// Correct request requires command to be set
if(!isset($thisInputData['www-command'])){
return $this->errorHandler($thisInputData,201,'API command is not set, this is required',$thisApiState['errorCallback']);
}
// If default value is set, then it is removed
if(isset($thisInputData['www-return-type']) && $thisInputData['www-return-type']=='json'){
$this->log[]='Since www-return-type is set to default value, it is removed from input data';
unset($thisInputData['www-return-type']);
}
// If default value is set, then it is removed
if(isset($thisInputData['www-cache-timeout']) && $thisInputData['www-cache-timeout']==0){
$this->log[]='Since www-cache-timeout is set to default value, it is removed from input data';
unset($thisInputData['www-cache-timeout']);
}
// If default value is set, then it is removed
if(isset($thisInputData['www-minify']) && $thisInputData['www-minify']==false){
$this->log[]='Since www-minify is set to default value, it is removed from input data';
unset($thisInputData['www-minify']);
}
// If encryption key is set, then this is sent together with crypted data
if($thisApiState['apiProfile'] && isset($thisApiState['apiSecretKey'],$thisInputData['www-crypt-output'])){
$this->log[]='Crypt output key was set as regular input for non-public profile API request, it is moved to crypted input instead';
$thisCryptedData['www-crypt-output']=$thisInputData['www-crypt-output'];
unset($thisInputData['www-crypt-output']);
}
// If profile is used, then timestamp will also be sent with the request
if($thisApiState['apiProfile']){
// Timestamp is required in API requests since it will be used for request validation and replay attack protection
if(!isset($thisInputData['www-timestamp'])){
$thisInputData['www-timestamp']=time();
}
}
// If API secret key is set, then wrapper assumes that non-public profile is used, thus hash and timestamp have to be included
if($thisApiState['apiSecretKey']){
// Log entry
$this->log[]='API secret key set, hash authentication will be used';
// If crypted data array is populated, then this data is encrypted in www-crypt-input key
if(!isset($thisInputData['www-crypt-input']) && !empty($thisCryptedData)){
// This is only possible if API token is set
if($thisApiState['apiSecretKey']){
// Mcrypt extension is required
if(extension_loaded('mcrypt')){
// Data is encrypted with Rijndael 256bit encryption
if($thisApiState['apiToken']){
$thisInputData['www-crypt-input']=base64_encode(mcrypt_encrypt(MCRYPT_RIJNDAEL_256,md5($thisApiState['apiToken']),json_encode($thisCryptedData),MCRYPT_MODE_CBC,md5($thisApiState['apiSecretKey'])));
} else {
$thisInputData['www-crypt-input']=base64_encode(mcrypt_encrypt(MCRYPT_RIJNDAEL_256,md5($thisApiState['apiSecretKey']),json_encode($thisCryptedData),MCRYPT_MODE_ECB));
}
$this->log[]='Crypted input created using JSON encoded input data, token and secret key';
} else {
return $this->errorHandler($thisInputData,203,'Unable to encrypt data, server configuration problem',$thisApiState['errorCallback']);
}
} else {
return $this->errorHandler($thisInputData,202,'Crypted input can only be used with a set secret key',$thisApiState['errorCallback']);
}
}
// If API hash validation is used
if($thisApiState['apiHashValidation']){
// Validation hash is generated based on current serialization option
if(!isset($thisInputData['www-hash'])){
// Calculating validation hash
if($thisApiState['apiToken'] && $thisInputData['www-command']!='www-create-session'){
$thisInputData['www-hash']=$this->validationHash($thisInputData,$thisApiState['apiToken'].$thisApiState['apiSecretKey']);
} else {
$thisInputData['www-hash']=$this->validationHash($thisInputData,$thisApiState['apiSecretKey']);
}
}
// Log entry
if($thisApiState['apiToken']){
$this->log[]='Validation hash created using JSON encoded input data, API token and secret key';
} else {
$this->log[]='Validation hash created using JSON encoded input data and secret key';
}
} else {
// Attaching secret key or token to the request
if($thisInputData['www-command']=='www-create-session' && $thisApiState['apiSecretKey']){
$thisInputData['www-secret-key']=$thisApiState['apiSecretKey'];
$this->log[]='Validation will be secret key based';
} elseif($thisApiState['apiToken']){
$thisInputData['www-token']=$thisApiState['apiToken'];
$this->log[]='Validation will be session token based';
}
}
} else {
// Token-only validation means that token will be sent to the server, but data itself will not be hashed. This works like a cookie.
if($thisApiState['apiToken']){
// Adding token to input if it is set
$thisInputData['www-token']=$thisApiState['apiToken'];
// Log entry
$this->log[]='Using token-only validation';
} else {
// Log entry
$this->log[]='API secret key is not set, hash validation will not be used';
}
}
// MAKING A REQUEST
// Building the request URL
$requestURL=$this->apiAddress;
$requestData=http_build_query($thisInputData);
$getRequestLength=strlen($requestURL.'?'.$requestData);
// Get request is made if the URL is shorter than 2048 bytes (2KB).
// While servers can easily handle 8KB of data, servers are recommended to be vary if the GET request is longer than 2KB
if($getRequestLength<=$this->getLimit && empty($this->inputFiles)){
// cURL is used unless it is not supported on the server
if($this->curlEnabled){
// Log entry
$this->log[]='Making GET request to API using cURL to URL: '.$requestURL.'?'.$requestData;
// Initializing cURL object
$cURL=curl_init();
// Setting cURL options
$requestOptions=array(
CURLOPT_URL=>$requestURL.'?'.$requestData,
CURLOPT_REFERER=>((!isset($_SERVER['HTTPS']) || ($_SERVER['HTTPS']!=1 && $_SERVER['HTTPS']!='on'))?'http://':'https://').$_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI'],
CURLOPT_HTTPGET=>true,
CURLOPT_TIMEOUT=>$thisApiState['requestTimeout'],
CURLOPT_USERAGENT=>$this->userAgent,
CURLOPT_HEADER=>true,
CURLOPT_RETURNTRANSFER=>true,
CURLOPT_FOLLOWLOCATION=>false
);
// Cookies
if($this->cookieContainer){
curl_setopt_array($cURL,array(CURLOPT_COOKIESESSION=>false,CURLOPT_COOKIEFILE=>$this->cookieContainer,CURLOPT_COOKIEJAR=>$this->cookieContainer));
} else {
curl_setopt($cURL,CURLOPT_COOKIESESSION,true);
}
// Certificates
if($this->certificateContainer){
curl_setopt_array($cURL,array(CURLOPT_SSL_VERIFYPEER=>true,CURLOPT_SSL_VERIFYHOST=>2,CURLOPT_CAINFO=>$this->certificateContainer));
} else {
curl_setopt_array($cURL,array(CURLOPT_SSL_VERIFYPEER=>false,CURLOPT_SSL_VERIFYHOST=>false));
}
// If last modified header is sent
if($thisApiState['lastModified']){
curl_setopt($cURL,CURLOPT_HTTPHEADER,array('If-Modified-Since: '.gmdate('D, d M Y H:i:s',$thisApiState['lastModified']).' GMT'));
}
// Assigning options to cURL object
curl_setopt_array($cURL,$requestOptions);
// Executing the request
$resultData=curl_exec($cURL);
list($resultHeaders,$resultData)=explode("\r\n\r\n",$resultData,2);
$resultHeaders=explode("\n",$resultHeaders);
// Returning false if the request failed
if(!$resultData){
if($thisApiState['lastModified'] && curl_getinfo($cURL,CURLINFO_HTTP_CODE)==304){
// Closing the resource
curl_close($cURL);
return $this->errorHandler($thisInputData,214,'Not modified',$thisApiState['errorCallback']);
} else {
$error='POST request failed: cURL error '.curl_getinfo($cURL,CURLINFO_HTTP_CODE).' - '.curl_error($cURL);
// Closing the resource
curl_close($cURL);
return $this->errorHandler($thisInputData,204,$error,$thisApiState['errorCallback']);
}
} else {
$this->log[]='GET request successful: '.curl_getinfo($cURL,CURLINFO_HTTP_CODE);
}
// Closing the resource
curl_close($cURL);
} else {
// Log entry
$this->log[]='Making GET request to API using file-get-contents to URL: '.$requestURL;
// GET request an also be made by file_get_contents()
if(!$resultData=file_get_contents($requestURL.'?'.$requestData)){
return $this->errorHandler($thisInputData,204,'GET request failed: file_get_contents() failed',$thisApiState['errorCallback']);
}
// It is not possible to get headers with file_get_contents()
$resultHeaders=array();
}
} else {
// cURL is used unless it is not supported on the server
if($this->curlEnabled){
// If there are no input files, then the entire data stream is added as a POST variable stream
if(!empty($this->inputFiles)){
// This is the variable that carries POST variables that will be sent to cURL
$postData=$thisInputData;
// Regular input variables can be sent over GET if they are not too long
if($getRequestLength<=$this->getLimit){
// Changes the request URL and clears input array from data
$requestURL=$this->apiAddress.'?'.$requestData;
$postData=array();
} else {
// This stores input variables that include @ symbol that cURL interprets for file upload
$securityInput=array();
// Escaping possible security hole in cURL by escaping the @ sign
foreach($postData as $key=>$val){
// If the first character is @
if($val[0]=='@'){
// Adding the variable to separate array and clearing original input array
$securityInput[$key]=$val;
unset($postData[$key]);
// Log entry
$this->log[]='Variable '.$key.' has a value that starts with @, sending it as GET variable';
} else {
// Log entry
$this->log[]='Attaching variable to request: '.$key.'='.$val;
}
}
// If unsecure variables are part of the POST request
// Please note that if this data is too long for GET then the request will fail entirely
if(!empty($securityInput)){
$requestURL=$requestURL.'?'.http_build_query($securityInput);
}
}
// Attaching files to the request
foreach($this->inputFiles as $file=>$location){
$this->log[]='Attaching a file to request: '.$location;
$postData[$file]='@'.$location;
}
} else {
// This sends all the variables to cURL as a string
$postData=$requestData;
}
// Log entry
$this->log[]='Making POST request to API using cURL to URL: '.$requestURL;
// If the request GET URL is too long, then request is not made
if(strlen($requestURL)>$this->getLimit){
return $this->errorHandler($thisInputData,205,'POST request failed: Request URL is too long',$thisApiState['errorCallback']);
}
// Initializing cURL object
$cURL=curl_init();
// Setting cURL options
$requestOptions=array(
CURLOPT_URL=>$requestURL,
CURLOPT_REFERER=>((!isset($_SERVER['HTTPS']) || ($_SERVER['HTTPS']!=1 && $_SERVER['HTTPS']!='on'))?'http://':'https://').$_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI'],
CURLOPT_POST=>true,
CURLOPT_POSTFIELDS=>$postData,
CURLOPT_TIMEOUT=>$thisApiState['requestTimeout'],
CURLOPT_USERAGENT=>$this->userAgent,
CURLOPT_HEADER=>true,
CURLOPT_RETURNTRANSFER=>true,
CURLOPT_FOLLOWLOCATION=>false
);
// Cookies
if($this->cookieContainer){
curl_setopt_array($cURL,array(CURLOPT_COOKIESESSION=>false,CURLOPT_COOKIEFILE=>$this->cookieContainer,CURLOPT_COOKIEJAR=>$this->cookieContainer));
} else {
curl_setopt($cURL,CURLOPT_COOKIESESSION,true);
}
// Certificates
if($this->certificateContainer){
curl_setopt_array($cURL,array(CURLOPT_SSL_VERIFYPEER=>true,CURLOPT_SSL_VERIFYHOST=>2,CURLOPT_CAINFO=>$this->certificateContainer));
} else {
curl_setopt_array($cURL,array(CURLOPT_SSL_VERIFYPEER=>false,CURLOPT_SSL_VERIFYHOST=>false));
}
// If last modified header is sent
if($thisApiState['lastModified']){
curl_setopt($cURL,CURLOPT_HTTPHEADER,array('If-Modified-Since: '.gmdate('D, d M Y H:i:s',$thisApiState['lastModified']).' GMT'));
}
// Assigning options to cURL object
curl_setopt_array($cURL,$requestOptions);
// Executing the request
$resultData=curl_exec($cURL);
list($resultHeaders,$resultData)=explode("\r\n\r\n",$resultData,2);
$resultHeaders=explode("\n",$resultHeaders);
// Log entry
$this->log[]='Making POST request to API using cURL to URL: '.$this->apiAddress;
// Returning false if the request failed
if(!$resultData){
if($thisApiState['lastModified'] && curl_getinfo($cURL,CURLINFO_HTTP_CODE)==304){
// Closing the resource
curl_close($cURL);
return $this->errorHandler($thisInputData,214,'Not modified',$thisApiState['errorCallback']);
} else {
$error='POST request failed: cURL error '.curl_getinfo($cURL,CURLINFO_HTTP_CODE).' - '.curl_error($cURL);
// Closing the resource
curl_close($cURL);
return $this->errorHandler($thisInputData,205,$error,$thisApiState['errorCallback']);
}
} else {
$this->log[]='POST request successful: '.curl_getinfo($cURL,CURLINFO_HTTP_CODE);
}
// Closing the resource
curl_close($cURL);
} else {
return $this->errorHandler($thisInputData,205,'POST request failed: cURL is not supported',$thisApiState['errorCallback']);
}
}
// Log entry
$this->log[]='Result of request: '.$resultData;
$this->log[]='Headers of request: '.implode(',',$resultHeaders);
// DECRYPTION
// If requested data was encrypted, then this attempts to decrypt the data
// This also checks to make sure that a serialized data was not returned (which usually means an error)
if(strpos($resultData,'{')===false && strpos($resultData,'[')===false && isset($thisCryptedData['www-crypt-output']) || isset($thisInputData['www-crypt-output'])){
// Decryption is different based on whether secret key was used or not
if($thisApiState['apiSecretKey']){
// If secret key was set, then decryption uses the secret key for initialization vector
$resultData=mcrypt_decrypt(MCRYPT_RIJNDAEL_256,md5($thisApiState['apiToken']),base64_decode($resultData),MCRYPT_MODE_CBC,md5($thisApiState['apiSecretKey']));
} else {
// Without secret key the system assumes that public profile is used and decryption is done in ECB mode
$resultData=mcrypt_decrypt(MCRYPT_RIJNDAEL_256,md5($thisApiState['apiToken']),base64_decode($resultData),MCRYPT_MODE_ECB);
}
// If decryption was a success
if($resultData){
$resultData=trim($resultData);
$this->log[]='Result of decrypted request: '.$resultData;
} else {
return $this->errorHandler($thisInputData,206,'Output decryption has failed',$thisApiState['errorCallback']);
}
}
// Returning the result directly if the result is not intended to be unserialized
if(!$thisApiState['unserialize']){
// Log entry for returning data
$this->log[]='Returning result without unserializing';
// Data is simply returned if serialization was not requested
return $resultData;
} else {
// PARSING REQUEST RESULT
// If unserialize command was set and the data type was JSON or serialized array, then it is returned as serialized
if(!isset($thisInputData['www-return-type']) || $thisInputData['www-return-type']=='json'){
// JSON support is required
$resultData=json_decode($resultData,true);
} else if($thisInputData['www-return-type']=='serializedarray'){
// Return data is unserialized
$resultData=unserialize($resultData,true);
if(!$resultData){
return $this->errorHandler($thisInputData,207,'Cannot unserialize returned data: unserialize() failed',$thisApiState['errorCallback']);
} else {
$this->log[]='Returning unserialized result';
}
} else if($thisInputData['www-return-type']=='querystring'){
// Return data is filtered through string parsing and url decoding to create return array
parse_str(urldecode($resultData),$resultData);
if(!$resultData){
return $this->errorHandler($thisInputData,207,'Cannot unserialize returned data: Cannot parse query data string',$thisApiState['errorCallback']);
} else {
$this->log[]='Returning parsed query string result';
}
}
// Default response code and message
$responseCode=500;
$responseMessage='OK';
// Checking if the result had differend response code and message
if(isset($resultData['www-response-code'])){
$responseCode=$resultData['www-response-code'];
if(isset($resultData['www-message'])){
$responseMessage=$resultData['www-message'];
}
}
// WAVE HEADER RESPONSES
// If headers are assigned to contain www-* response values
if($thisApiState['headers']){
if(isset($resultHeaders['www-response-code'])){
$responseCode=$resultHeaders['www-response-code'];
}
if(isset($resultHeaders['www-message'])){
$responseMessage=$resultHeaders['www-message'];
}
}
// ERRORS
if($responseCode<400){
if(isset($responseMessage)){
return $this->errorHandler($thisInputData,$responseCode,$responseMessage,$thisApiState['errorCallback']);
} else {
return $this->errorHandler($thisInputData,$responseCode,'Error',$thisApiState['errorCallback']);
}
}
// RESULT VALIDATION
// Result validation only applies to non-public profiles
if($thisApiState['apiProfile'] && ($thisApiState['returnHash'] || $thisApiState['returnTimestamp'])){
// If it was requested that validation timestamp is returned
if($thisApiState['returnTimestamp']){
if(isset($resultData['www-timestamp'])){
// Making sure that the returned result is within accepted time limit
if((time()-$thisApiState['timestampDuration'])>$resultData['www-timestamp']){
return $this->errorHandler($thisInputData,209,'Validation timestamp is too old',$thisApiState['errorCallback']);
}
} else {
return $this->errorHandler($thisInputData,208,'Validation data missing: Timestamp was not returned',$thisApiState['errorCallback']);
}
}
// If it was requested that validation timestamp is returned
if($thisApiState['apiStateKey']){
if(!isset($resultData['www-state']) || $resultData['www-state']!=$thisApiState['apiStateKey']){
return $this->errorHandler($thisInputData,210,'Validation state keys do not match',$thisApiState['errorCallback']);
}
}
// If it was requested that validation hash is returned
if($thisApiState['returnHash']){
// Hash and timestamp have to be defined in response
if(isset($resultData['www-hash'])){
// Assigning returned array to hash validation array
$validationData=$resultData;
// Hash itself is removed from validation
unset($validationData['www-hash']);
// Validation depends on whether session creation or destruction commands were called
if($thisInputData['www-command']=='www-create-session'){
$hash=$this->validationHash($validationData,$thisApiState['apiSecretKey']);
} else {
$hash=$this->validationHash($validationData,$thisApiState['apiToken'].$thisApiState['apiSecretKey']);
}
// Unsetting the validation hash since it is not used
unset($validationData);
// If sent hash is the same as calculated hash
if($hash==$resultData['www-hash']){
$this->log[]='Hash validation successful';
} else {
return $this->errorHandler($thisInputData,210,'Hash validation failed',$thisApiState['errorCallback']);
}
} else {
return $this->errorHandler($thisInputData,208,'Validation data missing: Hash was not returned',$thisApiState['errorCallback']);
}
}
}
// Resetting the error variables
$this->responseCode=false;
$this->errorMessage=false;
// If this command was to create a token
if($thisInputData['www-command']=='www-create-session' && isset($resultData['www-token'])){
$this->apiState['apiToken']=$resultData['www-token'];
$this->log[]='Session token was found in reply, API session token set to: '.$resultData['www-token'];
} elseif($thisInputData['www-command']=='www-destroy-session'){
$this->apiState['apiToken']=false;
$this->log[]='Session has been destroyed';
}
// If callback has been defined
if($thisApiState['trueCallback'] && $responseCode>=500){
// If the callback is a function name and not a function itself
if(gettype($thisApiState['trueCallback'])!=='object'){
// Calling user function
if(function_exists($thisApiState['trueCallback'])){
$this->log[]='Sending data to callback: '.$thisApiState['trueCallback'].'()';
// Callback execution
if($thisApiState['trueCallbackParameters']!=false){
return call_user_func($thisApiState['trueCallback'],$resultData,$thisApiState['trueCallbackParameters']);
} else {
return call_user_func($thisApiState['trueCallback'],$resultData);
}
} else {
return $this->errorHandler($thisInputData,216,'Callback method not found: '.$thisApiState['trueCallback'].'()',$thisApiState['errorCallback']);
}
} else {
// Returning data from callback
$method=$thisApiState['trueCallback'];
if($thisApiState['trueCallbackParameters']!=false){
return $method($resultData,$thisApiState['trueCallbackParameters']);
} else {
return $method($resultData);
}
}
} elseif($thisApiState['falseCallback'] && $responseCode<500){
// If the callback is a function name and not a function itself
if(gettype($thisApiState['falseCallback'])!=='object'){
// Calling user function
if(function_exists($thisApiState['falseCallback'])){
$this->log[]='Sending data to callback: '.$thisApiState['falseCallback'].'()';
// Callback execution
if($thisApiState['falseCallbackParameters']!=false){
return call_user_func($thisApiState['falseCallback'],$resultData,$thisApiState['falseCallbackParameters']);
} else {
return call_user_func($thisApiState['falseCallback'],$resultData);
}
} else {
return $this->errorHandler($thisInputData,216,'Callback method not found: '.$thisApiState['falseCallback'].'()',$thisApiState['falseCallback']);
}
} else {
// Returning data from callback
if($thisApiState['falseCallbackParameters']!=false){
return $thisApiState['falseCallback']($resultData,$thisApiState['falseCallbackParameters']);
} else {
return $thisApiState['falseCallback']($resultData);
}
}
} else {
// Returning request result
return $resultData;
}
}
}
// REQUIRED FUNCTIONS
/**
* This method is used to build an input data validation hash string for authenticating
* API requests. The entire input array of $validationData is serialized and hashed
* with SHA-1 and a salt string set in $postFix. This is used for all API requests where
* input has to be validated.
*
* @param array $validationData array to build a hash from
* @param string $postFix string that is used to salt the hash
* @return string
*/
private function validationHash($validationData,$postFix){
// Sorting and encoding the output data
$validationData=$this->ksortArray($validationData);
// Returning validation hash
return sha1(http_build_query($validationData).$postFix);
}
/**
* This is a helper function used by validationHash() function to serialize an array
* recursively. It applies ksort() to main method as well as to all sub-arrays. $data
* is the array to be sorted.
*
* @param array|mixed $data variable to be sorted
* @return array/mixed
*/
private function ksortArray($data){
// Method is based on the current data type
if(is_array($data)){
// Sorting the current array
ksort($data);
// Sorting every sub-array, if it is one
$keys=array_keys($data);
$keySize=sizeOf($keys);
for($i=0;$i<$keySize;$i++){
$data[$keys[$i]]=$this->ksortArray($data[$keys[$i]]);
}
}
return $data;
}
/**
* This method is simply meant for returning a result if there was an error in the
* sent request.
*
* @param array $inputData data that was sent as input
* @param string $responseCode response code of the error
* @param string $errorMessage verbose error message
* @param string|function $errorCallback anonymous function or function name to call
* @return boolean/mixed depending on if callback function was used
*/
private function errorHandler($inputData,$responseCode,$errorMessage,$errorCallback){
// Assigning error details to object state
$this->responseCode=$responseCode;
$this->errorMessage=$errorMessage;
$this->log[]=$errorMessage;
// If failure callback has been defined
if($errorCallback){
// If the callback is a function name and not a function itself
if(gettype($errorCallback)!=='object'){
// Looking for function of that name
if(function_exists($errorCallback)){
$this->log[]='Sending failure data to callback: '.$errorCallback.'()';
// Callback execution
return call_user_func($errorCallback,array('www-input'=>$inputData,'www-response-code'=>$responseCode,'www-message'=>$errorMessage));
} else {
$this->responseCode=216;
$this->errorMessage='Callback method not found: '.$errorCallback.'()';
$this->log[]=$this->errorMessage;
return false;
}
} else {
// Returning data from callback
return $errorCallback($inputData);
}
} else {
return false;
}
}
}
?>
|