<?php /** * AMFSerializer manages the job of translating PHP objects into * the actionscript equivalent via amf. The main method of the serializer * is the serialize method which takes and AMFObject as it's argument * and builds the resulting amf body. * * @license http://opensource.org/licenses/gpl-license.php GNU Public License * @copyright (c) 2003 amfphp.org * @package flashservices * @subpackage io * @version $Id$ */ define("MAX_STORED_OBJECTS", 1024); include_once(AMFPHP_BASE . "amf/io/AMFBaseSerializer.php"); class AMFSerializer extends AMFBaseSerializer { /* Classes that are serialized as recordsets */ var $amf0StoredObjects = array(); var $storedObjects = array(); var $storedDefinitions = 0; var $storedStrings = array(); var $outBuffer; var $encounteredStrings = array(); var $native = false; /* * AMFSerializer is the constructor function. You must pass the * method an AMFOutputStream as the single argument. * * @param object $stream The AMFOutputStream */ function AMFSerializer() { AMFBaseSerializer::AMFBaseSerializer(); } /** * writeBoolean writes the boolean code (0x01) and the data to the output stream * * @param bool $d The boolean value */ function writeBoolean($d) { $this->writeByte(1); // write the boolean flag $this->writeByte($d); // write the boolean byte } /** * writeString writes the string code (0x02) and the UTF8 encoded * string to the output stream. * Note: strings are truncated to 64k max length. Use XML as type * to send longer strings * * @param string $d The string data */ function writeString($d) { $count = strlen($d); if($count < 65536) { $this->writeByte(2); $this->writeUTF($d); } else { $this->writeByte(12); $this->writeLongUTF($d); } } /** * writeXML writes the xml code (0x0F) and the XML string to the output stream * Note: strips whitespace * @param string $d The XML string */ function writeXML($d) { if(!$this->writeReferenceIfExists($d)) { $this->writeByte(15); $this->writeLongUTF(preg_replace('/\>(\n|\r|\r\n| |\t)*\</','><',trim($d))); } } /** * writeData writes the date code (0x0B) and the date value to the output stream * * @param date $d The date value */ function writeDate($d) { $this->writeByte(11); // write date code $this->writeDouble($d); // write date (milliseconds from 1970) /** * write timezone * ?? this is wierd -- put what you like and it pumps it back into flash at the current GMT ?? * have a look at the amf it creates... */ $this->writeInt(0); } /** * writeNumber writes the number code (0x00) and the numeric data to the output stream * All numbers passed through remoting are floats. * * @param int $d The numeric data */ function writeNumber($d) { $this->writeByte(0); // write the number code $this->writeDouble(floatval($d)); // write the number as a double } /** * writeNull writes the null code (0x05) to the output stream */ function writeNull() { $this->writeByte(5); // null is only a 0x05 flag } /** * writeArray first deterines if the PHP array contains all numeric indexes * or a mix of keys. Then it either writes the array code (0x0A) or the * object code (0x03) and then the associated data. * * @param array $d The php array */ function writeArray($d) { if($this->writeReferenceIfExists($d)) { return; } $numeric = array(); // holder to store the numeric keys $string = array(); // holder to store the string keys $len = count($d); // get the total number of entries for the array $largestKey = -1; foreach($d as $key => $data) { // loop over each element if (is_int($key) && ($key >= 0)) { // make sure the keys are numeric $numeric[$key] = $data; // The key is an index in an array $largestKey = max($largestKey, $key); } else { $string[$key] = $data; // The key is a property of an object } } $num_count = count($numeric); // get the number of numeric keys $str_count = count($string); // get the number of string keys if ( ($num_count > 0 && $str_count > 0) || ($num_count > 0 && $largestKey != $num_count - 1)) { // this is a mixed array $this->writeByte(8); // write the mixed array code $this->writeLong($num_count); // write the count of items in the array $this->writeObjectFromArray($numeric + $string); // write the numeric and string keys in the mixed array } else if ($num_count > 0) { // this is just an array $num_count = count($numeric); // get the new count $this->writeByte(10); // write the array code $this->writeLong($num_count); // write the count of items in the array for($i = 0 ; $i < $num_count ; $i++) { // write all of the array elements $this->writeData($numeric[$i]); } } else if($str_count > 0) { // this is an object $this->writeByte(3); // this is an object so write the object code $this->writeObjectFromArray($string); // write the object name/value pairs } else { //Patch submitted by Jason Justman $this->writeByte(10); // make this an array still $this->writeInt(0); // give it 0 elements $this->writeInt(0); // give it an element pad, this looks like a bug in Flash, //but keeps the next alignment proper } } function writeReferenceIfExists($d) { if(count($this->amf0StoredObjects) >= MAX_STORED_OBJECTS) { return false; } if(is_array($d)) { $this->amf0StoredObjects[] = ""; return false; } if(($key = patched_array_search($d, $this->amf0StoredObjects, true)) !== FALSE) { $this->writeReference($key); return true; } else { $this->amf0StoredObjects[] = & $d; return false; } } function writeReference($num) { $this->writeByte(0x07); $this->writeInt($num); } /** * Write a plain numeric array without anything fancy */ function writePlainArray($d) { if(!$this->writeReferenceIfExists($d)) { $num_count = count($d); $this->writeByte(10); // write the mixed array code $this->writeLong($num_count); // write the count of items in the array for($i = 0 ; $i < $num_count ; $i++) { // write all of the array elements $this->writeData($d[$i]); } } } /** * writeObject handles writing a php array with string or mixed keys. It does * not write the object code as that is handled by the writeArray and this method * is shared with the CustomClass writer which doesn't use the object code. * * @param array $d The php array with string keys */ function writeObjectFromArray($d) { foreach($d as $key => $data) { // loop over each element $this->writeUTF($key); // write the name of the object $this->writeData($data); // write the value of the object } $this->writeInt(0); // write the end object flag 0x00, 0x00, 0x09 $this->writeByte(9); } /** * writeObject handles writing a php array with string or mixed keys. It does * not write the object code as that is handled by the writeArray and this method * is shared with the CustomClass writer which doesn't use the object code. * * @param array $d The php array with string keys */ function writeAnonymousObject($d) { if(!$this->writeReferenceIfExists($d)) { $this->writeByte(3); $objVars = (array) $d; foreach($d as $key => $data) { // loop over each element if($key[0] != "\0") { $this->writeUTF($key); // write the name of the object $this->writeData($data); // write the value of the object } } $this->writeInt(0); // write the end object flag 0x00, 0x00, 0x09 $this->writeByte(9); } } /** * writePHPObject takes an instance of a class and writes the variables defined * in it to the output stream. * To accomplish this we just blanket grab all of the object vars with get_object_vars * * @param object $d The object to serialize the properties */ function writeTypedObject($d) { if($this->writeReferenceIfExists($d)) { return; } $this->writeByte(16); // write the custom class code $classname = $this->getClassName($d); $this->writeUTF($classname); // write the class name if(AMFPHP_PHP5) { $objVars = $d; } else { $objVars = (array) $d; } foreach($objVars as $key => $data) { // loop over each element if($key[0] != "\0") { $this->writeUTF($key); // write the name of the object $this->writeData($data); // write the value of the object } } $this->writeInt(0); // write the end object flag 0x00, 0x00, 0x09 $this->writeByte(9); } /** * writeRecordSet is the abstracted method to write a custom class recordset object. * Any recordset from any datasource can be written here, it just needs to be properly formatted * beforehand. * * This was unrolled with at the expense of readability for a * 10 fold increase in speed in large recordsets * * @param object $rs The formatted RecordSet object */ function writeRecordSet(&$rs) { //Low-level everything here to make things faster //This is the bottleneck of AMFPHP, hence the attention in making things faster if($this->writeReferenceIfExists($rs)) { return; } $ob = ""; $data = $rs->rows; if($GLOBALS['amfphp']['encoding'] == 'amf0') { $this->writeByte(16); // write the custom class code $this->writeUTF("RecordSet"); // write the class name $this->writeUTF("serverInfo"); //Start writing inner object $this->writeByte(3); // this is an object so write the object code //Write total count $this->writeUTF("totalCount"); $this->writeNumber($rs->getRowCount()); //Write initial data $this->writeUTF("initialData"); //Inner numeric array $colnames = $rs->columns; $num_count = count($rs->rows); $this->writeByte(10); // write the mixed array code $this->writeLong($num_count); // write the count of items in the array //Allow recordsets to create their own serialized data, which is faster //since the recordset array is traversed only once $numcols = count($colnames); $ob = ""; $be = $this->isBigEndian; $fc = pack('N', $numcols); for($i = 0 ; $i < $num_count ; $i++) { // write all of the array elements $ob .= "\12" . $fc; for($j = 0; $j < $numcols; $j++) { // write all of the array elements $d = $data[$i][$j]; if (is_string($d)) { // type as string $os = $this->rsCharsetHandler->transliterate($d); //string flag, string length, and string $ob .= "\2" . pack('n', strlen($os)) . $os; } elseif (is_float($d) || is_int($d)) { // type as double $ob .= "\0"; $b = pack('d', $d); // pack the bytes if ($be) { // if we are a big-endian processor $r = strrev($b); } else { // add the bytes to the output $r = $b; } $ob .= $r; } elseif (is_bool($d)) { //type as bool $ob .= "\1"; $ob .= pack('c', $d); } elseif (is_null($d)) { // null $ob .= "\5"; } } } $this->outBuffer .= $ob; //Write cursor $this->writeUTF("cursor"); $this->writeNumber(1); //Write service name $this->writeUTF("serviceName"); $this->writeString("PageAbleResult"); //Write column names $this->writeUTF("columnNames"); $this->writePlainArray($colnames, 'string'); //Write version number $this->writeUTF("version"); $this->writeNumber(1); //Write id $this->writeUTF("id"); $this->writeString($rs->getID()); //End inner serverInfo object $this->writeInt(0); // write the end object flag 0x00, 0x00, 0x09 $this->writeByte(9); //End outer recordset object $this->writeInt(0); // write the end object flag 0x00, 0x00, 0x09 $this->writeByte(9); $this->paging = -1; } else { $numObjects= 0; $this->writeAmf3ArrayCollectionPreamble(); //Amf3 array code $this->writeByte(0x09); $numObjects++; $numRows = count($rs->rows); $toPack = 2*$numRows + 1; //Write the number of rows $this->writeAmf3Int($toPack); //No string keys in this array $this->writeByte(0x01); $numCols = count($rs->columns); $columnStringOffsets = array(); if($numRows > 0) { $j = 0; $colNames = array(); $rows = $rs->rows; foreach ($rows as $key => $line) { //Usually we don't use class defs in the serializer since we don't //have sealed objects in php, but for recordsets we do use them //since they are well suited for what we have to do (the same keys //across all objects) if($key == 0) { $this->outBuffer .= "\12"; $this->writeAmf3Int($numCols << 4 | 3); $this->outBuffer .= "\1"; foreach($rs->columns as $key => $val) { $this->writeAmf3String($val); } $defOffset = $this->getAmf3Int( ($this->storedDefinitions) << 2 | 1 ); $this->storedDefinitions++; } else { $this->outBuffer .= "\12" . $defOffset; } $numObjects++; for($i = 0; $i < $numCols; $i++) { //Write the col name $value = $line[$i]; if(is_string($value)) { $this->outBuffer .= "\6"; $value = $this->rsCharsetHandler->transliterate($value); $this->writeAmf3String($value, true); } elseif(is_int($value)) { //int $this->writeAmf3Number($value); } elseif(is_float($value)) { //double $this->outBuffer .= "\5"; $b = pack("d", $value); // pack the bytes if ($this->isBigEndian) { // if we are a big-endian processor $r = strrev($b); } else { // add the bytes to the output $r = $b; } $this->outBuffer .= $r; } elseif(is_bool($value)) { $this->outBuffer .= $value ? "\3" : "\2"; } else { $this->outBuffer .= "\1"; //null } } //End object } } //Add fake objects to make sure the object counter still works for($i = 0; $i < $numObjects; $i++) { $this->storedObjects[] = ""; } } } /** * writeData checks to see if the type was declared and then either * auto negotiates the type or relies on the user defined type to * serialize the data into amf * * Note that autoNegotiateType was eliminated in order to tame the * call stack which was getting huge and was causing leaks * * manualType allows the developer to explicitly set the type of * the returned data. The returned data is validated for most of the * cases when possible. Some datatypes like xml and date have to * be returned this way in order for the Flash client to correctly serialize them * * recordsets appears top on the list because that will probably be the most * common hit in this method. Followed by the * datatypes that have to be manually set. Then the auto negotiatable types last. * The order may be changed for optimization. * * @param mixed $d The data * @param string $type The optional type */ function writeData(& $d) { if (is_int($d) || is_float($d)) { // double $this->writeNumber($d); return; } elseif (is_string($d)) { // string $this->writeString($d); return; } elseif (is_bool($d)) { // boolean $this->writeBoolean($d); return; } elseif (is_null($d)) { // null $this->writeNull(); return; } elseif ($GLOBALS['amfphp']['encoding'] == 'amf3') { $this->writeByte(0x11); $this->writeAmf3Data($d); return; } elseif (is_array($d)) { // array $this->writeArray($d); return; } elseif (is_resource($d)) { // resource $type = get_resource_type($d); list($type, $subtype) = $this->sanitizeType($type); } elseif (is_object($d)) { $className = strtolower(get_class($d)); if(array_key_exists($className, $this->resourceObjects)) { $type = "__RECORDSET__"; $subtype = $this->resourceObjects[strtolower(get_class($d))]; } else if(AMFPHP_PHP5 && $className == 'domdocument') { $this->writeXML($d->saveXml()); return; } else if(!AMFPHP_PHP5 && $className == 'domdocument') { $this->writeXML($d->dump_mem()); return; } elseif($className == "simplexmlelement") { $this->writeXML($d->asXML()); return; } else if($className == 'stdclass' && !isset($d->_explicitType)) { $this->writeAnonymousObject($d); return; } elseif(is_a($d, 'ArrayAccess') || is_a($d, 'ArrayObject')) { $this->writeArray($d); return; } else { $this->writeTypedObject($d); return; } } else { $type = gettype($d); } switch ($type) { case "__RECORDSET__" : $classname = $subtype . "Adapter"; // full class name $includeFile = include_once(AMFPHP_BASE . "shared/adapters/" . $classname . ".php"); // try to load the recordset library from the sql folder if (!$includeFile) { trigger_error("The recordset filter class " . $classname . " was not found", E_USER_ERROR); } $recordSet = new $classname($d); // returns formatted recordset $this->writeRecordSet($recordSet); // writes the recordset formatted for Flash break; default: // non of the above so lets assume its a Custom Class thats defined in the client $this->writeTypedObject($unsanitizedType, $d); // trigger_error("Unsupported Datatype"); break; } } /******************************************************************************** * AMF3 related code *******************************************************************************/ function writeAmf3Data(& $d) { if (is_int($d)) { //int $this->writeAmf3Number($d); return; } elseif(is_float($d)) { //double $this->outBuffer .= "\5"; $this->writeDouble($d); return; } elseif (is_string($d)) { // string $this->outBuffer .= "\6"; $this->writeAmf3String($d); return; } elseif (is_bool($d)) { // boolean $this->writeAmf3Bool($d); return; } elseif (is_null($d)) { // null $this->writeAmf3Null(); return; } elseif (is_array($d)) { // array $this->writeAmf3Array($d); return; } elseif (is_resource($d)) { // resource $type = get_resource_type($d); list($type, $subtype) = $this->sanitizeType($type); } elseif (is_object($d)) { $className = strtolower(get_class($d)); if(array_key_exists($className, $this->resourceObjects)) { $type = "__RECORDSET__"; $subtype = $this->resourceObjects[strtolower(get_class($d))]; } else if(AMFPHP_PHP5 && $className == 'domdocument') { $this->writeAmf3Xml($d->saveXml()); return; } else if(!AMFPHP_PHP5 && $className == 'domdocument') { $this->writeAmf3Xml($d->dump_mem()); return; } elseif($className == "simplexmlelement") { $this->writeAmf3Xml($d->asXML()); return; } elseif($className == 'bytearray') { $this->writeAmf3ByteArray($d->data); return; } elseif(is_a($d, 'ArrayAccess') || is_a($d, 'ArrayObject')) { $this->writeAmf3Array($d, true); return; } else { $this->writeAmf3Object($d); return; } } else { $type = gettype($d); } switch ($type) { case "__RECORDSET__" : $classname = $subtype . "Adapter"; // full class name $includeFile = include_once(AMFPHP_BASE . "shared/adapters/" . $classname . ".php"); // try to load the recordset library from the sql folder if (!$includeFile) { trigger_error("The recordset filter class " . $classname . " was not found"); } $GLOBALS['amfphp']['stringOffset'] = count($this->storedStrings); $recordSet = new $classname($d); // returns formatted recordset $this->writeRecordSet($recordSet); // writes the recordset formatted for Flash break; default: // non of the above so lets assume its a Custom Class thats defined in the client //$this->writeTypedObject($unsanitizedType, $d); trigger_error("Unsupported Datatype: " . $type); break; } } /** * Write an ArrayCollection */ function writeAmf3ArrayCollectionPreamble() { $this->writeByte(0x0a); $this->writeByte(0x07); $this->writeAmf3String("flex.messaging.io.ArrayCollection"); $this->storedDefinitions++; $this->storedObjects[] = ""; } function writeAmf3Null() { //Write the null code (0x1) to the output stream. $this->outBuffer .= "\1"; } function writeAmf3Bool($d) { $this->outBuffer .= $d ? "\3" : "\2"; } function writeAmf3Int($d) { //Sign contraction - the high order bit of the resulting value must match every bit removed from the number //Clear 3 bits $d &= 0x1fffffff; if($d < 0x80) { $this->outBuffer .= chr($d); } elseif($d < 0x4000) { $this->outBuffer .= chr($d >> 7 & 0x7f | 0x80) . chr($d & 0x7f); } elseif($d < 0x200000) { $this->outBuffer .= chr($d >> 14 & 0x7f | 0x80) . chr($d >> 7 & 0x7f | 0x80) . chr($d & 0x7f); } else { $this->outBuffer .= chr($d >> 22 & 0x7f | 0x80) . chr($d >> 15 & 0x7f | 0x80) . chr($d >> 8 & 0x7f | 0x80) . chr($d & 0xff); } } function writeAmf3String($d, $raw = false) { if( $d == "" ) { //Write 0x01 to specify the empty ctring $this->outBuffer .= "\1"; } else { if( !isset($this->storedStrings[$d])) { if(strlen($d) < 64) { $this->storedStrings[$d] = $this->encounteredStrings; } if(!$raw) { $d = $this->charsetHandler->transliterate($d); } $handle = strlen($d); $this->writeAmf3Int($handle*2 + 1); $this->outBuffer .= $d; $this->encounteredStrings++; return $this->encounteredStrings - 1; } else { $key = $this->storedStrings[$d]; $handle = $key << 1; $this->writeAmf3Int($handle); return $key; } } } function writeAmf3Array($d, $arrayCollectionable = false) { //Circular referencing is disabled in arrays //Because if the array contains only primitive values, //Then === will say that the two arrays are strictly equal //if they contain the same values, even if they are really distinct //if(($key = patched_array_search($d, $this->storedObjects, TRUE)) === FALSE ) //{ if(count($this->storedObjects) < MAX_STORED_OBJECTS) { $this->storedObjects[] = & $d; } $numeric = array(); // holder to store the numeric keys $string = array(); // holder to store the string keys $len = count($d); // get the total number of entries for the array $largestKey = -1; foreach($d as $key => $data) { // loop over each element if (is_int($key) && ($key >= 0)) { // make sure the keys are numeric $numeric[$key] = $data; // The key is an index in an array $largestKey = max($largestKey, $key); } else { $string[$key] = $data; // The key is a property of an object } } $num_count = count($numeric); // get the number of numeric keys $str_count = count($string); // get the number of string keys if (($str_count > 0 && $num_count == 0) || ($num_count > 0 && $largestKey != $num_count - 1)) { // this is a mixed array $this->writeAmf3ObjectFromArray($numeric + $string); // write the numeric and string keys in the mixed array } else { // this is just an array if($arrayCollectionable) { $this->writeAmf3ArrayCollectionPreamble(); } $num_count = count($numeric); $this->outBuffer .= "\11"; $handle = $num_count * 2 + 1; $this->writeAmf3Int($handle); foreach($string as $key => $val) { $this->writeAmf3String($key); $this->writeAmf3Data($val); } $this->writeAmf3String(""); //End start hash for($i = 0; $i < $num_count; $i++) { $this->writeAmf3Data($numeric[$i]); } } //} //else //{ // $handle = $key << 1; // $this->outBuffer .= "\11"; // $this->writeAmf3Int($handle); //} } function writeAmf3ObjectFromArray($d) { //Type this as a dynamic object $this->outBuffer .= "\12\13\1"; foreach($d as $key => $val) { $this->writeAmf3String($key); $this->writeAmf3Data($val); } //Now we close the open object $this->outBuffer .= "\1"; } /* public void WriteAMF3DateTime(DateTime value) { if( !_objectReferences.Contains(value) ) { _objectReferences.Add(value, _objectReferences.Count); int handle = 1; WriteAMF3IntegerData(handle); // Write date (milliseconds from 1970). DateTime timeStart = new DateTime(1970, 1, 1, 0, 0, 0); string timezoneCompensation = System.Configuration.ConfigurationSettings.AppSettings["timezoneCompensation"]; if( timezoneCompensation != null && ( timezoneCompensation.ToLower() == "auto" ) ) { value = value.ToUniversalTime(); } TimeSpan span = value.Subtract(timeStart); long milliSeconds = (long)span.TotalMilliseconds; long date = BitConverter.DoubleToInt64Bits((double)milliSeconds); this.WriteLong(date); } else { int handle = (int)_objectReferences[value]; handle = handle << 1; WriteAMF3IntegerData(handle); } } */ function getAmf3Int($d) { $d &= 0x1fffffff; if($d < 0x80) { return chr($d); } elseif($d < 0x4000) { return chr($d >> 7 & 0x7f | 0x80) . chr($d & 0x7f); } elseif($d < 0x200000) { return chr($d >> 14 & 0x7f | 0x80) . chr($d >> 7 & 0x7f | 0x80) . chr($d & 0x7f); } else { return chr($d >> 22 & 0x7f | 0x80) . chr($d >> 15 & 0x7f | 0x80) . chr($d >> 8 & 0x7f | 0x80) . chr($d & 0xff); } } function writeAmf3Number($d) { if($d >= -268435456 && $d <= 268435455)//check valid range for 29bits { $this->outBuffer .= "\4"; $this->writeAmf3Int($d); } else { //overflow condition would occur upon int conversion $this->outBuffer .= "\5"; $this->writeDouble($d); } } function writeAmf3Xml($d) { $d = preg_replace('/\>(\n|\r|\r\n| |\t)*\</','><',trim($d)); $this->writeByte(0x07); $this->writeAmf3String($d); } function writeAmf3ByteArray($d) { $this->writeByte(0x0C); $this->writeAmf3String($d, true); } function writeAmf3Object($d) { //Write the object tag $this->outBuffer .= "\12"; if( ($key = patched_array_search($d, $this->storedObjects, TRUE)) === FALSE && $key === FALSE) { if(count($this->storedObjects) < MAX_STORED_OBJECTS) { $this->storedObjects[] = & $d; } $this->storedDefinitions++; //Type the object as an array if(AMFPHP_PHP5) { $obj = $d; } else { $obj = (array) $d; } $realObj = array(); foreach($obj as $key => $val) { if($key[0] != "\0" && $key != '_explicitType') //Don't show private members { $realObj[$key] = $val; } } //Type this as a dynamic object $this->outBuffer .= "\13"; $classname = $this->getClassName($d); $this->writeAmf3String($classname); foreach($realObj as $key => $val) { $this->writeAmf3String($key); $this->writeAmf3Data($val); } //Now we close the open object $this->outBuffer .= "\1"; } else { $handle = $key << 1; $this->writeAmf3Int($handle); } } } ?>