Login   Register  
PHP Classes
elePHPant
Icontem

File: engine/class.www-wrapper.js

Recommend this page to a friend!
Stumble It! Stumble It! Bookmark in del.icio.us Bookmark in del.icio.us
  Classes of Kristo Vaher  >  Wave Framework  >  engine/class.www-wrapper.js  >  Download  
File: engine/class.www-wrapper.js
Role: Auxiliary data
Content type: text/plain
Description: JavaScript Wrapper
Class: Wave Framework
MVC framework for building Web sites and APIs
Author: By
Last change: Fixed a problem where access-control header settings were not set properly. Removed code that was used for testing purposes and accidentally submitted to live. Added JSONP support for JavaScript wrapper as well as JSONV support (which is like JSONP, except it defines a variable instead of callback). Added a new 'file-extensions' configuration setting that is used when routing a request to File Handler. This allows to use '.' symbol in regular website URL's, since File Handler requests are whitelisted with configuration now. Methods getState(), getCookie() and getSession() now return 'null' values, if they have not been set, instead of false.
Date: 1 year ago
Size: 49,509 bytes
 

Contents

Class file image Download
/*
 * Wave Framework <http://www.waveframework.com>
 * JavaScript API Wrapper Class
 *
 * Main purpose of an API Wrapper is to make it easier to make API requests over HTTP to a system 
 * built on Wave Framework. API Wrapper class does everything for the developer without requiring 
 * the developer to learn the ins and outs of technical details about how to build an API request. 
 * Wave Framework comes with two separate API authentication methods, one more secure than the 
 * other, both which are handled by this Wrapper class. JavaScript API Wrapper does not support 
 * sending data in encrypted form or decrypting encrypted data from a response.
 *
 * @package    API
 * @author     Kristo Vaher <kristo@waher.net>
 * @copyright  Copyright (c) 2012, Kristo Vaher
 * @license    GNU Lesser General Public License Version 3
 * @tutorial   /doc/pages/wrapper_js.htm
 * @since      2.0.1
 * @version    3.6.9
 */

/*
 * Wrapper object creation requires an 'address', which is the address that Wrapper will make 
 * API requests to. If this is not defined, then 'address' assumes that the system it makes 
 * requests to is the same where the API is loaded from. 'language' is a language keyword from 
 * the system that API makes a connection with and is used whenever language-specific results 
 * are returned from API.
 * 
 * @param string $address API address, default value is current domain presumed API address
 * @param string $language language keyword, default value is current document language
 * @return object
 */
function WWW_Wrapper(address,language){

	// Finding a default, if not set
	if(address==null && document.baseURI!=undefined){
		address=document.baseURI+'json.api';
	} else if(address==null){
		// This is primarily for Internet Explorer that does not have baseURI
		var baseTags=document.getElementsByTagName('base');
		if(baseTags.length > 0) {
			address=baseTags[0].href+'json.api';
		} else {
			address='json.api';
		}
	} 
	
	// Finding a default, if not set
	if(language==null && document.documentElement.lang!=null){
		language=document.documentElement.lang;
	}

	/* 
	 * This is the address and URL of the API that the Wrapper will connect to. The API address 
	 * must be for Wave Framework API. This value is set either in object creation or when 
	 * setting 'www-address' input variable.
	 */
	var apiAddress=address;
	
	/*
	 * This holds the current language of the API, it can be useful if the API commands return 
	 * language-specific responses and translations from the API. This variable is set by sending 
	 * 'www-language' input variable.
	 */
	var apiLanguage=language;
	
	/*
	 * This holds information about current API state, such as profile name, secret key and 
	 * various API-related flags for callbacks, asyncrhonous status and more. This variable is 
	 * passed around per each API call.
	 */
	var apiState={
		apiProfile:false,
		apiSecretKey:false,
		apiToken:false,
		apiPublicToken:false,
		apiHashValidation:true,
		apiStateKey:false,
		apiVersion:false,
		returnHash:false,
		headers:false,
		returnTimestamp:false,
		trueCallback:false,
		falseCallback:false,
		errorCallback:false,
		trueCallbackParameters:false,
		falseCallbackParameters:false,
		timestampDuration:60,
		hiddenWindowCounter:0,
		hiddenScriptCounter:0,
		apiSubmitFormId:false,
		asynchronous:false,
		unserialize:true,
		JSONP:false,
		JSONV:false
	}
	
	/*
	 * This variable holds the last known error message returned from the API.
	 */
	var errorMessage=false;
	
	/*
	 * This variable holds the last known response code returned from the API.
	 */
	var responseCode=false;
	
	/*
	 * Input data is a variable that stores all the plain-text input sent with the API request, 
	 * it's a key-value pair of variables and their values for the API.
	 */
	var inputData=new Object();
	
	/*
	 * This is an array that gathers log information about the requests made through the API 
	 * that can be used for debugging purposes should something go wrong.
	 */
	var log=new Array();
	
	/*
	 * This is the user-agent string of the API Wrapper. At the moment it is not possible to 
	 * set custom headers with AJAX requests, so this variable is unused in the class and only 
	 * defined for future purpose.
	 */
	var userAgent='WWWFramework/3.6.9 (JS)';
	
	/*
	 * This is the GET string maximum length. Most servers should easily be able to deal with 
	 * 2048 bytes of request string length, but this value can be changed by submitting a 
	 * different length with 'www-get-length' input value.
	 */
	var getLimit=2048;
	
	/*
	 * If this value is set, then API log will be reset after each API request. This value can 
	 * be sent with 'www-reset-log' keyword sent to Wrapper.
	 */
	var resetLog=true;
		
	// Log entry
	log.push('Wave API Wrapper object created with API address: '+address);
	
	// This is used to refer to current object
	var that=this;
	
	// SETTINGS
		
		/*
		 * This method returns current log of the API wrapper. If 'implode' is set, then the 
		 * value of 'implode' is used as a character to implode the log with. Otherwise the log 
		 * is returned as an array.
		 *
		 * @param string $implode String to implode the log entries with
		 * @return array or string, depending on implode setting
		 */
		this.returnLog=function(implode){
			log.push('Returning log');
			// Imploding, if requested
			if(implode==null){
				return log;
			} else {
				return log.join(implode);
			}
		}
		
		/*
		 * This method clears the API log. This method can be called manually or is called 
		 * automatically if log is assigned to be reset with each new API request made by the 
		 * object.
		 * 
		 * @return boolean
		 */
		this.clearLog=function(){
			log=new Array();
			log.push('Log cleared');
			return true;
		}
		
		/*
		 * This method returns currently used token, if one exists. This can be stored for 
		 * subsequent requests with Wrapper (or manually over HTTP).
		 *
		 * @return string or false if token does not exist
		 */
		this.getToken=function(){
			// Returning from the state
			return apiState.apiToken;
		}
		
	// INPUT
		
		/*
		 * This method is used to set an input value in the API Wrapper. 'input' is the key 
		 * to set and 'value' is the value of the input key. 'input' can also be an array, 
		 * in which case multiple input values will be set in the same call. This method calls 
		 * private inputSetter() function that checks the input value for any internal flags 
		 * that might not actually be sent as an input to the API.
		 *
		 * @param string/object $input input data keyword or an object of input data
		 * @param string $value input value
		 * @return boolean
		 */
		this.setInput=function(input,value){
		
			//Default value
			if(value==null || value==false){
				value=0;
			}
			// If this is an array then it populates input array recursively
			if(typeof(input)==='object'){
				for(var node in input){
					inputSetter(node,input[node]);
				}
			} else {
				inputSetter(input,value);
			}
			return true;
			
		}
		
		/*
		 * This is a helper function that setInput() method uses to actually assign 'value' 
		 * to the 'input' keyword. A lot of the keywords set carry additional functionality 
		 * that may entirely be API Wrapper specific. This method also creates a log entry 
		 * for any value that is changed or set.
		 * 
		 * @param string $input input data key
		 * @param string $value value to be set
		 * @return boolean
		 */
		var inputSetter=function(input,value){
		
			// Input is set based on key, as some keys have additional functionality
			switch(input){
				case 'www-api':
					apiState.apiAddress=value;
					log.push('API address changed to: '+value);
					break;
				case 'www-hash-validation':
					apiState.apiHashValidation=value;
					if(value){
						log.push('API hash validation is used');
					} else {
						log.push('API hash validation is not used');
					}
					break;
				case 'www-secret-key':
					apiState.apiSecretKey=value;
					log.push('API secret key set to: '+value);
					break;
				case 'www-token':
					apiState.apiToken=value;
					log.push('API session token set to: '+value);
					break;
				case 'www-profile':
					apiState.apiProfile=value;
					log.push('API profile set to: '+value);
					break;
				case 'www-version':
					apiState.apiVersion=value;
					log.push('API version set to: '+value);
					break;
				case 'www-jsonp':
					if(value){
						apiState.JSONP=value;
						log.push('Request is made using JSONP');
					} else {
						apiState.JSONP=false;
						log.push('JSONP is not used for the request');
					}
					break;
				case 'www-jsonv':
					if(value){
						apiState.JSONV=value;
						log.push('Request is made using JSONV');
					} else {
						apiState.JSONV=false;
						log.push('JSONV is not used for the request');
					}
					break;
				case 'www-state':
					apiState.apiStateKey=value;
					log.push('API state check key set to: '+value);
					break;
				case 'www-headers':
					// Headers cannot be set for responses
					log.push('Cannot request framework specific headers to be sent as headers from server');
					break;
				case 'www-unserialize':
					if(value){
						apiState.unserialize=true;
						log.push('Returned result will be automatically unserialized');
					} else {
						apiState.unserialize=false;
						log.push('Returned result will not be automatically unserialized');
					}
					break;
				case 'www-asynchronous':
					if(value){
						apiState.asynchronous=true;
						log.push('Request will be made asynchronously');
					} else {
						apiState.asynchronous=false;
						log.push('Request will not be made asynchronously');
					}
					break;
				case 'www-return-hash':
					apiState.returnHash=value;
					if(value){
						log.push('API request will require hash validation');
					} else {
						log.push('API request will not require hash validation');
					}
					break;
				case 'www-return-timestamp':
					apiState.returnTimestamp=value;
					if(value){
						log.push('API request will require timestamp validation');
					} else {
						log.push('API request will not require timestamp validation');
					}
					break;
				case 'www-public-token':
					apiState.apiPublicToken=value;
					if(value){
						log.push('API public token set to: '+value);
					} else {
						log.push('API public token unset');
					}
					break;
				case 'www-return-type':
					inputData[input]=value;
					log.push('Input value of "'+input+'" set to: '+value);
					if(value!='json'){
						apiState.unserialize=false;
						log.push('API result cannot be unserialized, setting unserialize flag to false');
					}
					break;
				case 'www-language':
					apiLanguage=value;
					if(value){
						log.push('API result language set to: '+value);
					} else {
						log.push('API result language uninitialized');
					}
					break;
				case 'www-true-callback':
					apiState.trueCallback=value;
					if(value){
						if(typeof(value)!=='function'){
							log.push('API return true/success callback set to: '+value+'()');
						} else {
							log.push('API return true/success callback uses an anonymous function');
						}
					}
					break;
				case 'www-false-callback':
					apiState.falseCallback=value;
					if(value){
						if(typeof(value)!=='function'){
							log.push('API return false/failure callback set to: '+value+'()');
						} else {
							log.push('API return false/failure callback uses an anonymous function');
						}
					}
					break;
				case 'www-error-callback':
					apiState.errorCallback=value;
					if(value){
						if(typeof(value)!=='function'){
							log.push('API return error callback set to: '+value+'()');
						} else {
							log.push('API return error uses an anonymous function');
						}
					}
					break;
				case 'www-true-callback-parameters':
					apiState.trueCallbackParameters=value;
					if(value){
						log.push('API return true/success callback parameters set');
					}
					break;
				case 'www-false-callback-parameters':
					apiState.falseCallbackParameters=value;
					if(value){
						log.push('API return false/failure callback parameters set');
					}
					break;
				case 'www-get-limit':
					getLimit=value;
					log.push('Maximum GET string length is set to: '+value);
					break;
				case 'www-reset-log':
					resetLog=value;
					if(value){
						log.push('Log is reset after each new request');
					} else {
						log.push('Log is kept for multiple requests');
					}
					break;
				case 'www-timestamp-duration':
					apiState.timestampDuration=value;
					log.push('API valid timestamp duration set to: '+value);
					break;
				case 'www-output':
					log.push('Ignoring www-output setting, wrapper always requires output to be set to true');
					break;
				case 'www-form':
					if(value){
						that.setForm(value);
					} else if(value==false){
						that.clearForm();
					}
					break;
				default:
					if(value==true){
						value=1;
					} else if(value==false){
						value=0;
					}
					inputData[input]=value;
					log.push('Input value of "'+input+'" set to: '+value);
					break;				
			}
			return true;
			
		}
		
		/*
		 * This method sets the form ID that is used to fetch input data from. This form can 
		 * be used for uploading files with JavaScript API Wrapper or making it easy to send 
		 * large form-based requests to API over AJAX.
		 * 
		 * @param string $formId form ID value
		 * @return boolean
		 */
		this.setForm=function(formId){
		
			// Sets the form handler
			apiState.apiSubmitFormId=formId;
			// This forces another content type to stop browsers from pre-formatting the hidden iFrame content
			inputData['www-content-type']='text/html';
			log.push('Form ID set: '+formId);
			return true;
			
		}
		
		/*
		 * This method unsets the attached form from the API request.
		 *
		 * @return boolean
		 */
		this.clearForm=function(){
		
			// Resetting content type
			if(inputData['www-content-type']!=null){
				delete inputData['www-content-type'];
			}
			log.push('Form ID removed');
			apiState.apiSubmitFormId=false;
			return true;
			
		}
		
		/*
		 * This function simply deletes current input values
		 *
		 * @param boolean $reset whether to also reset authentication and other state data
		 * @return boolean
		 */
		this.clearInput=function(reset){
		
			// If authentication was also set for deletion
			if(reset!=null && reset==true){
				// Settings
				apiState.apiProfile=false;
				apiState.apiSecretKey=false;
				apiState.apiToken=false;
				apiState.apiPublicToken=false;
				apiState.apiHashValidation=true;
				apiState.apiVersion=false;
				apiState.returnHash=false;
				apiState.returnTimestamp=false;
				apiState.timestampDuration=60;
				apiState.JSONP=false;
				apiState.JSONV=false;
			}
			
			// Resetting the API state test key
			apiState.apiStateKey=false;
			// Neutralizing state settings
			apiState.unserialize=true;
			apiState.asynchronous=false;
			// Neutralizing callbacks and submit form
			apiState.trueCallback=false;
			apiState.falseCallback=false;
			apiState.errorCallback=false;
			apiState.trueCallbackParameters=false;
			apiState.falseCallbackParameters=false;
			apiState.apiSubmitFormId=false;
			// Input data
			inputData=new Object();
			// Log entry
			log.push('Input data, crypted input and file data is unset');
			return true;
			
		}
		
	// SENDING REQUEST		
		
		/*
		 * This method executes the API request by building the request based on set input data 
		 * and set forms and sending it to API using XmlHttpRequest() or through hidden iFrame 
		 * forms. It also builds all validations as well as validates the returned response 
		 * from the server and calls callback functions, if they are set. It is possible to 
		 * send input variables directly with a single call by supplying the 'variable' array. 
		 * Form ID can also be sent with the request directly.
		 *
		 * @param object $variables object of keys and values to use as input data
		 * @param string $formId form ID value
		 * @return object/void returns object on only non-async requests
		 */
		this.sendRequest=function(variables,formId){
		
			// If log is assigned to be reset with each new API request
			if(resetLog){
				that.clearLog();
			}
		
			// In case variables have been sent with a single request
			if(variables!=null && typeof(variables)=='object'){
				for(var key in variables){
					// Setting variable through input setter
					that.setInput(key,variables[key]);
				}
			}
			
			// In case form has been set
			if(formId!=null){
				that.setForm(formId);
			}
			
			// Storing input data
			var thisInputData=clone(inputData);
			// Current state settings
			var thisApiState=clone(apiState);
			
			// Assigning authentication options that are sent with the request
			if(thisApiState.apiProfile!=false){
				thisInputData['www-profile']=thisApiState.apiProfile;
			}
			// Assigning API version, if it is set
			if(thisApiState.apiVersion!=false){
				thisInputData['www-version']=thisApiState.apiVersion;
			}
			// Assigning the state check key
			if(thisApiState.apiStateKey!=false){
				thisInputData['www-state']=thisApiState.apiStateKey;
			}
			// Assigning return-timestamp flag to request
			if(thisApiState.returnTimestamp==true || thisApiState.returnTimestamp==1){
				thisInputData['www-return-timestamp']=1;
			}
			// Assigning return-hash flag to request
			if(thisApiState.returnHash==true || thisApiState.returnHash==1){
				thisInputData['www-return-hash']=1;
			}
			// Assigning public API token as part of the request
			if(thisApiState.apiPublicToken){
				thisInputData['www-public-token']=thisApiState.apiPublicToken;
			}
			// If language is set
			if(apiLanguage!=null && apiLanguage!=false){
				thisInputData['www-language']=apiLanguage;
			}
			// If JSONP is used, then assigning the JSONP function name that will be called
			if(thisApiState.JSONP==true){
				if(typeof thisApiState.JSONP=='string'){
					thisInputData['www-jsonp']=thisApiState.JSONP;
				} else {
					thisInputData['www-jsonp']='wwwJSONP';
				}
				log.push('JSONP return method is set as '+thisInputData['www-jsonp']);
			} else if(thisApiState.JSONV==true){
				if(typeof thisApiState.JSONV=='string'){
					thisInputData['www-jsonv']=thisApiState.JSONV;
				} else {
					thisInputData['www-jsonv']='wwwJSONV';
				}
				log.push('JSONV variable name is set as '+thisInputData['www-jsonv']);
			}

			// Clearing input data
			that.clearInput(false);

			// Log entry
			log.push('Starting to build request');
		
			// Correct request requires command to be set
			if(thisInputData['www-command']==null){
				return errorHandler(thisInputData,201,'API command is not set, this is required',thisApiState.errorCallback);
			}
		
			// If default value is set, then it is removed
			if(thisInputData['www-return-type']!=null && thisInputData['www-return-type']=='json'){
				log.push('Since www-return-type is set to default value, it is removed from input data');
				delete thisInputData['www-return-type'];
			}
			// If default value is set, then it is removed
			if(thisInputData['www-cache-timeout']!=null && thisInputData['www-cache-timeout']==0){
				log.push('Since www-cache-timeout is set to default value, it is removed from input data');
				delete thisInputData['www-cache-timeout'];
			}
			// If default value is set, then it is removed
			if(thisInputData['www-minify']!=null && thisInputData['www-minify']==0){
				log.push('Since www-minify is set to default value, it is removed from input data');
				delete thisInputData['www-minify'];
			}
			
			// If profile is used, then timestamp will also be sent with the request
			if(thisApiState.apiProfile){
				// Timestamp is required in API requests since it will be used for request validation and replay attack protection
				if(thisInputData['www-timestamp']==null){
					thisInputData['www-timestamp']=Math.floor(new Date().getTime()/1000);
				}
			}
						
			// If API profile and secret key are set, then wrapper assumes that non-public profile is used, thus hash and timestamp have to be included
			if(thisApiState.apiSecretKey){
			
				// Log entry
				log.push('API secret key set, hash authentication will be used');
				
				// If API hash validation is used
				if(thisApiState.apiHashValidation){
				
					// Validation hash is generated based on current serialization option
					if(thisInputData['www-hash']==null){
						// Validation requires a different hash
						var validationData=clone(thisInputData);
						// Calculating validation hash
						if(thisApiState.apiToken && thisInputData['www-command']!='www-create-session'){
							thisInputData['www-hash']=validationHash(validationData,thisApiState.apiToken+thisApiState.apiSecretKey);
						} else {
							thisInputData['www-hash']=validationHash(validationData,thisApiState.apiSecretKey);
						}
					}

					// Log entry
					if(thisApiState.apiToken){
						log.push('Validation hash created using JSON encoded input data, API token and secret key');
					} else {
						log.push('Validation hash created using JSON encoded input data and secret key');
					}
					
				} else {
				
					// Attaching secret key or token to the request
					if(thisInputData['www-command']=='www-create-session' && thisApiState.apiSecretKey){
						thisInputData['www-secret-key']=thisApiState.apiSecretKey;
						log.push('Validation will be secret key based');
					} else if(thisApiState.apiToken){
						thisInputData['www-token']=thisApiState.apiToken;
						log.push('Validation will be session token based');
					}
					
				}
				
			} else {
		
				// Token-only validation means that token will be sent to the server, but data itself will not be hashed. This works like a cookie.
				if(thisApiState.apiToken){
					// Adding token to input if it is set
					thisInputData['www-token']=thisApiState.apiToken;
					// Log entry
					log.push('Validation will be session token based');
				} else {
					// Log entry
					log.push('API secret key is not set, hash validation will not be used');
				}
				
			}
			
			// MAKING A REQUEST
				
				// Command is made slightly differently depending on whether files are to be uploaded or not
				if(thisApiState.apiSubmitFormId==false){
				
					// Default method
					var method='GET';
					// Getting input variables
					var requestData=buildRequestData(thisInputData);
					// Request string
					var requestString=apiAddress+'?'+requestData;
					// Creating request handler
					var XMLHttp=new XMLHttpRequest();
				
					// POST request is made if the URL is longer than 2048 bytes (2KB).
					// While servers can easily handle 8KB of data, servers are recommended to be vary if the GET request is longer than 2KB
					if(requestString.length>getLimit){
						// Log entries
						log.push('More than '+getLimit+' bytes would be sent, POST request will be used');
						method='POST';
						// JSONP cannot be used to submit a form
						if(thisApiState.JSONP==true || thisApiState.JSONV==true){
							return errorHandler(thisInputData,218,'JSONP/JSONV cannot be used for POST requests',thisApiState.errorCallback);
						}
					}
					
					// Separate functionality for JSONP, JSONV, synchronous and asynchronous requests
					if(thisApiState.JSONP==true){
						
						// Hidden JSONP script that makes the request
						apiState.hiddenScriptCounter++;
						var hiddenScript=document.createElement('script');
						var hiddenScriptName='WWW_API_Wrapper_Hidden_JSONP_'+apiState.hiddenScriptCounter;
						hiddenScript.type='text/javascript';
						hiddenScript.id=hiddenScriptName;
						hiddenScript.name=hiddenScriptName;
						hiddenScript.src=requestString;
						// This cleans up after itself and removes both new script tags
						hiddenScript.onload=function(){
							log.push('JSONP temporary script has been removed');
							document.head.removeChild(hiddenScript);
						};
						// This creates the script which makes the request and parses the result
						document.head.appendChild(hiddenScript);
						// Adding a log entry
						log.push('Making the JSONP request to URL: '+apiAddress);
						// Returning true, since the rest is out of the hands of this class
						return true;
						
					} else if(thisApiState.JSONV==true){
					
						// Hidden JSONP script that makes the request
						apiState.hiddenScriptCounter++;
						var hiddenScript=document.createElement('script');
						var hiddenScriptName='WWW_API_Wrapper_Hidden_JSONV_'+apiState.hiddenScriptCounter;
						hiddenScript.type='text/javascript';
						hiddenScript.id=hiddenScriptName;
						hiddenScript.name=hiddenScriptName;
						hiddenScript.src=requestString;
						// This cleans up after itself and removes both new script tags
						hiddenScript.onload=function(){
							eval('parseResult('+thisInputData['www-jsonv']+',thisInputData,thisApiState);');
							eval('delete '+thisInputData['www-jsonv']+';');
							document.head.removeChild(hiddenScript);
						};
						// This creates the script which makes the request and parses the result
						document.head.appendChild(hiddenScript);
						// Adding a log entry
						log.push('Making the JSONV request to URL: '+apiAddress);
						// Returning true, since the rest is out of the hands of this class
						return true;
						
					} else if(thisApiState.asynchronous){
						
						// Log entry
						log.push('Making '+method+' request to URL: '+apiAddress);
						// AJAX states
						XMLHttp.onreadystatechange=function(){
							if(XMLHttp.readyState===4){
								// Result based on status
								if(XMLHttp.status===200 || XMLHttp.status===304){
									log.push('Result of the request: '+XMLHttp.responseText);
									return parseResult(XMLHttp.responseText,thisInputData,thisApiState);
								} else {
									if(method=='POST'){
										return errorHandler(thisInputData,205,'POST request failed: '+XMLHttp.statusText,thisApiState.errorCallback);
									} else {
										return errorHandler(thisInputData,204,'GET request failed: '+XMLHttp.statusText,thisApiState.errorCallback);
									}
								}  
							}  
						};
						// Sending the request
						if(method=='POST'){
							XMLHttp.open(method,apiAddress,true);
							// Request header and method for POST
							XMLHttp.setRequestHeader('Content-Type','application/x-www-form-urlencoded');
							// Making the request
							XMLHttp.send(requestData);
						} else {
							XMLHttp.open(method,requestString,true);
							// Making the request
							XMLHttp.send(null);
						}
						
					} else {
					
						// Log entry
						log.push('Making '+method+' request to URL: '+apiAddress);
						// Sending the request
						if(method=='POST'){
							XMLHttp.open(method,apiAddress,false);
							// Request header and method for POST
							XMLHttp.setRequestHeader('Content-Type','application/x-www-form-urlencoded');
							// Making the request
							XMLHttp.send(requestData);
						} else {
							XMLHttp.open(method,requestString,false);
							// Making the request
							XMLHttp.send(null);
						}
						// Result based on status
						if(XMLHttp.status===200 || XMLHttp.status===304){  
							log.push('Result of the request: '+XMLHttp.responseText);
							return parseResult(XMLHttp.responseText,thisInputData,thisApiState);
						} else {
							if(method=='POST'){
								return errorHandler(thisInputData,205,'POST request failed: '+XMLHttp.statusText,thisApiState.errorCallback);
							} else {
								return errorHandler(thisInputData,204,'GET request failed: '+XMLHttp.statusText,thisApiState.errorCallback);
							}
						}
						
					}
					
				} else {
				
					// JSONP cannot be used to submit a form
					if(thisApiState.JSONP==true){
						return errorHandler(thisInputData,217,'JSONP cannot be used to submit a form',thisApiState.errorCallback);
					}
				
					// Getting the hidden form
					var apiSubmitForm=document.getElementById(thisApiState.apiSubmitFormId);
					
					if(apiSubmitForm==null){
						return errorHandler(thisInputData,215,'Form not found: '+thisApiState.apiSubmitFormId,thisApiState.errorCallback);
					}
				
					// Hidden iFrame
					apiState.hiddenWindowCounter++;
					var hiddenWindow=document.createElement('iframe');
					var hiddenWindowName='WWW_API_Wrapper_Hidden_iFrame_'+apiState.hiddenWindowCounter;
					hiddenWindow.id=hiddenWindowName;
					hiddenWindow.name=hiddenWindowName;
					hiddenWindow.style.display='none';
					apiSubmitForm.appendChild(hiddenWindow);
					
					//Old parameters
                    var old_formAction=apiSubmitForm.action;
                    var old_formTarget=apiSubmitForm.target;
                    var old_formMethod=apiSubmitForm.method;
					var old_formEnctype=apiSubmitForm.enctype;
					
					// Preparing form submission
					apiSubmitForm.method='POST';
					apiSubmitForm.action=apiAddress;
					apiSubmitForm.setAttribute('enctype','multipart/form-data'); // Done differently because of IE8
					apiSubmitForm.setAttribute('encoding','multipart/form-data'); // IE6 wants this
					apiSubmitForm.target=hiddenWindowName;
					
					// Input data
					var counter=0;
					var hiddenFields=new Object();
					for(var node in thisInputData){
						counter++;
						hiddenFields[counter]=document.createElement('input');
						hiddenFields[counter].id=('www_hidden_form_data_'+counter);
						hiddenFields[counter].name=node;
						hiddenFields[counter].value=thisInputData[node];
						hiddenFields[counter].type='hidden';
						apiSubmitForm.appendChild(hiddenFields[counter]);
						// Log entry
						log.push('Attaching variable to form request: '+node+'='+thisInputData[node]);
					}
					
					// This is on-load function for iFrame
					var onLoad=function(){
						// Resetting the form data
						if(old_formMethod!=null){
							apiSubmitForm.method=old_formMethod;
						} else {
							apiSubmitForm.method='';
						}
						if(old_formAction!=null){
							apiSubmitForm.action=old_formAction;
						} else {
							apiSubmitForm.action='';
						}
						if(old_formEnctype!=null){
							apiSubmitForm.setAttribute('enctype',old_formEnctype);
							apiSubmitForm.setAttribute('encoding',old_formEnctype);
						} else {
							apiSubmitForm.setAttribute('enctype','');
							apiSubmitForm.setAttribute('encoding','');
						}
						if(old_formTarget!=null){
							apiSubmitForm.target=old_formTarget;
						} else {
							apiSubmitForm.target='';
						}
						// Removing created elements
						for(var i=1;i<=counter;i++){
							if(hiddenFields[i]!=null){
								hiddenFields[i].parentNode.removeChild(hiddenFields[i]);
							}
						}
						// Parsing the result
						var resultData=hiddenWindow.contentWindow.document.body.innerHTML;
						// Log entry
						log.push('Result of the request: '+resultData);
						resultData=parseResult(resultData,thisInputData,thisApiState);
						// Removing hidden iFrame
						setTimeout(function(){apiSubmitForm.removeChild(hiddenWindow);},100);
						return resultData;
					}
					
					// Hidden iFrame onload function
					// Two versions, one IE compatible, another one not
					if(hiddenWindow.attachEvent==null){
						hiddenWindow.onload=onLoad;
					} else {
						hiddenWindow.attachEvent('onload',onLoad);
					}

					// Log entry
					log.push('Making POST request to URL: '+apiAddress);
					
					// Submitting form
					apiSubmitForm.submit();	
					
				}
			
		}
				
		/*
		 * JavaScript API Wrapper handles asynchronous requests and all of request callbacks, 
		 * which allows to make multiple API requests at the same time or in sequence. This 
		 * method validates the response data, if validation is requested and executes set 
		 * callbacks with the results. 'resultData' is the response from the API call, 
		 * 'thisInputData' is the original input sent to the request and 'thisApiState' is 
		 * the API Wrapper state at the time of the request.
		 *
		 * @param string $resultData result string from response
		 * @param object $thisInputData data that was sent as input
		 * @param object $thisApiState api state for this request
		 * @return object/string response data from request depending on settings
		 */
		var parseResult=function(resultData,thisInputData,thisApiState){
		
			// Returning the result directly if the result is not intended to be unserialized
			if(!thisApiState.unserialize){
			
				// Log entry for returning data
				log.push('Returning result without unserializing');
				// Data is simply returned if serialization was not requested
				return resultData;
				
			} else {
				
				// PARSING REQUEST RESULT
			
					// The result is only parsed if it is not already an object
					if(typeof(resultData)!=='object'){
						// If unserialize command was set and the data type was JSON or serialized array, then it is returned as serialized
						if(thisInputData['www-return-type']==null || thisInputData['www-return-type']=='json'){
							// JSON support is required
							if(typeof(JSON)!=='undefined'){
								resultData=JSON.parse(resultData);
							} else if(typeof(jQuery)!=='undefined'){
								// Attempting to unserialize with jQuery
								resultData=jQuery.parseJSON(resultData);
							} else {
								// Could not unserialize
								return errorHandler(thisInputData,207,'Cannot unserialize returned data',thisApiState.errorCallback);
							}
							log.push('Returning JSON object');
						} else if(thisApiState.unserialize){
							// Every other unserialization attempt fails
							return errorHandler(thisInputData,207,'Cannot unserialize returned data',thisApiState.errorCallback);
						}
					}
					
					// If error was detected
					if(resultData['www-response-code']!=null && resultData['www-response-code']<400){
						if(resultData['www-message']!=null){
							return errorHandler(thisInputData,resultData['www-response-code'],resultData['www-message'],thisApiState.errorCallback);
						} else {
							return errorHandler(thisInputData,resultData['www-response-code'],'',thisApiState.errorCallback);
						}
					}
				
				// RESULT VALIDATION
				
					// Result validation only applies to non-public profiles
					if(thisApiState.apiProfile && (thisApiState.returnTimestamp || thisApiState.returnHash)){
						
						// If it was requested that validation timestamp is returned
						if(thisApiState.returnTimestamp){
							if(resultData['www-timestamp']!=null){
								// Making sure that the returned result is within accepted time limit
								if(((Math.floor(new Date().getTime()/1000))-thisApiState.timestampDuration)>resultData['www-timestamp']){
									return errorHandler(thisInputData,209,'Validation timestamp is too old',thisApiState.errorCallback);
								}
							} else {
								return errorHandler(thisInputData,208,'Validation data missing: Timestamp was not returned',thisApiState.errorCallback);
							}
						}
						
						// If it was requested that validation timestamp is returned
						if(thisApiState.apiStateKey){
							if(resultData['www-state']==null || resultData['www-state']!=thisApiState.apiStateKey){
								return errorHandler(thisInputData,210,'Validation state keys do not match',thisApiState.errorCallback);
							}
						}
						
						// If it was requested that validation hash is returned
						if(thisApiState.returnHash){
							// Hash and timestamp have to be defined in response
							if(resultData['www-hash']!=null){
							
								// Assigning returned array to hash validation array
								var validationData=clone(resultData);
								// Hash itself is removed from validation
								delete validationData['www-hash'];
								
								// Validation depends on whether session creation or destruction commands were called
								if(thisInputData['www-command']=='www-create-session'){
									var resultHash=validationHash(validationData,thisApiState.apiSecretKey);
								} else {
									var resultHash=validationHash(validationData,thisApiState.apiToken+thisApiState.apiSecretKey);
								}
								
								// If sent hash is the same as calculated hash
								if(resultHash==resultData['www-hash']){
									log.push('Hash validation successful');
								} else {
									return errorHandler(thisInputData,210,'Hash validation failed',thisApiState.errorCallback);
								}
								
							} else {
								return errorHandler(thisInputData,208,'Validation data missing: Hash was not returned',thisApiState.errorCallback);
							}
						}
					
					}
								
				// Resetting the error variables
				responseCode=false;
				errorMessage=false;
				
				// If this command was to create a token
				if(thisInputData['www-command']=='www-create-session' && resultData['www-token']!=null){
					apiState.apiToken=resultData['www-token'];
					log.push('Session token was found in reply, API session token set to: '+resultData['www-token']);
				} else if(thisInputData['www-command']=='www-destroy-session'){
					apiState.apiToken=false;
					log.push('Session has been destroyed');
				}
				
				// If callback function is set
				if(thisApiState.trueCallback && resultData['www-response-code']!=null && resultData['www-response-code']>=500){
					// If the callback is a function name and not a function itself
					if(typeof(thisApiState.trueCallback)!=='function'){
						// Calling user function
						var thisCallback=this.window[thisApiState.trueCallback];
						if(typeof(thisCallback)==='function'){
							log.push('Sending data to callback: '+thisApiState.trueCallback+'()');
							// Callback execution
							if(thisApiState.trueCallbackParameters!=false){
								return thisCallback.call(this,resultData,thisApiState.trueCallbackParameters);
							} else {
								return thisCallback.call(this,resultData);
							}
						} else {
							return errorHandler(thisInputData,216,'Callback method not found: '+thisApiState.trueCallback+'()',thisApiState.errorCallback);
						}
					} else {
						// Returning data from callback
						if(thisApiState.trueCallbackParameters!=false){
							return thisApiState.trueCallback(resultData,thisApiState.trueCallbackParameters);
						} else {
							return thisApiState.trueCallback(resultData);
						}
					}
				} else if(thisApiState.falseCallback && resultData['www-response-code']!=null && resultData['www-response-code']<500){
					// If the callback is a function name and not a function itself
					if(typeof(thisApiState.falseCallback)!=='function'){
						// Calling user function
						var thisCallback=this.window[thisApiState.falseCallback];
						if(typeof(thisCallback)==='function'){
							log.push('Sending failure data to callback: '+thisApiState.falseCallback+'()');
							// Callback execution
							if(thisApiState.falseCallbackParameters!=false){
								return thisCallback.call(this,resultData,thisApiState.falseCallbackParameters);
							} else {
								return thisCallback.call(this,resultData);
							}
						} else {
							return errorHandler(thisInputData,216,'Callback method not found: '+thisApiState.falseCallback+'()',thisApiState.errorCallback);
						}
					} else {
						// Returning data from callback
						if(thisApiState.falseCallbackParameters!=false){
							return thisApiState.falseCallback(resultData,thisApiState.falseCallbackParameters);
						} else {
							return thisApiState.falseCallback(resultData);
						}
					}
				} else {
					// Returning request result
					return resultData;
				}	
			
			}			

		}
		
	// REQUIRED FUNCTIONS
	
		/*
		 * This method is simply meant for returning a result if there was an error in the sent request
		 *
		 * @param object $thisInputData input data sent to the request
		 * @param string $thisResponseCode response code value
		 * @param string $thisErrorMessage returned error message text
		 * @param string/function $thisErrorCallback anonymous function or function name to be called
		 * @return boolean/mixed depending on whether callback function is called or not
		 */
		var errorHandler=function(thisInputData,thisResponseCode,thisErrorMessage,thisErrorCallback){
		
			// Assigning error details to object state
			responseCode=thisResponseCode;
			errorMessage=thisErrorMessage;
			log.push(errorMessage);
			
			// If failure callback has been defined
			if(thisErrorCallback){
				// If the callback is a function name and not a function itself
				if(typeof(thisErrorCallback)!=='function'){
					// Looking for function of that name
					var thisCallback=this.window[thisErrorCallback];
					if(typeof(thisCallback)==='function'){
						log.push('Sending failure data to callback: '+thisErrorCallback+'()');
						// Callback execution
						return thisCallback.call(this,{'www-input':thisInputData,'www-response-code':responseCode,'www-message':errorMessage});
					} else {
						responseCode=216;
						errorMessage='Callback method not found: '+thisErrorCallback+'()';
						log.push(errorMessage);
						return false;
					}
				} else {
					// Returning data from callback
					thisErrorCallback(thisInputData);
				}
			} else {
				return false;
			}
			
		}
		
		/*
		 * This helper method is used to clone one JavaScript object to another 'object' is 
		 * the JavaScript object to be converted.
		 *
		 * @param object $object object to be cloned
		 * @return object
		 */
		var clone=function(object){
		
			// Cloning based on type
			if(object==null || typeof(object)!=='object'){
				return object;
			}
			var tmp=object.constructor();
			for(var key in object){
				tmp[key]=clone(object[key]);
			}
			return tmp;
			
		}
		
		/*
		 * This method is used to build an input data validation hash string for authenticating 
		 * API requests. The entire input array of 'validationData' is serialized and hashed 
		 * with SHA-1 and a salt string set in 'postFix'. This is used for all API requests 
		 * where input has to be validated.
		 * 
		 * @param object $validationData data to be used for hash generation
		 * @param string $postFix will be appended prior to hash being generated
		 * @return string
		 */
		var validationHash=function(validationData,postFix){
		
			// Sorting and encoding the output data
			validationData=ksortArray(validationData);
			// Returning validation hash		
			return sha1(buildRequestData(validationData)+postFix);
			
		}
		
		/*
		 * This is a helper function used by validationHash() function to serialize an array 
		 * recursively. It applies ksort() to main method as well as to all sub-arrays. 'data' 
		 * is the array or object to be sorted.
		 *
		 * @param object/mixed $data variable to be sorted
		 * @return mixed
		 */
		var ksortArray=function(data){
		
			// Method is based on the current data type
			if(typeof(data)==='array' || typeof(data)==='object'){
				// Sorting the current array
				data=ksort(data);
				// Sorting every sub-array, if it is one
				for(var i in data){
					data[i]=ksortArray(data[i]);
				}
			}
			return data;
			
		}
		
		/*
		 * This is a method that is similar to PHP http_build_query() function. It builds a 
		 * GET request string of input variables set in 'data'.
		 *
		 * @param object $data object to build request data string from
		 * @return string
		 */
		var buildRequestData=function(data){
		
			// variables are stored in array
			var variables=new Array();
			for(var i in data){
				// Using the helper function
				var query=subRequestData(i,data[i]);
				if(query!=''){
					variables.push(query);
				}
			}
			return variables.join('&');
			
		}
		
		/* 
		 * This is a helper function for buildRequestData() method, it converts between 
		 * different ways data is represented in a GET request string.
		 * 
		 * @param string $key key value
		 * @param mixed $value variable value
		 * @return string 
		 */
		var subRequestData=function(key,value){
		
			// variables are stored in array
			var variables=new Array();
			if(value!=null){
				// Converting true/false to numeric
				if(value===true){
					value='1';
				} else if(value===false){
					value='0';
				}
				// Object will be parsed through subRequestData recursively
				if(typeof(value)==='object'){
					for(var i in value){
						if(value[i]!=null){
							variables.push(subRequestData(key+'['+i+']',value[i]));
						}
					}
					return variables.join('&');
				} else {
					return encodeValue(key)+'='+encodeValue(value);
				}
			} else {
				return '';
			}
			
		}
		
		/*
		 * This helper method converts certain characters into their suitable form that would 
		 * be accepted and same as in PHP. This is a modified version of encodeURIComponent() 
		 * function. 'data' is the string to be converted.
		 * 
		 * @param string $data string to encode
		 * @return string
		 */
		var encodeValue=function(data){
		
			// Series of filters applied to the value
			data=encodeURIComponent(data);
			data=data.replace('\'','%27');
			data=data.replace('!','%21');
			data=data.replace('(','%28');
			data=data.replace(')','%29');
			data=data.replace('*','%2A');
			data=data.replace('%20','+');
			return data;
			
		}
	
		/*
		 * This is a JavaScript method that works similarly to PHP's ksort() function and 
		 * applies to JavaScript objects. 'object' is the object to be sorted.
		 * 
		 * @param object $object object to sort by keys
		 * @return object
		 */
		var ksort=function(object){
		
			// Result will be gathered here
			var keys=new Array();
			var sorted=new Object();
			// Sorted keys gathered in sorting array
			for(var i in object){
				keys.push(i);
			}
			// Sorting the keys
			keys.sort();
			// Building a new object based on sorted keys
			for(var i in keys){
				sorted[keys[i]]=object[keys[i]];
			}
			return sorted;
			
		}
	
		/*
		 * This is a JavaScript equivalent of PHP's sha1() function. It calculates a hash 
		 * string from 'msg' string.
		 * 
		 * @author http://www.webtoolkit.info/javascript-sha1.html
		 * @param string $msg string to hash
		 * @return string
		 */
		var sha1=function(msg){
			function rotate_left(n,s) {
				var t4 = ( n<<s ) | (n>>>(32-s));
				return t4;
			}
			function lsb_hex(val) {
				var str="";
				var i;
				var vh;
				var vl;
				for( i=0; i<=6; i+=2 ) {
					vh = (val>>>(i*4+4))&0x0f;
					vl = (val>>>(i*4))&0x0f;
					str += vh.toString(16) + vl.toString(16);
				}
				return str;
			}
			function cvt_hex(val) {
				var str="";
				var i;
				var v;
				for( i=7; i>=0; i-- ) {
					v = (val>>>(i*4))&0x0f;
					str += v.toString(16);
				}
				return str;
			}
			function Utf8Encode(string) {
				string = string.replace(/\r\n/g,"\n");
				var utftext = "";
				for (var n = 0; n < string.length; n++) {
					var c = string.charCodeAt(n);
					if (c < 128) {
						utftext += String.fromCharCode(c);
					}
					else if((c > 127) && (c < 2048)) {
						utftext += String.fromCharCode((c >> 6) | 192);
						utftext += String.fromCharCode((c & 63) | 128);
					}
					else {
						utftext += String.fromCharCode((c >> 12) | 224);
						utftext += String.fromCharCode(((c >> 6) & 63) | 128);
						utftext += String.fromCharCode((c & 63) | 128);
					}
				}
				return utftext;
			}
			var blockstart;
			var i, j;
			var W = new Array(80);
			var H0 = 0x67452301;
			var H1 = 0xEFCDAB89;
			var H2 = 0x98BADCFE;
			var H3 = 0x10325476;
			var H4 = 0xC3D2E1F0;
			var A, B, C, D, E;
			var temp;
			msg = Utf8Encode(msg);
			var msg_len = msg.length;
			var word_array = new Array();
			for( i=0; i<msg_len-3; i+=4 ) {
				j = msg.charCodeAt(i)<<24 | msg.charCodeAt(i+1)<<16 |
				msg.charCodeAt(i+2)<<8 | msg.charCodeAt(i+3);
				word_array.push( j );
			}
			switch( msg_len % 4 ) {
				case 0:
					i = 0x080000000;
				break;
				case 1:
					i = msg.charCodeAt(msg_len-1)<<24 | 0x0800000;
				break;
				case 2:
					i = msg.charCodeAt(msg_len-2)<<24 | msg.charCodeAt(msg_len-1)<<16 | 0x08000;
				break;
				case 3:
					i = msg.charCodeAt(msg_len-3)<<24 | msg.charCodeAt(msg_len-2)<<16 | msg.charCodeAt(msg_len-1)<<8	| 0x80;
				break;
			}
			word_array.push( i );
			while( (word_array.length % 16) != 14 ) word_array.push( 0 );
			word_array.push(msg_len>>>29);
			word_array.push((msg_len<<3)&0x0ffffffff);
			for ( blockstart=0; blockstart<word_array.length; blockstart+=16 ){
				for( i=0; i<16; i++ ) W[i] = word_array[blockstart+i];
				for( i=16; i<=79; i++ ) W[i] = rotate_left(W[i-3] ^ W[i-8] ^ W[i-14] ^ W[i-16], 1);
				A = H0;
				B = H1;
				C = H2;
				D = H3;
				E = H4;
				for( i= 0; i<=19; i++ ) {
					temp = (rotate_left(A,5) + ((B&C) | (~B&D)) + E + W[i] + 0x5A827999) & 0x0ffffffff;
					E = D;
					D = C;
					C = rotate_left(B,30);
					B = A;
					A = temp;
				}
				for( i=20; i<=39; i++ ) {
					temp = (rotate_left(A,5) + (B ^ C ^ D) + E + W[i] + 0x6ED9EBA1) & 0x0ffffffff;
					E = D;
					D = C;
					C = rotate_left(B,30);
					B = A;
					A = temp;
				}
				for( i=40; i<=59; i++ ) {
					temp = (rotate_left(A,5) + ((B&C) | (B&D) | (C&D)) + E + W[i] + 0x8F1BBCDC) & 0x0ffffffff;
					E = D;
					D = C;
					C = rotate_left(B,30);
					B = A;
					A = temp;
				}
				for( i=60; i<=79; i++ ) {
					temp = (rotate_left(A,5) + (B ^ C ^ D) + E + W[i] + 0xCA62C1D6) & 0x0ffffffff;
					E = D;
					D = C;
					C = rotate_left(B,30);
					B = A;
					A = temp;
				}
				H0 = (H0 + A) & 0x0ffffffff;
				H1 = (H1 + B) & 0x0ffffffff;
				H2 = (H2 + C) & 0x0ffffffff;
				H3 = (H3 + D) & 0x0ffffffff;
				H4 = (H4 + E) & 0x0ffffffff;
			}
			var temp = cvt_hex(H0) + cvt_hex(H1) + cvt_hex(H2) + cvt_hex(H3) + cvt_hex(H4);
			return temp.toLowerCase();
		}
		
}