| 
/*
 * Wave Framework <http://github.com/kristovaher/Wave-Framework>
 * 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 <[email protected] >
 * @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();
		}
		
}
 |