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: Re-implemented system version number and updated versioning documentation. You can also limit API version numbers with API profiles now.
Date: 8 years ago
Size: 100,558 bytes
 

Contents

Class file image Download
<?php

/**
 * Wave Framework <http://www.waveframework.com>
 * 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://www.waveframework.com"';
				} 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;
			
		}
	
}
	
?>
For more information send a message to info at phpclasses dot org.