- <?php
- /**
- * Zend Framework (http://framework.zend.com/)
- *
- * @link http://github.com/zendframework/zf2 for the canonical source repository
- * @copyright Copyright (c) 2005-2014 Zend Technologies USA Inc. (http://www.zend.com)
- * @license http://framework.zend.com/license/new-bsd New BSD License
- */
-
- namespace Zend\Json;
-
- use Iterator;
- use IteratorAggregate;
- use ReflectionClass;
- use Zend\Json\Exception\InvalidArgumentException;
- use Zend\Json\Exception\RecursionException;
-
- /**
- * Encode PHP constructs to JSON
- */
- class Encoder
- {
- /**
- * Whether or not to check for possible cycling
- *
- * @var bool
- */
- protected $cycleCheck;
-
- /**
- * Additional options used during encoding
- *
- * @var array
- */
- protected $options = array();
-
- /**
- * Array of visited objects; used to prevent cycling.
- *
- * @var array
- */
- protected $visited = array();
-
- /**
- * Constructor
- *
- * @param bool $cycleCheck Whether or not to check for recursion when encoding
- * @param array $options Additional options used during encoding
- * @return Encoder
- */
- protected function __construct($cycleCheck = false, $options = array())
- {
- $this->cycleCheck = $cycleCheck;
- $this->options = $options;
- }
-
- /**
- * Use the JSON encoding scheme for the value specified
- *
- * @param mixed $value The value to be encoded
- * @param bool $cycleCheck Whether or not to check for possible object recursion when encoding
- * @param array $options Additional options used during encoding
- * @return string The encoded value
- */
- public static function encode($value, $cycleCheck = false, $options = array())
- {
- $encoder = new static(($cycleCheck) ? true : false, $options);
-
- return $encoder->_encodeValue($value);
- }
-
- /**
- * Recursive driver which determines the type of value to be encoded
- * and then dispatches to the appropriate method. $values are either
- * - objects (returns from {@link _encodeObject()})
- * - arrays (returns from {@link _encodeArray()})
- * - basic datums (e.g. numbers or strings) (returns from {@link _encodeDatum()})
- *
- * @param $value mixed The value to be encoded
- * @return string Encoded value
- */
- protected function _encodeValue(&$value)
- {
- if (is_object($value)) {
- return $this->_encodeObject($value);
- } elseif (is_array($value)) {
- return $this->_encodeArray($value);
- }
-
- return $this->_encodeDatum($value);
- }
-
-
- /**
- * Encode an object to JSON by encoding each of the public properties
- *
- * A special property is added to the JSON object called '__className'
- * that contains the name of the class of $value. This is used to decode
- * the object on the client into a specific class.
- *
- * @param $value object
- * @return string
- * @throws RecursionException If recursive checks are enabled and the
- * object has been serialized previously
- */
- protected function _encodeObject(&$value)
- {
- if ($this->cycleCheck) {
- if ($this->_wasVisited($value)) {
-
- if (isset($this->options['silenceCyclicalExceptions'])
- && $this->options['silenceCyclicalExceptions']===true) {
-
- return '"* RECURSION (' . str_replace('\\', '\\\\', get_class($value)) . ') *"';
-
- } else {
- throw new RecursionException(
- 'Cycles not supported in JSON encoding, cycle introduced by '
- . 'class "' . get_class($value) . '"'
- );
- }
- }
-
- $this->visited[] = $value;
- }
-
- $props = '';
-
- if (method_exists($value, 'toJson')) {
- $props =',' . preg_replace("/^\{(.*)\}$/","\\1", $value->toJson());
- } else {
- if ($value instanceof IteratorAggregate) {
- $propCollection = $value->getIterator();
- } elseif ($value instanceof Iterator) {
- $propCollection = $value;
- } else {
- $propCollection = get_object_vars($value);
- }
-
- foreach ($propCollection as $name => $propValue) {
- if (isset($propValue)) {
- $props .= ','
- . $this->_encodeValue($name)
- . ':'
- . $this->_encodeValue($propValue);
- }
- }
- }
-
- $className = get_class($value);
- return '{"__className":'
- . $this->_encodeString($className)
- . $props . '}';
- }
-
-
- /**
- * Determine if an object has been serialized already
- *
- * @param mixed $value
- * @return bool
- */
- protected function _wasVisited(&$value)
- {
- if (in_array($value, $this->visited, true)) {
- return true;
- }
-
- return false;
- }
-
-
- /**
- * JSON encode an array value
- *
- * Recursively encodes each value of an array and returns a JSON encoded
- * array string.
- *
- * Arrays are defined as integer-indexed arrays starting at index 0, where
- * the last index is (count($array) -1); any deviation from that is
- * considered an associative array, and will be encoded as such.
- *
- * @param $array array
- * @return string
- */
- protected function _encodeArray(&$array)
- {
- $tmpArray = array();
-
- // Check for associative array
- if (!empty($array) && (array_keys($array) !== range(0, count($array) - 1))) {
- // Associative array
- $result = '{';
- foreach ($array as $key => $value) {
- $key = (string) $key;
- $tmpArray[] = $this->_encodeString($key)
- . ':'
- . $this->_encodeValue($value);
- }
- $result .= implode(',', $tmpArray);
- $result .= '}';
- } else {
- // Indexed array
- $result = '[';
- $length = count($array);
- for ($i = 0; $i < $length; $i++) {
- $tmpArray[] = $this->_encodeValue($array[$i]);
- }
- $result .= implode(',', $tmpArray);
- $result .= ']';
- }
-
- return $result;
- }
-
-
- /**
- * JSON encode a basic data type (string, number, boolean, null)
- *
- * If value type is not a string, number, boolean, or null, the string
- * 'null' is returned.
- *
- * @param mixed $value
- * @return string
- */
- protected function _encodeDatum(&$value)
- {
- $result = 'null';
-
- if (is_int($value) || is_float($value)) {
- $result = (string) $value;
- $result = str_replace(',', '.', $result);
- } elseif (is_string($value)) {
- $result = $this->_encodeString($value);
- } elseif (is_bool($value)) {
- $result = $value ? 'true' : 'false';
- }
-
- return $result;
- }
-
-
- /**
- * JSON encode a string value by escaping characters as necessary
- *
- * @param string $string
- * @return string
- */
- protected function _encodeString(&$string)
- {
- // Escape these characters with a backslash or unicode escape:
- // " \ / \n \r \t \b \f
- $search = array('\\', "\n", "\t", "\r", "\b", "\f", '"', '\'', '&', '<', '>', '/');
- $replace = array('\\\\', '\\n', '\\t', '\\r', '\\b', '\\f', '\\u0022', '\\u0027', '\\u0026', '\\u003C', '\\u003E', '\\/');
- $string = str_replace($search, $replace, $string);
-
- // Escape certain ASCII characters:
- // 0x08 => \b
- // 0x0c => \f
- $string = str_replace(array(chr(0x08), chr(0x0C)), array('\b', '\f'), $string);
- $string = self::encodeUnicodeString($string);
-
- return '"' . $string . '"';
- }
-
-
- /**
- * Encode the constants associated with the ReflectionClass
- * parameter. The encoding format is based on the class2 format
- *
- * @param ReflectionClass $cls
- * @return string Encoded constant block in class2 format
- */
- private static function _encodeConstants(ReflectionClass $cls)
- {
- $result = "constants : {";
- $constants = $cls->getConstants();
-
- $tmpArray = array();
- if (!empty($constants)) {
- foreach ($constants as $key => $value) {
- $tmpArray[] = "$key: " . self::encode($value);
- }
-
- $result .= implode(', ', $tmpArray);
- }
-
- return $result . "}";
- }
-
-
- /**
- * Encode the public methods of the ReflectionClass in the
- * class2 format
- *
- * @param ReflectionClass $cls
- * @return string Encoded method fragment
- *
- */
- private static function _encodeMethods(ReflectionClass $cls)
- {
- $methods = $cls->getMethods();
- $result = 'methods:{';
-
- $started = false;
- foreach ($methods as $method) {
- if (! $method->isPublic() || !$method->isUserDefined()) {
- continue;
- }
-
- if ($started) {
- $result .= ',';
- }
- $started = true;
-
- $result .= '' . $method->getName(). ':function(';
-
- if ('__construct' != $method->getName()) {
- $parameters = $method->getParameters();
- $paramCount = count($parameters);
- $argsStarted = false;
-
- $argNames = "var argNames=[";
- foreach ($parameters as $param) {
- if ($argsStarted) {
- $result .= ',';
- }
-
- $result .= $param->getName();
-
- if ($argsStarted) {
- $argNames .= ',';
- }
-
- $argNames .= '"' . $param->getName() . '"';
-
- $argsStarted = true;
- }
- $argNames .= "];";
-
- $result .= "){"
- . $argNames
- . 'var result = ZAjaxEngine.invokeRemoteMethod('
- . "this, '" . $method->getName()
- . "',argNames,arguments);"
- . 'return(result);}';
- } else {
- $result .= "){}";
- }
- }
-
- return $result . "}";
- }
-
-
- /**
- * Encode the public properties of the ReflectionClass in the class2
- * format.
- *
- * @param ReflectionClass $cls
- * @return string Encode properties list
- *
- */
- private static function _encodeVariables(ReflectionClass $cls)
- {
- $properties = $cls->getProperties();
- $propValues = get_class_vars($cls->getName());
- $result = "variables:{";
- $cnt = 0;
-
- $tmpArray = array();
- foreach ($properties as $prop) {
- if (! $prop->isPublic()) {
- continue;
- }
-
- $tmpArray[] = $prop->getName()
- . ':'
- . self::encode($propValues[$prop->getName()]);
- }
- $result .= implode(',', $tmpArray);
-
- return $result . "}";
- }
-
- /**
- * Encodes the given $className into the class2 model of encoding PHP
- * classes into JavaScript class2 classes.
- * NOTE: Currently only public methods and variables are proxied onto
- * the client machine
- *
- * @param $className string The name of the class, the class must be
- * instantiable using a null constructor
- * @param $package string Optional package name appended to JavaScript
- * proxy class name
- * @return string The class2 (JavaScript) encoding of the class
- * @throws InvalidArgumentException
- */
- public static function encodeClass($className, $package = '')
- {
- $cls = new \ReflectionClass($className);
- if (! $cls->isInstantiable()) {
- throw new InvalidArgumentException("'{$className}' must be instantiable");
- }
-
- return "Class.create('$package$className',{"
- . self::_encodeConstants($cls) .","
- . self::_encodeMethods($cls) .","
- . self::_encodeVariables($cls) .'});';
- }
-
-
- /**
- * Encode several classes at once
- *
- * Returns JSON encoded classes, using {@link encodeClass()}.
- *
- * @param array $classNames
- * @param string $package
- * @return string
- */
- public static function encodeClasses(array $classNames, $package = '')
- {
- $result = '';
- foreach ($classNames as $className) {
- $result .= static::encodeClass($className, $package);
- }
-
- return $result;
- }
-
- /**
- * Encode Unicode Characters to \u0000 ASCII syntax.
- *
- * This algorithm was originally developed for the
- * Solar Framework by Paul M. Jones
- *
- * @link http://solarphp.com/
- * @link https://github.com/solarphp/core/blob/master/Solar/Json.php
- * @param string $value
- * @return string
- */
- public static function encodeUnicodeString($value)
- {
- $strlenVar = strlen($value);
- $ascii = "";
-
- /**
- * Iterate over every character in the string,
- * escaping with a slash or encoding to UTF-8 where necessary
- */
- for ($i = 0; $i < $strlenVar; $i++) {
- $ordVarC = ord($value[$i]);
-
- switch (true) {
- case (($ordVarC >= 0x20) && ($ordVarC <= 0x7F)):
- // characters U-00000000 - U-0000007F (same as ASCII)
- $ascii .= $value[$i];
- break;
-
- case (($ordVarC & 0xE0) == 0xC0):
- // characters U-00000080 - U-000007FF, mask 110XXXXX
- // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
- $char = pack('C*', $ordVarC, ord($value[$i + 1]));
- $i += 1;
- $utf16 = self::_utf82utf16($char);
- $ascii .= sprintf('\u%04s', bin2hex($utf16));
- break;
-
- case (($ordVarC & 0xF0) == 0xE0):
- // characters U-00000800 - U-0000FFFF, mask 1110XXXX
- // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
- $char = pack('C*', $ordVarC,
- ord($value[$i + 1]),
- ord($value[$i + 2]));
- $i += 2;
- $utf16 = self::_utf82utf16($char);
- $ascii .= sprintf('\u%04s', bin2hex($utf16));
- break;
-
- case (($ordVarC & 0xF8) == 0xF0):
- // characters U-00010000 - U-001FFFFF, mask 11110XXX
- // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
- $char = pack('C*', $ordVarC,
- ord($value[$i + 1]),
- ord($value[$i + 2]),
- ord($value[$i + 3]));
- $i += 3;
- $utf16 = self::_utf82utf16($char);
- $ascii .= sprintf('\u%04s', bin2hex($utf16));
- break;
-
- case (($ordVarC & 0xFC) == 0xF8):
- // characters U-00200000 - U-03FFFFFF, mask 111110XX
- // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
- $char = pack('C*', $ordVarC,
- ord($value[$i + 1]),
- ord($value[$i + 2]),
- ord($value[$i + 3]),
- ord($value[$i + 4]));
- $i += 4;
- $utf16 = self::_utf82utf16($char);
- $ascii .= sprintf('\u%04s', bin2hex($utf16));
- break;
-
- case (($ordVarC & 0xFE) == 0xFC):
- // characters U-04000000 - U-7FFFFFFF, mask 1111110X
- // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
- $char = pack('C*', $ordVarC,
- ord($value[$i + 1]),
- ord($value[$i + 2]),
- ord($value[$i + 3]),
- ord($value[$i + 4]),
- ord($value[$i + 5]));
- $i += 5;
- $utf16 = self::_utf82utf16($char);
- $ascii .= sprintf('\u%04s', bin2hex($utf16));
- break;
- }
- }
-
- return $ascii;
- }
-
- /**
- * Convert a string from one UTF-8 char to one UTF-16 char.
- *
- * Normally should be handled by mb_convert_encoding, but
- * provides a slower PHP-only method for installations
- * that lack the multibyte string extension.
- *
- * This method is from the Solar Framework by Paul M. Jones
- *
- * @link http://solarphp.com
- * @param string $utf8 UTF-8 character
- * @return string UTF-16 character
- */
- protected static function _utf82utf16($utf8)
- {
- // Check for mb extension otherwise do by hand.
- if (function_exists('mb_convert_encoding')) {
- return mb_convert_encoding($utf8, 'UTF-16', 'UTF-8');
- }
-
- switch (strlen($utf8)) {
- case 1:
- // this case should never be reached, because we are in ASCII range
- // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
- return $utf8;
-
- case 2:
- // return a UTF-16 character from a 2-byte UTF-8 char
- // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
- return chr(0x07 & (ord($utf8{0}) >> 2))
- . chr((0xC0 & (ord($utf8{0}) << 6))
- | (0x3F & ord($utf8{1})));
-
- case 3:
- // return a UTF-16 character from a 3-byte UTF-8 char
- // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
- return chr((0xF0 & (ord($utf8{0}) << 4))
- | (0x0F & (ord($utf8{1}) >> 2)))
- . chr((0xC0 & (ord($utf8{1}) << 6))
- | (0x7F & ord($utf8{2})));
- }
-
- // ignoring UTF-32 for now, sorry
- return '';
- }
- }