diff --git a/src/Encoder.php b/src/Encoder.php index 34a90655f..8d3d126a4 100644 --- a/src/Encoder.php +++ b/src/Encoder.php @@ -146,18 +146,22 @@ protected function _encodeObject(&$value) $props = ''; - if ($value instanceof \Iterator) { - $propCollection = $value; + if (method_exists($value, 'toJson')) { + $props =',' . preg_replace("/^\{(.*)\}$/","\\1",$value->toJson()); } else { - $propCollection = get_object_vars($value); - } + if ($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); + foreach ($propCollection as $name => $propValue) { + if (isset($propValue)) { + $props .= ',' + . $this->_encodeValue($name) + . ':' + . $this->_encodeValue($propValue); + } } } diff --git a/src/Json.php b/src/Json.php index 5a4c7b11b..6111537d0 100644 --- a/src/Json.php +++ b/src/Json.php @@ -120,8 +120,12 @@ public static function decode($encodedValue, $objectDecodeType = self::TYPE_OBJE */ public static function encode($valueToEncode, $cycleCheck = false, $options = array()) { - if (is_object($valueToEncode) && method_exists($valueToEncode, 'toJson')) { - return $valueToEncode->toJson(); + if (is_object($valueToEncode)) { + if (method_exists($valueToEncode, 'toJson')) { + return $valueToEncode->toJson(); + } elseif (method_exists($valueToEncode, 'toArray')) { + return self::encode($valueToEncode->toArray(), $cycleCheck, $options); + } } // Pre-encoding look for Zend_Json_Expr objects and replacing by tmp ids @@ -196,6 +200,107 @@ protected static function _recursiveJsonExprFinder( } return $value; } + /** + * Return the value of an XML attribute text or the text between + * the XML tags + * + * In order to allow Zend_Json_Expr from xml, we check if the node + * matchs the pattern that try to detect if it is a new Zend_Json_Expr + * if it matches, we return a new Zend_Json_Expr instead of a text node + * + * @param SimpleXMLElement $simpleXmlElementObject + * @return Zend_Json_Expr|string + */ + protected static function _getXmlValue($simpleXmlElementObject) + { + $pattern = '/^[\s]*new Zend[_\\]Json[_\\]Expr[\s]*\([\s]*[\"\']{1}(.*)[\"\']{1}[\s]*\)[\s]*$/'; + $matchings = array(); + $match = preg_match($pattern, $simpleXmlElementObject, $matchings); + if ($match) { + return new Expr($matchings[1]); + } else { + return (trim(strval($simpleXmlElementObject))); + } + } + /** + * _processXml - Contains the logic for xml2json + * + * The logic in this function is a recursive one. + * + * The main caller of this function (i.e. fromXml) needs to provide + * only the first two parameters i.e. the SimpleXMLElement object and + * the flag for ignoring or not ignoring XML attributes. The third parameter + * will be used internally within this function during the recursive calls. + * + * This function converts the SimpleXMLElement object into a PHP array by + * calling a recursive (protected static) function in this class. Once all + * the XML elements are stored in the PHP array, it is returned to the caller. + * + * Throws a Zend\Json\RecursionException if the XML tree is deeper than the allowed limit. + * + * @param SimpleXMLElement $simpleXmlElementObject + * @param boolean $ignoreXmlAttributes + * @param integer $recursionDepth + * @return array + */ + protected static function _processXml($simpleXmlElementObject, $ignoreXmlAttributes, $recursionDepth = 0) + { + // Keep an eye on how deeply we are involved in recursion. + if ($recursionDepth > self::$maxRecursionDepthAllowed) { + // XML tree is too deep. Exit now by throwing an exception. + throw new RecursionException( + "Function _processXml exceeded the allowed recursion depth of " + . self::$maxRecursionDepthAllowed + ); + } + + $children = $simpleXmlElementObject->children(); + $name = $simpleXmlElementObject->getName(); + $value = self::_getXmlValue($simpleXmlElementObject); + $attributes = (array) $simpleXmlElementObject->attributes(); + + if (!count($children)) { + if (!empty($attributes) && !$ignoreXmlAttributes) { + foreach ($attributes['@attributes'] as $k => $v) { + $attributes['@attributes'][$k] = self::_getXmlValue($v); + } + if (!empty($value)) { + $attributes['@text'] = $value; + } + return array($name => $attributes); + } + + return array($name => $value); + } + + $childArray = array(); + foreach ($children as $child) { + $childname = $child->getName(); + $element = self::_processXml($child,$ignoreXmlAttributes,$recursionDepth + 1); + if (array_key_exists($childname, $childArray)) { + if (empty($subChild[$childname])) { + $childArray[$childname] = array($childArray[$childname]); + $subChild[$childname] = true; + } + $childArray[$childname][] = $element[$childname]; + } else { + $childArray[$childname] = $element[$childname]; + } + } + + if (!empty($attributes) && !$ignoreXmlAttributes) { + foreach ($attributes['@attributes'] as $k => $v) { + $attributes['@attributes'][$k] = self::_getXmlValue($v); + } + $childArray['@attributes'] = $attributes['@attributes']; + } + + if (!empty($value)) { + $childArray['@text'] = $value; + } + + return array($name => $childArray); + } /** * fromXml - Converts XML to JSON @@ -242,110 +347,6 @@ public static function fromXml ($xmlStringContents, $ignoreXmlAttributes=true) { return($jsonStringOutput); } - /** - * _processXml - Contains the logic for xml2json - * - * The logic in this function is a recursive one. - * - * The main caller of this function (i.e. fromXml) needs to provide - * only the first two parameters i.e. the SimpleXMLElement object and - * the flag for ignoring or not ignoring XML attributes. The third parameter - * will be used internally within this function during the recursive calls. - * - * This function converts the SimpleXMLElement object into a PHP array by - * calling a recursive (protected static) function in this class. Once all - * the XML elements are stored in the PHP array, it is returned to the caller. - * - * @static - * @access protected - * @param SimpleXMLElement $simpleXmlElementObject XML element to be converted - * @param boolean $ignoreXmlAttributes Include or exclude XML attributes in - * the xml2json conversion process. - * @param int $recursionDepth Current recursion depth of this function - * @return mixed - On success, a PHP associative array of traversed XML elements - * @throws Zend\Json\Exception\RecursionException if the XML tree is deeper than the allowed limit - */ - protected static function _processXml ($simpleXmlElementObject, $ignoreXmlAttributes, $recursionDepth=0) { - // Keep an eye on how deeply we are involved in recursion. - if ($recursionDepth > self::$maxRecursionDepthAllowed) { - // XML tree is too deep. Exit now by throwing an exception. - throw new RecursionException( - "Function _processXml exceeded the allowed recursion depth of " . - self::$maxRecursionDepthAllowed); - } // End of if ($recursionDepth > self::$maxRecursionDepthAllowed) - - if ($recursionDepth == 0) { - // Store the original SimpleXmlElementObject sent by the caller. - // We will need it at the very end when we return from here for good. - $callerProvidedSimpleXmlElementObject = $simpleXmlElementObject; - } // End of if ($recursionDepth == 0) - - if ($simpleXmlElementObject instanceof \SimpleXMLElement) { - // Get a copy of the simpleXmlElementObject - $copyOfSimpleXmlElementObject = $simpleXmlElementObject; - // Get the object variables in the SimpleXmlElement object for us to iterate. - $simpleXmlElementObject = get_object_vars($simpleXmlElementObject); - } // End of if (get_class($simpleXmlElementObject) == "SimpleXMLElement") - - // It needs to be an array of object variables. - if (is_array($simpleXmlElementObject)) { - // Initialize a result array. - $resultArray = array(); - // Is the input array size 0? Then, we reached the rare CDATA text if any. - if (count($simpleXmlElementObject) <= 0) { - // Let us return the lonely CDATA. It could even be - // an empty element or just filled with whitespaces. - return (trim(strval($copyOfSimpleXmlElementObject))); - } // End of if (count($simpleXmlElementObject) <= 0) - - // Let us walk through the child elements now. - foreach($simpleXmlElementObject as $key=>$value) { - // Check if we need to ignore the XML attributes. - // If yes, you can skip processing the XML attributes. - // Otherwise, add the XML attributes to the result array. - if(($ignoreXmlAttributes == true) && (is_string($key)) && ($key == "@attributes")) { - continue; - } // End of if(($ignoreXmlAttributes == true) && ($key == "@attributes")) - - // Let us recursively process the current XML element we just visited. - // Increase the recursion depth by one. - $recursionDepth++; - $resultArray[$key] = self::_processXml ($value, $ignoreXmlAttributes, $recursionDepth); - - // Decrease the recursion depth by one. - $recursionDepth--; - } // End of foreach($simpleXmlElementObject as $key=>$value) { - - if ($recursionDepth == 0) { - // That is it. We are heading to the exit now. - // Set the XML root element name as the root [top-level] key of - // the associative array that we are going to return to the original - // caller of this recursive function. - $tempArray = $resultArray; - $resultArray = array(); - $resultArray[$callerProvidedSimpleXmlElementObject->getName()] = $tempArray; - } // End of if ($recursionDepth == 0) - - return($resultArray); - } else { - // We are now looking at either the XML attribute text or - // the text between the XML tags. - - // In order to allow Zend_Json_Expr from xml, we check if the node - // matchs the pattern that try to detect if it is a new Zend_Json_Expr - // if it matches, we return a new Zend_Json_Expr instead of a text node - $pattern = '/^[\s]*new Zend_Json_Expr[\s]*\([\s]*[\"\']{1}(.*)[\"\']{1}[\s]*\)[\s]*$/'; - $matchings = array(); - $match = preg_match($pattern, $simpleXmlElementObject, $matchings); - if ($match) { - return new Expr($matchings[1]); - } else { - return (trim(strval($simpleXmlElementObject))); - } - - } // End of if (is_array($simpleXmlElementObject)) - } // End of function _processXml. - /** * Pretty-print JSON string * @@ -366,24 +367,32 @@ public static function prettyPrint($json, $options = array()) $ind = $options['indent']; } + $inLiteral = false; foreach($tokens as $token) { if($token == "") continue; $prefix = str_repeat($ind, $indent); - if($token == "{" || $token == "[") { + if(!$inLiteral && ($token == "{" || $token == "[")) { $indent++; if($result != "" && $result[strlen($result)-1] == "\n") { $result .= $prefix; } $result .= "$token\n"; - } else if($token == "}" || $token == "]") { + } else if(!$inLiteral && ($token == "}" || $token == "]")) { $indent--; $prefix = str_repeat($ind, $indent); $result .= "\n$prefix$token"; - } else if($token == ",") { + } else if(!$inLiteral && $token == ",") { $result .= "$token\n"; } else { - $result .= $prefix.$token; + $result .= ($inLiteral ? '' : $prefix) . $token; + + // Count # of unescaped double-quotes in token, subtract # of + // escaped double-quotes and if the result is odd then we are + // inside a string literal + if ((substr_count($token, "\"")-substr_count($token, "\\\"")) % 2 != 0) { + $inLiteral = !$inLiteral; + } } } return $result; diff --git a/test/JsonTest.php b/test/JsonTest.php index 4670a986f..58891ca61 100644 --- a/test/JsonTest.php +++ b/test/JsonTest.php @@ -772,6 +772,84 @@ public function testDefaultTypeObject() $this->assertInstanceOf('stdClass', Json\Decoder::decode('{"var":"value"}')); } + /** + * @group ZF-10185 + */ + public function testJsonPrettyPrintWorksWithArrayNotationInStringLiteral() + { + $o = new \stdClass(); + $o->test = 1; + $o->faz = 'fubar'; + + // The escaped double-quote in item 'stringwithjsonchars' ensures that + // escaped double-quotes don't throw off prettyPrint's string literal detection + $test = array( + 'simple'=>'simple test string', + 'stringwithjsonchars'=>'\"[1,2]', + 'complex'=>array( + 'foo'=>'bar', + 'far'=>'boo', + 'faz'=>array( + 'obj'=>$o + ) + ) + ); + $pretty = Json\Json::prettyPrint(Json\Json::encode($test), array("indent" => " ")); + $expected = <<assertSame($expected, $pretty); + } + + /** + * @group ZF-11167 + */ + public function testEncodeWillUseToArrayMethodWhenAvailable() + { + $o = new ZF11167_ToArrayClass(); + $objJson = Json\Json::encode($o); + $arrJson = Json\Json::encode($o->toArray()); + $this->assertSame($arrJson, $objJson); + } + + /** + * @group ZF-11167 + */ + public function testEncodeWillUseToJsonWhenBothToJsonAndToArrayMethodsAreAvailable() + { + $o = new ZF11167_ToArrayToJsonClass(); + $objJson = Json\Json::encode($o); + $this->assertEquals('"bogus"', $objJson); + $arrJson = Json\Json::encode($o->toArray()); + $this->assertNotSame($objJson, $arrJson); + } + + /** + * @group ZF-9521 + */ + public function testWillEncodeArrayOfObjectsEachWithToJsonMethod() + { + $array = array('one'=>new ToJsonClass()); + $expected = '{"one":{"__className":"ZendTest\\\\Json\\\\ToJSONClass","firstName":"John","lastName":"Doe","email":"john@doe.com"}}'; + + Json\Json::$useBuiltinEncoderDecoder = true; + $json = Json\Encoder::encode($array); + $this->assertEquals($expected, $json); + } + } /** @@ -826,6 +904,41 @@ public function toJSON() } } +/** + * Serializable class exposing a toArray() method + * @see ZF-11167 + */ +class ZF11167_ToArrayClass +{ + private $_firstName = 'John'; + + private $_lastName = 'Doe'; + + private $_email = 'john@doe.com'; + + public function toArray() + { + $data = array( + 'firstName' => $this->_firstName, + 'lastName' => $this->_lastName, + 'email' => $this->_email + ); + return $data; + } +} + +/** + * Serializable class exposing both toArray() and toJson() methods + * @see ZF-11167 + */ +class ZF11167_ToArrayToJsonClass extends ZF11167_ToArrayClass +{ + public function toJson() + { + return Json\Json::encode('bogus'); + } +} + /** * ISSUE ZF-4946 * diff --git a/test/JsonXmlTest.php b/test/JsonXmlTest.php index 0ad24855e..8571c0cee 100644 --- a/test/JsonXmlTest.php +++ b/test/JsonXmlTest.php @@ -508,6 +508,118 @@ public function testUsingXML7() } */ + /** + * @group ZF-3257 + */ + public function testUsingXML8() { + + // Set the XML contents that will be tested here. + $xmlStringContents = << +bar + +EOT; + + // There are not going to be any XML attributes in this test XML. + // Hence, set the flag to ignore XML attributes. + $ignoreXmlAttributes = false; + $jsonContents = ""; + $ex = null; + + // Convert XML to JSON now. + // fromXml function simply takes a String containing XML contents as input. + try { + $jsonContents = Json\Json::fromXml($xmlStringContents, $ignoreXmlAttributes); + } catch (Exception $ex) { + ; + } + $this->assertSame($ex, null, "Zend_JSON::fromXml returned an exception."); + + // Convert the JSON string into a PHP array. + $phpArray = Json\Json::decode($jsonContents, Json\Json::TYPE_ARRAY); + // Test if it is not a NULL object. + $this->assertNotNull($phpArray, "JSON result for XML input 1 is NULL"); + + $this->assertSame("bar", $phpArray['a']['@text'], "The text element of a is not correct"); + $this->assertSame("foo", $phpArray['a']['b']['@attributes']['id'], "The id attribute of b is not correct"); + + } + + /** + * @group ZF-11385 + * @expectedException Zend\Json\Exception\RecursionException + * @dataProvider providerNestingDepthIsHandledProperly + */ + public function testNestingDepthIsHandledProperlyWhenNestingDepthExceedsMaximum($xmlStringContents) + { + Json\Json::$maxRecursionDepthAllowed = 1; + Json\Json::fromXml($xmlStringContents, true); + } + + /** + * @group ZF-11385 + * @dataProvider providerNestingDepthIsHandledProperly + */ + public function testNestingDepthIsHandledProperlyWhenNestingDepthDoesNotExceedMaximum($xmlStringContents) + { + try { + Json\Json::$maxRecursionDepthAllowed = 25; + $jsonString = Json\Json::fromXml($xmlStringContents, true); + $jsonArray = Json\Json::decode($jsonString, Json\Json::TYPE_ARRAY); + $this->assertNotNull($jsonArray, "JSON decode result is NULL"); + $this->assertSame('A', $jsonArray['response']['message_type']['defaults']['close_rules']['after_responses']); + } catch ( Zend\Json\Exception\RecursionException $ex ) { + $this->fail('Zend_Json::fromXml does not implement recursion check properly'); + } + } + + /** + * XML document provider for ZF-11385 tests + * @return array + */ + public static function providerNestingDepthIsHandledProperly() + { + $xmlStringContents = << + success + 200 OK + + A + B + C + D + E + F + G + H + A + B + C + D + E + A + B + C + D + E + + + A + + B + C + A + B + C + D + + + 0.0790269374847 + +EOT; + return array(array($xmlStringContents)); + } + }