<?php
/**
 * AMFDeserializer takes the raw amf input stream and converts it PHP objects
 * representing the data.
 * 
 * @license http://opensource.org/licenses/gpl-license.php GNU Public License
 * @copyright (c) 2003 amfphp.org
 * @package flashservices
 * @subpackage io
 * @version $Id$
 */

/**
 * Required classes
 */ 
require_once(AMFPHP_BASE . "util/AMFBody.php");
require_once(AMFPHP_BASE . "util/AMFHeader.php");
require_once(AMFPHP_BASE . "util/DateWrapper.php");

class AMFDeserializer {
	/**
	 * The raw data input
	 * 
	 * @access private 
	 * @var string 
	 */
	var $raw_data;

	/**
	 * The current seek cursor of the stream
	 * 
	 * @access private 
	 * @var int 
	 */
	var $current_byte;

	/**
	 * The length of the stream.  Since this class is not actually using a stream
	 * the entire content of the stream is passed in as the initial argument so the
	 * length can be determined.
	 * 
	 * @access private 
	 * @var int 
	 */
	var $content_length;
	
	/**
	 * The number of headers in the packet.
	 * 
	 * @access private 
	 * @var int 
	 */
	var $header_count;

	/**
	 * The content of the packet headers
	 * 
	 * @access private 
	 * @var string 
	 */
	var $headers;

	/**
	 * The number of bodys in the packet.
	 * 
	 * @access private 
	 * @var int 
	 */
	var $body_count;

	/**
	 * The content of the body elements
	 * 
	 * @access private 
	 * @var string 
	 */
	var $body;

	/**
	 * The object to store the amf data.
	 * 
	 * @access private 
	 * @var object 
	 */
	var $amfdata;

	/**
	 * The instance of the amfinput stream object
	 * 
	 * @access private 
	 * @var object 
	 */
	var $inputStream;
	
	/**
	 * metaInfo
	 */
	var $meta;

	/**
	 * Constructor method for the deserializer.  Constructing the deserializer converts the input stream
	 * content to a AMFObject.
	 * 
	 * @param object $is The referenced input stream
	 */
	function AMFDeserializer($rd) {
		$this->isBigEndian = AMFPHP_BIG_ENDIAN;
		$this->current_byte = 0;
		$this->raw_data = $rd;  // store the stream in this object
		$this->content_length = strlen($this->raw_data); // grab the total length of this stream
		$this->charsetHandler = new CharsetHandler('flashtophp');
	} 

	/**
	 * deserialize invokes this class to transform the raw data into valid object
	 * 
	 * @param object $amfdata The object to put the deserialized data in
	 */
	function deserialize (&$amfdata) {
		$this->amfdata = &$amfdata;
		$this->readHeader(); // read the binary header
		$this->readBody(); // read the binary body
	} 
	/**
	 * returns the built AMFObject from the deserialization operation
	 * 
	 * @return object The deserialized AMFObject
	 */
	function getAMFObject() {
		return $this->amfdata;
	} 

	/**
	 * readHeader converts that header section of the amf message into php obects.
	 * Header information typically contains meta data about the message.
	 */
	function readHeader() {

		$topByte = $this->readByte(); // ignore the first two bytes --  version or something
		$secondByte = $this->readByte(); //0 for Flash,
														//1 for FlashComm                   
		//Disable debug events for FlashComm
		$GLOBALS['amfphp']['isFlashComm'] = $secondByte == 1;
		
		//If firstByte != 0, then the AMF data is corrupted, for example the transmission 
		//
		if(!($topByte == 0 || $topByte == 3))
		{
			trigger_error("Malformed AMF message, connection may have dropped");
			exit();
		}
		$this->header_count = $this->readInt(); //  find the total number of header elements
		while ($this->header_count--) { // loop over all of the header elements
			$name = $this->readUTF();
			$required = $this->readByte() == 1; // find the must understand flag
			//$length   = $this->readLong(); // grab the length of  the header element
			$this->current_byte += 4; // grab the length of the header element
			$type = $this->readByte();  // grab the type of the element
			$content = $this->readData($type); // turn the element into real data
			$this->amfdata->addHeader(new AMFHeader($name, $required, $content)); // save the name/value into the headers array
		}

	} 

	/**
	 * readBody converts the payload of the message into php objects.
	 */
	function readBody() {
		$this->body_count = $this->readInt(); // find the total number  of body elements
		while ($this->body_count--) { // loop over all of the body elements
			
			$target = $this->readUTF();
			$response = $this->readUTF(); //    the response that the client understands
			
			//$length = $this->readLong(); // grab the length of    the body element
			$this->current_byte += 4;
			$type = $this->readByte();  // grab the type of the element
			$data = $this->readData($type); // turn the argument elements into real data
			$this->amfdata->addBody(new AMFBody($target, $response, $data)); // add the body element to the body object
			
		} 
	} 

	/**
	 * readObject reads the name/value properties of the amf message and converts them into
	 * their equivilent php representation
	 * 
	 * @return array The php array with the object data
	 */
	function readObject() {
		$ret = array(); // init the array
		$key = $this->readUTF(); // grab the key
		for ($type = $this->readByte(); $type != 9; $type = $this->readByte()) {
			$val = $this->readData($type); // grab the value
			$ret[$key] = $val; // save the name/value pair in the array
			$key = $this->readUTF(); // get the next name
		}
		return $ret; // return the array
	} 
	
	/**
	 * readMixedObject reads the name/value properties of the amf message and converts
	 * numeric looking keys to numeric keys
	 * 
	 * @return array The php array with the object data
	 */
	function readMixedObject() {
		$ret = array(); // init the array
		$key = $this->readUTF(); // grab the key
		for ($type = $this->readByte(); $type != 9; $type = $this->readByte()) {
			$val = $this->readData($type); // grab the value
			if(is_numeric($key))
			{
				$key = (float) $key;
			}
			$ret[$key] = $val; // save the name/value pair in the array
			$key = $this->readUTF(); // get the next name
		}
		return $ret; // return the array
	} 

	/**
	 * readArray turns an all numeric keyed actionscript array into a php array.
	 * 
	 * @return array The php array
	 */
	function readArray() {
		$ret = array(); // init the array object
		$length = $this->readLong(); // get the length  of the array
		for ($i = 0; $i < $length; $i++) { // loop over all of the elements in the data
			$type = $this->readByte();  // grab the type for each element
			$ret[] = $this->readData($type); // grab each element
		} 
		return $ret; // return the data
		
	} 

	/**
	 * readMixedArray turns an array with numeric and string indexes into a php array
	 * 
	 * @return array The php array with mixed indexes
	 */
	function readMixedArray() {
		//$length   = $this->readLong(); // get the length  property set by flash
		$this->current_byte += 4;
		return $this->readMixedObject(); // return the body of mixed array
	} 

	/**
	 * readCustomClass reads the amf content associated with a class instance which was registered
	 * with Object.registerClass.  In order to preserve the class name an additional property is assigned
	 * to the object "_explicitType".  This property will be overwritten if it existed within the class already.
	 * 
	 * @return object The php representation of the object
	 */
	function readCustomClass() {
		$typeIdentifier = str_replace('..', '', $this->readUTF());
		
		$value = $this->readObject(); // the rest of the bytes are an object without the 0x03 header
		$value["_explicitType"] = $typeIdentifier; // save that type because we may need it if we can find a way to add debugging features
		
		//Check out if class exists
		$mappedClass = str_replace('.', '/', $typeIdentifier);
		if(isset($GLOBALS['amfphp']['incomingClassMappings'][$typeIdentifier]))
		{
			$mappedClass = str_replace('.', '/', $GLOBALS['amfphp']['incomingClassMappings'][$typeIdentifier]);
		}
		
		$include = FALSE;
		if(file_exists($GLOBALS['amfphp']['customMappingsPath'] . $mappedClass . '.php'))
		{
			$include = $GLOBALS['amfphp']['customMappingsPath'] . $mappedClass . '.php';
		}
		elseif(file_exists($GLOBALS['amfphp']['customMappingsPath'] . $mappedClass . '.class.php'))
		{
			$include = $GLOBALS['amfphp']['customMappingsPath'] . $mappedClass . '.class.php';
		}
		
		if($include !== FALSE)
		{
			include_once($include);
			$lastPlace = strrpos('/' . $mappedClass, '/');
			$classname = substr($mappedClass, $lastPlace);
			if(class_exists($classname))
			{
				$clazz = new $classname;
				//Map this baby
				foreach($value as $key => $contents)
				{
					if($key != '_explicitType')
					{   
						//Input values
						$clazz->$key = $contents;
					}
				}
				
				if(method_exists ( $clazz, 'init' ))
				{
					$clazz->init();
				}
				$value = $clazz;
			}
		}
		
		return $value; // return the object
	} 

	/**
	 * readDate reads a date from the amf message and returns the time in ms.
	 * This method is still under development.
	 * 
	 * @return long The date in ms.
	 */
	function readDate() {
		$ms = $this->readDouble();  // date in milliseconds from 01/01/1970
		$int = $this->readInt(); // nasty way to get timezone
		if ($int > 720) {
			$int = - (65536 - $int);
		} 
		$int *= -60;
		//$int *= 1000;
		//$min = $int % 60;
		//$timezone = "GMT " . - $hr . ":" . abs($min);
		// end nastiness
		
		//We store the last timezone found in date fields in the request
		//FOr most purposes, it's expected that the timezones
		//don't change from one date object to the other (they change per client though)
		DateWrapper::setTimezone($int);
		return $ms; 
	}

	/**
	 * readReference replaces the old readFlushedSO. It treats where there
	 * are references to other objects. Currently it does not resolve the
	 * object as this would involve a serious amount of overhead, unless
	 * you have a genius idea
	 * 
	 * @return String 
	 */
	function readReference() {
		$reference = $this->readInt();
		return "(unresolved object #$reference)";
	} 

	/**
	 * readData is the main switch for mapping a type code to an actual
	 * implementation for deciphering it.
	 * 
	 * @param mixed $type The $type integer
	 * @return mixed The php version of the data in the message block
	 */
	function readData($type) {
		switch ($type) {
			case 0: // number
				$data = $this->readDouble();
				break;
			case 1: // boolean
				$data = $this->readByte() == 1;
				break;
			case 2: // string
				$data = $this->readUTF();
				break;
			case 3: // object Object
				$data = $this->readObject();
				break;
			case 5: // null
				$data = null;
				break;
			case 6: // undefined
				$data = null;
				break;
			case 7: // Circular references are returned here
				$data = $this->readReference();
				break;
			case 8: // mixed array with numeric and string keys
				$data = $this->readMixedArray();
				break;
			case 10: // array
				$data = $this->readArray();
				break;
			case 11: // date
				$data = $this->readDate();
				break;
			case 12: // string, strlen(string) > 2^16
				$data = $this->readLongUTF();
				break;
			case 13: // mainly internal AS objects
				$data = null;
				break;
			case 15: // XML
				$data = $this->readLongUTF();
				break;
			case 16: // Custom Class
				$data = $this->readCustomClass();
				break;
			default: // unknown case
				trigger_error("Found unhandled type with code: $type");
				exit();
				break;
		} 
		return $data;
	} 
	
	/********************************************************************************
	 *                       This used to be in AmfInputStream
	 ********************************************************************************

	/**
	 * readByte grabs the next byte from the data stream and returns it.
	 * 
	 * @return int The next byte converted into an integer
	 */
	function readByte() {
		return ord($this->raw_data[$this->current_byte++]); // return the next byte
	}

	/**
	 * readInt grabs the next 2 bytes and returns the next two bytes, shifted and combined
	 * to produce the resulting integer
	 * 
	 * @return int The resulting integer from the next 2 bytes
	 */
	function readInt() {
		return ((ord($this->raw_data[$this->current_byte++]) << 8) |
			ord($this->raw_data[$this->current_byte++])); // read the next 2 bytes, shift and add
	} 

	/**
	 * readLong grabs the next 4 bytes shifts and combines them to produce an integer
	 * 
	 * @return int The resulting integer from the next 4 bytes
	 */
	function readLong() {
		return ((ord($this->raw_data[$this->current_byte++]) << 24) |
			(ord($this->raw_data[$this->current_byte++]) << 16) |
			(ord($this->raw_data[$this->current_byte++]) << 8) |
			ord($this->raw_data[$this->current_byte++])); // read the next 4 bytes, shift and add
	} 

	/**
	 * readDouble reads the floating point value from the bytes stream and properly orders
	 * the bytes depending on the system architecture.
	 * 
	 * @return float The floating point value of the next 8 bytes
	 */
	function readDouble() {
		$bytes = substr($this->raw_data, $this->current_byte, 8);
		$this->current_byte += 8;
		if ($this->isBigEndian) {
			 $bytes = strrev($bytes);
		} 
		$zz = unpack("dflt", $bytes); // unpack the bytes
		return $zz['flt']; // return the number from the associative array
	} 

	/**
	 * readUTF first grabs the next 2 bytes which represent the string length.
	 * Then it grabs the next (len) bytes of the resulting string.
	 * 
	 * @return string The utf8 decoded string
	 */
	function readUTF() {
		$length = $this->readInt(); // get the length of the string (1st 2 bytes)
		//BUg fix:: if string is empty skip ahead
		if($length == 0)
		{
			return "";
		}
		else
		{
			$val = substr($this->raw_data, $this->current_byte, $length); // grab the string
			$this->current_byte += $length; // move the seek head to the end of the string
			return $this->charsetHandler->transliterate($val); // return the string
		}
	} 

	/**
	 * readLongUTF first grabs the next 4 bytes which represent the string length.
	 * Then it grabs the next (len) bytes of the resulting in the string
	 * 
	 * @return string The utf8 decoded string
	 */
	function readLongUTF() {
		$length = $this->readLong(); // get the length of the string (1st 4 bytes)
		$val = substr($this->raw_data, $this->current_byte, $length); // grab the string
		$this->current_byte += $length; // move the seek head to the end of the string
		return $this->charsetHandler->transliterate($val); // return the string
	} 
} 

?>