PHP Classes

File: engine/class.www-api.php

Recommend this page to a friend!
  Classes of Kristo Vaher   Wave Framework   engine/class.www-api.php   Download  
File: engine/class.www-api.php
Role: Class source
Content type: text/plain
Description: API Class
Class: Wave Framework
MVC framework for building Web sites and APIs
Author: By
Last change: Update of engine/class.www-api.php
Date: 2 months ago
Size: 100,590 bytes
 

Contents

Class file image Download
<?php /** * Wave Framework <http://github.com/kristovaher/Wave-Framework> * API Class * * API class is one of the core classes of Wave Framework. Every command and function in Wave * Framework is executed through API object. API class implements State class - which stores * configuration - and executes any and all functionality that is built within Wave Framework. * It is not recommended to modify this class, in fact this class is defined as final. Methods * of this class take user input, load MVC objects and execute their methods and return data * in the appropriate format. * * @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/api.htm * @since 1.0.0 * @version 3.7.0 */ final class WWW_API { /** * This variable is used to store results of API call. It acts as a buffer for API, which * will be checked when another API call is made with the exact same input data. */ private $commandBuffer=array(); /** * This holds data about API profiles from /resources/api.profiles.ini, content of which will * be checked by API whenever API call is made that is not public. */ private $apiProfiles=array(); /** * This variable holds data about API observers from /resources/api.observers.ini file. If * an API call is made, then this variable content will be checked to execute additional * API calls, if defined. */ private $apiObservers=array(); /** * This is an array that stores data that will be logged by Logger object, if Logger is used * in the system. Content length and other data is stored for logging purposes in this array. */ public $apiLoggerData=array(); /** * This variable stores performance related timestamps, if splitTime() method is called * by the API. */ private $splitTimes=array(); /** * This variable stores the initialized State object that carries a lot of configuration * and environment data and functionality. */ public $state=false; /** * This variable defines whether APC is available in the server environment. If this variable * is true, then some caching methods will utilize APC instead of filesystem. */ public $apc=false; /** * This variable defines whether Memcache is available in the server environment. If this * variable is true, then some caching methods will utilize Memcache instead of filesystem. */ public $memcache=false; /** * This variable holds database cache, since this connection can be different from the main * database connection used by the system, while still using the same class for the requests. */ public $databaseCache=false; /** * This is a counter that stores the depth of API calls. Since API calls can execute other * API calls, this variable is used to determine some caching and buffer related data when * specific API call is references by API class. */ public $callIndex=0; /** * This is an index of cache files that have been referenced within the system. This is used * so that certain calls do not have to be repeated, if the same cache is referred multiple * times within a single request. */ public $cacheIndex=array(); /** * This variable holds data about API execution call-index values and whether this specific * API call can be cached or not. This is for internal maintenance when dealing with which * API call to cache and which not. */ public $noCache=array(); /** * This holds the return type information of API calls. This can be later fetched by controllers * to see what type of data is being requested from the API. */ public $returnTypes=array(); /** * This method stores version number for Models, Views and Controller files loaded through the API. * These files have to be set in the subfolders of /model/, /view/ and /controller folders. If files * are not found, then the most recent version is used. If this is set to false, then core files * are used instead. */ public $requestedVersion=false; /** * This holds configuration value from State and turns on internal logging, if configuration * has internal logging enabled. If this remains false, then internal log entries will not * be stored. */ private $internalLogging=false; /** * This is an array that stores all the internal log entries that will be written to filesystem * once API class has finished dealing with the request. */ private $internalLog=array(); /** * If this variable is set, then API, when getting a result from the controller, does not process * anything any further */ private $closeProcess=false; /** * API object construction accepts State object in $state and array data of API profiles * as $apiProfiles. If State is not defined, then API class attempts to automatically * create a new State object, thus API class is highly dependent on State class being * present. API object construction also loads Factory class, if it is not defined and * tests if server supports APC or not. If API profiles are not submitted to API during * construction, then API will attempt to load API profiles from the *.ini file. Same * applies to observers. * * @param boolean|object $state WWW_State object * @param boolean|array $apiProfiles array of API profile data * @return WWW_API */ final public function __construct($state=false,$apiProfiles=false){ // API expects to be able to use State object if($state){ $this->state=$state; } else { // If State object does not exist, it is defined and loaded as State if(!class_exists('WWW_State',false)){ require(__DIR__.DIRECTORY_SEPARATOR.'class.www-state.php'); } $this->state=new WWW_State(); } // If internal logging is used if($this->state->data['internal-logging'] && $this->state->data['internal-logging']!=''){ $this->internalLogging=$this->state->data['internal-logging']; } // Factory class is loaded, if it doesn't already exist, since MVC classes require it if(!class_exists('WWW_Factory',false)){ require(__DIR__.DIRECTORY_SEPARATOR.'class.www-factory.php'); } // If APC is enabled if(extension_loaded('apc') && function_exists('ini_get') && ini_get('apc.enabled')==1 && $this->state->data['apc']==1){ $this->apc=true; } // If Memcache is enabled if($this->state->data['memcache'] && extension_loaded('memcache')){ // New memcache element $this->memcache=new Memcache; // Connecting to memcache if(!$this->memcache->connect($this->state->data['memcache-host'],$this->state->data['memcache-port'])){ trigger_error('Memcache connection failed, reverting to other caching methods',E_USER_WARNING); $this->memcache=false; } } elseif($this->state->data['cache-database']){ // If database type is set to 'any' and regular database connection is used, then cache database uses the regular connection as well if($this->state->data['cache-database-type']=='any' && $this->state->databaseConnection!=false){ // Assigning the general database to also act as cache database connection $this->databaseCache=&$this->state->databaseConnection; } else { // If cache database settings are not set, then loading configuration from main database settings if(!$this->state->data['cache-database-name']){ $this->state->data['cache-database-name']=$this->state->data['database-name']; } if(!$this->state->data['cache-database-type']){ $this->state->data['cache-database-type']=$this->state->data['database-type']; } if(!$this->state->data['cache-database-host']){ $this->state->data['cache-database-host']=$this->state->data['database-host']; } if(!$this->state->data['cache-database-username']){ $this->state->data['cache-database-username']=$this->state->data['database-username']; } if(!$this->state->data['cache-database-password']){ $this->state->data['cache-database-password']=$this->state->data['database-password']; } if(!$this->state->data['cache-database-errors']){ $this->state->data['cache-database-errors']=$this->state->data['database-errors']; } if(!$this->state->data['cache-database-persistent']){ $this->state->data['cache-database-persistent']=$this->state->data['database-persistent']; } // Checking if database configuration is valid if(isset($this->state->data['cache-database-name'],$this->state->data['cache-database-type'],$this->state->data['cache-database-host'],$this->state->data['cache-database-username'],$this->state->data['cache-database-password'])){ // If database object is already used by State and it is exactly the same as the one assigned for caching, then that same link will be used if($this->state->databaseConnection && $this->state->data['cache-database-host']==$this->state->data['database-host'] && $this->state->data['cache-database-username']==$this->state->data['database-username'] && $this->state->data['cache-database-password']==$this->state->data['database-password'] && $this->state->data['cache-database-name']==$this->state->data['database-name']){ // State file has the correct cache if the Configuration options were loaded $this->databaseCache=$this->state->databaseConnection; } else { // If the class has not been defined yet if(!class_exists('WWW_Database',false)){ require(__ROOT__.'engine'.DIRECTORY_SEPARATOR.'class.www-database.php'); } // This object will be used for caching functions later on $this->databaseCache=new WWW_Database($this->state->data['cache-database-type'],$this->state->data['cache-database-host'],$this->state->data['cache-database-name'],$this->state->data['cache-database-username'],$this->state->data['cache-database-password'],((isset($this->state->data['cache-database-errors']))?$this->state->data['cache-database-errors']:false),((isset($this->state->data['cache-database-persistent']))?$this->state->data['cache-database-persistent']:false)); } } else { // Some of the settings were incorrect or missing, so database caching won't be used trigger_error('Database caching configuration incorrect, reverting to other caching methods',E_USER_WARNING); } } } // System attempts to load API keys from the default location if they were not defined if(!$apiProfiles){ // Profiles can be loaded from overrides folder as well if(file_exists(__ROOT__.'overrides'.DIRECTORY_SEPARATOR.'resources'.DIRECTORY_SEPARATOR.'api.profiles.ini')){ $sourceUrl=__ROOT__.'overrides'.DIRECTORY_SEPARATOR.'resources'.DIRECTORY_SEPARATOR.'api.profiles.ini'; } elseif(file_exists(__ROOT__.'resources'.DIRECTORY_SEPARATOR.'api.profiles.ini')){ $sourceUrl=__ROOT__.'resources'.DIRECTORY_SEPARATOR.'api.profiles.ini'; } else { return false; } // This data can also be stored in cache $cacheUrl=__ROOT__.'filesystem'.DIRECTORY_SEPARATOR.'cache'.DIRECTORY_SEPARATOR.'resources'.DIRECTORY_SEPARATOR.'api.profiles.tmp'; // Testing if cache for profiles already exists $cacheTime=$this->cacheTime($cacheUrl); // If source file has been modified since cache creation if(!$cacheTime || filemtime($sourceUrl)>$cacheTime){ // Profiles are parsed from INI file in the resources folder $apiProfiles=parse_ini_file($sourceUrl,true,INI_SCANNER_RAW); $this->setCache($cacheUrl,$apiProfiles); } else { // Returning data from cache $apiProfiles=$this->getCache($cacheUrl); } } // Assigning API profiles $this->apiProfiles=$apiProfiles; // Observers can be loaded from overrides folder as well if(file_exists(__ROOT__.'overrides'.DIRECTORY_SEPARATOR.'resources'.DIRECTORY_SEPARATOR.'api.observers.ini')){ $sourceUrl=__ROOT__.'overrides'.DIRECTORY_SEPARATOR.'resources'.DIRECTORY_SEPARATOR.'api.observers.ini'; } elseif(file_exists(__ROOT__.'resources'.DIRECTORY_SEPARATOR.'api.observers.ini')){ $sourceUrl=__ROOT__.'resources'.DIRECTORY_SEPARATOR.'api.observers.ini'; } else { return false; } // This data can also be stored in cache $cacheUrl=__ROOT__.'filesystem'.DIRECTORY_SEPARATOR.'cache'.DIRECTORY_SEPARATOR.'resources'.DIRECTORY_SEPARATOR.'api.observers.tmp'; // Testing if cache for observers already exists $cacheTime=$this->cacheTime($cacheUrl); // If source file has been modified since cache creation if(!$cacheTime || filemtime($sourceUrl)>$cacheTime){ // Profiles are parsed from INI file in the resources folder $this->apiObservers=parse_ini_file($sourceUrl,true,INI_SCANNER_RAW); $this->setCache($cacheUrl,$this->apiObservers); } else { // Returning data from cache $this->apiObservers=$this->getCache($cacheUrl); } } /** * Once API object is not used anymore, the object attempts to write internal log to * filesystem if internal log is used and has any log data to store. It also closes * Memcache connection if such is used. * * @return null */ final public function __destruct(){ // Storing internal logging data if($this->internalLogging && !empty($this->internalLog)){ // Inserting or appending the log data into internal log file $signature=md5($_SERVER['REMOTE_ADDR'].' '.$_SERVER['HTTP_USER_AGENT']); file_put_contents(__ROOT__.'filesystem'.DIRECTORY_SEPARATOR.'logs'.DIRECTORY_SEPARATOR.'internal_'.$signature.'_signature.tmp',$_SERVER['REMOTE_ADDR'].' '.$_SERVER['HTTP_USER_AGENT']); file_put_contents(__ROOT__.'filesystem'.DIRECTORY_SEPARATOR.'logs'.DIRECTORY_SEPARATOR.'internal_'.$signature.'_data.tmp',json_encode($this->internalLog)."\n",FILE_APPEND); } // Closing Memcache connection if($this->memcache){ $this->memcache->close(); } } // API COMMAND /** * This is one of the two core methods of API class. It accepts input data from * $apiInputData which is an array of keys and values. Some keys are API dependent * flags with the wave prefix of 'www-'. $useBuffer setting defines if buffer can be * used, which means that if the same exact input has already been sent within the * same HTTP request, then it returns data from buffer rather than going through the * process again. $apiValidation is a flag that sets whether API profiles are * validated or not, this setting is turned off for internal API calls that have * already been validated. $useLogger is a flag that tells API that Logger class * is used by the system. This method validates the API call, loads MVC objects and * executes their methods and sends the result to output() function. * * @param array $apiInputData array of input data * @param boolean $useBuffer whether API calls are buffered per request * @param boolean $apiValidation if API uses profile validation or not, can also hold an array of exceptions * @param boolean $useLogger whether logger array is updated during execution * @return array/string depending on API request input data */ final public function command($apiInputData=array(),$useBuffer=false,$apiValidation=true,$useLogger=false){ // Increasing the counter of API calls $this->callIndex++; // If internal logging is enabled if($this->internalLogging){ $this->logEntry('input-data',$apiInputData); } // DEFAULT VALUES, COMMAND AND BUFFER CHECK // This stores information about current API command state // Various defaults are loaded from State $apiState=array( 'call-index'=>$this->callIndex, 'cache-timeout'=>0, 'cache-load-timeout'=>0, 'command'=>false, 'content-type-header'=>(isset($apiInputData['www-content-type']))?$apiInputData['www-content-type']:false, 'custom-header'=>false, 'hash'=>false, 'hash-validation'=>true, 'headers'=>(isset($apiInputData['www-headers']))?$apiInputData['www-headers']:false, 'ip-session'=>false, 'last-modified'=>$this->state->data['request-time'], 'minify-output'=>(isset($apiInputData['www-minify']))?$apiInputData['www-minify']:false, 'disable-callbacks'=>(isset($apiInputData['www-disable-callbacks']))?$apiInputData['www-disable-callbacks']:false, 'crypt-output'=>false, 'profile'=>$this->state->data['api-public-profile'], 'push-output'=>(isset($apiInputData['www-output']))?$apiInputData['www-output']:1, 'jsonp'=>(isset($apiInputData['www-jsonp']) && $apiInputData['www-jsonp']!='')?$apiInputData['www-jsonp']:false, 'jsonv'=>(isset($apiInputData['www-jsonv']) && $apiInputData['www-jsonv']!='')?$apiInputData['www-jsonv']:false, 'return-hash'=>(isset($apiInputData['www-return-hash']))?$apiInputData['www-return-hash']:false, 'return-timestamp'=>(isset($apiInputData['www-return-timestamp']))?$apiInputData['www-return-timestamp']:false, 'return-type'=>(isset($apiInputData['www-return-type']))?$apiInputData['www-return-type']:'json', 'secret-key'=>false, 'token'=>false, 'state-key'=>(isset($apiInputData['www-state']))?$apiInputData['www-state']:false, 'commands'=>'*', 'token-file'=>false, 'token-directory'=>false, 'token-timeout'=>false, 'version'=>false ); // Setting the return type to cache index $this->returnTypes[$apiState['call-index']]=$apiState['return-type']; // Turning output off if the HTTP HEAD request is made if($this->state->data['http-request-method']=='HEAD'){ $apiState['push-output']=0; } // If API command is not set and is not set in input array either, the system will return an error if(isset($apiInputData['www-command'])){ $apiState['command']=strtolower($apiInputData['www-command']); } elseif(isset($apiInputData['www-controller'])){ $apiState['command']=$apiInputData['www-controller'].'-'.strtolower($this->state->data['http-request-method']); } else { return $this->output(array('www-message'=>'API command not set','www-response-code'=>101),$apiState); } // Input observer if(isset($this->apiObservers[$apiState['command']]) && isset($this->apiObservers[$apiState['command']]['input']) && $this->apiObservers[$apiState['command']]['input']!=''){ $this->command(array('www-return-type'=>'php','www-command'=>$this->apiObservers[$apiState['command']]['input'],'www-output'=>0,'www-return-hash'=>0,'www-content-type'=>false,'www-minify'=>false,'www-crypt-output'=>false,'www-cache-tags'=>false)+$apiInputData,((isset($this->apiObservers[$apiState['command']]['input-buffer']) && $this->apiObservers[$apiState['command']]['input-buffer']==true)?true:false),false); } // If session data is set if(!isset($apiInputData['www-session']) || $apiInputData['www-session']==1){ if(!empty($this->state->data['session-data'])){ // Grabbing session data for input $apiInputData['www-session']=$this->state->data['session-data']; // Unsetting the session cookie and other variables from the input data unset($apiInputData['www-session'][$this->state->data['session-fingerprint-key']],$apiInputData['www-session'][$this->state->data['session-timestamp-key']],$apiInputData['www-session'][$this->state->data['session-token-key']]); } } // Sorting the input array $apiInputData=$this->ksortArray($apiInputData); // Existing response is checked from buffer if it exists if($useBuffer){ $commandBufferAddress=md5($apiState['command'].'&'.serialize($apiInputData).'&'.$this->requestedVersion); // If result already exists in buffer then it is simply returned if(isset($this->commandBuffer[$commandBufferAddress])){ return $this->commandBuffer[$commandBufferAddress]; } } // If this is set, then models, controllers and views are all loaded from subfolders in their appropriate directories, if they are set // If the file for that particular version is not found, then the most recent version is used if(isset($apiInputData['www-version'])){ // Since API version is a folder, this escapes the illegal characters $this->requestedVersion=preg_replace('[^a-zA-Z0-9]','',$apiInputData['www-version']); // This tests if this version number is allowed or if it included illegal characters if($this->requestedVersion!=$apiInputData['www-version'] || !in_array($this->requestedVersion,$this->state->data['api-versions'])){ $this->requestedVersion=false; return $this->output(array('www-message'=>'API version cannot be used: '.$apiInputData['www-version'],'www-response-code'=>117),$apiState); } elseif($this->requestedVersion==$this->state->data['version-api']){ // The most recent version number is set to false, which disables folder checking for these versions for performance reasons $this->requestedVersion=false; } } // This notifies state what language is used if(isset($apiInputData['www-language'])){ if(in_array($apiInputData['www-language'],$this->state->data['languages'])){ $this->state->data['language']=$apiInputData['www-language']; } else { return $this->output(array('www-message'=>'Language cannot be found: '.$apiInputData['www-language'],'www-response-code'=>116),$apiState); } } // System specific output cannot be pushed to headers if the request is PHP-specific if($apiState['return-type']=='php' && $apiState['headers']){ $apiState['headers']=false; } // This tests if cache value sent through input is valid if($apiState['command']!='www-create-session'){ // If cache timeout is set then it is also applied as default to load timeout if(isset($apiInputData['www-cache-timeout']) && $apiInputData['www-cache-timeout']>=0){ $apiState['cache-timeout']=$apiInputData['www-cache-timeout']; $apiState['cache-load-timeout']=$apiInputData['www-cache-timeout']; } // Loading cache timestamp must be less than the actual cache timeout setting that is set if(isset($apiInputData['www-cache-load-timeout']) && $apiInputData['www-cache-load-timeout']>=0 && $apiInputData['www-cache-load-timeout']<=$apiState['cache-timeout']){ $apiState['cache-load-timeout']=$apiInputData['www-cache-load-timeout']; } } // VALIDATING PROFILE BASED INPUT DATA // API profile data is loaded only if API validation is used if($apiValidation){ // Current API profile is assigned to state // This is useful for controller development if you wish to restrict certain controllers to only certain API profiles if(isset($apiInputData['www-profile']) && $apiInputData['www-profile']!=$this->state->data['api-public-profile']){ $apiState['profile']=$apiInputData['www-profile']; } else { // Public profile is assigned $apiState['profile']=$this->state->data['api-public-profile']; // If public request token is sent with the request if(isset($apiInputData[$this->state->data['session-token-key']]) && $apiInputData[$this->state->data['session-token-key']]!=''){ $this->state->data['api-public-token']=$apiInputData[$this->state->data['session-token-key']]; } // Testing if public request token is required and that it matches the one stored in sessions if($this->state->data['api-public-token'] && $this->state->getUser() && $this->state->getSession($this->data['session-token-key'])!=$this->state->data['api-public-token']){ // If profile is set to be disabled return $this->output(array('www-message'=>'API public requests require a correct public request token','www-response-code'=>102),$apiState); } } // This checks whether API profile information is defined in /resources/api.profiles.php file if(isset($this->apiProfiles[$apiState['profile']])){ // Testing if API profile is disabled or not if(isset($this->apiProfiles[$apiState['profile']]['disabled']) && $this->apiProfiles[$apiState['profile']]['disabled']==1){ // If profile is set to be disabled return $this->output(array('www-message'=>'API profile is disabled: '.$apiState['profile'],'www-response-code'=>104),$apiState); } // Making sure that the API profile is allowed to request this version API if($this->requestedVersion && isset($this->apiProfiles[$apiState['profile']]['versions']) && $this->apiProfiles[$apiState['profile']]['versions']!='*' && !in_array($this->requestedVersion,explode(',',$this->apiProfiles[$apiState['profile']]['versions']))){ // If version number is allowed for this profile return $this->output(array('www-message'=>'API version cannot be used: '.$this->requestedVersion,'www-response-code'=>117),$apiState); } // Testing if IP is in valid range if(isset($this->apiProfiles[$apiState['profile']]['ip']) && $this->apiProfiles[$apiState['profile']]['ip']!='*' && !in_array($this->state->data['client-ip'],explode(',',$this->apiProfiles[$apiState['profile']]['ip']))){ // If profile has IP set and current IP is not allowed return $this->output(array('www-message'=>'API profile not allowed from this IP: '.$apiState['profile'].' '.$this->state->data['client-ip'],'www-response-code'=>105),$apiState); } // Profile commands are filtered only if they are set if(isset($this->apiProfiles[$apiState['profile']]['commands']) && $apiState['command']!='www-create-session' && $apiState['command']!='www-destroy-session' && $apiState['command']!='www-validate-session'){ $apiState['commands']=explode(',',$this->apiProfiles[$apiState['profile']]['commands']); if((in_array('*',$apiState['commands']) && in_array('!'.$apiState['command'],$apiState['commands'])) || (!in_array('*',$apiState['commands']) && !in_array($apiState['command'],$apiState['commands']))){ // If profile has IP set and current IP is not allowed return $this->output(array('www-message'=>'API command is not allowed for this profile: '.$apiState['command'].' by '.$apiState['profile'],'www-response-code'=>106),$apiState); } } // These options only affect non-public profiles if($apiState['profile']!=$this->state->data['api-public-profile']){ // If hash validation is turned off if(isset($this->apiProfiles[$apiState['profile']]['hash-validation']) && $this->apiProfiles[$apiState['profile']]['hash-validation']==0){ $apiState['hash-validation']=false; } // Returns an error if secret key is not set for an API profile if(isset($this->apiProfiles[$apiState['profile']]['secret-key'])){ // only checked if hash-based validation is used if($apiState['hash-validation']){ // Hash value has to be set and not be empty if(!isset($apiInputData['www-hash']) || $apiInputData['www-hash']==''){ return $this->output(array('www-message'=>'API request validation hash is missing','www-response-code'=>110),$apiState); } else { // Validation hash $apiState['hash']=$apiInputData['www-hash']; } } // Secret key $apiState['secret-key']=$this->apiProfiles[$apiState['profile']]['secret-key']; } else { return $this->output(array('www-message'=>'API profile configuration incorrect: secret key missing from configuration for '.$apiState['profile'],'www-response-code'=>109),$apiState); } // Checks for whether token timeout is set on the API profile if(isset($this->apiProfiles[$apiState['profile']]['token-timeout']) && $this->apiProfiles[$apiState['profile']]['token-timeout']!=0){ // Since this is not null, token based validation is used $apiState['token-timeout']=$this->apiProfiles[$apiState['profile']]['token-timeout']; } // If permissions are set for the API profile, then writing this information to State if(isset($this->apiProfiles[$apiState['profile']]['permissions'])){ // Permissions are stored as a comma-separated string $this->state->data['api-permissions']=explode(',',$this->apiProfiles[$apiState['profile']]['permissions']); } } // If access control header is set in configuration if(isset($this->apiProfiles[$apiState['profile']]['access-control'])){ $this->state->setHeader('Access-Control-Allow-Origin: '.$this->apiProfiles[$apiState['profile']]['access-control']); } elseif($this->state->data['access-control']){ $this->state->setHeader('Access-Control-Allow-Origin: '.$this->state->data['access-control']); } } else { return $this->output(array('www-message'=>'API profile not found: '.$apiState['profile'],'www-response-code'=>103),$apiState); } } // API PROFILE TOKEN, TIMESTAMP, HASH VALIDATION, CRYPTED INPUT HANDLING AND SESSION CREATION // API profile validation happens only if non-public profile is actually set if($apiValidation && $apiState['profile'] && $apiState['profile']!=$this->state->data['api-public-profile']){ // TOKEN CHECKS // Session filename is a simple hashed API profile name $apiState['token-file']=md5($apiState['profile'].$this->apiProfiles[$apiState['profile']]['secret-key']).'.tmp'; $apiState['token-file-ip']=md5($this->state->data['client-ip'].$apiState['profile'].$this->apiProfiles[$apiState['profile']]['secret-key']).'.tmp'; // Session folder in filesystem $apiState['token-directory']=$this->state->data['directory-system'].'filesystem'.DIRECTORY_SEPARATOR.'tokens'.DIRECTORY_SEPARATOR.substr($apiState['token-file'],0,2).DIRECTORY_SEPARATOR; $apiState['token-directory-ip']=$this->state->data['directory-system'].'filesystem'.DIRECTORY_SEPARATOR.'tokens'.DIRECTORY_SEPARATOR.substr($apiState['token-file-ip'],0,2).DIRECTORY_SEPARATOR; // Checking if valid token is active // It is possible that the token was created with linked IP, which is checked here if(file_exists($apiState['token-directory-ip'].$apiState['token-file-ip']) && (!$apiState['token-timeout'] || filemtime($apiState['token-directory-ip'].$apiState['token-file-ip'])>=($this->state->data['request-time']-$apiState['token-timeout']))){ // Loading contents of current token file and timestamp sync to API state $tmp=explode(':',file_get_contents($apiState['token-directory-ip'].$apiState['token-file-ip'])); // Assigning token and timestamp sync numbers $apiState['token']=$tmp[0]; $apiState['token-timestamp-sync']=$tmp[1]; // This updates the last-modified time, thus postponing the time how long the token is considered valid touch($apiState['token-directory-ip'].$apiState['token-file-ip']); // Setting the IP directories to regular addresses $apiState['token-file']=$apiState['token-file-ip']; $apiState['token-directory']=$apiState['token-directory-ip']; } elseif(file_exists($apiState['token-directory'].$apiState['token-file']) && (!$apiState['token-timeout'] || filemtime($apiState['token-directory'].$apiState['token-file'])>=($this->state->data['request-time']-$apiState['token-timeout']))){ // Loading contents of current token file and timestamp sync to API state $tmp=explode(':',file_get_contents($apiState['token-directory-ip'].$apiState['token-file'])); // Assigning token and timestamp sync numbers $apiState['token']=$tmp[0]; $apiState['token-timestamp-sync']=$tmp[1]; // This updates the last-modified time, thus postponing the time how long the token is considered valid touch($apiState['token-directory'].$apiState['token-file']); } elseif($apiState['command']=='www-create-session' && isset($apiInputData['www-ip-session']) && $apiInputData['www-ip-session']==true){ $apiState['ip-session']=true; // Setting the IP directories to regular addresses $apiState['token-file']=$apiState['token-file-ip']; $apiState['token-directory']=$apiState['token-directory-ip']; // This is used for timestamp synchronization $apiState['token-timestamp-sync']=(isset($apiInputData['www-timestamp']) && $apiInputData['www-timestamp']!='')?($this->state->data['request-time']-$apiInputData['www-timestamp']):0; } elseif($apiState['command']!='www-create-session'){ // Token is not required for commands that create or destroy existing tokens return $this->output(array('www-message'=>'API session token does not exist or is timed out','www-response-code'=>111),$apiState); } // TIMESTAMP VALIDATION // Returns an error if timestamp validation is required but www-timestamp is not provided if(isset($this->apiProfiles[$apiState['profile']]['timestamp-timeout']) && $apiState['command']!='www-create-session'){ // Timestamp value has to be set and not be empty if(!isset($apiInputData['www-timestamp']) || $apiInputData['www-timestamp']==''){ return $this->output(array('www-message'=>'API request validation timestamp is missing','www-response-code'=>107),$apiState); } elseif($this->apiProfiles[$apiState['profile']]['timestamp-timeout']<($this->state->data['request-time']-($apiInputData['www-timestamp']+$apiState['token-timestamp-sync']))){ return $this->output(array('www-message'=>'API request timestamp is too old: '.($this->state->data['request-time']-($apiInputData['www-timestamp']+$apiState['token-timestamp-sync'])).' seconds, '.$this->apiProfiles[$apiState['profile']]['timestamp-timeout'].' allowed','www-response-code'=>108),$apiState); } } // TOKEN AND HASH VALIDATION // If hash validation is used if($apiState['hash-validation']){ // Validation hash is calculated from input data $validationData=$apiInputData; // Session input is not considered for validation hash and is unset unset($validationData['www-hash'],$validationData['www-session'],$validationData[$this->state->data['session-name']]); // Unsetting any possible exceptions (such as file uploads and cookie input) if(is_array($apiValidation) && !empty($apiValidation)){ foreach($apiValidation as $unset){ unset($validationData[$unset]); } } // If token is set then this is used for validation as long as the command is not www-create-session if($apiState['token'] && $apiState['command']!='www-create-session'){ // Non-session creating hash validation is a little different and takes into account both token and the secret key $validationHash=sha1(http_build_query($validationData).$apiState['token'].$apiState['secret-key']); } else { // Session creation commands have validation hashes built only with the secret key $validationHash=sha1(http_build_query($validationData).$apiState['secret-key']); } // Unsetting validation data array unset($validationData); // If validation hashes do not match if($validationHash!=$apiState['hash']){ return $this->output(array('www-message'=>'API profile authentication failed: input hash validation failed','www-response-code'=>112),$apiState); } } elseif($apiState['command']!='www-create-session' && (!isset($apiInputData['www-token']) || $apiState['token']!=$apiInputData['www-token'])){ // If hash validation is used, then request can be made with a token or secret key alone and if these do not match then authentication error is thrown return $this->output(array('www-message'=>'API profile authentication failed: session token is missing or incorrect','www-response-code'=>112),$apiState); } elseif($apiState['command']=='www-create-session' && (!isset($apiInputData['www-secret-key']) || $apiInputData['www-secret-key']!=$apiState['secret-key'])){ // If hash validation is used, then request can be made with a token or secret key alone and if these do not match then authentication error is thrown return $this->output(array('www-message'=>'API profile authentication failed: secret key is missing or incorrect','www-response-code'=>112),$apiState); } // HANDLING CRYPTED INPUT // If crypted input is set if(isset($apiInputData['www-crypt-input'])){ // Mcrypt is required for decryption if(extension_loaded('mcrypt')){ // Rijndael 256 bit decryption is used in CBC mode if($apiState['token'] && $apiState['command']!='www-create-session'){ $decryptedData=$this->decryptData($apiInputData['www-crypt-input'],$apiState['token'],$apiState['secret-key']); } else { $decryptedData=$this->decryptData($apiInputData['www-crypt-input'],$apiState['secret-key']); } if($decryptedData){ // Unserializing crypted data with JSON $decryptedData=json_decode($decryptedData,true); // Unserialization can fail if the data is not in correct format if($decryptedData && is_array($decryptedData)){ // Merging crypted input with set input data $apiInputData=$decryptedData+$apiInputData; } else { return $this->output(array('www-message'=>'Problem decrypting encrypted data: decrypted data is not a JSON encoded array','www-response-code'=>114),$apiState); } } else { return $this->output(array('www-message'=>'Problem decrypting encrypted data: decryption failed','www-response-code'=>114),$apiState); } } else { return $this->output(array('www-message'=>'Problem decrypting encrypted data: no methods to decrypt data','www-response-code'=>114),$apiState); } // This variable is not used anymore unset($apiInputData['www-crypt-input']); } // If this is set, then the value of this is used to crypt the output if(isset($apiInputData['www-crypt-output'])){ $apiState['crypt-output']=$apiInputData['www-crypt-output']; } // SESSION CREATION, DESTRUCTION AND VALIDATION COMMANDS // These two commands are an exception to the rule, these are the only non-public profile commands that can be executed without requiring a valid token if($apiState['command']=='www-create-session'){ // If session token subdirectory does not exist, it is created if(!is_dir($apiState['token-directory'])){ if(!mkdir($apiState['token-directory'],0755)){ return $this->output(array('www-message'=>'Server configuration error: cannot create session token folder at '.$apiState['token-directory'],'www-response-code'=>100),$apiState); } } // Token for API access is generated simply from current profile name and request time $apiState['token']=md5($apiState['profile'].$this->state->data['request-time'].$this->state->data['server-ip'].$this->state->data['request-id'].microtime()); // Session token file is created and token itself is returned to the user agent as a successful request if(file_put_contents($apiState['token-directory'].$apiState['token-file'],$apiState['token'].':'.$apiState['token-timestamp-sync'])){ // Token is returned to user agent together with current token timeout setting if($apiState['token-timeout']){ // Returning current IP together with the session if($apiState['ip-session']){ return $this->output(array('www-message'=>'API session token created','www-token'=>$apiState['token'],'www-token-timeout'=>$apiState['token-timeout'],'www-ip-session'=>$this->state->data['client-ip'],'www-response-code'=>500),$apiState); } else { return $this->output(array('www-message'=>'API session token created','www-token'=>$apiState['token'],'www-token-timeout'=>$apiState['token-timeout'],'www-response-code'=>500),$apiState); } } else { // Since token timeout is not set, the token is assumed to be infinite return $this->output(array('www-message'=>'API session token created','www-token'=>$apiState['token'],'www-token-timeout'=>'infinite','www-response-code'=>500),$apiState); } } else { return $this->output(array('www-message'=>'Server configuration error: cannot create session token file at '.$apiState['token-directory'].$apiState['token-file'],'www-response-code'=>100),$apiState); } } elseif($apiState['command']=='www-destroy-session'){ // Making sure that the token file exists, then removing it if(file_exists($apiState['token-directory'].$apiState['token-file'])){ unlink($apiState['token-directory'].$apiState['token-file']); } // Returning success message return $this->output(array('www-message'=>'API session destroyed','www-response-code'=>500),$apiState); } elseif($apiState['command']=='www-validate-session'){ // This simply returns output return $this->output(array('www-message'=>'API session validation successful','www-response-code'=>500),$apiState); } } else if(in_array($apiState['command'],array('www-create-session','www-destroy-session','www-validate-session'))){ // Since public profile is used, the session-related tokens cannot be used return $this->output(array('www-message'=>'API session token commands cannot be used with public profile','www-response-code'=>113),$apiState); } // CACHE HANDLING IF CACHE IS USED // Result of the API call is stored in this variable $apiResult=false; // This stores a flag about whether cache is used or not $this->apiLoggerData['cache-used']=false; $this->apiLoggerData['www-command']=$apiState['command']; // Calculating cache folder locations, if either load or write cache is used if($apiState['cache-load-timeout'] || $apiState['cache-timeout']){ // Calculating cache validation string $cacheValidator=$apiInputData; // If session namespace is defined, it is removed from cookies for cache validation unset($cacheValidator[$this->state->data['session-name']],$cacheValidator['www-headers'],$cacheValidator['www-cache-tags'],$cacheValidator['www-hash'],$cacheValidator['www-state'],$cacheValidator['www-timestamp'],$cacheValidator['www-crypt-output'],$cacheValidator['www-cache-timeout'],$cacheValidator['www-cache-load-timeout'],$cacheValidator['www-return-type'],$cacheValidator['www-output'],$cacheValidator['www-return-hash'],$cacheValidator['www-return-timestamp'],$cacheValidator['www-content-type'],$cacheValidator['www-minify'],$cacheValidator['www-ip-session'],$cacheValidator['www-disable-callbacks'],$cacheValidator[$this->state->data['session-token-key']]); // MD5 is used for slight performance benefits over sha1() when calculating cache validation hash string $cacheValidator=md5($apiState['command'].'&'.serialize($cacheValidator).'&'.$apiState['return-type'].'&'.$apiState['push-output'].'&'.$this->state->data['version-system'].'&'.$this->requestedVersion); // Cache filename consists of API command, serialized input data, return type and whether API output is used. $cacheFile=$cacheValidator.'.tmp'; // Setting cache folder $cacheFolder=$this->state->data['directory-system'].'filesystem'.DIRECTORY_SEPARATOR.'cache'.DIRECTORY_SEPARATOR.'output'.DIRECTORY_SEPARATOR.substr($cacheFile,0,2).DIRECTORY_SEPARATOR; } // If cache is actually loaded if($apiState['cache-load-timeout']){ // If cache file exists, it will be parsed and set as API value if($this->cacheTime($cacheFolder.$cacheFile)){ // Setting the path of cache file, since it exists $this->cacheIndex[$this->callIndex]=$cacheFolder.$cacheFile; // Current cache timeout is used to return to browser information about how long browser should store this result $apiState['last-modified']=$this->cacheTime($cacheFolder.$cacheFile); // If server detects its cache to still within cache limit if($apiState['last-modified']>=($this->state->data['request-time']-$apiState['cache-load-timeout'])){ // If this request has already been made and the last-modified timestamp is exactly the same if($apiState['push-output'] && $this->state->data['http-if-modified-since'] && $this->state->data['http-if-modified-since']==$apiState['last-modified']){ // Adding log data if($useLogger){ $this->apiLoggerData['cache-used']=true; $this->apiLoggerData['response-code']=304; } // Cache headers (Last modified is never sent with 304 header) if($this->state->data['limiter-authentication']==true || isset($this->state->data['session-data'][$this->state->data['session-user-key']]) || isset($this->state->data['session-data'][$this->state->data['session-permissions-key']])){ header('Cache-Control: private,max-age='.($apiState['last-modified']+$apiState['cache-timeout']-$this->state->data['request-time']).''); } else { header('Cache-Control: public,max-age='.($apiState['last-modified']+$apiState['cache-timeout']-$this->state->data['request-time']).''); } // This tells caching engine to take cookies and content encoding into account header('Vary: Accept-Encoding,Cookie'); // Expires header based on timeout header('Expires: '.gmdate('D, d M Y H:i:s',($apiState['last-modified']+$apiState['cache-timeout'])).' GMT'); // Returning 304 header header('HTTP/1.1 304 Not Modified'); return true; } // System loads the result from cache file based on return data type $apiResult=$this->getCache($cacheFolder.$cacheFile); // Since cache was used $this->apiLoggerData['cache-used']=true; } else { // Since cache seems to be outdated, last modified time is reset to request time $apiState['last-modified']=$this->state->data['request-time']; } } } // SOLVING API RESULT IF RESULT WAS NOT FOUND IN CACHE // If cache was not used and command result is not yet defined, system will execute the API command if(!$apiResult){ // API command is solved into bits to be parsed $commandBits=explode('-',$apiState['command'],2); // Class name is found based on command $className='WWW_controller_'.$commandBits[0]; // Class is defined and loaded, if it is not already defined if(!class_exists($className,false)){ // Overrides can be used for controllers if($this->requestedVersion && file_exists($this->state->data['directory-system'].'overrides'.DIRECTORY_SEPARATOR.'controllers'.DIRECTORY_SEPARATOR.$this->requestedVersion.DIRECTORY_SEPARATOR.'controller.'.$commandBits[0].'.php')){ require($this->state->data['directory-system'].'overrides'.DIRECTORY_SEPARATOR.'controllers'.DIRECTORY_SEPARATOR.$this->requestedVersion.DIRECTORY_SEPARATOR.'controller.'.$commandBits[0].'.php'); } elseif($this->requestedVersion && file_exists($this->state->data['directory-system'].'controllers'.DIRECTORY_SEPARATOR.$this->requestedVersion.DIRECTORY_SEPARATOR.'controller.'.$commandBits[0].'.php')){ require($this->state->data['directory-system'].'controllers'.DIRECTORY_SEPARATOR.$this->requestedVersion.DIRECTORY_SEPARATOR.'controller.'.$commandBits[0].'.php'); } elseif(file_exists($this->state->data['directory-system'].'overrides'.DIRECTORY_SEPARATOR.'controllers'.DIRECTORY_SEPARATOR.'controller.'.$commandBits[0].'.php')){ require($this->state->data['directory-system'].'overrides'.DIRECTORY_SEPARATOR.'controllers'.DIRECTORY_SEPARATOR.'controller.'.$commandBits[0].'.php'); } elseif(file_exists($this->state->data['directory-system'].'controllers'.DIRECTORY_SEPARATOR.'controller.'.$commandBits[0].'.php')){ require($this->state->data['directory-system'].'controllers'.DIRECTORY_SEPARATOR.'controller.'.$commandBits[0].'.php'); } else { // Since an error was detected, system pushes for output immediately return $this->output(array('www-message'=>'API request recognized, but unable to handle: '.$apiState['command'],'www-response-code'=>115),$apiState); } } // Second half of the command string is used to solve the function that is called by the command if(isset($commandBits[1])){ // Solving method name, dashes are underscored $methodName=str_replace('-','_',$commandBits[1]); // New controller is created based on API call $controller=new $className($this,$this->callIndex); // If command method does not exist, 501 page is returned or error triggered if(!method_exists($controller,$methodName) || !is_callable(array($controller,$methodName))){ // Since an error was detected, system pushes for output immediately return $this->output(array('www-message'=>'API request recognized, but unable to handle: '.$apiState['command'],'www-response-code'=>115),$apiState); } // Gathering every possible echoed result from method call ob_start(); // Result of the command is solved with this call // Input data is also submitted to this function $apiResult=$controller->$methodName($apiInputData); // If the method does not return anything in the result, then building an API result array if($apiResult==null){ $apiResult=array('www-message'=>'OK','www-response-code'=>500); } elseif(!is_array($apiResult)){ $apiResult=array('www-message'=>'OK','www-response-code'=>500,'www-output'=>$apiResult); } // Catching everything that was echoed and adding to array, unless output is already populated if(ob_get_length()>0){ // If string was returned from previous result, then output buffer is ignored if(!isset($apiResult['www-data'])){ $apiResult['www-data']=ob_get_clean(); } else { ob_end_clean(); } } else { ob_end_clean(); } } else { // Since an error was detected, system pushes for output immediately return $this->output(array('www-message'=>'API request recognized, but unable to handle: '.$apiState['command'],'www-response-code'=>115),$apiState); } // If process has been set to be closed if($this->closeProcess){ return true; } // If cache timeout was set then the result is stored as a cache in the filesystem if($apiState['cache-timeout']){ // If cache has not been disallowd by any of the API calls if(!isset($this->noCache[$apiState['call-index']]) || $this->noCache[$apiState['call-index']]==false){ // If cache subdirectory does not exist, it is created if(!$this->memcache && !$this->apc && !$this->databaseCache &&!is_dir($cacheFolder)){ if(!mkdir($cacheFolder,0755)){ return $this->output(array('www-message'=>'Server configuration error: cannot create cache folder at '.$cacheFolder,'www-response-code'=>100),$apiState); } } // Cache is stored together with tags, if requested if(isset($apiInputData['www-cache-tags']) && $apiInputData['www-cache-tags']!=''){ $this->setCache($cacheFolder.$cacheFile,$apiResult,$apiInputData['www-cache-tags']); } else { $this->setCache($cacheFolder.$cacheFile,$apiResult); } } else { // Setting cache timeout to 0, since cache is not stored $apiState['cache-timeout']=0; } } } // SENDING RESULT TO OUTPUT // Output observer if(isset($this->apiObservers[$apiState['command']]) && isset($this->apiObservers[$apiState['command']]['output']) && $this->apiObservers[$apiState['command']]['output']!=''){ // Output observer is called differently based on whether the returned result was an array or not if(!is_array($apiResult)){ $this->command(array('result'=>$apiResult,'www-return-type'=>'php','www-command'=>$this->apiObservers[$apiState['command']]['output'],'www-output'=>0,'www-return-hash'=>0,'www-content-type'=>false,'www-minify'=>false,'www-crypt-output'=>false,'www-cache-tags'=>false),((isset($this->apiObservers[$apiState['command']]['output-buffer']) && $this->apiObservers[$apiState['command']]['output-buffer']==true)?true:false),false); } else { $this->command(array('www-cache-timeout'=>((isset($this->apiObservers[$apiState['command']]['output-buffer']))?$this->apiObservers[$apiState['command']]['output-buffer']:0),'www-return-type'=>'php','www-command'=>$this->apiObservers[$apiState['command']]['output'],'www-output'=>0,'www-return-hash'=>0,'www-content-type'=>false,'www-minify'=>false,'www-crypt-output'=>false,'www-cache-tags'=>false)+$apiResult,((isset($this->apiObservers[$apiState['command']]['output-buffer']) && $this->apiObservers[$apiState['command']]['output-buffer']==true)?true:false),false); } } // If buffer is not disabled, response is checked from buffer if($useBuffer){ // Storing result in buffer $this->commandBuffer[$commandBufferAddress]=$this->output($apiResult,$apiState,$useLogger); // Returning result from newly created buffer return $this->commandBuffer[$commandBufferAddress]; } else { // System returns correctly formatted output data return $this->output($apiResult,$apiState,$useLogger); } } // OUTPUT /** * This is one of the two core methods of API class. Method is private and is only * called within the class. This method is used to parse the data returned from API * and returned to the user agent or system based on requested format. $apiResult is * an array that has been returned from command() method, $apiState and $useLogger are * also defined when the method is called. It returns the data as a PHP array, XML * string, INI string or any other format and with or without HTTP response headers. * * @param array $apiResult result of the API call * @param array $apiState various settings at the time of API request * @param boolean $useLogger whether logger is used * @return array/string depending on API request */ final private function output($apiResult,$apiState,$useLogger=true){ // If internal logging is enabled if($this->internalLogging){ $this->logEntry('output-data',$apiResult); } // Simple flag for error check, this is used for output encryption $errorFound=false; // Errors are detected based on response code if(isset($apiResult['www-response-code']) && $apiResult['www-response-code']<400){ if($apiState['return-type']=='php'){ // Throwing a PHP warning, if the error is either system or API Wrapper specific if(isset($apiResult['www-message'])){ trigger_error($apiResult['www-message'],E_USER_WARNING); } else { trigger_error('Undefined error with error code #'.$apiResult['www-response-code'],E_USER_WARNING); } return false; } $errorFound=true; } // This filters the result through various PHP and header specific commands if($apiState['disable-callbacks']==false && (!isset($apiResult['www-disable-callbacks']) || $apiResult['www-disable-callbacks']==false)){ $this->apiCallbacks($apiResult,$useLogger); } // Unsetting various output variables that are not needed unset($apiResult['www-disable-callbacks']); // DATA CONVERSION FROM RESULT TO REQUESTED FORMAT // If output is overwritten with www-data key, for example from output buffer if(isset($apiResult['www-data'])){ // Actual output is stored in www-output key $apiResult=$apiResult['www-data']; } else { // If state was defined if($apiState['state-key']){ $apiResult['www-state']=$apiState['state-key']; } // OUTPUT HASH VALIDATION // If timestamp is required to be returned if($apiState['return-timestamp']){ $apiResult['www-timestamp']=time(); } // If request demanded a hash to also be returned // This is only valid when the result is not an 'error' and has a secret key set if($apiState['return-hash'] && $apiState['secret-key']){ // Hash is written to returned result // Session creation and destruction commands return data is hashed without token if(!$apiState['token-timeout'] || $apiState['command']=='www-create-session'){ $apiResult['www-hash']=sha1(http_build_query($this->ksortArray($apiResult)),$apiState['secret-key']); } else { $apiResult['www-hash']=sha1(http_build_query($this->ksortArray($apiResult)),$apiState['token'].$apiState['secret-key']); } } // If www-* prefix headers were meant for headers-only if($apiState['headers']){ // Wave Framework specific keys $filter=array('www-response-code','www-message','www-token','www-token-timeout','www-ip-session','www-disable-callbacks','www-data','www-timestamp','www-hash','www-set-header','www-set-cookie','www-unset-cookie','www-set-session','www-unset-session','www-unset-header','www-temporary-redirect','www-permanent-redirect','www-xml-namespace','www-xml-root','www-xml-numeric'); foreach($filter as $f){ if(isset($apiResult[$f])){ header($f.':'.$apiResult[$f]); unset($apiResult[$f]); } } } // Data is custom-formatted based on request switch($apiState['return-type']){ case 'json': // Encodes the resulting array in JSON $apiResult=json_encode($apiResult); break; case 'xml': // Result array is turned into an XML string $apiResult=$this->toXML($apiResult); break; case 'binary': // If the result is empty string or empty array or false, then binary returns a 0, otherwise it returns 1 if((isset($apiResult['www-response-code']) && $apiResult['www-response-code']>=500) || (!isset($apiResult['www-response-code']) && !empty($apiResult))){ $apiResult=1; } else { $apiResult=0; } break; case 'rss': // Result array is turned into an XML string // The data should be formatted based on RSS 2.0 specification $apiResult=$this->toXML($apiResult,'rss'); break; case 'atom': // Result array is turned into an XML string // The data should be formatted based on Atom RSS specification $apiResult=$this->toXML($apiResult,'atom'); break; case 'csv': // Result array is turned into a CSV file $apiResult=$this->toCSV($apiResult); break; case 'serialized': // Array is simply serialized $apiResult=serialize($apiResult); break; case 'query': // Array is built into serialized query string $apiResult=http_build_query($apiResult); break; case 'print': // Array is built into serialized query string $apiResult='<pre>'.print_r($apiResult,true).'</pre>'; break; case 'ini': // This converts result into an INI string $apiResult=$this->toINI($apiResult); break; case 'php': // If PHP is used, then it can not be 'echoed' out due to being a PHP variable, so this is turned off $apiState['push-output']=0; break; case 'output': if(isset($apiResult['www-data'])){ $apiResult=$apiResult['www-data']; } else { $apiResult=''; } break; default: trigger_error('Return type not set or incorrect',E_USER_ERROR); break; } // If JSONP function wrapper name is set if($apiState['jsonp']){ $apiResult=$apiState['jsonp'].'('.$apiResult.');'; } // If JSONV variable name is set if($apiState['jsonv']){ $apiResult='var '.$apiState['jsonv'].'='.$apiResult.';'; } } // MINIFICATION // If minification is requested from API if($apiState['minify-output']==1){ // Including minification class if it is not yet defined if(!class_exists('WWW_Minify',false)){ require(__DIR__.DIRECTORY_SEPARATOR.'class.www-minifier.php'); } // Minification is based on the type of class switch($apiState['return-type']){ case 'xml': // XML minification eliminates extra spaces and newlines and other formatting $apiResult=WWW_Minifier::minifyXML($apiResult); break; case 'html': // HTML minification eliminates extra spaces and newlines and other formatting $apiResult=WWW_Minifier::minifyHTML($apiResult); break; case 'js': // JavaScript minification eliminates extra spaces and newlines and other formatting $apiResult=WWW_Minifier::minifyJS($apiResult); break; case 'css': // CSS minification eliminates extra spaces and newlines and other formatting $apiResult=WWW_Minifier::minifyCSS($apiResult); break; case 'rss': // RSS minification eliminates extra spaces and newlines and other formatting $apiResult=WWW_Minifier::minifyXML($apiResult); break; case 'atom': // RSS minification eliminates extra spaces and newlines and other formatting $apiResult=WWW_Minifier::minifyXML($apiResult); break; } } // OUTPUT ENCRYPTION if($apiState['push-output'] && $apiState['crypt-output'] && !$errorFound){ // Returned result will be with plain text instead of requested format, but only if header is not already overwritten if(!$apiState['content-type-header']){ $apiState['content-type-header']='Content-Type: text/plain;charset=utf-8'; } // If token timeout is set, then profile must be defined if($apiState['secret-key']){ // If secret key is set, then output will be crypted with CBC mode $apiResult=$this->encryptData($apiResult,$apiState['token'],$apiState['secret-key']); } else { // If secret key is not set (for public profiles), then output will be crypted with ECB mode $apiResult=$this->encryptData($apiResult,$apiState['token']); } } // FINAL OUTPUT OF RESULT // Result is printed out, headers and cache control are returned to the user agent, if output flag was set for the command if($apiState['push-output']){ // CACHE AND CUSTOM HEADERS // Cache control settings sent to the user agent depend on cache timeout settings if($apiState['cache-timeout']!=0){ // Cache control depends whether HTTP authentication is used or not if($this->state->data['limiter-authentication']==true || isset($this->state->data['session-data'][$this->state->data['session-user-key']]) || isset($this->state->data['session-data'][$this->state->data['session-permissions-key']])){ header('Cache-Control: private,max-age='.($apiState['last-modified']+$apiState['cache-timeout']-$this->state->data['request-time']).''); } else { header('Cache-Control: public,max-age='.($apiState['last-modified']+$apiState['cache-timeout']-$this->state->data['request-time']).''); } header('Expires: '.gmdate('D, d M Y H:i:s',($apiState['last-modified']+$apiState['cache-timeout'])).' GMT'); header('Last-Modified: '.gmdate('D, d M Y H:i:s',$apiState['last-modified']).' GMT'); } else { // When no cache is used, request tells specifically that header('Cache-Control: no-cache,no-store'); header('Expires: '.gmdate('D, d M Y H:i:s',$this->state->data['request-time']).' GMT'); header('Last-Modified: '.gmdate('D, d M Y H:i:s',$apiState['last-modified']).' GMT'); } // This tells caching engine to take cookies and content encoding into account header('Vary: Accept-Encoding,Cookie'); // If custom header was assigned, it is added if($apiState['custom-header']){ header($apiState['custom-header']); } // CONTENT TYPE HEADERS // If content type was set in the request then that is used for content type if($apiState['content-type-header']){ // UTF-8 is always used for returned data header('Content-Type: '.$apiState['content-type-header'].';charset=utf-8'); } else { // Data is echoed/printed based on return data type formatting with the proper header switch($apiState['return-type']){ case 'json': header('Content-Type: application/json;charset=utf-8;'); break; case 'xml': header('Content-Type: text/xml;charset=utf-8;'); break; case 'html': header('Content-Type: text/html;charset=utf-8'); break; case 'rss': header('Content-Type: application/rss+xml;charset=utf-8;'); break; case 'atom': header('Content-Type: application/atom+xml;charset=utf-8;'); break; case 'csv': header('Content-Type: text/csv;charset=utf-8;'); break; case 'js': header('Content-Type: application/javascript;charset=utf-8'); break; case 'css': header('Content-Type: text/css;charset=utf-8'); break; case 'vcard': header('Content-Type: text/vcard;charset=utf-8'); break; case 'print': header('Content-Type: text/html;charset=utf-8'); break; default: // Every other case assumes text/plain response from server header('Content-Type: text/plain;charset=utf-8'); break; } } // OUTPUT COMPRESSION // Gathering output in buffer ob_start(); // Since output was turned on, result is loaded into the output buffer echo $apiResult; // If output compression is turned on then the content is compressed if($this->state->data['output-compression']!=false){ // Different compression options can be used switch($this->state->data['output-compression']){ case 'deflate': // Notifying user agent of deflated output header('Content-Encoding: deflate'); $apiResult=gzdeflate(ob_get_clean(),9); break; case 'gzip': // Notifying user agent of gzipped output header('Content-Encoding: gzip'); $apiResult=gzencode(ob_get_clean(),9); break; default: $apiResult=ob_get_clean(); break; } } else { // Getting data from output buffer $apiResult=ob_get_clean(); } // PUSHING TO USER AGENT // Current output content length $contentLength=strlen($apiResult); // Content length is defined that can speed up website requests, letting user agent to determine file size header('Content-Length: '.$contentLength); // Data is only updated if logger is used if($useLogger){ // Notifying logger of content length $this->apiLoggerData['content-length']=$contentLength; } // Session commit, if headers have not been sent yet and the output is not nested if(ob_get_level()==0){ $this->state->commitHeaders(); } // Data is returned to the user agent echo $apiResult; // Processing is done return true; } else { // Since result was not output it is simply returned return $apiResult; } } // RESULT CALLBACKS /** * It is possible to execute certain callbacks with the API based on what data is * returned from API. It is possible to set headers with this method that will be * sent to returned output buffer. It is also possible to set and unset cookies and * sessions. It is also possible to redirect the user agent with a callback. $data * is the return data from the API and $logger and $returnType are defined from * output() method that makes the call to apiCallbacks(). This method is private * and cannot be used outside the class. * * @param array $data result data array * @param boolean $useLogger whether logger is used * @return boolean */ final private function apiCallbacks($data,$useLogger){ // HEADERS // This sets a specific header if(isset($data['www-set-header'])){ // It is possible to set multiple headers simultaneously if(is_array($data['www-set-header'])){ foreach($data['www-set-header'] as $header){ $this->state->setHeader($header); } } else { $this->state->setHeader($data['www-set-header']); } } // This sets a specific header if(isset($data['www-unset-header'])){ // It is possible to set multiple headers simultaneously if(is_array($data['www-unset-header'])){ foreach($data['www-unset-header'] as $header){ $this->state->unsetHeader($header); } } else { $this->state->unsetHeader($data['www-unset-header']); } } // COOKIES AND SESSIONS // This adds cookie from an array of settings if(isset($data['www-set-cookie']) && is_array($data['www-set-cookie'])){ // This is when a single cookie is created if(isset($data['www-set-cookie']['name'],$data['www-set-cookie']['value'])){ $this->state->setCookie($data['www-set-cookie']['name'],$data['www-set-cookie']['value'],$data['www-set-cookie']); } else { foreach($data['www-set-cookie'] as $cookie){ // Cookies require name and value to be set if(isset($cookie['name'],$cookie['value'])){ $this->state->setCookie($cookie['name'],$cookie['value'],$cookie); } } } } // This unsets cookie in user agent if(isset($data['www-unset-cookie'])){ // Multiple cookies can be unset simultaneously if(is_array($data['www-unset-cookie'])){ foreach($data['www-unset-cookie'] as $cookie){ $this->state->unsetCookie($cookie); } } else { $this->state->unsetCookie($data['www-unset-cookie']); } } // This adds a session if(isset($data['www-set-session'])){ // This is when a single cookie is created if(isset($data['www-set-session']['name'],$data['www-set-session']['value'])){ $this->state->setSession($data['www-set-session']['name'],$data['www-set-session']['value']); } else { // Session value must be an array foreach($data['www-set-session'] as $session){ if(isset($session['name'],$session['value'])){ $this->state->setSession($session['name'],$session['value']); } } } } // This unsets cookie in user agent // Multiple sessions can be unset simultaneously if(isset($data['www-unset-session'])){ if(is_array($data['www-unset-session'])){ foreach($data['www-unset-session'] as $session){ $this->state->unsetSession($session); } } else { $this->state->unsetSession($data['www-unset-session']); } } // REDIRECTS // It is possible to re-direct API after submission if(isset($data['www-temporary-redirect']) && $data['www-temporary-redirect']!=''){ // Adding log entry if($useLogger){ $this->apiLoggerData['response-code']=302; $this->apiLoggerData['temporary-redirect']=$data['www-temporary-redirect']; } // Redirection header header('Location: '.$data['www-temporary-redirect'],true,302); } elseif(isset($data['www-permanent-redirect']) && $data['www-permanent-redirect']!=''){ // Adding log entry if($useLogger){ $this->apiLoggerData['response-code']=301; $this->apiLoggerData['permanent-redirect']=$data['www-permanent-redirect']; } // Redirection header header('Location: '.$data['www-permanent-redirect'],true,301); } // Processing complete return true; } // SPECIAL RETURN TYPES /** * This allows to simply return a string from an API. * * @param string $stream text string to be returned * @return void */ final public function resultStream($stream){ // If headers should be sent with the file or not if(ob_get_level()==1){ // Returning the file contents as a string return array('www-data'=>$stream); } else { // Returning the file contents as a string return $stream; } } /** * This returns contents from a file as a response to API request. This can be used * for file downloads without revealing the actual file path in filesystem. * * @param string $location file location in filesystem * @param boolean|string $name name of the downloadable file, by default the name of the actual file * @param boolean|string $contentType this is set as a content type string in the response * @return mixed */ final public function resultFile($location,$name=false,$contentType=false){ // If file cannot be found, it cannot be returned if(!file_exists($location)){ trigger_error('Cannot find a file at '.$location,E_USER_WARNING); return false; } // If headers should be sent with the file or not if(ob_get_level()==1){ // Finding the filename if(!$name){ $tempName=explode(DIRECTORY_SEPARATOR,$location); $name=array_pop($tempName); } // Headers header('Cache-Control: no-cache,no-store'); header('Expires: '.gmdate('D, d M Y H:i:s',$this->state->data['request-time']).' GMT'); header('Last-Modified: '.gmdate('D, d M Y H:i:s',filemtime($location)).' GMT'); header('Vary: Accept-Encoding,Cookie'); if($contentType){ header('Content-Type: '.$contentType); header('Content-Disposition: filename='.$name); } else { header('Content-Type: application/octet-stream;'); header('Content-Disposition: attachment; filename='.$name); } header('Content-Length: '.filesize($location)); // Printing out the file and closing the process readfile($location); // This tells the API not to process results sent from a controller $this->closeProcess=true; } else { // Returning the file contents as a string return file_get_contents($location); } } // RESULT CONVERSIONS /** * This is a method that converts an array to an XML string. It can convert to both * common XML as well as to RSS format. $apiResult is the data sent to the request. * If $type is set to 'rss' then RSS formatting is used, otherwise a regular XML is * returned. This is an internal method used by output() call. * * @param array $apiResult array data returned from API call * @param boolean|string $type If set to 'rss' or 'atom', then transforms to RSS tags, else as XML * @return string */ final private function toXML($apiResult,$type=false){ // XML Header $xml='<?xml version="1.0" encoding="utf-8"?>'; // If RSS/ATOM namespace is set or not if($type=='rss' || $type=='atom'){ // If namespace is not set, then default Wave Framework namespace is used if(!isset($apiResult['www-xml-namespace'])){ $namespace='xmlns:www="http://github.com/kristovaher/Wave-Framework"'; } else { // Finding the namespace from the namespace URL $namespace=str_replace('xmlns:','',$apiResult['www-xml-namespace']); $namespace='xmlns:'.$namespace; unset($apiResult['www-xml-namespace']); } } else { if(isset($apiResult['www-xml-root'])){ $rootNode=$apiResult['www-xml-root']; unset($apiResult['www-xml-root']); } else { $rootNode='www'; } } // Numeric XML nodes will be defined with this if(isset($apiResult['www-xml-numeric'])){ $numeric=$apiResult['www-xml-numeric']; unset($apiResult['www-xml-numeric']); } else { $numeric='node'; } // Content header // Different XML header is used based on whether it is an RSS or not if($type=='rss'){ $xml.='<rss version="2.0" '.$namespace.'>'; } elseif($type=='atom'){ $xml.='<feed xmlns="http://www.w3.org/2005/Atom" '.$namespace.'>'; } else { $xml.='<'.$rootNode.'>'; } // This is the recursive function used $xml.=$this->toXMLnode($apiResult,$numeric); if($type=='rss'){ $xml.='</rss>'; } elseif($type=='atom'){ $xml.='</feed>'; } else { $xml.='</'.$rootNode.'>'; } // Returning the string return $xml; } /** * This is a helper method for toXML() method and is used to build an XML node. This * method is private and is not used elsewhere. $numeric is what is the tag name for * keys that are numeric (such as numeric array keys). * * @param array $data data array to convert * @param string $numeric node name for numeric values from the array * @return string */ final private function toXMLnode($data,$numeric='node'){ // By default the result is empty $return=''; foreach($data as $key=>$val){ // Keys that start with @ symbol are considered attribute containers if($key[0]!='@'){ // Attributes gatherer $attributes=''; // If attributes are set if(isset($data['@'.$key]) && is_array($data['@'.$key])){ foreach($data['@'.$key] as $attKey=>$attVal){ $attributes.=' '.$attKey.'="'.htmlspecialchars($attVal).'"'; } } // If element is an array then this function is called again recursively if(is_array($val)){ // XML does not allow numeric nodes, so generic $numeric value is used if(is_numeric($key)){ $return.='<'.$numeric.$attributes.'>'; } else { $return.='<'.$key.$attributes.'>'; } // Recursive call $return.=$this->toXMLnode($val,$numeric); if(is_numeric($key)){ $return.='</'.$numeric.'>'; } else { $return.='</'.$key.'>'; } } else { // XML does not allow numeric nodes, so generic $numeric value is used if(is_numeric($key)){ // Data is filtered for special characters $return.='<'.$numeric.$attributes.'>'.htmlspecialchars($val).'</'.$numeric.'>'; } else { $return.='<'.$key.$attributes.'>'.htmlspecialchars($val).'</'.$key.'>'; } } } } // Returning the snippet return $return; } /** * This method converts an array to CSV format, based on the structure of the array. * It uses tabs as a column separator and separates values by commas, if sub-arrays * are used. $apiResult is the data sent by output() method. * * @param array $apiResult data returned from API call * @return string */ final private function toCSV($apiResult){ // First element of the array is output $tmp=array_slice($apiResult,0,1,true); $first=array_shift($tmp); // Resulting rows are stored in this value $result=array(); // If the first array element is also an array then multidimensional CSV will be output if(is_array($first)){ // System assumes that in a multidimensional array the keys are the column names $result[]=implode("\t",array_keys($first)); // Rows will be processed as a result foreach($apiResult as $subResult){ foreach($subResult as $key=>$subSubResult){ // If result is still an array, then the values are imploded with commas if(is_array($subSubResult)){ $subSubResult=implode(',',$subSubResult); } // Potential characters will be replaced $subResult[$key]=str_replace(array("\n","\t","\r"),array('\n','\t',''),$subSubResult); } // Rows are separated with a tab character $result[]=implode("\t",$subResult); } } else { // Since first element was not an array, it is assumed that other rows are not either foreach($apiResult as $subResult){ // If other rows are an array, then the result is imploded with a comma if(is_array($subResult)){ $result[]=str_replace(array("\n","\t","\r"),array('\n','\t',''),implode(',',$subResult)); } else { $result[]=str_replace(array("\n","\t","\r"),array('\n','\t',''),$subResult); } } } // Result is imploded and returned return implode("\n",$result); } /** * This attempts to convert the data array of $apiResult to INI format. It handles * also subarrays and other possible conditions in the array. * * @param array $apiResult data returned from API call * @return string */ final private function toINI($apiResult){ // Rows of INI file are stored in this variable $result=array(); // Every array value is parsed separately foreach($apiResult as $key=>$value){ // Separate handling based on whether the value is an array or not if(is_array($value)){ // If value is an array then INI group is created for the value $result[]='['.$key.']'; // All of the group values are output foreach($value as $subkey=>$subvalue){ // If this sub-value is another array then it is imploded with commas if(is_array($subvalue)){ foreach($subvalue as $subsubkey=>$subsubvalue){ // If another array is set as a value if(is_array($subsubvalue)){ foreach($subsubvalue as $k=>$v){ if(is_array($v)){ $subsubvalue[$k]=str_replace('"','\"',serialize($v)); } else { $subsubvalue[$k]=str_replace('"','\"',$v); } } $result[]=preg_replace('/[^a-zA-Z0-9]/i','',$subkey).'['.preg_replace('/[^a-zA-Z0-9]/i','',$subsubkey).']="'.implode(',',$subsubvalue).'"'; } else { $result[]=preg_replace('/[^a-zA-Z0-9]/i','',$subkey).'['.preg_replace('/[^a-zA-Z0-9]/i','',$subsubkey).']="'.str_replace('"','\"',$subsubvalue).'"'; } } } else { // If the value was not an array, then value is simply output in INI format if(is_numeric($subvalue)){ $result[]=preg_replace('/[^a-zA-Z0-9]/i','',$subkey).'='.$subvalue; } else { $result[]=preg_replace('/[^a-zA-Z0-9]/i','',$subkey).'="'.str_replace('"','\"',$subvalue).'"'; } } } } else { // If the value was not an array, then value is simply output in INI format if(is_numeric($value)){ $result[]=preg_replace('/[^a-zA-Z0-9]/i','',$key).'='.$value; } else { $result[]=preg_replace('/[^a-zA-Z0-9]/i','',$key).'="'.str_replace('"','\"',$value).'"'; } } } // Result is imploded into line-breaks for INI format return implode("\n",$result); } // ENCRYPTION AND DECRYPTION /** * This method uses API class internal encryption function to encrypt $data string with * a key and a secret key (if set). If only $key is set, then ECB mode is used for * Rijndael encryption. * * @param string $data data to be encrypted * @param string $key key used for encryption * @param boolean|string $secretKey used for calculating initialization vector (IV) * @return string */ final public function encryptData($data,$key,$secretKey=false){ if($secretKey){ return base64_encode(mcrypt_encrypt(MCRYPT_RIJNDAEL_256,md5($key),$data,MCRYPT_MODE_CBC,md5($secretKey))); } else { return base64_encode(mcrypt_encrypt(MCRYPT_RIJNDAEL_256,md5($key),$data,MCRYPT_MODE_ECB)); } } /** * This will decrypt Rijndael encoded data string, set with $data. $key and $secretKey * should be the same that they were when the data was encrypted. * * @param string $data data to be decrypted * @param string $key key used for decryption * @param boolean|string $secretKey used for calculating initialization vector (IV) * @return string */ final public function decryptData($data,$key,$secretKey=false){ if($secretKey){ return trim(mcrypt_decrypt(MCRYPT_RIJNDAEL_256,md5($key),base64_decode($data),MCRYPT_MODE_CBC,md5($secretKey))); } else { return trim(mcrypt_decrypt(MCRYPT_RIJNDAEL_256,md5($key),base64_decode($data),MCRYPT_MODE_ECB)); } } // CACHE AND BUFFER /** * This method unsets all cache that has been stored with a specific tag keyword. * $tags variable can both be a string or an array of keywords. Every cache related * to those keywords will be removed. * * @param string|array $tags an array or comma separated list of tags that the cache was stored under * @return boolean */ final public function unsetTaggedCache($tags){ // Multiple tags can be removed at the same time if(!is_array($tags)){ $tags=explode(',',$tags); } foreach($tags as $tag){ // If this tag has actually been used, it has a file in the filesystem if(file_exists($this->state->data['directory-system'].'filesystem'.DIRECTORY_SEPARATOR.'cache'.DIRECTORY_SEPARATOR.'tags'.DIRECTORY_SEPARATOR.md5($tag).'.tmp')){ // Tag file can have links to multiple cache files $links=explode("\n",file_get_contents($this->state->data['directory-system'].'filesystem'.DIRECTORY_SEPARATOR.'cache'.DIRECTORY_SEPARATOR.'tags'.DIRECTORY_SEPARATOR.md5($tag).'.tmp')); foreach($links as $link){ // This deletes cache file or removes if from APC storage $this->unsetCache($link); } // Removing the tag link file itself unlink($this->state->data['directory-system'].'filesystem'.DIRECTORY_SEPARATOR.'cache'.DIRECTORY_SEPARATOR.'tags'.DIRECTORY_SEPARATOR.md5($tag).'.tmp'); } } return true; } /** * This method is used to clear current API command buffer. This is an optimization * method and should be used only of a lot of API calls are made that might fill the * memory allocated to PHP. What this method does is that it tells API object to empty * the internal variable that stores the results of all API calls that have already been * sent to API during this single request. * * @return boolean */ final public function clearBuffer(){ $this->commandBuffer=array(); return true; } /** * This method returns currently existing cache for currently executed API call, if it * exists. This allows you to always load cache from system in case a new response cannot * be generated. It returns cache with the key $key. * * @param string $key current API call index * @return mixed depending if cache is found, false if failed */ final public function getExistingCache($key){ // Returns cache based on cache key if(isset($this->cacheIndex[$key])){ return $this->getCache($this->cacheIndex[$key]); } else { return false; } } /** * If cache exists for currently executed API call, then this method returns the UNIX * timestamp of the time when that cache was written. It returns cache timestamp with * the key $key. * * @param string $key current API call index * @return integer or false, if timestamp does not exist */ final public function getExistingCacheTime($key){ // Returns cache time based on cache key if(isset($this->cacheIndex[$key])){ return $this->cacheTime($this->cacheIndex[$key]); } else { return false; } } /** * This method can be used to store cache for whatever needs by storing $key and * giving it a value of $value. Cache tagging can also be used with custom tag by * sending a keyword with $tags or an array of keywords. * * @param string $keyAddress unique cache URL, name or key * @param mixed $value variable value to be stored * @param boolean|array|string $tags tags array or comma-separated list of tags to attach to cache * @param boolean $custom whether cache is stored in custom cache folder * @return boolean */ final public function setCache($keyAddress,$value,$tags=false,$custom=false){ // User cache does not have an address if($custom){ // User cache location $keyAddress=__ROOT__.'filesystem'.DIRECTORY_SEPARATOR.'cache'.DIRECTORY_SEPARATOR.'custom'.DIRECTORY_SEPARATOR.md5($keyAddress.'&'.$this->state->data['version-system']).'.tmp'; } // If tag is attached to cache if($tags){ if(!is_array($tags)){ $tags=explode(',',$tags); } foreach($tags as $t){ if(!file_put_contents($this->state->data['directory-system'].'filesystem'.DIRECTORY_SEPARATOR.'cache'.DIRECTORY_SEPARATOR.'tags'.DIRECTORY_SEPARATOR.md5($t).'.tmp',$keyAddress."\n",FILE_APPEND)){ trigger_error('Cannot store cache tag at '.$keyAddress,E_USER_ERROR); } } } // Storing variable to cache if($this->memcache){ // Storing the variable in Memcache if(!$this->memcache->set($this->state->data['session-name'].$keyAddress,$value)){ trigger_error('Cannot store file cache in Memcache',E_USER_ERROR); } // Memcache requires additional field to store the timestamp of cache $this->memcache->set($this->state->data['session-name'].$keyAddress.'-time',$this->state->data['request-time']); } elseif($this->apc){ // Storing the value in APC storage if(!apc_store($keyAddress,$value)){ trigger_error('Cannot store file cache in APC',E_USER_ERROR); } // APC requires additional field to store the timestamp of cache apc_store($keyAddress.'-time',$this->state->data['request-time']); } else { // Cache can be stored in database or in filesystem if($this->databaseCache){ // Input data must be serialized $value=serialize($value); // Attempting to write cache in database if(!$this->databaseCache->dbCommand(' INSERT INTO '.$this->state->data['cache-database-table-name'].' SET '.$this->state->data['cache-database-address-column'].'=?, '.$this->state->data['cache-database-timestamp-column'].'=?, '.$this->state->data['cache-database-data-column'].'=? ON DUPLICATE KEY UPDATE '.$this->state->data['cache-database-data-column'].'=?, '.$this->state->data['cache-database-timestamp-column'].'=?;',array(md5($this->state->data['session-name'].$keyAddress),$this->state->data['request-time'],$value,$value,$this->state->data['request-time']))){ trigger_error('Cannot store file cache in database',E_USER_ERROR); } } else { // Attempting to write cache in filesystem if(!file_put_contents($keyAddress,serialize($value))){ trigger_error('Cannot store file cache at '.$keyAddress,E_USER_ERROR); } } } return true; } /** * This method fetches data from cache based on cache keyword $key, if cache exists. * This should be the same keyword that was used in setCache() method, when storing * cache. $limit sets the timestamp after which cache won't be accepted anymore and * $custom sets if the cache has been called by MVC Objects or not. * * @param string $keyAddress unique cache URL, name or key * @param boolean|integer $limit this is timestamp after which cache won't result an accepted value * @param boolean $custom whether cache is stored in custom cache folder * @return mixed or false if cache is not found */ final public function getCache($keyAddress,$limit=false,$custom=false){ // User cache does not have an address if($custom){ $keyAddress=__ROOT__.'filesystem'.DIRECTORY_SEPARATOR.'cache'.DIRECTORY_SEPARATOR.'custom'.DIRECTORY_SEPARATOR.md5($keyAddress.'&'.$this->state->data['version-system']).'.tmp'; } // If limit is used if($limit && ($this->state->data['request-time']-$limit)>$this->cacheTime($keyAddress)){ return false; } // Accessing cache if($this->memcache){ return $this->memcache->get($this->state->data['session-name'].$keyAddress); } elseif($this->apc){ return apc_fetch($keyAddress); } else { // Cache can be stored in database or in filesystem if($this->databaseCache){ // Attempting to load cache from database $result=$this->databaseCache->dbSingle('SELECT '.$this->state->data['cache-database-data-column'].' FROM '.$this->state->data['cache-database-table-name'].' WHERE '.$this->state->data['cache-database-address-column'].'=?;',array(md5($this->state->data['session-name'].$keyAddress))); if($result){ return unserialize($result[$this->state->data['cache-database-data-column']]); } } else { // Testing if file exists if(file_exists($keyAddress)){ return unserialize(file_get_contents($keyAddress)); } } } // Cache was not found return false; } /** * This function returns the timestamp of when the cache of keyword $keyAddress, was created, * if such a cache exists. * * @param string $keyAddress unique cache URL, name or key * @param boolean $custom whether cache is stored in custom cache folder * @return integer or false if cache is not found */ final public function cacheTime($keyAddress,$custom=false){ // User cache does not have an address if($custom){ $keyAddress=__ROOT__.'filesystem'.DIRECTORY_SEPARATOR.'cache'.DIRECTORY_SEPARATOR.'custom'.DIRECTORY_SEPARATOR.md5($keyAddress.'&'.$this->state->data['version-system']).'.tmp'; } // Accessing cache if($this->memcache){ return $this->memcache->get($this->state->data['session-name'].$keyAddress.'-time'); } elseif($this->apc){ if(apc_exists(array($keyAddress,$keyAddress.'-time'))){ return apc_fetch($keyAddress.'-time'); } } else { // Cache can be stored in database or in filesystem if($this->databaseCache){ // Attempting to load cache from database $result=$this->databaseCache->dbSingle('SELECT '.$this->state->data['cache-database-timestamp-column'].' FROM '.$this->state->data['cache-database-table-name'].' WHERE '.$this->state->data['cache-database-address-column'].'=?;',array(md5($this->state->data['session-name'].$keyAddress))); if($result){ return $result[$this->state->data['cache-database-timestamp-column']]; } } else { // Testing if cache file exists if(file_exists($keyAddress)){ return filemtime($keyAddress); } } } // Cache was not found return false; } /** * This method removes cache that was stored with the keyword $keyAddress, if such a cache exists. * * @param string $keyAddress unique cache URL, name or key * @param boolean $custom whether cache is stored in custom cache folder * @return boolean */ final public function unsetCache($keyAddress,$custom=false){ // User cache does not have an address if($custom){ $keyAddress=__ROOT__.'filesystem'.DIRECTORY_SEPARATOR.'cache'.DIRECTORY_SEPARATOR.'custom'.DIRECTORY_SEPARATOR.md5($keyAddress.'&'.$this->state->data['version-system']).'.tmp'; } // Accessing cache if($this->memcache){ $this->memcache->delete($this->state->data['session-name'].$keyAddress); $this->memcache->delete($this->state->data['session-name'].$keyAddress.'-time'); return true; } elseif($this->apc){ // Testing if key exists if(apc_exists($keyAddress)){ apc_delete($keyAddress); apc_delete($keyAddress.'-time'); return true; } } else { // Cache can be stored in database or in filesystem if($this->databaseCache){ // Attempting to write cache in database if($this->databaseCache->dbCommand('DELETE FROM '.$this->state->data['cache-database-table-name'].' WHERE '.$this->state->data['cache-database-address-column'].'=?;',array(md5($this->state->data['session-name'].$keyAddress)))){ return true; } } else { // Testing if cache file exists if(file_exists($keyAddress)){ return unlink($keyAddress); } } } // Deleting cache has failed return false; } // INTERNAL LOGGING /** * This method attempts to write an entry to internal log. Log entry is stored with * a $key and entry itself should be the $data. $key is needed to easily find the * log entry later on. * * @param string $key descriptive key that the log entry will be stored under * @param mixed $data data entered in log * @return boolean */ final public function logEntry($key,$data=false){ // If data is not set then key will be used as the logger message if(!$data){ $data=$key; $key='log'; } // Only applies if internal logging is turned on if($this->internalLogging && ((in_array('*',$this->internalLogging) && !in_array('!'.$key,$this->internalLogging)) || in_array($key,$this->internalLogging))){ // Preparing a log entry object $entry=array($key=>$data); // Adding log entry to log array $this->internalLog[]=$entry; // Unsetting log entry container unset($entry); return true; } else { return false; } } // PERFORMANCE LOGGING /** * This method is a timer that can be used to grade performance within the system. * When this method is called with some $key first, it will start the timer and write * an entry to log about it. If the same $key is called again, then a log entry is * created with the amount of microseconds that have passed since the last time this * method was called with this $key. * * @param string $key identifier for splitTime group * @return float */ final public function splitTime($key='api'){ // Checking if split time exists if(isset($this->splitTimes[$key])){ $this->logEntry('splitTime for ['.$key.']','Seconds since last call: '.number_format((microtime(true)-$this->splitTimes[$key]),6)); } else { $this->logEntry('splitTime for ['.$key.']','Seconds since last call: 0.000000 seconds'); } // Setting new microtime $this->splitTimes[$key]=microtime(true); // Returning current microtime return $this->splitTimes[$key]; } // DATA HANDLING /** * This method simply filters a string and returns the filtered string. Various exception * characters can be set in $exceptions string and these will not be filtered. You can set * the type to 'integer', 'float', 'numeric', 'alpha' or 'alphanumeric'. * * @param string $string value to be filtered * @param string $type filtering type, can be 'integer', 'float', 'numeric', 'alpha' or 'alphanumeric' * @param boolean|string $exceptions is a regular expression character string of all characters used as additional exceptions * @return string */ final public function filter($string,$type,$exceptions=false){ // If exceptions are set they will be escaped if($exceptions){ $exceptions=implode('\\',str_split($exceptions)); } // If exceptions are set, then we escape any non alphanumeric character in the exception string if($exceptions){ $exceptions=preg_replace('/[^[:alnum:]]/ui','\\\\\0',$exceptions); } // Filtering is done based on filter type switch($type){ case 'integer': return preg_replace('/[^0-9'.$exceptions.']/','',$string); break; case 'float': return preg_replace('/[^0-9\.\,'.$exceptions.']/','',$string); break; case 'numeric': return preg_replace('/[^0-9\.\,\-\+\ \(\)'.$exceptions.']/','',$string); break; case 'alpha': return preg_replace('/[^[:alpha:]'.$exceptions.']/ui','',$string); break; case 'alphanumeric': return preg_replace('/[^[:alnum:]'.$exceptions.']/ui','',$string); break; default: return $string; break; } } /** * This helper method is used to sort an array (and sub-arrays) based on keys. It * essentially applies ksort() method recursively to an array. * * @param array $data array to be sorted * @return array */ final 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]]); } } // Returning sorted array return $data; } } ?>