From ee212be951cf2f4300074f9fa0421e7850e8cae2 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Fri, 31 Aug 2012 14:44:59 -0500 Subject: [PATCH] [zendframework/zf2#2284][ZF2-507] Updated README - Notice about Date header --- .coveralls.yml | 3 + .gitattributes | 6 + .gitignore | 14 + .php_cs | 43 + .travis.yml | 35 + CONTRIBUTING.md | 229 ++++ LICENSE.txt | 27 + README.md | 9 + composer.json | 42 + phpunit.xml.dist | 34 + phpunit.xml.travis | 34 + src/Decoder.php | 563 +++++++++ src/Encoder.php | 574 +++++++++ src/Exception/BadMethodCallException.php | 20 + src/Exception/ExceptionInterface.php | 18 + src/Exception/InvalidArgumentException.php | 19 + src/Exception/RecursionException.php | 18 + src/Exception/RuntimeException.php | 18 + src/Expr.php | 67 ++ src/Json.php | 384 ++++++ src/Server/Cache.php | 97 ++ src/Server/Client.php | 199 ++++ src/Server/Error.php | 184 +++ src/Server/Exception/ErrorException.php | 24 + src/Server/Exception/ExceptionInterface.php | 21 + src/Server/Exception/HttpException.php | 22 + .../Exception/InvalidArgumentException.php | 22 + src/Server/Exception/RuntimeException.php | 22 + src/Server/Request.php | 278 +++++ src/Server/Request/Http.php | 51 + src/Server/Response.php | 279 +++++ src/Server/Response/Http.php | 67 ++ src/Server/Server.php | 545 +++++++++ src/Server/Smd.php | 461 ++++++++ src/Server/Smd/Service.php | 452 ++++++++ test/JsonTest.php | 1033 +++++++++++++++++ test/JsonXmlTest.php | 606 ++++++++++ test/Server/CacheTest.php | 121 ++ test/Server/ClientTest.php | 262 +++++ test/Server/ErrorTest.php | 152 +++ test/Server/RequestTest.php | 266 +++++ test/Server/ResponseTest.php | 184 +++ test/Server/Smd/ServiceTest.php | 315 +++++ test/Server/SmdTest.php | 388 +++++++ test/ServerTest.php | 476 ++++++++ test/TestAsset/TestIteratorAggregate.php | 27 + test/bootstrap.php | 34 + 47 files changed, 8745 insertions(+) create mode 100644 .coveralls.yml create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 .php_cs create mode 100644 .travis.yml create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE.txt create mode 100644 README.md create mode 100644 composer.json create mode 100644 phpunit.xml.dist create mode 100644 phpunit.xml.travis create mode 100644 src/Decoder.php create mode 100644 src/Encoder.php create mode 100644 src/Exception/BadMethodCallException.php create mode 100644 src/Exception/ExceptionInterface.php create mode 100644 src/Exception/InvalidArgumentException.php create mode 100644 src/Exception/RecursionException.php create mode 100644 src/Exception/RuntimeException.php create mode 100644 src/Expr.php create mode 100644 src/Json.php create mode 100644 src/Server/Cache.php create mode 100644 src/Server/Client.php create mode 100644 src/Server/Error.php create mode 100644 src/Server/Exception/ErrorException.php create mode 100644 src/Server/Exception/ExceptionInterface.php create mode 100644 src/Server/Exception/HttpException.php create mode 100644 src/Server/Exception/InvalidArgumentException.php create mode 100644 src/Server/Exception/RuntimeException.php create mode 100644 src/Server/Request.php create mode 100644 src/Server/Request/Http.php create mode 100644 src/Server/Response.php create mode 100644 src/Server/Response/Http.php create mode 100644 src/Server/Server.php create mode 100644 src/Server/Smd.php create mode 100644 src/Server/Smd/Service.php create mode 100644 test/JsonTest.php create mode 100644 test/JsonXmlTest.php create mode 100644 test/Server/CacheTest.php create mode 100644 test/Server/ClientTest.php create mode 100644 test/Server/ErrorTest.php create mode 100644 test/Server/RequestTest.php create mode 100644 test/Server/ResponseTest.php create mode 100644 test/Server/Smd/ServiceTest.php create mode 100644 test/Server/SmdTest.php create mode 100644 test/ServerTest.php create mode 100644 test/TestAsset/TestIteratorAggregate.php create mode 100644 test/bootstrap.php diff --git a/.coveralls.yml b/.coveralls.yml new file mode 100644 index 000000000..53bda829c --- /dev/null +++ b/.coveralls.yml @@ -0,0 +1,3 @@ +coverage_clover: clover.xml +json_path: coveralls-upload.json +src_dir: src diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..85dc9a8c8 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,6 @@ +/test export-ignore +/vendor export-ignore +.gitattributes export-ignore +.gitignore export-ignore +.travis.yml export-ignore +.php_cs export-ignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..4cac0a218 --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +.buildpath +.DS_Store +.idea +.project +.settings/ +.*.sw* +.*.un~ +nbproject +tmp/ + +clover.xml +coveralls-upload.json +phpunit.xml +vendor diff --git a/.php_cs b/.php_cs new file mode 100644 index 000000000..bf4b799f3 --- /dev/null +++ b/.php_cs @@ -0,0 +1,43 @@ +notPath('TestAsset') + ->notPath('_files') + ->filter(function (SplFileInfo $file) { + if (strstr($file->getPath(), 'compatibility')) { + return false; + } + }); +$config = Symfony\CS\Config\Config::create(); +$config->level(null); +$config->fixers( + array( + 'braces', + 'duplicate_semicolon', + 'elseif', + 'empty_return', + 'encoding', + 'eof_ending', + 'function_call_space', + 'function_declaration', + 'indentation', + 'join_function', + 'line_after_namespace', + 'linefeed', + 'lowercase_keywords', + 'parenthesis', + 'multiple_use', + 'method_argument_space', + 'object_operator', + 'php_closing_tag', + 'psr0', + 'remove_lines_between_uses', + 'short_tag', + 'standardize_not_equal', + 'trailing_spaces', + 'unused_use', + 'visibility', + 'whitespacy_lines', + ) +); +$config->finder($finder); +return $config; diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 000000000..fe909ecb1 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,35 @@ +sudo: false + +language: php + +matrix: + fast_finish: true + include: + - php: 5.5 + - php: 5.6 + env: + - EXECUTE_TEST_COVERALLS=true + - EXECUTE_CS_CHECK=true + - php: 7 + - php: hhvm + allow_failures: + - php: 7 + - php: hhvm + +notifications: + irc: "irc.freenode.org#zftalk.dev" + email: false + +before_install: + - if [[ $EXECUTE_TEST_COVERALLS != 'true' ]]; then phpenv config-rm xdebug.ini || return 0 ; fi + +install: + - composer install --no-interaction --prefer-source + +script: + - if [[ $EXECUTE_TEST_COVERALLS == 'true' ]]; then ./vendor/bin/phpunit -c phpunit.xml.travis --coverage-clover clover.xml ; fi + - if [[ $EXECUTE_TEST_COVERALLS != 'true' ]]; then ./vendor/bin/phpunit -c phpunit.xml.travis ; fi + - if [[ $EXECUTE_CS_CHECK == 'true' ]]; then ./vendor/bin/php-cs-fixer fix -v --diff --dry-run --config-file=.php_cs ; fi + +after_script: + - if [[ $EXECUTE_TEST_COVERALLS == 'true' ]]; then ./vendor/bin/coveralls ; fi diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..723791e83 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,229 @@ +# CONTRIBUTING + +## RESOURCES + +If you wish to contribute to Zend Framework, please be sure to +read/subscribe to the following resources: + + - [Coding Standards](https://github.com/zendframework/zf2/wiki/Coding-Standards) + - [Contributor's Guide](http://framework.zend.com/participate/contributor-guide) + - ZF Contributor's mailing list: + Archives: http://zend-framework-community.634137.n4.nabble.com/ZF-Contributor-f680267.html + Subscribe: zf-contributors-subscribe@lists.zend.com + - ZF Contributor's IRC channel: + #zftalk.dev on Freenode.net + +If you are working on new features or refactoring [create a proposal](https://github.com/zendframework/zend-json/issues/new). + +## Reporting Potential Security Issues + +If you have encountered a potential security vulnerability, please **DO NOT** report it on the public +issue tracker: send it to us at [zf-security@zend.com](mailto:zf-security@zend.com) instead. +We will work with you to verify the vulnerability and patch it as soon as possible. + +When reporting issues, please provide the following information: + +- Component(s) affected +- A description indicating how to reproduce the issue +- A summary of the security vulnerability and impact + +We request that you contact us via the email address above and give the project +contributors a chance to resolve the vulnerability and issue a new release prior +to any public exposure; this helps protect users and provides them with a chance +to upgrade and/or update in order to protect their applications. + +For sensitive email communications, please use [our PGP key](http://framework.zend.com/zf-security-pgp-key.asc). + +## RUNNING TESTS + +> ### Note: testing versions prior to 2.4 +> +> This component originates with Zend Framework 2. During the lifetime of ZF2, +> testing infrastructure migrated from PHPUnit 3 to PHPUnit 4. In most cases, no +> changes were necessary. However, due to the migration, tests may not run on +> versions < 2.4. As such, you may need to change the PHPUnit dependency if +> attempting a fix on such a version. + +To run tests: + +- Clone the repository: + + ```console + $ git clone git@github.com:zendframework/zend-json.git + $ cd + ``` + +- Install dependencies via composer: + + ```console + $ curl -sS https://getcomposer.org/installer | php -- + $ ./composer.phar install + ``` + + If you don't have `curl` installed, you can also download `composer.phar` from https://getcomposer.org/ + +- Run the tests via `phpunit` and the provided PHPUnit config, like in this example: + + ```console + $ ./vendor/bin/phpunit + ``` + +You can turn on conditional tests with the phpunit.xml file. +To do so: + + - Copy `phpunit.xml.dist` file to `phpunit.xml` + - Edit `phpunit.xml` to enable any specific functionality you + want to test, as well as to provide test values to utilize. + +## Running Coding Standards Checks + +This component uses [php-cs-fixer](http://cs.sensiolabs.org/) for coding +standards checks, and provides configuration for our selected checks. +`php-cs-fixer` is installed by default via Composer. + +To run checks only: + +```console +$ ./vendor/bin/php-cs-fixer fix . -v --diff --dry-run --config-file=.php_cs +``` + +To have `php-cs-fixer` attempt to fix problems for you, omit the `--dry-run` +flag: + +```console +$ ./vendor/bin/php-cs-fixer fix . -v --diff --config-file=.php_cs +``` + +If you allow php-cs-fixer to fix CS issues, please re-run the tests to ensure +they pass, and make sure you add and commit the changes after verification. + +## Recommended Workflow for Contributions + +Your first step is to establish a public repository from which we can +pull your work into the master repository. We recommend using +[GitHub](https://github.com), as that is where the component is already hosted. + +1. Setup a [GitHub account](http://github.com/), if you haven't yet +2. Fork the repository (http://github.com/zendframework/zend-json) +3. Clone the canonical repository locally and enter it. + + ```console + $ git clone git://github.com:zendframework/zend-json.git + $ cd zend-json + ``` + +4. Add a remote to your fork; substitute your GitHub username in the command + below. + + ```console + $ git remote add {username} git@github.com:{username}/zend-json.git + $ git fetch {username} + ``` + +### Keeping Up-to-Date + +Periodically, you should update your fork or personal repository to +match the canonical ZF repository. Assuming you have setup your local repository +per the instructions above, you can do the following: + + +```console +$ git checkout master +$ git fetch origin +$ git rebase origin/master +# OPTIONALLY, to keep your remote up-to-date - +$ git push {username} master:master +``` + +If you're tracking other branches -- for example, the "develop" branch, where +new feature development occurs -- you'll want to do the same operations for that +branch; simply substitute "develop" for "master". + +### Working on a patch + +We recommend you do each new feature or bugfix in a new branch. This simplifies +the task of code review as well as the task of merging your changes into the +canonical repository. + +A typical workflow will then consist of the following: + +1. Create a new local branch based off either your master or develop branch. +2. Switch to your new local branch. (This step can be combined with the + previous step with the use of `git checkout -b`.) +3. Do some work, commit, repeat as necessary. +4. Push the local branch to your remote repository. +5. Send a pull request. + +The mechanics of this process are actually quite trivial. Below, we will +create a branch for fixing an issue in the tracker. + +```console +$ git checkout -b hotfix/9295 +Switched to a new branch 'hotfix/9295' +``` + +... do some work ... + + +```console +$ git commit +``` + +... write your log message ... + + +```console +$ git push {username} hotfix/9295:hotfix/9295 +Counting objects: 38, done. +Delta compression using up to 2 threads. +Compression objects: 100% (18/18), done. +Writing objects: 100% (20/20), 8.19KiB, done. +Total 20 (delta 12), reused 0 (delta 0) +To ssh://git@github.com/{username}/zend-json.git + b5583aa..4f51698 HEAD -> master +``` + +To send a pull request, you have two options. + +If using GitHub, you can do the pull request from there. Navigate to +your repository, select the branch you just created, and then select the +"Pull Request" button in the upper right. Select the user/organization +"zendframework" as the recipient. + +If using your own repository - or even if using GitHub - you can use `git +format-patch` to create a patchset for us to apply; in fact, this is +**recommended** for security-related patches. If you use `format-patch`, please +send the patches as attachments to: + +- zf-devteam@zend.com for patches without security implications +- zf-security@zend.com for security patches + +#### What branch to issue the pull request against? + +Which branch should you issue a pull request against? + +- For fixes against the stable release, issue the pull request against the + "master" branch. +- For new features, or fixes that introduce new elements to the public API (such + as new public methods or properties), issue the pull request against the + "develop" branch. + +### Branch Cleanup + +As you might imagine, if you are a frequent contributor, you'll start to +get a ton of branches both locally and on your remote. + +Once you know that your changes have been accepted to the master +repository, we suggest doing some cleanup of these branches. + +- Local branch cleanup + + ```console + $ git branch -d + ``` + +- Remote branch removal + + ```console + $ git push {username} : + ``` diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 000000000..6eab5aa14 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,27 @@ +Copyright (c) 2005-2015, Zend Technologies USA, Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + * Neither the name of Zend Technologies USA, Inc. nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md new file mode 100644 index 000000000..e25b9ae8c --- /dev/null +++ b/README.md @@ -0,0 +1,9 @@ +# zend-json + +`Zend\Json` provides convenience methods for serializing native PHP to JSON and +decoding JSON to native PHP. For more information on JSON, visit the JSON +[project site](http://www.json.org/). + + +- File issues at https://github.com/zendframework/zend-json/issues +- Documentation is at http://framework.zend.com/manual/current/en/index.html#zend-json diff --git a/composer.json b/composer.json new file mode 100644 index 000000000..b22abb758 --- /dev/null +++ b/composer.json @@ -0,0 +1,42 @@ +{ + "name": "zendframework/zend-json", + "description": "provides convenience methods for serializing native PHP to JSON and decoding JSON to native PHP", + "license": "BSD-3-Clause", + "keywords": [ + "zf2", + "json" + ], + "homepage": "https://github.com/zendframework/zend-json", + "autoload": { + "psr-4": { + "Zend\\Json": "src/" + } + }, + "require": { + "php": ">=5.3.3", + "zendframework/zend-stdlib": "self.version" + }, + "require-dev": { + "zendframework/zend-http": "self.version", + "zendframework/zend-server": "self.version", + "fabpot/php-cs-fixer": "1.7.*", + "satooshi/php-coveralls": "dev-master", + "phpunit/PHPUnit": "~4.0" + }, + "suggest": { + "zendframework/zend-http": "Zend\\Http component", + "zendframework/zend-server": "Zend\\Server component", + "zendframework/zendxml": "To support Zend\\Json\\Json::fromXml() usage" + }, + "extra": { + "branch-alias": { + "dev-master": "2.4-dev", + "dev-develop": "2.5-dev" + } + }, + "autoload-dev": { + "psr-4": { + "ZendTest\\Json\\": "test/" + } + } +} \ No newline at end of file diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 000000000..dcee0921f --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,34 @@ + + + + + ./test/ + + + + + + disable + + + + + + ./src + + + + + + + + + + + diff --git a/phpunit.xml.travis b/phpunit.xml.travis new file mode 100644 index 000000000..dcee0921f --- /dev/null +++ b/phpunit.xml.travis @@ -0,0 +1,34 @@ + + + + + ./test/ + + + + + + disable + + + + + + ./src + + + + + + + + + + + diff --git a/src/Decoder.php b/src/Decoder.php new file mode 100644 index 000000000..7241e7e92 --- /dev/null +++ b/src/Decoder.php @@ -0,0 +1,563 @@ +source = self::decodeUnicodeString($source); + $this->sourceLength = strlen($this->source); + $this->token = self::EOF; + $this->offset = 0; + + switch ($decodeType) { + case Json::TYPE_ARRAY: + case Json::TYPE_OBJECT: + $this->decodeType = $decodeType; + break; + default: + throw new InvalidArgumentException("Unknown decode type '{$decodeType}', please use one of the constants Json::TYPE_*"); + } + + // Set pointer at first token + $this->_getNextToken(); + } + + /** + * Decode a JSON source string + * + * Decodes a JSON encoded string. The value returned will be one of the + * following: + * - integer + * - float + * - boolean + * - null + * - StdClass + * - array + * - array of one or more of the above types + * + * By default, decoded objects will be returned as associative arrays; to + * return a StdClass object instead, pass {@link Zend_Json::TYPE_OBJECT} to + * the $objectDecodeType parameter. + * + * @static + * @access public + * @param string $source String to be decoded + * @param int $objectDecodeType How objects should be decoded; should be + * either or {@link Zend_Json::TYPE_ARRAY} or + * {@link Zend_Json::TYPE_OBJECT}; defaults to TYPE_ARRAY + * @return mixed + */ + public static function decode($source, $objectDecodeType = Json::TYPE_OBJECT) + { + $decoder = new self($source, $objectDecodeType); + return $decoder->_decodeValue(); + } + + /** + * Recursive driving routine for supported toplevel tops + * + * @return mixed + */ + protected function _decodeValue() + { + switch ($this->token) { + case self::DATUM: + $result = $this->tokenValue; + $this->_getNextToken(); + return($result); + break; + case self::LBRACE: + return($this->_decodeObject()); + break; + case self::LBRACKET: + return($this->_decodeArray()); + break; + default: + return null; + break; + } + } + + /** + * Decodes an object of the form: + * { "attribute: value, "attribute2" : value,...} + * + * If Zend_Json_Encoder was used to encode the original object then + * a special attribute called __className which specifies a class + * name that should wrap the data contained within the encoded source. + * + * Decodes to either an array or StdClass object, based on the value of + * {@link $decodeType}. If invalid $decodeType present, returns as an + * array. + * + * @return array|StdClass + * @throws Zend\Json\Exception\RuntimeException + */ + protected function _decodeObject() + { + $members = array(); + $tok = $this->_getNextToken(); + + while ($tok && $tok != self::RBRACE) { + if ($tok != self::DATUM || ! is_string($this->tokenValue)) { + throw new RuntimeException('Missing key in object encoding: ' . $this->source); + } + + $key = $this->tokenValue; + $tok = $this->_getNextToken(); + + if ($tok != self::COLON) { + throw new RuntimeException('Missing ":" in object encoding: ' . $this->source); + } + + $tok = $this->_getNextToken(); + $members[$key] = $this->_decodeValue(); + $tok = $this->token; + + if ($tok == self::RBRACE) { + break; + } + + if ($tok != self::COMMA) { + throw new RuntimeException('Missing "," in object encoding: ' . $this->source); + } + + $tok = $this->_getNextToken(); + } + + switch ($this->decodeType) { + case Json::TYPE_OBJECT: + // Create new StdClass and populate with $members + $result = new \stdClass(); + foreach ($members as $key => $value) { + if ($key === '') { + $key = '_empty_'; + } + $result->$key = $value; + } + break; + case Json::TYPE_ARRAY: + default: + $result = $members; + break; + } + + $this->_getNextToken(); + return $result; + } + + /** + * Decodes a JSON array format: + * [element, element2,...,elementN] + * + * @return array + * @throws Zend\Json\Exception\RuntimeException + */ + protected function _decodeArray() + { + $result = array(); + $starttok = $tok = $this->_getNextToken(); // Move past the '[' + $index = 0; + + while ($tok && $tok != self::RBRACKET) { + $result[$index++] = $this->_decodeValue(); + + $tok = $this->token; + + if ($tok == self::RBRACKET || !$tok) { + break; + } + + if ($tok != self::COMMA) { + throw new RuntimeException('Missing "," in array encoding: ' . $this->source); + } + + $tok = $this->_getNextToken(); + } + + $this->_getNextToken(); + return $result; + } + + + /** + * Removes whitespace characters from the source input + */ + protected function _eatWhitespace() + { + if (preg_match( + '/([\t\b\f\n\r ])*/s', + $this->source, + $matches, + PREG_OFFSET_CAPTURE, + $this->offset) + && $matches[0][1] == $this->offset) + { + $this->offset += strlen($matches[0][0]); + } + } + + + /** + * Retrieves the next token from the source stream + * + * @return int Token constant value specified in class definition + * @throws Zend\Json\Exception\RuntimeException + */ + protected function _getNextToken() + { + $this->token = self::EOF; + $this->tokenValue = null; + $this->_eatWhitespace(); + + if ($this->offset >= $this->sourceLength) { + return(self::EOF); + } + + $str = $this->source; + $str_length = $this->sourceLength; + $i = $this->offset; + $start = $i; + + switch ($str{$i}) { + case '{': + $this->token = self::LBRACE; + break; + case '}': + $this->token = self::RBRACE; + break; + case '[': + $this->token = self::LBRACKET; + break; + case ']': + $this->token = self::RBRACKET; + break; + case ',': + $this->token = self::COMMA; + break; + case ':': + $this->token = self::COLON; + break; + case '"': + $result = ''; + do { + $i++; + if ($i >= $str_length) { + break; + } + + $chr = $str{$i}; + + if ($chr == '\\') { + $i++; + if ($i >= $str_length) { + break; + } + $chr = $str{$i}; + switch ($chr) { + case '"' : + $result .= '"'; + break; + case '\\': + $result .= '\\'; + break; + case '/' : + $result .= '/'; + break; + case 'b' : + $result .= "\x08"; + break; + case 'f' : + $result .= "\x0c"; + break; + case 'n' : + $result .= "\x0a"; + break; + case 'r' : + $result .= "\x0d"; + break; + case 't' : + $result .= "\x09"; + break; + case '\'' : + $result .= '\''; + break; + default: + throw new RuntimeException("Illegal escape sequence '{$chr}'"); + } + } elseif ($chr == '"') { + break; + } else { + $result .= $chr; + } + } while ($i < $str_length); + + $this->token = self::DATUM; + //$this->tokenValue = substr($str, $start + 1, $i - $start - 1); + $this->tokenValue = $result; + break; + case 't': + if (($i+ 3) < $str_length && substr($str, $start, 4) == "true") { + $this->token = self::DATUM; + } + $this->tokenValue = true; + $i += 3; + break; + case 'f': + if (($i+ 4) < $str_length && substr($str, $start, 5) == "false") { + $this->token = self::DATUM; + } + $this->tokenValue = false; + $i += 4; + break; + case 'n': + if (($i+ 3) < $str_length && substr($str, $start, 4) == "null") { + $this->token = self::DATUM; + } + $this->tokenValue = NULL; + $i += 3; + break; + } + + if ($this->token != self::EOF) { + $this->offset = $i + 1; // Consume the last token character + return($this->token); + } + + $chr = $str{$i}; + if ($chr == '-' || $chr == '.' || ($chr >= '0' && $chr <= '9')) { + if (preg_match('/-?([0-9])*(\.[0-9]*)?((e|E)((-|\+)?)[0-9]+)?/s', + $str, $matches, PREG_OFFSET_CAPTURE, $start) && $matches[0][1] == $start) { + + $datum = $matches[0][0]; + + if (is_numeric($datum)) { + if (preg_match('/^0\d+$/', $datum)) { + throw new RuntimeException("Octal notation not supported by JSON (value: {$datum})"); + } else { + $val = intval($datum); + $fVal = floatval($datum); + $this->tokenValue = ($val == $fVal ? $val : $fVal); + } + } else { + throw new RuntimeException("Illegal number format: {$datum}"); + } + + $this->token = self::DATUM; + $this->offset = $start + strlen($datum); + } + } else { + throw new RuntimeException('Illegal Token'); + } + + return $this->token; + } + + /** + * Decode Unicode Characters from \u0000 ASCII syntax. + * + * This algorithm was originally developed for the + * Solar Framework by Paul M. Jones + * + * @link http://solarphp.com/ + * @link http://svn.solarphp.com/core/trunk/Solar/Json.php + * @param string $value + * @return string + */ + public static function decodeUnicodeString($chrs) + { + $chrs = (string)$chrs; + $delim = substr($chrs, 0, 1); + $utf8 = ''; + $strlen_chrs = strlen($chrs); + + for ($i = 0; $i < $strlen_chrs; $i++) { + + $substr_chrs_c_2 = substr($chrs, $i, 2); + $ord_chrs_c = ord($chrs[$i]); + + switch (true) { + case preg_match('/\\\u[0-9A-F]{4}/i', substr($chrs, $i, 6)): + // single, escaped unicode character + $utf16 = chr(hexdec(substr($chrs, ($i + 2), 2))) + . chr(hexdec(substr($chrs, ($i + 4), 2))); + $utf8char = self::_utf162utf8($utf16); + $search = array('\\', "\n", "\t", "\r", chr(0x08), chr(0x0C), '"', '\'', '/'); + if (in_array($utf8char, $search)) { + $replace = array('\\\\', '\\n', '\\t', '\\r', '\\b', '\\f', '\\"', '\\\'', '\\/'); + $utf8char = str_replace($search, $replace, $utf8char); + } + $utf8 .= $utf8char; + $i += 5; + break; + case ($ord_chrs_c >= 0x20) && ($ord_chrs_c <= 0x7F): + $utf8 .= $chrs{$i}; + break; + case ($ord_chrs_c & 0xE0) == 0xC0: + // characters U-00000080 - U-000007FF, mask 110XXXXX + //see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + $utf8 .= substr($chrs, $i, 2); + ++$i; + break; + case ($ord_chrs_c & 0xF0) == 0xE0: + // characters U-00000800 - U-0000FFFF, mask 1110XXXX + // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + $utf8 .= substr($chrs, $i, 3); + $i += 2; + break; + case ($ord_chrs_c & 0xF8) == 0xF0: + // characters U-00010000 - U-001FFFFF, mask 11110XXX + // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + $utf8 .= substr($chrs, $i, 4); + $i += 3; + break; + case ($ord_chrs_c & 0xFC) == 0xF8: + // characters U-00200000 - U-03FFFFFF, mask 111110XX + // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + $utf8 .= substr($chrs, $i, 5); + $i += 4; + break; + case ($ord_chrs_c & 0xFE) == 0xFC: + // characters U-04000000 - U-7FFFFFFF, mask 1111110X + // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + $utf8 .= substr($chrs, $i, 6); + $i += 5; + break; + } + } + + return $utf8; + } + + /** + * Convert a string from one UTF-16 char to one UTF-8 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 $utf16 UTF-16 character + * @return string UTF-8 character + */ + protected static function _utf162utf8($utf16) + { + // Check for mb extension otherwise do by hand. + if (function_exists('mb_convert_encoding')) { + return mb_convert_encoding($utf16, 'UTF-8', 'UTF-16'); + } + + $bytes = (ord($utf16{0}) << 8) | ord($utf16{1}); + + switch (true) { + case ((0x7F & $bytes) == $bytes): + // 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 chr(0x7F & $bytes); + + case (0x07FF & $bytes) == $bytes: + // return a 2-byte UTF-8 character + // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + return chr(0xC0 | (($bytes >> 6) & 0x1F)) + . chr(0x80 | ($bytes & 0x3F)); + + case (0xFFFF & $bytes) == $bytes: + // return a 3-byte UTF-8 character + // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + return chr(0xE0 | (($bytes >> 12) & 0x0F)) + . chr(0x80 | (($bytes >> 6) & 0x3F)) + . chr(0x80 | ($bytes & 0x3F)); + } + + // ignoring UTF-32 for now, sorry + return ''; + } +} diff --git a/src/Encoder.php b/src/Encoder.php new file mode 100644 index 000000000..10b4ed81c --- /dev/null +++ b/src/Encoder.php @@ -0,0 +1,574 @@ +cycleCheck = $cycleCheck; + $this->options = $options; + } + + /** + * Use the JSON encoding scheme for the value specified + * + * @param mixed $value The value to be encoded + * @param boolean $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 self(($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 Zend\Json\Exception\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 boolean + */ + 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 $value 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 $cls ReflectionClass + * @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 $cls ReflectionClass + * @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 $cls ReflectionClass + * @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 Zend\Json\Exception\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 .= self::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 http://svn.solarphp.com/core/trunk/Solar/JSON.php + * @param string $value + * @return string + */ + public static function encodeUnicodeString($value) + { + $strlen_var = 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 < $strlen_var; $i++) { + $ord_var_c = ord($value[$i]); + + switch (true) { + case (($ord_var_c >= 0x20) && ($ord_var_c <= 0x7F)): + // characters U-00000000 - U-0000007F (same as ASCII) + $ascii .= $value[$i]; + break; + + case (($ord_var_c & 0xE0) == 0xC0): + // characters U-00000080 - U-000007FF, mask 110XXXXX + // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + $char = pack('C*', $ord_var_c, ord($value[$i + 1])); + $i += 1; + $utf16 = self::_utf82utf16($char); + $ascii .= sprintf('\u%04s', bin2hex($utf16)); + break; + + case (($ord_var_c & 0xF0) == 0xE0): + // characters U-00000800 - U-0000FFFF, mask 1110XXXX + // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + $char = pack('C*', $ord_var_c, + ord($value[$i + 1]), + ord($value[$i + 2])); + $i += 2; + $utf16 = self::_utf82utf16($char); + $ascii .= sprintf('\u%04s', bin2hex($utf16)); + break; + + case (($ord_var_c & 0xF8) == 0xF0): + // characters U-00010000 - U-001FFFFF, mask 11110XXX + // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + $char = pack('C*', $ord_var_c, + 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 (($ord_var_c & 0xFC) == 0xF8): + // characters U-00200000 - U-03FFFFFF, mask 111110XX + // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + $char = pack('C*', $ord_var_c, + 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 (($ord_var_c & 0xFE) == 0xFC): + // characters U-04000000 - U-7FFFFFFF, mask 1111110X + // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + $char = pack('C*', $ord_var_c, + 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 ''; + } +} diff --git a/src/Exception/BadMethodCallException.php b/src/Exception/BadMethodCallException.php new file mode 100644 index 000000000..11d22c133 --- /dev/null +++ b/src/Exception/BadMethodCallException.php @@ -0,0 +1,20 @@ + + * $foo = array( + * 'integer' =>9, + * 'string' =>'test string', + * 'function' => Zend_Json_Expr( + * 'function() { window.alert("javascript function encoded by Zend_Json") }' + * ), + * ); + * + * Zend_Json::encode($foo, false, array('enableJsonExprFinder' => true)); + * // it will returns json encoded string: + * // {"integer":9,"string":"test string","function":function() {window.alert("javascript function encoded by Zend_Json")}} + * + * + * @category Zend + * @package Zend_Json + * @subpackage Expr + */ +class Expr +{ + /** + * Storage for javascript expression. + * + * @var string + */ + protected $expression; + + /** + * Constructor + * + * @param string $expression the expression to hold. + */ + public function __construct($expression) + { + $this->expression = (string) $expression; + } + + /** + * Cast to string + * + * @return string holded javascript expression. + */ + public function __toString() + { + return $this->expression; + } +} diff --git a/src/Json.php b/src/Json.php new file mode 100644 index 000000000..65b9e74b6 --- /dev/null +++ b/src/Json.php @@ -0,0 +1,384 @@ +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 + $javascriptExpressions = array(); + if (isset($options['enableJsonExprFinder']) + && ($options['enableJsonExprFinder'] == true) + ) { + $valueToEncode = self::_recursiveJsonExprFinder($valueToEncode, $javascriptExpressions); + } + + // Encoding + if (function_exists('json_encode') && self::$useBuiltinEncoderDecoder !== true) { + $encodedResult = json_encode( + $valueToEncode, + JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP + ); + } else { + $encodedResult = Encoder::encode($valueToEncode, $cycleCheck, $options); + } + + //only do post-processing to revert back the Zend_Json_Expr if any. + if (count($javascriptExpressions) > 0) { + $count = count($javascriptExpressions); + for ($i = 0; $i < $count; $i++) { + $magicKey = $javascriptExpressions[$i]['magicKey']; + $value = $javascriptExpressions[$i]['value']; + + $encodedResult = str_replace( + //instead of replacing "key:magicKey", we replace directly magicKey by value because "key" never changes. + '"' . $magicKey . '"', + $value, + $encodedResult + ); + } + } + + return $encodedResult; + } + + /** + * Check & Replace Zend_Json_Expr for tmp ids in the valueToEncode + * + * Check if the value is a Zend_Json_Expr, and if replace its value + * with a magic key and save the javascript expression in an array. + * + * NOTE this method is recursive. + * + * NOTE: This method is used internally by the encode method. + * + * @see encode + * @param mixed $valueToCheck a string - object property to be encoded + * @return void + */ + protected static function _recursiveJsonExprFinder( + &$value, array &$javascriptExpressions, $currentKey = null + ) { + if ($value instanceof Expr) { + // TODO: Optimize with ascii keys, if performance is bad + $magicKey = "____" . $currentKey . "_" . (count($javascriptExpressions)); + $javascriptExpressions[] = array( + + //if currentKey is integer, encodeUnicodeString call is not required. + "magicKey" => (is_int($currentKey)) ? $magicKey : Encoder::encodeUnicodeString($magicKey), + "value" => $value->__toString(), + ); + $value = $magicKey; + } elseif (is_array($value)) { + foreach ($value as $k => $v) { + $value[$k] = self::_recursiveJsonExprFinder($value[$k], $javascriptExpressions, $k); + } + } elseif (is_object($value)) { + foreach ($value as $k => $v) { + $value->$k = self::_recursiveJsonExprFinder($value->$k, $javascriptExpressions, $k); + } + } + 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 + * matches 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 + * + * Converts a XML formatted string into a JSON formatted string. + * The value returned will be a string in JSON format. + * + * The caller of this function needs to provide only the first parameter, + * which is an XML formatted String. The second parameter is optional, which + * lets the user to select if the XML attributes in the input XML string + * should be included or ignored in xml2json conversion. + * + * This function converts the XML formatted string into a PHP array by + * calling a recursive (protected static) function in this class. Then, it + * converts that PHP array into JSON by calling the "encode" static function. + * + * NOTE: Encoding native javascript expressions via Zend_Json_Expr is not possible. + * + * @static + * @access public + * @param string $xmlStringContents XML String to be converted + * @param boolean $ignoreXmlAttributes Include or exclude XML attributes in + * the xml2json conversion process. + * @return mixed - JSON formatted string on success + * @throws \Zend\Json\Exception\RuntimeException if the input not a XML formatted string + */ + public static function fromXml ($xmlStringContents, $ignoreXmlAttributes=true) + { + // Load the XML formatted string into a Simple XML Element object. + $simpleXmlElementObject = simplexml_load_string($xmlStringContents); + + // If it is not a valid XML content, throw an exception. + if ($simpleXmlElementObject == null) { + throw new RuntimeException('Function fromXml was called with an invalid XML formatted string.'); + } // End of if ($simpleXmlElementObject == null) + + $resultArray = null; + + // Call the recursive function to convert the XML into a PHP array. + $resultArray = self::_processXml($simpleXmlElementObject, $ignoreXmlAttributes); + + // Convert the PHP array to JSON using Zend_Json encode method. + // It is just that simple. + $jsonStringOutput = self::encode($resultArray); + return($jsonStringOutput); + } + + /** + * Pretty-print JSON string + * + * Use 'indent' option to select indentation string - by default it's a tab + * + * @param string $json Original JSON string + * @param array $options Encoding options + * @return string + */ + public static function prettyPrint($json, $options = array()) + { + $tokens = preg_split('|([\{\}\]\[,])|', $json, -1, PREG_SPLIT_DELIM_CAPTURE); + $result = ""; + $indent = 0; + + $ind = "\t"; + if (isset($options['indent'])) { + $ind = $options['indent']; + } + + $inLiteral = false; + foreach ($tokens as $token) { + if ($token == "") continue; + + $prefix = str_repeat($ind, $indent); + if (!$inLiteral && ($token == "{" || $token == "[")) { + $indent++; + if ($result != "" && $result[strlen($result)-1] == "\n") { + $result .= $prefix; + } + $result .= "$token\n"; + } elseif (!$inLiteral && ($token == "}" || $token == "]")) { + $indent--; + $prefix = str_repeat($ind, $indent); + $result .= "\n$prefix$token"; + } elseif (!$inLiteral && $token == ",") { + $result .= "$token\n"; + } else { + $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/src/Server/Cache.php b/src/Server/Cache.php new file mode 100644 index 000000000..eb254d812 --- /dev/null +++ b/src/Server/Cache.php @@ -0,0 +1,97 @@ +getServiceMap()->toJson()); + ErrorHandler::stop(); + + if (0 === $test) { + return false; + } + + return true; + } + + /** + * Retrieve a cached SMD + * + * On success, returns the cached SMD (a JSON string); an failure, returns + * boolean false. + * + * @param string $filename + * @return string|false + */ + public static function getSmd($filename) + { + if (!is_string($filename) + || !file_exists($filename) + || !is_readable($filename)) + { + return false; + } + + ErrorHandler::start(); + $smd = file_get_contents($filename); + ErrorHandler::stop(); + + if (false === $smd) { + return false; + } + + return $smd; + } + + /** + * Delete a file containing a cached SMD + * + * @param string $filename + * @return bool + */ + public static function deleteSmd($filename) + { + if (is_string($filename) && file_exists($filename)) { + unlink($filename); + return true; + } + + return false; + } +} diff --git a/src/Server/Client.php b/src/Server/Client.php new file mode 100644 index 000000000..22fb57c0f --- /dev/null +++ b/src/Server/Client.php @@ -0,0 +1,199 @@ +httpClient = $httpClient ?: new HttpClient(); + $this->serverAddress = $server; + } + + /** + * Sets the HTTP client object to use for connecting the JSON-RPC server. + * + * @param HttpClient $httpClient New HTTP client to use. + * @return Client Self instance. + */ + public function setHttpClient(HttpClient $httpClient) + { + $this->httpClient = $httpClient; + return $this; + } + + /** + * Gets the HTTP client object. + * + * @return HttpClient HTTP client. + */ + public function getHttpClient() + { + return $this->httpClient; + } + + /** + * The request of the last method call. + * + * @return Request Request instance. + */ + public function getLastRequest() + { + return $this->lastRequest; + } + + /** + * The response received from the last method call. + * + * @return Response Response instance. + */ + public function getLastResponse() + { + return $this->lastResponse; + } + + /** + * Perform an JSOC-RPC request and return a response. + * + * @param Request $request Request. + * @return Response Response. + * @throws Exception\HttpException When HTTP communication fails. + */ + public function doRequest($request) + { + $this->lastRequest = $request; + + $httpRequest = $this->httpClient->getRequest(); + if ($httpRequest->getUriString() === null) { + $this->httpClient->setUri($this->serverAddress); + } + + $headers = $httpRequest->getHeaders(); + $headers->addHeaders(array( + 'Content-Type' => 'application/json', + 'Accept' => 'application/json', + )); + + if (!$headers->get('User-Agent')) { + $headers->addHeaderLine('User-Agent', 'Zend_Json_Server_Client'); + } + + $this->httpClient->setRawBody($request->__toString()); + $this->httpClient->setMethod('POST'); + $httpResponse = $this->httpClient->send(); + + if (!$httpResponse->isSuccess()) { + throw new Exception\HttpException( + $httpResponse->getReasonPhrase(), + $httpResponse->getStatusCode() + ); + } + + $response = new Response(); + + $this->lastResponse = $response; + + // import all response data form JSON HTTP response + $response->loadJson($httpResponse->getBody()); + + return $response; + } + + /** + * Send an JSON-RPC request to the service (for a specific method). + * + * @param string $method Name of the method we want to call. + * @param array $params Array of parameters for the method. + * @return mixed Method call results. + * @throws Exception\ErrorException When remote call fails. + */ + public function call($method, $params = array()) + { + $request = $this->createRequest($method, $params); + + $response = $this->doRequest($request); + + if ($response->isError()) { + $error = $response->getError(); + throw new Exception\ErrorException( + $error->getMessage(), + $error->getCode() + ); + } + + return $response->getResult(); + } + + /** + * Create request object. + * + * @param string $method Method to call. + * @param array $params List of arguments. + * @return Request Created request. + */ + protected function createRequest($method, array $params) + { + $request = new Request(); + $request->setMethod($method) + ->setParams($params) + ->setId(++$this->id); + return $request; + } +} diff --git a/src/Server/Error.php b/src/Server/Error.php new file mode 100644 index 000000000..0ccda9827 --- /dev/null +++ b/src/Server/Error.php @@ -0,0 +1,184 @@ +setMessage($message) + ->setCode($code) + ->setData($data); + } + + /** + * Set error code + * + * @param int $code + * @return \Zend\Json\Server\Error + */ + public function setCode($code) + { + if (!is_scalar($code)) { + return $this; + } + + $code = (int) $code; + if (in_array($code, $this->allowedCodes)) { + $this->code = $code; + } elseif (in_array($code, range(-32099, -32000))) { + $this->code = $code; + } + + return $this; + } + + /** + * Get error code + * + * @return int|null + */ + public function getCode() + { + return $this->code; + } + + /** + * Set error message + * + * @param string $message + * @return \Zend\Json\Server\Error + */ + public function setMessage($message) + { + if (!is_scalar($message)) { + return $this; + } + + $this->message = (string) $message; + return $this; + } + + /** + * Get error message + * + * @return string + */ + public function getMessage() + { + return $this->message; + } + + /** + * Set error data + * + * @param mixed $data + * @return \Zend\Json\Server\Error + */ + public function setData($data) + { + $this->data = $data; + return $this; + } + + /** + * Get error data + * + * @return mixed + */ + public function getData() + { + return $this->data; + } + + /** + * Cast error to array + * + * @return array + */ + public function toArray() + { + return array( + 'code' => $this->getCode(), + 'message' => $this->getMessage(), + 'data' => $this->getData(), + ); + } + + /** + * Cast error to JSON + * + * @return string + */ + public function toJson() + { + return \Zend\Json\Json::encode($this->toArray()); + } + + /** + * Cast to string (JSON) + * + * @return string + */ + public function __toString() + { + return $this->toJson(); + } +} diff --git a/src/Server/Exception/ErrorException.php b/src/Server/Exception/ErrorException.php new file mode 100644 index 000000000..1d5cf2288 --- /dev/null +++ b/src/Server/Exception/ErrorException.php @@ -0,0 +1,24 @@ + $value) { + $method = 'set' . ucfirst($key); + if (in_array($method, $methods)) { + $this->$method($value); + } elseif ($key == 'jsonrpc') { + $this->setVersion($value); + } + } + return $this; + } + + /** + * Add a parameter to the request + * + * @param mixed $value + * @param string $key + * @return \Zend\Json\Server\Request + */ + public function addParam($value, $key = null) + { + if ((null === $key) || !is_string($key)) { + $index = count($this->params); + $this->params[$index] = $value; + } else { + $this->params[$key] = $value; + } + + return $this; + } + + /** + * Add many params + * + * @param array $params + * @return \Zend\Json\Server\Request + */ + public function addParams(array $params) + { + foreach ($params as $key => $value) { + $this->addParam($value, $key); + } + return $this; + } + + /** + * Overwrite params + * + * @param array $params + * @return \Zend\Json\Server\Request + */ + public function setParams(array $params) + { + $this->params = array(); + return $this->addParams($params); + } + + /** + * Retrieve param by index or key + * + * @param int|string $index + * @return mixed|null Null when not found + */ + public function getParam($index) + { + if (array_key_exists($index, $this->params)) { + return $this->params[$index]; + } + + return null; + } + + /** + * Retrieve parameters + * + * @return array + */ + public function getParams() + { + return $this->params; + } + + /** + * Set request method + * + * @param string $name + * @return \Zend\Json\Server\Request + */ + public function setMethod($name) + { + if (!preg_match($this->methodRegex, $name)) { + $this->isMethodError = true; + } else { + $this->method = $name; + } + return $this; + } + + /** + * Get request method name + * + * @return string + */ + public function getMethod() + { + return $this->method; + } + + /** + * Was a bad method provided? + * + * @return bool + */ + public function isMethodError() + { + return $this->isMethodError; + } + + /** + * Set request identifier + * + * @param mixed $name + * @return \Zend\Json\Server\Request + */ + public function setId($name) + { + $this->id = (string) $name; + return $this; + } + + /** + * Retrieve request identifier + * + * @return mixed + */ + public function getId() + { + return $this->id; + } + + /** + * Set JSON-RPC version + * + * @param string $version + * @return \Zend\Json\Server\Request + */ + public function setVersion($version) + { + if ('2.0' == $version) { + $this->version = '2.0'; + } else { + $this->version = '1.0'; + } + return $this; + } + + /** + * Retrieve JSON-RPC version + * + * @return string + */ + public function getVersion() + { + return $this->version; + } + + /** + * Set request state based on JSON + * + * @param string $json + * @return void + */ + public function loadJson($json) + { + $options = Json\Json::decode($json, Json\Json::TYPE_ARRAY); + $this->setOptions($options); + } + + /** + * Cast request to JSON + * + * @return string + */ + public function toJson() + { + $jsonArray = array( + 'method' => $this->getMethod() + ); + if (null !== ($id = $this->getId())) { + $jsonArray['id'] = $id; + } + $params = $this->getParams(); + if (!empty($params)) { + $jsonArray['params'] = $params; + } + if ('2.0' == $this->getVersion()) { + $jsonArray['jsonrpc'] = '2.0'; + } + + return Json\Json::encode($jsonArray); + } + + /** + * Cast request to string (JSON) + * + * @return string + */ + public function __toString() + { + return $this->toJson(); + } +} diff --git a/src/Server/Request/Http.php b/src/Server/Request/Http.php new file mode 100644 index 000000000..78a053a73 --- /dev/null +++ b/src/Server/Request/Http.php @@ -0,0 +1,51 @@ +rawJson = $json; + if (!empty($json)) { + $this->loadJson($json); + } + } + + /** + * Get JSON from raw POST body + * + * @return string + */ + public function getRawJson() + { + return $this->rawJson; + } +} diff --git a/src/Server/Response.php b/src/Server/Response.php new file mode 100644 index 000000000..8365f626e --- /dev/null +++ b/src/Server/Response.php @@ -0,0 +1,279 @@ + $value) { + $method = 'set' . ucfirst($key); + if (in_array($method, $methods)) { + $this->$method($value); + } elseif ($key == 'jsonrpc') { + $this->setVersion($value); + } + } + return $this; + } + + /** + * Set response state based on JSON + * + * @param string $json + * @return void + */ + public function loadJson($json) + { + $options = Json::decode($json, Json::TYPE_ARRAY); + $this->setOptions($options); + } + + /** + * Set result + * + * @param mixed $value + * @return Response + */ + public function setResult($value) + { + $this->result = $value; + return $this; + } + + /** + * Get result + * + * @return mixed + */ + public function getResult() + { + return $this->result; + } + + // RPC error, if response results in fault + /** + * Set result error + * + * @param Error $error + * @return Response + */ + public function setError(Error $error) + { + $this->error = $error; + return $this; + } + + /** + * Get response error + * + * @return null|Error + */ + public function getError() + { + return $this->error; + } + + /** + * Is the response an error? + * + * @return bool + */ + public function isError() + { + return $this->getError() instanceof Error; + } + + /** + * Set request ID + * + * @param mixed $name + * @return Response + */ + public function setId($name) + { + $this->id = $name; + return $this; + } + + /** + * Get request ID + * + * @return mixed + */ + public function getId() + { + return $this->id; + } + + /** + * Set JSON-RPC version + * + * @param string $version + * @return Response + */ + public function setVersion($version) + { + $version = (string) $version; + if ('2.0' == $version) { + $this->version = '2.0'; + } else { + $this->version = null; + } + + return $this; + } + + /** + * Retrieve JSON-RPC version + * + * @return string + */ + public function getVersion() + { + return $this->version; + } + + /** + * Cast to JSON + * + * @return string + */ + public function toJson() + { + if ($this->isError()) { + $response = array( + 'error' => $this->getError()->toArray(), + 'id' => $this->getId(), + ); + } else { + $response = array( + 'result' => $this->getResult(), + 'id' => $this->getId(), + ); + } + + if (null !== ($version = $this->getVersion())) { + $response['jsonrpc'] = $version; + } + + return \Zend\Json\Json::encode($response); + } + + /** + * Retrieve args + * + * @return mixed + */ + public function getArgs() + { + return $this->args; + } + + /** + * Set args + * + * @param mixed $args + * @return self + */ + public function setArgs($args) + { + $this->args = $args; + return $this; + } + + /** + * Set service map object + * + * @param Smd $serviceMap + * @return Response + */ + public function setServiceMap($serviceMap) + { + $this->serviceMap = $serviceMap; + return $this; + } + + /** + * Retrieve service map + * + * @return Smd|null + */ + public function getServiceMap() + { + return $this->serviceMap; + } + + /** + * Cast to string (JSON) + * + * @return string + */ + public function __toString() + { + return $this->toJson(); + } +} diff --git a/src/Server/Response/Http.php b/src/Server/Response/Http.php new file mode 100644 index 000000000..f712fa46c --- /dev/null +++ b/src/Server/Response/Http.php @@ -0,0 +1,67 @@ +sendHeaders(); + if (!$this->isError() && null === $this->getId()) { + return ''; + } + + return parent::toJson(); + } + + /** + * Send headers + * + * If headers are already sent, do nothing. If null ID, send HTTP 204 + * header. Otherwise, send content type header based on content type of + * service map. + * + * @return void + */ + public function sendHeaders() + { + if (headers_sent()) { + return; + } + + if (!$this->isError() && (null === $this->getId())) { + header('HTTP/1.1 204 No Content'); + return; + } + + if (null === ($smd = $this->getServiceMap())) { + return; + } + + $contentType = $smd->getContentType(); + if (!empty($contentType)) { + header('Content-Type: ' . $contentType); + } + } +} diff --git a/src/Server/Server.php b/src/Server/Server.php new file mode 100644 index 000000000..ec4516e0f --- /dev/null +++ b/src/Server/Server.php @@ -0,0 +1,545 @@ + count($function)))) { + throw new Exception\InvalidArgumentException('Unable to attach function; invalid'); + } + + if (!is_callable($function)) { + throw new Exception\InvalidArgumentException('Unable to attach function; does not exist'); + } + + $argv = null; + if (2 < func_num_args()) { + $argv = func_get_args(); + $argv = array_slice($argv, 2); + } + + if (is_string($function)) { + $method = Reflection::reflectFunction($function, $argv, $namespace); + } else { + $class = array_shift($function); + $action = array_shift($function); + $reflection = Reflection::reflectClass($class, $argv, $namespace); + $methods = $reflection->getMethods(); + $found = false; + foreach ($methods as $method) { + if ($action == $method->getName()) { + $found = true; + break; + } + } + if (!$found) { + $this->fault('Method not found', Error::ERROR_INVALID_METHOD); + return $this; + } + } + + $definition = $this->_buildSignature($method); + $this->_addMethodServiceMap($definition); + + return $this; + } + + /** + * Register a class with the server + * + * @param string $class + * @param string $namespace Ignored + * @param mixed $argv Ignored + * @return Server + */ + public function setClass($class, $namespace = '', $argv = null) + { + if (2 < func_num_args()) { + $argv = func_get_args(); + $argv = array_slice($argv, 2); + } + + $reflection = Reflection::reflectClass($class, $argv, $namespace); + + foreach ($reflection->getMethods() as $method) { + $definition = $this->_buildSignature($method, $class); + $this->_addMethodServiceMap($definition); + } + return $this; + } + + /** + * Indicate fault response + * + * @param string $fault + * @param int $code + * @return false + */ + public function fault($fault = null, $code = 404, $data = null) + { + $error = new Error($fault, $code, $data); + $this->getResponse()->setError($error); + return $error; + } + + /** + * Handle request + * + * @param Request $request + * @return null|Response + */ + public function handle($request = false) + { + if ((false !== $request) && (!$request instanceof Request)) { + throw new Exception\InvalidArgumentException('Invalid request type provided; cannot handle'); + } elseif ($request) { + $this->setRequest($request); + } + + // Handle request + $this->_handle(); + + // Get response + $response = $this->_getReadyResponse(); + + // Emit response? + if (!$this->returnResponse) { + echo $response; + return; + } + + // or return it? + return $response; + } + + /** + * Load function definitions + * + * @param array|Definition $definition + * @return void + */ + public function loadFunctions($definition) + { + if (!is_array($definition) && (!$definition instanceof Definition)) { + throw new Exception\InvalidArgumentException('Invalid definition provided to loadFunctions()'); + } + + foreach ($definition as $key => $method) { + $this->table->addMethod($method, $key); + $this->_addMethodServiceMap($method); + } + } + + public function setPersistence($mode) + { + } + + /** + * Set request object + * + * @param Request $request + * @return Server + */ + public function setRequest(Request $request) + { + $this->request = $request; + return $this; + } + + /** + * Get JSON-RPC request object + * + * @return Request + */ + public function getRequest() + { + if (null === ($request = $this->request)) { + $this->setRequest(new Request\Http()); + } + return $this->request; + } + + /** + * Set response object + * + * @param Response $response + * @return Server + */ + public function setResponse(Response $response) + { + $this->response = $response; + return $this; + } + + /** + * Get response object + * + * @return Response + */ + public function getResponse() + { + if (null === ($response = $this->response)) { + $this->setResponse(new Response\Http()); + } + return $this->response; + } + + /** + * Set return response flag + * + * If true, {@link handle()} will return the response instead of + * automatically sending it back to the requesting client. + * + * The response is always available via {@link getResponse()}. + * + * @param boolean $flag + * @return Server + */ + public function setReturnResponse($flag = true) + { + $this->returnResponse = ($flag) ? true : false; + return $this; + } + + /** + * Retrieve return response flag + * + * @return boolean + */ + public function getReturnResponse() + { + return $this->returnResponse; + } + + // overloading for SMD metadata + /** + * Overload to accessors of SMD object + * + * @param string $method + * @param array $args + * @return mixed + */ + public function __call($method, $args) + { + if (preg_match('/^(set|get)/', $method, $matches)) { + if (in_array($method, $this->_getSmdMethods())) { + if ('set' == $matches[1]) { + $value = array_shift($args); + $this->getServiceMap()->$method($value); + return $this; + } else { + return $this->getServiceMap()->$method(); + } + } + } + return null; + } + + /** + * Retrieve SMD object + * + * @return Smd + */ + public function getServiceMap() + { + if (null === $this->serviceMap) { + $this->serviceMap = new Smd(); + } + return $this->serviceMap; + } + + /** + * Add service method to service map + * + * @param Method\Definition $method + * @return void + */ + protected function _addMethodServiceMap(Method\Definition $method) + { + $serviceInfo = array( + 'name' => $method->getName(), + 'return' => $this->_getReturnType($method), + ); + $params = $this->_getParams($method); + $serviceInfo['params'] = $params; + $serviceMap = $this->getServiceMap(); + if (false !== $serviceMap->getService($serviceInfo['name'])) { + $serviceMap->removeService($serviceInfo['name']); + } + $serviceMap->addService($serviceInfo); + } + + /** + * Translate PHP type to JSON type + * + * @param string $type + * @return string + */ + protected function _fixType($type) + { + return $type; + } + + /** + * Get default params from signature + * + * @param array $args + * @param array $params + * @return array + */ + protected function _getDefaultParams(array $args, array $params) + { + $defaultParams = array_slice($params, count($args)); + foreach ($defaultParams as $param) { + $value = null; + if (array_key_exists('default', $param)) { + $value = $param['default']; + } + array_push($args, $value); + } + return $args; + } + + /** + * Get method param type + * + * @param Method\Definition $method + * @return string|array + */ + protected function _getParams(Method\Definition $method) + { + $params = array(); + foreach ($method->getPrototypes() as $prototype) { + foreach ($prototype->getParameterObjects() as $key => $parameter) { + if (!isset($params[$key])) { + $params[$key] = array( + 'type' => $parameter->getType(), + 'name' => $parameter->getName(), + 'optional' => $parameter->isOptional(), + ); + if (null !== ($default = $parameter->getDefaultValue())) { + $params[$key]['default'] = $default; + } + $description = $parameter->getDescription(); + if (!empty($description)) { + $params[$key]['description'] = $description; + } + continue; + } + $newType = $parameter->getType(); + if (!is_array($params[$key]['type'])) { + if ($params[$key]['type'] == $newType) { + continue; + } + $params[$key]['type'] = (array) $params[$key]['type']; + } elseif (in_array($newType, $params[$key]['type'])) { + continue; + } + array_push($params[$key]['type'], $parameter->getType()); + } + } + return $params; + } + + /** + * Set response state + * + * @return Response + */ + protected function _getReadyResponse() + { + $request = $this->getRequest(); + $response = $this->getResponse(); + + $response->setServiceMap($this->getServiceMap()); + if (null !== ($id = $request->getId())) { + $response->setId($id); + } + if (null !== ($version = $request->getVersion())) { + $response->setVersion($version); + } + + return $response; + } + + /** + * Get method return type + * + * @param Method\Definition $method + * @return string|array + */ + protected function _getReturnType(Method\Definition $method) + { + $return = array(); + foreach ($method->getPrototypes() as $prototype) { + $return[] = $prototype->getReturnType(); + } + if (1 == count($return)) { + return $return[0]; + } + return $return; + } + + /** + * Retrieve list of allowed SMD methods for proxying + * + * @return array + */ + protected function _getSmdMethods() + { + if (null === $this->smdMethods) { + $this->smdMethods = array(); + $methods = get_class_methods('Zend\\Json\\Server\\Smd'); + foreach ($methods as $key => $method) { + if (!preg_match('/^(set|get)/', $method)) { + continue; + } + if (strstr($method, 'Service')) { + continue; + } + $this->smdMethods[] = $method; + } + } + return $this->smdMethods; + } + + /** + * Internal method for handling request + * + * @return void + */ + protected function _handle() + { + $request = $this->getRequest(); + + if (!$request->isMethodError() && (null === $request->getMethod())) { + return $this->fault('Invalid Request', Error::ERROR_INVALID_REQUEST); + } + + if ($request->isMethodError()) { + return $this->fault('Invalid Request', Error::ERROR_INVALID_REQUEST); + } + + $method = $request->getMethod(); + if (!$this->table->hasMethod($method)) { + return $this->fault('Method not found', Error::ERROR_INVALID_METHOD); + } + + $params = $request->getParams(); + $invocable = $this->table->getMethod($method); + $serviceMap = $this->getServiceMap(); + $service = $serviceMap->getService($method); + $serviceParams = $service->getParams(); + + if (count($params) < count($serviceParams)) { + $params = $this->_getDefaultParams($params, $serviceParams); + } + + //Make sure named parameters are passed in correct order + if (is_string( key( $params ) )) { + + $callback = $invocable->getCallback(); + if ('function' == $callback->getType()) { + $reflection = new ReflectionFunction( $callback->getFunction() ); + } else { + + $reflection = new ReflectionMethod( + $callback->getClass(), + $callback->getMethod() + ); + } + + $orderedParams = array(); + foreach ($reflection->getParameters() as $refParam) { + if (isset( $params[ $refParam->getName() ] )) { + $orderedParams[ $refParam->getName() ] = $params[ $refParam->getName() ]; + } elseif ($refParam->isOptional()) { + $orderedParams[ $refParam->getName() ] = null; + } else { + return $this->fault('Invalid params', Error::ERROR_INVALID_PARAMS); + } + } + $params = $orderedParams; + } + + try { + $result = $this->_dispatch($invocable, $params); + } catch (\Exception $e) { + return $this->fault($e->getMessage(), $e->getCode(), $e); + } + + $this->getResponse()->setResult($result); + } +} diff --git a/src/Server/Smd.php b/src/Server/Smd.php new file mode 100644 index 000000000..f1b9981bf --- /dev/null +++ b/src/Server/Smd.php @@ -0,0 +1,461 @@ + $value) { + $method = 'set' . ucfirst($key); + if (method_exists($this, $method)) { + $this->$method($value); + } + } + return $this; + } + + /** + * Set transport + * + * @param string $transport + * @return \Zend\Json\Server\Smd + */ + public function setTransport($transport) + { + if (!in_array($transport, $this->transportTypes)) { + throw new InvalidArgumentException("Invalid transport '{$transport}' specified"); + } + $this->transport = $transport; + return $this; + } + + /** + * Get transport + * + * @return string + */ + public function getTransport() + { + return $this->transport; + } + + /** + * Set envelope + * + * @param string $envelopeType + * @return Smd + */ + public function setEnvelope($envelopeType) + { + if (!in_array($envelopeType, $this->envelopeTypes)) { + throw new InvalidArgumentException("Invalid envelope type '{$envelopeType}'"); + } + $this->envelope = $envelopeType; + return $this; + } + + /** + * Retrieve envelope + * + * @return string + */ + public function getEnvelope() + { + return $this->envelope; + } + + // Content-Type of response; default to application/json + /** + * Set content type + * + * @param string $type + * @return \Zend\Json\Server\Smd + */ + public function setContentType($type) + { + if (!preg_match($this->contentTypeRegex, $type)) { + throw new InvalidArgumentException("Invalid content type '{$type}' specified"); + } + $this->contentType = $type; + return $this; + } + + /** + * Retrieve content type + * + * @return string + */ + public function getContentType() + { + return $this->contentType; + } + + /** + * Set service target + * + * @param string $target + * @return Smd + */ + public function setTarget($target) + { + $this->target = (string) $target; + return $this; + } + + /** + * Retrieve service target + * + * @return string + */ + public function getTarget() + { + return $this->target; + } + + /** + * Set service ID + * + * @param string $Id + * @return Smd + */ + public function setId($id) + { + $this->id = (string) $id; + return $this->id; + } + + /** + * Get service id + * + * @return string + */ + public function getId() + { + return $this->id; + } + + /** + * Set service description + * + * @param string $description + * @return Smd + */ + public function setDescription($description) + { + $this->description = (string) $description; + return $this->description; + } + + /** + * Get service description + * + * @return string + */ + public function getDescription() + { + return $this->description; + } + + /** + * Indicate whether or not to generate Dojo-compatible SMD + * + * @param bool $flag + * @return Smd + */ + public function setDojoCompatible($flag) + { + $this->dojoCompatible = (bool) $flag; + return $this; + } + + /** + * Is this a Dojo compatible SMD? + * + * @return bool + */ + public function isDojoCompatible() + { + return $this->dojoCompatible; + } + + /** + * Add Service + * + * @param Smd\Service|array $service + * @return Smd + */ + public function addService($service) + { + if ($service instanceof Smd\Service) { + $name = $service->getName(); + } elseif (is_array($service)) { + $service = new Smd\Service($service); + $name = $service->getName(); + } else { + throw new InvalidArgumentException('Invalid service passed to addService()'); + } + + if (array_key_exists($name, $this->services)) { + throw new RuntimeException('Attempt to register a service already registered detected'); + } + $this->services[$name] = $service; + return $this; + } + + /** + * Add many services + * + * @param array $services + * @return Smd + */ + public function addServices(array $services) + { + foreach ($services as $service) { + $this->addService($service); + } + return $this; + } + + /** + * Overwrite existing services with new ones + * + * @param array $services + * @return Smd + */ + public function setServices(array $services) + { + $this->services = array(); + return $this->addServices($services); + } + + /** + * Get service object + * + * @param string $name + * @return boolean|Smd\Service + */ + public function getService($name) + { + if (array_key_exists($name, $this->services)) { + return $this->services[$name]; + } + return false; + } + + /** + * Return services + * + * @return array + */ + public function getServices() + { + return $this->services; + } + + /** + * Remove service + * + * @param string $name + * @return boolean + */ + public function removeService($name) + { + if (array_key_exists($name, $this->services)) { + unset($this->services[$name]); + return true; + } + return false; + } + + /** + * Cast to array + * + * @return array + */ + public function toArray() + { + if ($this->isDojoCompatible()) { + return $this->toDojoArray(); + } + + $transport = $this->getTransport(); + $envelope = $this->getEnvelope(); + $contentType = $this->getContentType(); + $SMDVersion = self::SMD_VERSION; + $service = compact('transport', 'envelope', 'contentType', 'SMDVersion'); + + if (null !== ($target = $this->getTarget())) { + $service['target'] = $target; + } + if (null !== ($id = $this->getId())) { + $service['id'] = $id; + } + + $services = $this->getServices(); + if (!empty($services)) { + $service['services'] = array(); + foreach ($services as $name => $svc) { + $svc->setEnvelope($envelope); + $service['services'][$name] = $svc->toArray(); + } + $service['methods'] = $service['services']; + } + + return $service; + } + + /** + * Export to DOJO-compatible SMD array + * + * @return array + */ + public function toDojoArray() + { + $SMDVersion = '.1'; + $serviceType = 'JSON-RPC'; + $service = compact('SMDVersion', 'serviceType'); + + $target = $this->getTarget(); + + $services = $this->getServices(); + if (!empty($services)) { + $service['methods'] = array(); + foreach ($services as $name => $svc) { + $method = array( + 'name' => $name, + 'serviceURL' => $target, + ); + $params = array(); + foreach ($svc->getParams() as $param) { + $paramName = array_key_exists('name', $param) ? $param['name'] : $param['type']; + $params[] = array( + 'name' => $paramName, + 'type' => $param['type'], + ); + } + if (!empty($params)) { + $method['parameters'] = $params; + } + $service['methods'][] = $method; + } + } + + return $service; + } + + /** + * Cast to JSON + * + * @return string + */ + public function toJson() + { + return \Zend\Json\Json::encode($this->toArray()); + } + + /** + * Cast to string (JSON) + * + * @return string + */ + public function __toString() + { + return $this->toJson(); + } +} diff --git a/src/Server/Smd/Service.php b/src/Server/Smd/Service.php new file mode 100644 index 000000000..010942425 --- /dev/null +++ b/src/Server/Smd/Service.php @@ -0,0 +1,452 @@ + 'is_string', + 'optional' => 'is_bool', + 'default' => null, + 'description' => 'is_string', + ); + + /** + * Service params + * @var array + */ + protected $params = array(); + + /** + * Mapping of parameter types to JSON-RPC types + * @var array + */ + protected $paramMap = array( + 'any' => 'any', + 'arr' => 'array', + 'array' => 'array', + 'assoc' => 'object', + 'bool' => 'boolean', + 'boolean' => 'boolean', + 'dbl' => 'float', + 'double' => 'float', + 'false' => 'boolean', + 'float' => 'float', + 'hash' => 'object', + 'integer' => 'integer', + 'int' => 'integer', + 'mixed' => 'any', + 'nil' => 'null', + 'null' => 'null', + 'object' => 'object', + 'string' => 'string', + 'str' => 'string', + 'struct' => 'object', + 'true' => 'boolean', + 'void' => 'null', + ); + + /** + * Allowed transport types + * @var array + */ + protected $transportTypes = array( + 'POST', + ); + + /** + * Constructor + * + * @param string|array $spec + * @throws Zend\Json\Server\Exception\InvalidArgumentException if no name provided + */ + public function __construct($spec) + { + if (is_string($spec)) { + $this->setName($spec); + } elseif (is_array($spec)) { + $this->setOptions($spec); + } + + if (null == $this->getName()) { + throw new InvalidArgumentException('SMD service description requires a name; none provided'); + } + } + + /** + * Set object state + * + * @param array $options + * @return Zend\Json\Server\Smd\Service + */ + public function setOptions(array $options) + { + $methods = get_class_methods($this); + foreach ($options as $key => $value) { + if ('options' == strtolower($key)) { + continue; + } + $method = 'set' . ucfirst($key); + if (in_array($method, $methods)) { + $this->$method($value); + } + } + return $this; + } + + /** + * Set service name + * + * @param string $name + * @return Zend\Json\Server\Smd\Service + * @throws Zend\Json\Server\Exception\InvalidArgumentException + */ + public function setName($name) + { + $name = (string) $name; + if (!preg_match($this->nameRegex, $name)) { + throw new InvalidArgumentException("Invalid name '{$name} provided for service; must follow PHP method naming conventions"); + } + $this->name = $name; + return $this; + } + + /** + * Retrieve name + * + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * Set Transport + * + * Currently limited to POST + * + * @param string $transport + * @return Zend\Json\Server\Smd\Service + */ + public function setTransport($transport) + { + if (!in_array($transport, $this->transportTypes)) { + throw new InvalidArgumentException("Invalid transport '{$transport}'; please select one of (" . implode(', ', $this->transportTypes) . ')'); + } + + $this->transport = $transport; + return $this; + } + + /** + * Get transport + * + * @return string + */ + public function getTransport() + { + return $this->transport; + } + + /** + * Set service target + * + * @param string $target + * @return Zend\Json\Server\Smd\Service + */ + public function setTarget($target) + { + $this->target = (string) $target; + return $this; + } + + /** + * Get service target + * + * @return string + */ + public function getTarget() + { + return $this->target; + } + + /** + * Set envelope type + * + * @param string $envelopeType + * @return Zend\Json\Server\Smd\Service + */ + public function setEnvelope($envelopeType) + { + if (!in_array($envelopeType, $this->envelopeTypes)) { + throw new InvalidArgumentException("Invalid envelope type '{$envelopeType}'; please specify one of (" . implode(', ', $this->envelopeTypes) . ')'); + } + + $this->envelope = $envelopeType; + return $this; + } + + /** + * Get envelope type + * + * @return string + */ + public function getEnvelope() + { + return $this->envelope; + } + + /** + * Add a parameter to the service + * + * @param string|array $type + * @param array $options + * @param int|null $order + * @return Zend\Json\Server\Smd\Service + */ + public function addParam($type, array $options = array(), $order = null) + { + if (is_string($type)) { + $type = $this->_validateParamType($type); + } elseif (is_array($type)) { + foreach ($type as $key => $paramType) { + $type[$key] = $this->_validateParamType($paramType); + } + } else { + throw new InvalidArgumentException('Invalid param type provided'); + } + + $paramOptions = array( + 'type' => $type, + ); + foreach ($options as $key => $value) { + if (in_array($key, array_keys($this->paramOptionTypes))) { + if (null !== ($callback = $this->paramOptionTypes[$key])) { + if (!$callback($value)) { + continue; + } + } + $paramOptions[$key] = $value; + } + } + + $this->params[] = array( + 'param' => $paramOptions, + 'order' => $order, + ); + + return $this; + } + + /** + * Add params + * + * Each param should be an array, and should include the key 'type'. + * + * @param array $params + * @return Zend\Json\Server\Smd\Service + */ + public function addParams(array $params) + { + ksort($params); + foreach ($params as $options) { + if (!is_array($options)) { + continue; + } + if (!array_key_exists('type', $options)) { + continue; + } + $type = $options['type']; + $order = (array_key_exists('order', $options)) ? $options['order'] : null; + $this->addParam($type, $options, $order); + } + return $this; + } + + /** + * Overwrite all parameters + * + * @param array $params + * @return Zend\Json\Server\Smd\Service + */ + public function setParams(array $params) + { + $this->params = array(); + return $this->addParams($params); + } + + /** + * Get all parameters + * + * Returns all params in specified order. + * + * @return array + */ + public function getParams() + { + $params = array(); + $index = 0; + foreach ($this->params as $param) { + if (null === $param['order']) { + if (array_search($index, array_keys($params), true)) { + ++$index; + } + $params[$index] = $param['param']; + ++$index; + } else { + $params[$param['order']] = $param['param']; + } + } + ksort($params); + return $params; + } + + /** + * Set return type + * + * @param string|array $type + * @return Zend\Json\Server\Smd\Service + */ + public function setReturn($type) + { + if (is_string($type)) { + $type = $this->_validateParamType($type, true); + } elseif (is_array($type)) { + foreach ($type as $key => $returnType) { + $type[$key] = $this->_validateParamType($returnType, true); + } + } else { + throw new InvalidArgumentException("Invalid param type provided ('" . gettype($type) . "')"); + } + $this->return = $type; + return $this; + } + + /** + * Get return type + * + * @return string|array + */ + public function getReturn() + { + return $this->return; + } + + /** + * Cast service description to array + * + * @return array + */ + public function toArray() + { + $name = $this->getName(); + $envelope = $this->getEnvelope(); + $target = $this->getTarget(); + $transport = $this->getTransport(); + $parameters = $this->getParams(); + $returns = $this->getReturn(); + + if (empty($target)) { + return compact('envelope', 'transport', 'parameters', 'returns'); + } + + return $paramInfo = compact('envelope', 'target', 'transport', 'parameters', 'returns'); + } + + /** + * Return JSON encoding of service + * + * @return string + */ + public function toJson() + { + $service = array($this->getName() => $this->toArray()); + return \Zend\Json\Json::encode($service); + } + + /** + * Cast to string + * + * @return string + */ + public function __toString() + { + return $this->toJson(); + } + + /** + * Validate parameter type + * + * @param string $type + * @param boolean $isReturn + * @return string + * @throws InvalidArgumentException + */ + protected function _validateParamType($type, $isReturn = false) + { + if (!is_string($type)) { + throw new InvalidArgumentException("Invalid param type provided ('{$type}')"); + } + + if (!array_key_exists($type, $this->paramMap)) { + $type = 'object'; + } + + $paramType = $this->paramMap[$type]; + if (!$isReturn && ('null' == $paramType)) { + throw new InvalidArgumentException("Invalid param type provided ('{$type}')"); + } + + return $paramType; + } +} diff --git a/test/JsonTest.php b/test/JsonTest.php new file mode 100644 index 000000000..072914ba8 --- /dev/null +++ b/test/JsonTest.php @@ -0,0 +1,1033 @@ +_originalUseBuiltinEncoderDecoderValue = Json\Json::$useBuiltinEncoderDecoder; + } + + public function tearDown() + { + Json\Json::$useBuiltinEncoderDecoder = $this->_originalUseBuiltinEncoderDecoderValue; + } + + public function testJSONWithPhpJSONExtension() + { + if (!extension_loaded('json')) { + $this->markTestSkipped('JSON extension is not loaded'); + } + Json\Json::$useBuiltinEncoderDecoder = false; + $this->_testJSON(array('string', 327, true, null)); + } + + public function testJSONWithBuiltins() + { + Json\Json::$useBuiltinEncoderDecoder = true; + $this->_testJSON(array('string', 327, true, null)); + } + + /** + * Test encoding and decoding in a single step + * @param array $values array of values to test against encode/decode + */ + protected function _testJSON($values) + { + $encoded = Json\Json::encode($values); + $this->assertEquals($values, Json\Json::decode($encoded)); + } + + /** + * test null encoding/decoding + */ + public function testNull() + { + $this->_testEncodeDecode(array(null)); + } + + + /** + * test boolean encoding/decoding + */ + public function testBoolean() + { + $this->assertTrue(Json\Decoder::decode(Json\Encoder::encode(true))); + $this->assertFalse(Json\Decoder::decode(Json\Encoder::encode(false))); + } + + + /** + * test integer encoding/decoding + */ + public function testInteger() + { + $this->_testEncodeDecode(array(-2)); + $this->_testEncodeDecode(array(-1)); + + $zero = Json\Decoder::decode(Json\Encoder::encode(0)); + $this->assertEquals(0, $zero, 'Failed 0 integer test. Encoded: ' . serialize(Json\Encoder::encode(0))); + } + + + /** + * test float encoding/decoding + */ + public function testFloat() + { + $this->_testEncodeDecode(array(-2.1, 1.2)); + } + + /** + * test string encoding/decoding + */ + public function testString() + { + $this->_testEncodeDecode(array('string')); + $this->assertEquals('', Json\Decoder::decode(Json\Encoder::encode('')), 'Empty string encoded: ' . serialize(Json\Encoder::encode(''))); + } + + /** + * Test backslash escaping of string + */ + public function testString2() + { + $string = 'INFO: Path \\\\test\\123\\abc'; + $expected = '"INFO: Path \\\\\\\\test\\\\123\\\\abc"'; + $encoded = Json\Encoder::encode($string); + $this->assertEquals($expected, $encoded, 'Backslash encoding incorrect: expected: ' . serialize($expected) . '; received: ' . serialize($encoded) . "\n"); + $this->assertEquals($string, Json\Decoder::decode($encoded)); + } + + /** + * Test newline escaping of string + */ + public function testString3() + { + $expected = '"INFO: Path\nSome more"'; + $string = "INFO: Path\nSome more"; + $encoded = Json\Encoder::encode($string); + $this->assertEquals($expected, $encoded, 'Newline encoding incorrect: expected ' . serialize($expected) . '; received: ' . serialize($encoded) . "\n"); + $this->assertEquals($string, Json\Decoder::decode($encoded)); + } + + /** + * Test tab/non-tab escaping of string + */ + public function testString4() + { + $expected = '"INFO: Path\\t\\\\tSome more"'; + $string = "INFO: Path\t\\tSome more"; + $encoded = Json\Encoder::encode($string); + $this->assertEquals($expected, $encoded, 'Tab encoding incorrect: expected ' . serialize($expected) . '; received: ' . serialize($encoded) . "\n"); + $this->assertEquals($string, Json\Decoder::decode($encoded)); + } + + /** + * Test double-quote escaping of string + */ + public function testString5() + { + $expected = '"INFO: Path \\u0022Some more\\u0022"'; + $string = 'INFO: Path "Some more"'; + $encoded = Json\Encoder::encode($string); + $this->assertEquals( + $expected, + $encoded, + 'Quote encoding incorrect: expected ' . serialize($expected) . '; received: ' . serialize($encoded) . "\n" + ); + $this->assertEquals($string, Json\Decoder::decode($encoded)); // Bug: does not accept \u0022 as token! + } + + /** + * Test decoding of unicode escaped special characters + */ + public function testStringOfHtmlSpecialCharsEncodedToUnicodeEscapes() + { + Json\Json::$useBuiltinEncoderDecoder = false; + $expected = '"\\u003C\\u003E\\u0026\\u0027\\u0022"'; + $string = '<>&\'"'; + $encoded = Json\Encoder::encode($string); + $this->assertEquals( + $expected, + $encoded, + 'Encoding error: expected ' . serialize($expected) . '; received: ' . serialize($encoded) . "\n" + ); + $this->assertEquals($string, Json\Decoder::decode($encoded)); + } + + /** + * Test decoding of unicode escaped ASCII (non-HTML special) characters + * + * Note: covers chars that MUST be escaped. Does not test any other non-printables. + */ + public function testStringOfOtherSpecialCharsEncodedToUnicodeEscapes() + { + Json\Json::$useBuiltinEncoderDecoder = false; + $string = "\\ - \n - \t - \r - " .chr(0x08). " - " .chr(0x0C). " - / - \v"; + $encoded = '"\u005C - \u000A - \u0009 - \u000D - \u0008 - \u000C - \u002F - \u000B"'; + $this->assertEquals($string, Json\Decoder::decode($encoded)); + } + + + /** + * test indexed array encoding/decoding + */ + public function testArray() + { + $array = array(1, 'one', 2, 'two'); + $encoded = Json\Encoder::encode($array); + $this->assertSame($array, Json\Decoder::decode($encoded), 'Decoded array does not match: ' . serialize($encoded)); + } + + /** + * test associative array encoding/decoding + */ + public function testAssocArray() + { + $this->_testEncodeDecode(array(array('one' => 1, 'two' => 2))); + } + + /** + * test associative array encoding/decoding, with mixed key types + */ + public function testAssocArray2() + { + $this->_testEncodeDecode(array(array('one' => 1, 2 => 2))); + } + + /** + * test associative array encoding/decoding, with integer keys not starting at 0 + */ + public function testAssocArray3() + { + $this->_testEncodeDecode(array(array(1 => 'one', 2 => 'two'))); + } + + /** + * test object encoding/decoding (decoding to array) + */ + public function testObject() + { + $value = new \stdClass(); + $value->one = 1; + $value->two = 2; + + $array = array('__className' => 'stdClass', 'one' => 1, 'two' => 2); + + $encoded = Json\Encoder::encode($value); + $this->assertSame($array, Json\Decoder::decode($encoded, Json\Json::TYPE_ARRAY)); + } + + /** + * test object encoding/decoding (decoding to stdClass) + */ + public function testObjectAsObject() + { + $value = new \stdClass(); + $value->one = 1; + $value->two = 2; + + $encoded = Json\Encoder::encode($value); + $decoded = Json\Decoder::decode($encoded, Json\Json::TYPE_OBJECT); + $this->assertTrue(is_object($decoded), 'Not decoded as an object'); + $this->assertTrue($decoded instanceof \StdClass, 'Not a StdClass object'); + $this->assertTrue(isset($decoded->one), 'Expected property not set'); + $this->assertEquals($value->one, $decoded->one, 'Unexpected value'); + } + + /** + * Test that arrays of objects decode properly; see issue #144 + */ + public function testDecodeArrayOfObjects() + { + $value = '[{"id":1},{"foo":2}]'; + $expect = array(array('id' => 1), array('foo' => 2)); + $this->assertEquals($expect, Json\Decoder::decode($value, Json\Json::TYPE_ARRAY)); + } + + /** + * Test that objects of arrays decode properly; see issue #107 + */ + public function testDecodeObjectOfArrays() + { + $value = '{"codeDbVar" : {"age" : ["int", 5], "prenom" : ["varchar", 50]}, "234" : [22, "jb"], "346" : [64, "francois"], "21" : [12, "paul"]}'; + $expect = array( + 'codeDbVar' => array( + 'age' => array('int', 5), + 'prenom' => array('varchar', 50), + ), + 234 => array(22, 'jb'), + 346 => array(64, 'francois'), + 21 => array(12, 'paul') + ); + $this->assertEquals($expect, Json\Decoder::decode($value, Json\Json::TYPE_ARRAY)); + } + + /** + * Test encoding and decoding in a single step + * @param array $values array of values to test against encode/decode + */ + protected function _testEncodeDecode($values) + { + foreach ($values as $value) { + $encoded = Json\Encoder::encode($value); + + if (is_array($value) || is_object($value)) { + $this->assertEquals($this->_toArray($value), Json\Decoder::decode($encoded, Json\Json::TYPE_ARRAY)); + } else { + $this->assertEquals($value, Json\Decoder::decode($encoded)); + } + } + } + + protected function _toArray($value) + { + if (!is_array($value) || !is_object($value)) { + return $value; + } + + $array = array(); + foreach ((array)$value as $k => $v) { + $array[$k] = $this->_toArray($v); + } + return $array; + } + + /** + * Test that version numbers such as 4.10 are encoded and decoded properly; + * See ZF-377 + */ + public function testEncodeReleaseNumber() + { + $value = '4.10'; + + $this->_testEncodeDecode(array($value)); + } + + /** + * Tests that spaces/linebreaks prior to a closing right bracket don't throw + * exceptions. See ZF-283. + */ + public function testEarlyLineBreak() + { + $expected = array('data' => array(1, 2, 3, 4)); + + $json = '{"data":[1,2,3,4' . "\n]}"; + $this->assertEquals($expected, Json\Decoder::decode($json, Json\Json::TYPE_ARRAY)); + + $json = '{"data":[1,2,3,4 ]}'; + $this->assertEquals($expected, Json\Decoder::decode($json, Json\Json::TYPE_ARRAY)); + } + + /** + * @group ZF-504 + */ + public function testEncodeEmptyArrayAsStruct() + { + $this->assertSame('[]', Json\Encoder::encode(array())); + } + + /** + * @group ZF-504 + */ + public function testDecodeBorkedJsonShouldThrowException1() + { + $this->setExpectedException('Zend\Json\Exception\RuntimeException'); + Json\Decoder::decode('[a"],["a],[][]'); + } + + /** + * @group ZF-504 + */ + public function testDecodeBorkedJsonShouldThrowException2() + { + $this->setExpectedException('Zend\Json\Exception\RuntimeException'); + Json\Decoder::decode('[a"],["a]'); + } + + /** + * @group ZF-504 + */ + public function testOctalValuesAreNotSupportedInJsonNotation() + { + $this->setExpectedException('Zend\Json\Exception\RuntimeException'); + Json\Decoder::decode('010'); + } + + /** + * Tests for ZF-461 + * + * Check to see that cycling detection works properly + */ + public function testZf461() + { + $item1 = new Item() ; + $item2 = new Item() ; + $everything = array() ; + $everything['allItems'] = array($item1, $item2) ; + $everything['currentItem'] = $item1 ; + + // should not fail + $encoded = Json\Encoder::encode($everything); + + // should fail + $this->setExpectedException('Zend\Json\Exception\RecursionException'); + Json\Encoder::encode($everything, true); + } + + /** + * Test for ZF-4053 + * + * Check to see that cyclical exceptions are silenced when + * $option['silenceCyclicalExceptions'] = true is used + */ + public function testZf4053() + { + $item1 = new Item() ; + $item2 = new Item() ; + $everything = array() ; + $everything['allItems'] = array($item1, $item2) ; + $everything['currentItem'] = $item1 ; + + $options = array('silenceCyclicalExceptions'=>true); + + Json\Json::$useBuiltinEncoderDecoder = true; + $encoded = Json\Json::encode($everything, true, $options); + $json = '{"allItems":[{"__className":"ZendTest\\\\Json\\\\Item"},{"__className":"ZendTest\\\\Json\\\\Item"}],"currentItem":"* RECURSION (ZendTest\\\\Json\\\\Item) *"}'; + + $this->assertEquals($json, $encoded); + } + + public function testEncodeObject() + { + $actual = new Object(); + $encoded = Json\Encoder::encode($actual); + $decoded = Json\Decoder::decode($encoded, Json\Json::TYPE_OBJECT); + + $this->assertTrue(isset($decoded->__className)); + $this->assertEquals('ZendTest\Json\Object', $decoded->__className); + $this->assertTrue(isset($decoded->foo)); + $this->assertEquals('bar', $decoded->foo); + $this->assertTrue(isset($decoded->bar)); + $this->assertEquals('baz', $decoded->bar); + $this->assertFalse(isset($decoded->_foo)); + } + + public function testEncodeClass() + { + $encoded = Json\Encoder::encodeClass('ZendTest\Json\Object'); + + $this->assertContains("Class.create('ZendTest\\Json\\Object'", $encoded); + $this->assertContains("ZAjaxEngine.invokeRemoteMethod(this, 'foo'", $encoded); + $this->assertContains("ZAjaxEngine.invokeRemoteMethod(this, 'bar'", $encoded); + $this->assertNotContains("ZAjaxEngine.invokeRemoteMethod(this, 'baz'", $encoded); + + $this->assertContains('variables:{foo:"bar",bar:"baz"}', $encoded); + $this->assertContains('constants : {FOO: "bar"}', $encoded); + } + + public function testEncodeClasses() + { + $encoded = Json\Encoder::encodeClasses(array('ZendTest\Json\Object', 'Zend\Json\Json')); + + $this->assertContains("Class.create('ZendTest\\Json\\Object'", $encoded); + $this->assertContains("Class.create('Zend\\Json\\Json'", $encoded); + } + + public function testToJSONSerialization() + { + $toJSONObject = new ToJSONClass(); + + $result = Json\Json::encode($toJSONObject); + + $this->assertEquals('{"firstName":"John","lastName":"Doe","email":"john@doe.com"}', $result); + } + + /** + * test encoding array with Zend_JSON_Expr + * + * @group ZF-4946 + */ + public function testEncodingArrayWithExpr() + { + $expr = new Json\Expr('window.alert("Zend JSON Expr")'); + $array = array('expr'=>$expr, 'int'=>9, 'string'=>'text'); + $result = Json\Json::encode($array, false, array('enableJsonExprFinder' => true)); + $expected = '{"expr":window.alert("Zend JSON Expr"),"int":9,"string":"text"}'; + $this->assertEquals($expected, $result); + } + + /** + * test encoding object with Zend_JSON_Expr + * + * @group ZF-4946 + */ + public function testEncodingObjectWithExprAndInternalEncoder() + { + Json\Json::$useBuiltinEncoderDecoder = true; + + $expr = new Json\Expr('window.alert("Zend JSON Expr")'); + $obj = new \stdClass(); + $obj->expr = $expr; + $obj->int = 9; + $obj->string = 'text'; + $result = Json\Json::encode($obj, false, array('enableJsonExprFinder' => true)); + $expected = '{"__className":"stdClass","expr":window.alert("Zend JSON Expr"),"int":9,"string":"text"}'; + $this->assertEquals($expected, $result); + } + + /** + * test encoding object with Zend_JSON_Expr + * + * @group ZF-4946 + */ + public function testEncodingObjectWithExprAndExtJSON() + { + if (!function_exists('json_encode')) { + $this->markTestSkipped('Test only works with ext/json enabled!'); + } + + Json\Json::$useBuiltinEncoderDecoder = false; + + $expr = new Json\Expr('window.alert("Zend JSON Expr")'); + $obj = new \stdClass(); + $obj->expr = $expr; + $obj->int = 9; + $obj->string = 'text'; + $result = Json\Json::encode($obj, false, array('enableJsonExprFinder' => true)); + $expected = '{"expr":window.alert("Zend JSON Expr"),"int":9,"string":"text"}'; + $this->assertEquals($expected, $result); + } + + /** + * test encoding object with ToJSON and Zend_JSON_Expr + * + * @group ZF-4946 + */ + public function testToJSONWithExpr() + { + Json\Json::$useBuiltinEncoderDecoder = true; + + $obj = new ToJSONWithExpr(); + $result = Json\Json::encode($obj, false, array('enableJsonExprFinder' => true)); + $expected = '{"expr":window.alert("Zend JSON Expr"),"int":9,"string":"text"}'; + $this->assertEquals($expected, $result); + } + + /** + * Regression tests for Zend_JSON_Expr and mutliple keys with the same name. + * + * @group ZF-4946 + */ + public function testEncodingMultipleNestedSwitchingSameNameKeysWithDifferentJSONExprSettings() + { + $data = array( + 0 => array( + "alpha" => new Json\Expr("function() {}"), + "beta" => "gamma", + ), + 1 => array( + "alpha" => "gamma", + "beta" => new Json\Expr("function() {}"), + ), + 2 => array( + "alpha" => "gamma", + "beta" => "gamma", + ) + ); + $result = Json\Json::encode($data, false, array('enableJsonExprFinder' => true)); + + $this->assertEquals( + '[{"alpha":function() {},"beta":"gamma"},{"alpha":"gamma","beta":function() {}},{"alpha":"gamma","beta":"gamma"}]', + $result + ); + } + + /** + * Regression tests for Zend_JSON_Expr and mutliple keys with the same name. + * + * @group ZF-4946 + */ + public function testEncodingMultipleNestedIteratedSameNameKeysWithDifferentJSONExprSettings() + { + $data = array( + 0 => array( + "alpha" => "alpha" + ), + 1 => array( + "alpha" => "beta", + ), + 2 => array( + "alpha" => new Json\Expr("gamma"), + ), + 3 => array( + "alpha" => "delta", + ), + 4 => array( + "alpha" => new Json\Expr("epsilon"), + ) + ); + $result = Json\Json::encode($data, false, array('enableJsonExprFinder' => true)); + + $this->assertEquals('[{"alpha":"alpha"},{"alpha":"beta"},{"alpha":gamma},{"alpha":"delta"},{"alpha":epsilon}]', $result); + } + + public function testDisabledJSONExprFinder() + { + Json\Json::$useBuiltinEncoderDecoder = true; + + $data = array( + 0 => array( + "alpha" => new Json\Expr("function() {}"), + "beta" => "gamma", + ), + ); + $result = Json\Json::encode($data); + + $this->assertEquals( + '[{"alpha":{"__className":"Zend\\\\Json\\\\Expr"},"beta":"gamma"}]', + $result + ); + } + + /** + * @group ZF-4054 + */ + public function testEncodeWithUtf8IsTransformedToPackedSyntax() + { + $data = array("Отмена"); + $result = Json\Encoder::encode($data); + + $this->assertEquals('["\u041e\u0442\u043c\u0435\u043d\u0430"]', $result); + } + + /** + * @group ZF-4054 + * + * This test contains assertions from the Solar Framework by Paul M. Jones + * @link http://solarphp.com + */ + public function testEncodeWithUtf8IsTransformedSolarRegression() + { + $expect = '"h\u00c3\u00a9ll\u00c3\u00b6 w\u00c3\u00b8r\u00c5\u201ad"'; + $this->assertEquals($expect, Json\Encoder::encode('héllö wørÅ‚d')); + $this->assertEquals('héllö wørÅ‚d', Json\Decoder::decode($expect)); + + $expect = '"\u0440\u0443\u0441\u0441\u0438\u0448"'; + $this->assertEquals($expect, Json\Encoder::encode("руссиш")); + $this->assertEquals("руссиш", Json\Decoder::decode($expect)); + } + + /** + * @group ZF-4054 + */ + public function testEncodeUnicodeStringSolarRegression() + { + $value = 'héllö wørÅ‚d'; + $expected = 'h\u00c3\u00a9ll\u00c3\u00b6 w\u00c3\u00b8r\u00c5\u201ad'; + $this->assertEquals($expected, Json\Encoder::encodeUnicodeString($value)); + + $value = "\xC3\xA4"; + $expected = '\u00e4'; + $this->assertEquals($expected, Json\Encoder::encodeUnicodeString($value)); + + $value = "\xE1\x82\xA0\xE1\x82\xA8"; + $expected = '\u10a0\u10a8'; + $this->assertEquals($expected, Json\Encoder::encodeUnicodeString($value)); + } + + /** + * @group ZF-4054 + */ + public function testDecodeUnicodeStringSolarRegression() + { + $expected = 'héllö wørÅ‚d'; + $value = 'h\u00c3\u00a9ll\u00c3\u00b6 w\u00c3\u00b8r\u00c5\u201ad'; + $this->assertEquals($expected, Json\Decoder::decodeUnicodeString($value)); + + $expected = "\xC3\xA4"; + $value = '\u00e4'; + $this->assertEquals($expected, Json\Decoder::decodeUnicodeString($value)); + + $value = '\u10a0'; + $expected = "\xE1\x82\xA0"; + $this->assertEquals($expected, Json\Decoder::decodeUnicodeString($value)); + } + + /** + * @group ZF-4054 + * + * This test contains assertions from the Solar Framework by Paul M. Jones + * @link http://solarphp.com + */ + public function testEncodeWithUtf8IsTransformedSolarRegressionEqualsJSONExt() + { + if (function_exists('json_encode') == false) { + $this->markTestSkipped('Test can only be run, when ext/json is installed.'); + } + + $this->assertEquals( + json_encode('héllö wørÅ‚d'), + Json\Encoder::encode('héllö wørÅ‚d') + ); + + $this->assertEquals( + json_encode("руссиш"), + Json\Encoder::encode("руссиш") + ); + } + + /** + * @group ZF-4946 + */ + public function testUtf8JSONExprFinder() + { + $data = array("Отмена" => new Json\Expr("foo")); + + Json\Json::$useBuiltinEncoderDecoder = true; + $result = Json\Json::encode($data, false, array('enableJsonExprFinder' => true)); + $this->assertEquals('{"\u041e\u0442\u043c\u0435\u043d\u0430":foo}', $result); + Json\Json::$useBuiltinEncoderDecoder = false; + + $result = Json\Json::encode($data, false, array('enableJsonExprFinder' => true)); + $this->assertEquals('{"\u041e\u0442\u043c\u0435\u043d\u0430":foo}', $result); + } + + /** + * @group ZF-4437 + */ + public function testCommaDecimalIsConvertedToCorrectJSONWithDot() + { + setlocale(LC_ALL, 'Spanish_Spain', 'es_ES', 'es_ES.utf-8'); + if (strcmp('1,2', (string)floatval(1.20)) != 0) { + $this->markTestSkipped('This test only works for platforms where "," is the decimal point separator.'); + } + Json\Json::$useBuiltinEncoderDecoder = true; + + $actual = Json\Encoder::encode(array(floatval(1.20), floatval(1.68))); + $this->assertEquals('[1.2,1.68]', $actual); + } + + public function testEncodeObjectImplementingIterator() + { + $iterator = new \ArrayIterator(array( + 'foo' => 'bar', + 'baz' => 5 + )); + $target = '{"__className":"ArrayIterator","foo":"bar","baz":5}'; + + Json\Json::$useBuiltinEncoderDecoder = true; + $this->assertEquals($target, Json\Json::encode($iterator)); + } + + /** + * @group ZF-12347 + */ + public function testEncodeObjectImplementingIteratorAggregate() + { + $iterator = new TestAsset\TestIteratorAggregate(); + $target = '{"__className":"ZendTest\\\\Json\\\\TestAsset\\\\TestIteratorAggregate","foo":"bar","baz":5}'; + + Json\Json::$useBuiltinEncoderDecoder = true; + $this->assertEquals($target, Json\Json::encode($iterator)); + } + + /** + * @group ZF-8663 + */ + public function testNativeJSONEncoderWillProperlyEncodeSolidusInStringValues() + { + $source = "bar"; + $target = '"\u003C\/foo\u003E\u003Cfoo\u003Ebar\u003C\/foo\u003E"'; + + // first test ext/json + Json\Json::$useBuiltinEncoderDecoder = false; + $this->assertEquals($target, Json\Json::encode($source)); + } + + public function testNativeJSONEncoderWillProperlyEncodeHtmlSpecialCharsInStringValues() + { + $source = "<>&'\""; + $target = '"\u003C\u003E\u0026\u0027\u0022"'; + + // first test ext/json + Json\Json::$useBuiltinEncoderDecoder = false; + $this->assertEquals($target, Json\Json::encode($source)); + } + + /** + * @group ZF-8663 + */ + public function testBuiltinJSONEncoderWillProperlyEncodeSolidusInStringValues() + { + $source = "bar"; + $target = '"\u003C\/foo\u003E\u003Cfoo\u003Ebar\u003C\/foo\u003E"'; + + // first test ext/json + Json\Json::$useBuiltinEncoderDecoder = true; + $this->assertEquals($target, Json\Json::encode($source)); + } + + public function testBuiltinJSONEncoderWillProperlyEncodeHtmlSpecialCharsInStringValues() + { + $source = "<>&'\""; + $target = '"\u003C\u003E\u0026\u0027\u0022"'; + + // first test ext/json + Json\Json::$useBuiltinEncoderDecoder = true; + $this->assertEquals($target, Json\Json::encode($source)); + } + + /** + * @group ZF-8918 + */ + public function testDecodingInvalidJSONShouldRaiseAnException() + { + $this->setExpectedException('Zend\Json\Exception\RuntimeException'); + Json\Json::decode(' some string '); + } + + /** + * Encoding an iterator using the internal encoder should handle undefined keys + * + * @group ZF-9416 + */ + public function testIteratorWithoutDefinedKey() + { + $inputValue = new \ArrayIterator(array('foo')); + $encoded = Json\Encoder::encode($inputValue); + $expectedDecoding = '{"__className":"ArrayIterator",0:"foo"}'; + $this->assertEquals($expectedDecoding, $encoded); + } + + /** + * The default json decode type should be TYPE_OBJECT + * + * @group ZF-8618 + */ + 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); + } + + /** + * @group ZF-7586 + */ + public function testWillDecodeStructureWithEmptyKeyToObjectProperly() + { + Json\Json::$useBuiltinEncoderDecoder = true; + + $json = '{"":"test"}'; + $object = Json\Json::decode($json, Json\Json::TYPE_OBJECT); + $this->assertTrue(isset($object->_empty_)); + $this->assertEquals('test', $object->_empty_); + } + +} + +/** + * Zend_JSONTest_Item: test item for use with testZf461() + */ +class Item +{ +} + +/** + * Zend_JSONTest_Object: test class for encoding classes + */ +class Object +{ + const FOO = 'bar'; + + public $foo = 'bar'; + public $bar = 'baz'; + + protected $_foo = 'fooled you'; + + public function foo($bar, $baz) + { + } + + public function bar($baz) + { + } + + protected function baz() + { + } +} + +class ToJSONClass +{ + private $_firstName = 'John'; + + private $_lastName = 'Doe'; + + private $_email = 'john@doe.com'; + + public function toJSON() + { + $data = array( + 'firstName' => $this->_firstName, + 'lastName' => $this->_lastName, + 'email' => $this->_email + ); + + return Json\Json::encode($data); + } +} + +/** + * Serializable class exposing a toArray() method + * @group 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 + * @group ZF-11167 + */ +class ZF11167_ToArrayToJsonClass extends ZF11167_ToArrayClass +{ + public function toJson() + { + return Json\Json::encode('bogus'); + } +} + +/** + * ISSUE ZF-4946 + * + */ +class ToJSONWithExpr +{ + private $_string = 'text'; + private $_int = 9; + private $_expr = 'window.alert("Zend JSON Expr")'; + + public function toJSON() + { + $data = array( + 'expr' => new Json\Expr($this->_expr), + 'int' => $this->_int, + 'string' => $this->_string + ); + + return Json\Json::encode($data, false, array('enableJsonExprFinder' => true)); + } +} diff --git a/test/JsonXmlTest.php b/test/JsonXmlTest.php new file mode 100644 index 000000000..73925212d --- /dev/null +++ b/test/JsonXmlTest.php @@ -0,0 +1,606 @@ + + + + + John Doe + + + 123-456-7890 + + + + + + Jane Doe + + + 123-456-0000 + + + + + + John Smith + + + 123-456-1111 + + + + + + Jane Smith + + + 123-456-9999 + + + + + +EOT; + + // There are not going to be any XML attributes in this test XML. + // Hence, set the flag to ignore XML attributes. + $ignoreXmlAttributes = true; + $jsonContents = ""; + + // Convert XML to JSON now. + // fromXml function simply takes a String containing XML contents as input. + $jsonContents = Json\Json::fromXml($xmlStringContents, $ignoreXmlAttributes); + + // 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"); + // Test for one of the expected fields in the JSON result. + $this->assertSame("Jane Smith", $phpArray['contacts']['contact'][3]['name'], "The last contact name converted from XML input 1 is not correct"); + } + + /** + * xml2json Test 2 + * It tests the conversion of book publication xml into JSON format. + * + * XML characteristic to be tested: XML containing an array of child elements with XML attributes. + * + */ + public function testUsingXML2() + { + // Set the XML contents that will be tested here. + $xmlStringContents = << + + + Code Generation in Action + JackHerrington + Manning + + + PHP Hacks + JackHerrington + O'Reilly + + + Podcasting Hacks + JackHerrington + O'Reilly + + + +EOT; + + // There are going to be XML attributes in this test XML. + // Hence, set the flag NOT to ignore XML attributes. + $ignoreXmlAttributes = false; + $jsonContents = ""; + + // Convert XML to JSON now. + // fromXml function simply takes a String containing XML contents as input. + $jsonContents = Json\Json::fromXml($xmlStringContents, $ignoreXmlAttributes); + + // 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 2 is NULL"); + // Test for one of the expected fields in the JSON result. + $this->assertSame("Podcasting Hacks", $phpArray['books']['book'][2]['title'], "The last book title converted from XML input 2 is not correct"); + // Test one of the expected XML attributes carried over in the JSON result. + $this->assertSame("3", $phpArray['books']['book'][2]['@attributes']['id'], "The last id attribute converted from XML input 2 is not correct"); + } + + /** + * xml2json Test 3 + * It tests the conversion of food menu xml into JSON format. + * + * XML characteristic to be tested: XML containing an array of child elements. + * + */ + public function testUsingXML3() + { + // Set the XML contents that will be tested here. + $xmlStringContents = << + + + Belgian Waffles + $5.95 + + two of our famous Belgian Waffles with plenty of real maple + syrup + + 650 + + + Strawberry Belgian Waffles + $7.95 + + light Belgian waffles covered with strawberries and whipped + cream + + 900 + + + Berry-Berry Belgian Waffles + $8.95 + + light Belgian waffles covered with an assortment of fresh + berries and whipped cream + + 900 + + + French Toast + $4.50 + + thick slices made from our homemade sourdough bread + + 600 + + + Homestyle Breakfast + $6.95 + + two eggs, bacon or sausage, toast, and our ever-popular hash + browns + + 950 + + + +EOT; + + // There are not going to be any XML attributes in this test XML. + // Hence, set the flag to ignore XML attributes. + $ignoreXmlAttributes = true; + $jsonContents = ""; + + // Convert XML to JSON now. + // fromXml function simply takes a String containing XML contents as input. + $jsonContents = Json\Json::fromXml($xmlStringContents, $ignoreXmlAttributes); + + // 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 3 is NULL"); + // Test for one of the expected fields in the JSON result. + $this->assertContains("Homestyle Breakfast", $phpArray['breakfast_menu']['food'][4], "The last breakfast item name converted from XML input 3 is not correct"); + } + + /** + * xml2json Test 4 + * It tests the conversion of RosettaNet purchase order xml into JSON format. + * + * XML characteristic to be tested: XML containing an array of child elements and multiple attributes. + * + */ + public function testUsingXML4() + { + // Set the XML contents that will be tested here. + $xmlStringContents = << + + + John Doe + john@nodomain.net + 1-123-456-7890 + + + + + Electronic Component + 25 microfarad 16 volt surface-mount tantalum capacitor + 42 + + + + + + CAPACITOR - FIXED - TANTAL - SOLID + + + Specific Features + R + + + Body Material + C + + + Terminal Position + A + + + Package: Outline Style + CP + + + Lead Form + D + + + Rated Capacitance + 0.000025 + + + Tolerance On Rated Capacitance (%) + 10 + + + Leakage Current (Short Term) + 0.0000001 + + + Rated Voltage + 16 + + + Operating Temperature + 140 + -10 + + + Mounting + Surface + + + + + + Capacitors 'R' Us, Inc. + 98-765-4321 + http://sylviaearle/capaciorsRus/wsdl/buyerseller-implementation.wsdl + + + + +EOT; + + // There are going to be XML attributes in this test XML. + // Hence, set the flag NOT to ignore XML attributes. + $ignoreXmlAttributes = false; + $jsonContents = ""; + + // Convert XML to JSON now. + // fromXml function simply takes a String containing XML contents as input. + $jsonContents = Json\Json::fromXml($xmlStringContents, $ignoreXmlAttributes); + + // 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 4 is NULL"); + // Test for one of the expected fields in the JSON result. + $this->assertContains("98-765-4321", $phpArray['PurchaseRequisition']['Item']['Vendor'], "The vendor id converted from XML input 4 is not correct"); + // Test for the presence of multiple XML attributes present that were carried over in the JSON result. + $this->assertContains("UNSPSC", $phpArray['PurchaseRequisition']['Item']['Specification']['Category']['@attributes'], "The type attribute converted from XML input 4 is not correct"); + $this->assertContains("32121501", $phpArray['PurchaseRequisition']['Item']['Specification']['Category']['@attributes'], "The value attribute converted from XML input 4 is not correct"); + $this->assertContains("Fixed capacitors", $phpArray['PurchaseRequisition']['Item']['Specification']['Category']['@attributes'], "The name attribute converted from XML input 4 is not correct"); + } // End of function testUsingXML4 + + /** + * xml2json Test 5 + * It tests the conversion of TV shows xml into JSON format. + * + * XML characteristic to be tested: XML containing simple CDATA. + * + */ + public function testUsingXML5() + { + // Set the XML contents that will be tested here. + $xmlStringContents = << + + + The Simpsons + + + + + + + +EOT; + + // There are not going to be any XML attributes in this test XML. + // Hence, set the flag to ignore XML attributes. + $ignoreXmlAttributes = true; + $jsonContents = ""; + + // Convert XML to JSON now. + // fromXml function simply takes a String containing XML contents as input. + $jsonContents = Json\Json::fromXml($xmlStringContents, $ignoreXmlAttributes); + + // 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 5 is NULL"); + // Test for one of the expected CDATA fields in the JSON result. + $this->assertContains("Lois & Clark", $phpArray['tvshows']['show'][1]['name'], "The CDATA name converted from XML input 5 is not correct"); + } + + /** + * xml2json Test 6 + * It tests the conversion of demo application xml into JSON format. + * + * XML characteristic to be tested: XML containing a large CDATA. + * + */ + public function testUsingXML6() + { + // Set the XML contents that will be tested here. + $xmlStringContents = << + + + Killer Demo + + + + John Doe + + + + LAMP + + + + Zend + + + + PHP + + + + + movie[0]->characters->addChild('character')); +getMovies()->movie[0]->characters->character->addChild('name', "Mr. Parser"); +getMovies()->movie[0]->characters->character->addChild('actor', "John Doe"); +// Add it as a child element. +getMovies()->movie[0]->addChild('rating', 'PG'); +getMovies()->movie[0]->rating->addAttribute("type", 'mpaa'); +echo getMovies()->asXML(); +?> + ]]> + + + + +EOT; + + // There are not going to be any XML attributes in this test XML. + // Hence, set the flag to ignore XML attributes. + $ignoreXmlAttributes = true; + $jsonContents = ""; + + // Convert XML to JSON now. + // fromXml function simply takes a String containing XML contents as input. + $jsonContents = Json\Json::fromXml($xmlStringContents, $ignoreXmlAttributes); + + // 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 6 is NULL"); + // Test for one of the expected fields in the JSON result. + $this->assertContains("Zend", $phpArray['demo']['framework']['name'], "The framework name field converted from XML input 6 is not correct"); + // Test for one of the expected CDATA fields in the JSON result. + $this->assertContains('echo getMovies()->asXML();', $phpArray['demo']['listing']['code'], "The CDATA code converted from XML input 6 is not correct"); + } + + /** + * xml2json Test 7 + * It tests the conversion of an invalid xml into JSON format. + * + * XML characteristic to be tested: XML containing invalid syntax. + * + */ +/* + public function testUsingXML7() + { + // Set the XML contents that will be tested here. + $xmlStringContents = << + + + + + +EOT; + + // There are not going to be any XML attributes in this test XML. + // Hence, set the flag to ignore XML attributes. + $ignoreXmlAttributes = true; + $jsonContents = ""; + + // Convert XML to JSON now. + // fromXml function simply takes a String containing XML contents as input. + $jsonContents = Zend_Json::fromXml($xmlStringContents, $ignoreXmlAttributes); + } +*/ + + /** + * @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) + { + 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']); + } + + /** + * 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)); + } + +} + + diff --git a/test/Server/CacheTest.php b/test/Server/CacheTest.php new file mode 100644 index 000000000..f01dffba2 --- /dev/null +++ b/test/Server/CacheTest.php @@ -0,0 +1,121 @@ +server = new Server\Server(); + $this->server->setClass('ZendTest\Json\Server\Foo', 'foo'); + $this->cacheFile = tempnam(sys_get_temp_dir(), 'zjs'); + + // if (!is_writeable(__DIR__)) { + if (!is_writeable($this->cacheFile)) { + $this->markTestSkipped('Cannot write test caches due to permissions'); + } + + if (file_exists($this->cacheFile)) { + unlink($this->cacheFile); + } + } + + /** + * Tears down the fixture, for example, close a network connection. + * This method is called after a test is executed. + * + * @return void + */ + public function tearDown() + { + if (file_exists($this->cacheFile)) { + unlink($this->cacheFile); + } + } + + public function testRetrievingSmdCacheShouldReturnFalseIfCacheDoesNotExist() + { + $this->assertFalse(Server\Cache::getSmd($this->cacheFile)); + } + + public function testSavingSmdCacheShouldReturnTrueOnSuccess() + { + $this->assertTrue(Server\Cache::saveSmd($this->cacheFile, $this->server)); + } + + public function testSavedCacheShouldMatchGeneratedCache() + { + $this->testSavingSmdCacheShouldReturnTrueOnSuccess(); + $json = $this->server->getServiceMap()->toJSON(); + $test = Server\Cache::getSmd($this->cacheFile); + $this->assertSame($json, $test); + } + + public function testDeletingSmdShouldReturnFalseOnFailure() + { + $this->assertFalse(Server\Cache::deleteSmd($this->cacheFile)); + } + + public function testDeletingSmdShouldReturnTrueOnSuccess() + { + $this->testSavingSmdCacheShouldReturnTrueOnSuccess(); + $this->assertTrue(Server\Cache::deleteSmd($this->cacheFile)); + } +} + +/** + * Class for testing JSON-RPC server caching + */ +class Foo +{ + /** + * Bar + * + * @param bool $one + * @param string $two + * @param mixed $three + * @return array + */ + public function bar($one, $two = 'two', $three = null) + { + return array($one, $two, $three); + } + + /** + * Baz + * + * @return void + */ + public function baz() + { + throw new \Exception('application error'); + } +} + + diff --git a/test/Server/ClientTest.php b/test/Server/ClientTest.php new file mode 100644 index 000000000..4429050c4 --- /dev/null +++ b/test/Server/ClientTest.php @@ -0,0 +1,262 @@ +httpAdapter = new TestAdapter(); + $this->httpClient = new HttpClient('http://foo', + array('adapter' => $this->httpAdapter)); + + $this->jsonClient = new Client('http://foo'); + $this->jsonClient->setHttpClient($this->httpClient); + } + + // HTTP Client + + public function testGettingDefaultHttpClient() + { + $jsonClient = new Client('http://foo'); + $httpClient = $jsonClient->getHttpClient(); + //$this->assertInstanceOf('Zend\\Http\\Client', $httpClient); + $this->assertSame($httpClient, $jsonClient->getHttpClient()); + } + + public function testSettingAndGettingHttpClient() + { + $jsonClient = new Client('http://foo'); + $this->assertNotSame($this->httpClient, $jsonClient->getHttpClient()); + + $jsonClient->setHttpClient($this->httpClient); + $this->assertSame($this->httpClient, $jsonClient->getHttpClient()); + } + + public function testSettingHttpClientViaContructor() + { + $jsonClient = new Client('http://foo', $this->httpClient); + $httpClient = $jsonClient->getHttpClient(); + $this->assertSame($this->httpClient, $httpClient); + } + + // Request & Response + + public function testLastRequestAndResponseAreInitiallyNull() + { + $this->assertNull($this->jsonClient->getLastRequest()); + $this->assertNull($this->jsonClient->getLastResponse()); + } + + public function testLastRequestAndResponseAreSetAfterRpcMethodCall() + { + $this->setServerResponseTo(true); + $this->jsonClient->call('foo'); + + //$this->assertInstanceOf('Zend\\Json\\Server\\Request', $this->jsonClient->getLastRequest()); + //$this->assertInstanceOf('Zend\\Json\\Server\\Response', $this->jsonClient->getLastResponse()); + } + + public function testSuccessfulRpcMethodCallWithNoParameters() + { + $expectedMethod = 'foo'; + $expectedReturn = 7; + + $this->setServerResponseTo($expectedReturn); + $this->assertSame($expectedReturn, $this->jsonClient->call($expectedMethod)); + + $request = $this->jsonClient->getLastRequest(); + $response = $this->jsonClient->getLastResponse(); + + $this->assertSame($expectedMethod, $request->getMethod()); + $this->assertSame(array(), $request->getParams()); + $this->assertSame($expectedReturn, $response->getResult()); + $this->assertFalse($response->isError()); + } + + public function testSuccessfulRpcMethodCallWithParameters() + { + $expectedMethod = 'foobar'; + $expectedParams = array(1, 1.1, true, 'foo' => 'bar'); + $expectedReturn = array(7, false, 'foo' => 'bar'); + + $this->setServerResponseTo($expectedReturn); + + $actualReturn = $this->jsonClient->call($expectedMethod, $expectedParams); + $this->assertSame($expectedReturn, $actualReturn); + + $request = $this->jsonClient->getLastRequest(); + $response = $this->jsonClient->getLastResponse(); + + $this->assertSame($expectedMethod, $request->getMethod()); + $params = $request->getParams(); + $this->assertSame(count($expectedParams), count($params)); + $this->assertSame($expectedParams[0], $params[0]); + $this->assertSame($expectedParams[1], $params[1]); + $this->assertSame($expectedParams[2], $params[2]); + $this->assertSame($expectedParams['foo'], $params['foo']); + + $this->assertSame($expectedReturn, $response->getResult()); + $this->assertFalse($response->isError()); + } + + // Faults + + public function testRpcMethodCallThrowsOnHttpFailure() + { + $status = 404; + $message = 'Not Found'; + $body = 'oops'; + + $response = $this->makeHttpResponseFrom($body, $status, $message); + $this->httpAdapter->setResponse($response); + + $this->setExpectedException('Zend\\Json\\Server\\Exception\\HttpException', $message, $status); + $this->jsonClient->call('foo'); + } + + public function testRpcMethodCallThrowsOnJsonRpcFault() + { + $code = -32050; + $message = 'foo'; + + $error = new Error($message, $code); + $response = new Response(); + $response->setError($error); + $json = $response->toJson(); + + $response = $this->makeHttpResponseFrom($json); + $this->httpAdapter->setResponse($response); + + $this->setExpectedException('Zend\\Json\\Server\\Exception\\ErrorException', $message, $code); + $this->jsonClient->call('foo'); + } + + // HTTP handling + + public function testSettingUriOnHttpClientIsNotOverwrittenByJsonRpcClient() + { + $changedUri = 'http://bar:80/'; + // Overwrite: http://foo:80 + $this->setServerResponseTo(null); + $this->jsonClient->getHttpClient()->setUri($changedUri); + $this->jsonClient->call('foo'); + $uri = $this->jsonClient->getHttpClient()->getUri()->toString(); + + $this->assertEquals($changedUri, $uri); + } + + public function testSettingNoHttpClientUriForcesClientToSetUri() + { + $baseUri = 'http://foo:80/'; + $this->httpAdapter = new TestAdapter(); + $this->httpClient = new HttpClient(null, array('adapter' => $this->httpAdapter)); + + $this->jsonClient = new Client($baseUri); + $this->jsonClient->setHttpClient($this->httpClient); + + $this->setServerResponseTo(null); + $this->assertNull($this->jsonClient->getHttpClient()->getRequest()->getUriString()); + $this->jsonClient->call('foo'); + $uri = $this->jsonClient->getHttpClient()->getUri(); + + $this->assertEquals($baseUri, $uri->toString()); + } + + public function testCustomHttpClientUserAgentIsNotOverridden() + { + $this->assertFalse( + $this->httpClient->getHeader('User-Agent'), + 'UA is null if no request was made' + ); + $this->setServerResponseTo(null); + $this->assertNull($this->jsonClient->call('method')); + $this->assertSame( + 'Zend_Json_Server_Client', + $this->httpClient->getHeader('User-Agent'), + 'If no custom UA is set, set Zend_Json_Server_Client' + ); + + $expectedUserAgent = 'Zend_Json_Server_Client (custom)'; + $this->httpClient->setHeaders(array('User-Agent' => $expectedUserAgent)); + + $this->setServerResponseTo(null); + $this->assertNull($this->jsonClient->call('method')); + $this->assertSame($expectedUserAgent, $this->httpClient->getHeader('User-Agent')); + } + + // Helpers + public function setServerResponseTo($nativeVars) + { + $response = $this->getServerResponseFor($nativeVars); + $this->httpAdapter->setResponse($response); + } + + public function getServerResponseFor($nativeVars) + { + $response = new Response(); + $response->setResult($nativeVars); + $json = $response->toJson(); + + $response = $this->makeHttpResponseFrom($json); + return $response; + } + + public function makeHttpResponseFrom($data, $status=200, $message='OK') + { + $headers = array("HTTP/1.1 $status $message", + "Status: $status", + 'Content-Type: application/json', + 'Content-Length: ' . strlen($data) + ); + return implode("\r\n", $headers) . "\r\n\r\n$data\r\n\r\n"; + } + + public function makeHttpResponseFor($nativeVars) + { + $response = $this->getServerResponseFor($nativeVars); + return HttpResponse::fromString($response); + } + + public function mockHttpClient() + { + $this->mockedHttpClient = $this->getMock('Zend\\Http\\Client'); + $this->jsonClient->setHttpClient($this->mockedHttpClient); + } +} diff --git a/test/Server/ErrorTest.php b/test/Server/ErrorTest.php new file mode 100644 index 000000000..e28bcb54d --- /dev/null +++ b/test/Server/ErrorTest.php @@ -0,0 +1,152 @@ +error = new Server\Error(); + } + + /** + * Tears down the fixture, for example, close a network connection. + * This method is called after a test is executed. + * + * @return void + */ + public function tearDown() + { + } + + public function testCodeShouldBeErrOtherByDefault() + { + $this->assertEquals(Server\Error::ERROR_OTHER, $this->error->getCode()); + } + + public function testSetCodeShouldCastToInteger() + { + $this->error->setCode('-32768'); + $this->assertEquals(-32768, $this->error->getCode()); + } + + public function testCodeShouldBeLimitedToStandardIntegers() + { + foreach (array(true, 'foo', array(), new \stdClass, 2.0, 25) as $code) { + $this->error->setCode($code); + $this->assertEquals(Server\Error::ERROR_OTHER, $this->error->getCode()); + } + } + + public function testCodeShouldAllowArbitraryAppErrorCodesInXmlRpcErrorCodeRange() + { + foreach (range(-32099, -32000) as $code) { + $this->error->setCode($code); + $this->assertEquals($code, $this->error->getCode()); + } + } + + public function testMessageShouldBeNullByDefault() + { + $this->assertNull($this->error->getMessage()); + } + + public function testSetMessageShouldCastToString() + { + foreach (array(true, 2.0, 25) as $message) { + $this->error->setMessage($message); + $this->assertEquals((string) $message, $this->error->getMessage()); + } + } + + public function testSetMessageToNonScalarShouldSilentlyFail() + { + foreach (array(array(), new \stdClass) as $message) { + $this->error->setMessage($message); + $this->assertNull($this->error->getMessage()); + } + } + + public function testDataShouldBeNullByDefault() + { + $this->assertNull($this->error->getData()); + } + + public function testShouldAllowArbitraryData() + { + foreach (array(true, 'foo', 2, 2.0, array(), new \stdClass) as $datum) { + $this->error->setData($datum); + $this->assertEquals($datum, $this->error->getData()); + } + } + + public function testShouldBeAbleToCastToArray() + { + $this->setupError(); + $array = $this->error->toArray(); + $this->validateArray($array); + } + + public function testShouldBeAbleToCastToJSON() + { + $this->setupError(); + $json = $this->error->toJSON(); + $this->validateArray(Json\Json::decode($json, Json\Json::TYPE_ARRAY)); + } + + public function testCastingToStringShouldCastToJSON() + { + $this->setupError(); + $json = $this->error->__toString(); + $this->validateArray(Json\Json::decode($json, Json\Json::TYPE_ARRAY)); + } + + public function setupError() + { + $this->error->setCode(Server\Error::ERROR_OTHER) + ->setMessage('Unknown Error') + ->setData(array('foo' => 'bar')); + } + + public function validateArray($error) + { + $this->assertTrue(is_array($error)); + $this->assertTrue(array_key_exists('code', $error)); + $this->assertTrue(array_key_exists('message', $error)); + $this->assertTrue(array_key_exists('data', $error)); + + $this->assertTrue(is_int($error['code'])); + $this->assertTrue(is_string($error['message'])); + $this->assertTrue(is_array($error['data'])); + + $this->assertEquals($this->error->getCode(), $error['code']); + $this->assertEquals($this->error->getMessage(), $error['message']); + $this->assertSame($this->error->getData(), $error['data']); + } +} diff --git a/test/Server/RequestTest.php b/test/Server/RequestTest.php new file mode 100644 index 000000000..fde4d9b4b --- /dev/null +++ b/test/Server/RequestTest.php @@ -0,0 +1,266 @@ +request = new \Zend\Json\Server\Request(); + } + + /** + * Tears down the fixture, for example, close a network connection. + * This method is called after a test is executed. + * + * @return void + */ + public function tearDown() + { + } + + public function testShouldHaveNoParamsByDefault() + { + $params = $this->request->getParams(); + $this->assertTrue(empty($params)); + } + + public function testShouldBeAbleToAddAParamAsValueOnly() + { + $this->request->addParam('foo'); + $params = $this->request->getParams(); + $this->assertEquals(1, count($params)); + $test = array_shift($params); + $this->assertEquals('foo', $test); + } + + public function testShouldBeAbleToAddAParamAsKeyValuePair() + { + $this->request->addParam('bar', 'foo'); + $params = $this->request->getParams(); + $this->assertEquals(1, count($params)); + $this->assertTrue(array_key_exists('foo', $params)); + $this->assertEquals('bar', $params['foo']); + } + + public function testInvalidKeysShouldBeIgnored() + { + $count = 0; + foreach (array(array('foo', true), array('foo', new \stdClass), array('foo', array())) as $spec) { + $this->request->addParam($spec[0], $spec[1]); + $this->assertNull($this->request->getParam('foo')); + $params = $this->request->getParams(); + ++$count; + $this->assertEquals($count, count($params)); + } + } + + public function testShouldBeAbleToAddMultipleIndexedParamsAtOnce() + { + $params = array( + 'foo', + 'bar', + 'baz', + ); + $this->request->addParams($params); + $test = $this->request->getParams(); + $this->assertSame($params, $test); + } + + public function testShouldBeAbleToAddMultipleNamedParamsAtOnce() + { + $params = array( + 'foo' => 'bar', + 'bar' => 'baz', + 'baz' => 'bat', + ); + $this->request->addParams($params); + $test = $this->request->getParams(); + $this->assertSame($params, $test); + } + + public function testShouldBeAbleToAddMixedIndexedAndNamedParamsAtOnce() + { + $params = array( + 'foo' => 'bar', + 'baz', + 'baz' => 'bat', + ); + $this->request->addParams($params); + $test = $this->request->getParams(); + $this->assertEquals(array_values($params), array_values($test)); + $this->assertTrue(array_key_exists('foo', $test)); + $this->assertTrue(array_key_exists('baz', $test)); + $this->assertTrue(in_array('baz', $test)); + } + + public function testSetParamsShouldOverwriteParams() + { + $this->testShouldBeAbleToAddMixedIndexedAndNamedParamsAtOnce(); + $params = array( + 'one', + 'two', + 'three', + ); + $this->request->setParams($params); + $this->assertSame($params, $this->request->getParams()); + } + + public function testShouldBeAbleToRetrieveParamByKeyOrIndex() + { + $this->testShouldBeAbleToAddMixedIndexedAndNamedParamsAtOnce(); + $params = $this->request->getParams(); + $this->assertEquals('bar', $this->request->getParam('foo'), var_export($params, 1)); + $this->assertEquals('baz', $this->request->getParam(1), var_export($params, 1)); + $this->assertEquals('bat', $this->request->getParam('baz'), var_export($params, 1)); + } + + public function testMethodShouldBeNullByDefault() + { + $this->assertNull($this->request->getMethod()); + } + + public function testMethodErrorShouldBeFalseByDefault() + { + $this->assertFalse($this->request->isMethodError()); + } + + public function testMethodAccessorsShouldWorkUnderNormalInput() + { + $this->request->setMethod('foo'); + $this->assertEquals('foo', $this->request->getMethod()); + } + + public function testSettingMethodWithInvalidNameShouldSetError() + { + foreach (array('1ad', 'abc-123', 'ad$$832r#@') as $method) { + $this->request->setMethod($method); + $this->assertNull($this->request->getMethod()); + $this->assertTrue($this->request->isMethodError()); + } + } + + public function testIdShouldBeNullByDefault() + { + $this->assertNull($this->request->getId()); + } + + public function testIdAccessorsShouldWorkUnderNormalInput() + { + $this->request->setId('foo'); + $this->assertEquals('foo', $this->request->getId()); + } + + public function testVersionShouldBeJSONRpcV1ByDefault() + { + $this->assertEquals('1.0', $this->request->getVersion()); + } + + public function testVersionShouldBeLimitedToV1AndV2() + { + $this->testVersionShouldBeJSONRpcV1ByDefault(); + $this->request->setVersion('2.0'); + $this->assertEquals('2.0', $this->request->getVersion()); + $this->request->setVersion('foo'); + $this->assertEquals('1.0', $this->request->getVersion()); + } + + public function testShouldBeAbleToLoadRequestFromJSONString() + { + $options = $this->getOptions(); + $json = Json\Json::encode($options); + $this->request->loadJSON($json); + + $this->assertEquals('foo', $this->request->getMethod()); + $this->assertEquals('foobar', $this->request->getId()); + $this->assertEquals($options['params'], $this->request->getParams()); + } + + public function testLoadingFromJSONShouldSetJSONRpcVersionWhenPresent() + { + $options = $this->getOptions(); + $options['jsonrpc'] = '2.0'; + $json = Json\Json::encode($options); + $this->request->loadJSON($json); + $this->assertEquals('2.0', $this->request->getVersion()); + } + + public function testShouldBeAbleToCastToJSON() + { + $options = $this->getOptions(); + $this->request->setOptions($options); + $json = $this->request->toJSON(); + $this->validateJSON($json, $options); + } + + public function testCastingToStringShouldCastToJSON() + { + $options = $this->getOptions(); + $this->request->setOptions($options); + $json = $this->request->__toString(); + $this->validateJSON($json, $options); + } + + /** + * @group ZF-6187 + */ + public function testMethodNamesShouldAllowDotNamespacing() + { + $this->request->setMethod('foo.bar'); + $this->assertEquals('foo.bar', $this->request->getMethod()); + } + + public function getOptions() + { + return array( + 'method' => 'foo', + 'params' => array( + 5, + 'four', + true, + ), + 'id' => 'foobar' + ); + } + + public function validateJSON($json, array $options) + { + $test = Json\Json::decode($json, Json\Json::TYPE_ARRAY); + $this->assertTrue(is_array($test), var_export($json, 1)); + + $this->assertTrue(array_key_exists('id', $test)); + $this->assertTrue(array_key_exists('method', $test)); + $this->assertTrue(array_key_exists('params', $test)); + + $this->assertTrue(is_string($test['id'])); + $this->assertTrue(is_string($test['method'])); + $this->assertTrue(is_array($test['params'])); + + $this->assertEquals($options['id'], $test['id']); + $this->assertEquals($options['method'], $test['method']); + $this->assertSame($options['params'], $test['params']); + } +} diff --git a/test/Server/ResponseTest.php b/test/Server/ResponseTest.php new file mode 100644 index 000000000..865d8619a --- /dev/null +++ b/test/Server/ResponseTest.php @@ -0,0 +1,184 @@ +response = new \Zend\Json\Server\Response(); + } + + public function testResultShouldBeNullByDefault() + { + $this->assertNull($this->response->getResult()); + } + + public function testResultAccessorsShouldWorkWithNormalInput() + { + foreach (array(true, 'foo', 2, 2.0, array(), array('foo' => 'bar')) as $result) { + $this->response->setResult($result); + $this->assertEquals($result, $this->response->getResult()); + } + } + + public function testResultShouldNotBeErrorByDefault() + { + $this->assertFalse($this->response->isError()); + } + + public function testSettingErrorShouldMarkRequestAsError() + { + $error = new Server\Error(); + $this->response->setError($error); + $this->assertTrue($this->response->isError()); + } + + public function testShouldBeAbleToRetrieveErrorObject() + { + $error = new Server\Error(); + $this->response->setError($error); + $this->assertSame($error, $this->response->getError()); + } + + public function testIdShouldBeNullByDefault() + { + $this->assertNull($this->response->getId()); + } + + public function testIdAccesorsShouldWorkWithNormalInput() + { + $this->response->setId('foo'); + $this->assertEquals('foo', $this->response->getId()); + } + + public function testVersionShouldBeNullByDefault() + { + $this->assertNull($this->response->getVersion()); + } + + public function testVersionShouldBeLimitedToV2() + { + $this->response->setVersion('2.0'); + $this->assertEquals('2.0', $this->response->getVersion()); + foreach (array('a', 1, '1.0', true) as $version) { + $this->response->setVersion($version); + $this->assertNull($this->response->getVersion()); + } + } + + public function testShouldBeAbleToLoadResponseFromJSONString() + { + $options = $this->getOptions(); + $json = Json\Json::encode($options); + $this->response->loadJSON($json); + + $this->assertEquals('foobar', $this->response->getId()); + $this->assertEquals($options['result'], $this->response->getResult()); + } + + public function testLoadingFromJSONShouldSetJSONRpcVersionWhenPresent() + { + $options = $this->getOptions(); + $options['jsonrpc'] = '2.0'; + $json = Json\Json::encode($options); + $this->response->loadJSON($json); + $this->assertEquals('2.0', $this->response->getVersion()); + } + + public function testResponseShouldBeAbleToCastToJSON() + { + $this->response->setResult(true) + ->setId('foo') + ->setVersion('2.0'); + $json = $this->response->toJSON(); + $test = Json\Json::decode($json, Json\Json::TYPE_ARRAY); + + $this->assertTrue(is_array($test)); + $this->assertTrue(array_key_exists('result', $test)); + $this->assertFalse(array_key_exists('error', $test), "'error' may not coexist with 'result'"); + $this->assertTrue(array_key_exists('id', $test)); + $this->assertTrue(array_key_exists('jsonrpc', $test)); + + $this->assertTrue($test['result']); + $this->assertEquals($this->response->getId(), $test['id']); + $this->assertEquals($this->response->getVersion(), $test['jsonrpc']); + } + + public function testResponseShouldCastErrorToJSONIfIsError() + { + $error = new Server\Error(); + $error->setCode(Server\Error::ERROR_INTERNAL) + ->setMessage('error occurred'); + $this->response->setId('foo') + ->setResult(true) + ->setError($error); + $json = $this->response->toJSON(); + $test = Json\Json::decode($json, Json\Json::TYPE_ARRAY); + + $this->assertTrue(is_array($test)); + $this->assertFalse(array_key_exists('result', $test), "'result' may not coexist with 'error'"); + $this->assertTrue(array_key_exists('error', $test)); + $this->assertTrue(array_key_exists('id', $test)); + $this->assertFalse(array_key_exists('jsonrpc', $test)); + + $this->assertEquals($this->response->getId(), $test['id']); + $this->assertEquals($error->getCode(), $test['error']['code']); + $this->assertEquals($error->getMessage(), $test['error']['message']); + } + + public function testCastToStringShouldCastToJSON() + { + $this->response->setResult(true) + ->setId('foo'); + $json = $this->response->__toString(); + $test = Json\Json::decode($json, Json\Json::TYPE_ARRAY); + + $this->assertTrue(is_array($test)); + $this->assertTrue(array_key_exists('result', $test)); + $this->assertFalse(array_key_exists('error', $test), "'error' may not coexist with 'result'"); + $this->assertTrue(array_key_exists('id', $test)); + $this->assertFalse(array_key_exists('jsonrpc', $test)); + + $this->assertTrue($test['result']); + $this->assertEquals($this->response->getId(), $test['id']); + } + + public function getOptions() + { + return array( + 'result' => array( + 5, + 'four', + true, + ), + 'id' => 'foobar' + ); + } +} diff --git a/test/Server/Smd/ServiceTest.php b/test/Server/Smd/ServiceTest.php new file mode 100644 index 000000000..6457a4156 --- /dev/null +++ b/test/Server/Smd/ServiceTest.php @@ -0,0 +1,315 @@ +service = new Service('foo'); + } + + public function testConstructorShouldThrowExceptionWhenNoNameSetWhenNullProvided() + { + $this->setExpectedException('Zend\Json\Server\Exception\InvalidArgumentException', 'requires a name'); + $service = new Service(null); + } + + public function testConstructorShouldThrowExceptionWhenNoNameSetWhenArrayProvided() + { + $this->setExpectedException('Zend\Json\Server\Exception\InvalidArgumentException', 'requires a name'); + $service = new Service(null); + } + + public function testSettingNameShouldThrowExceptionWhenContainingInvalidFormat() + { + $this->setExpectedException('Zend\Json\Server\Exception\InvalidArgumentException', 'Invalid name'); + $this->service->setName('0ab-?'); + } + + public function testSettingNameShouldThrowExceptionWhenContainingInvalidFormatStartingWithInt() + { + $this->setExpectedException('Zend\Json\Server\Exception\InvalidArgumentException', 'Invalid name'); + $this->service->setName('0ab-?'); + } + + public function testNameAccessorsShouldWorkWithNormalInput() + { + $this->assertEquals('foo', $this->service->getName()); + $this->service->setName('bar'); + $this->assertEquals('bar', $this->service->getName()); + } + + public function testTransportShouldDefaultToPost() + { + $this->assertEquals('POST', $this->service->getTransport()); + } + + public function testSettingTransportThrowsExceptionWhenSetToGet() + { + $this->setExpectedException('Zend\Json\Server\Exception\InvalidArgumentException', 'Invalid transport'); + $this->service->setTransport('GET'); + } + + public function testSettingTransportThrowsExceptionWhenSetToRest() + { + $this->setExpectedException('Zend\Json\Server\Exception\InvalidArgumentException', 'Invalid transport'); + $this->service->setTransport('REST'); + } + + public function testTransportAccessorsShouldWorkUnderNormalInput() + { + $this->service->setTransport('POST'); + $this->assertEquals('POST', $this->service->getTransport()); + } + + public function testTargetShouldBeNullInitially() + { + $this->assertNull($this->service->getTarget()); + } + + public function testTargetAccessorsShouldWorkUnderNormalInput() + { + $this->testTargetShouldBeNullInitially(); + $this->service->setTarget('foo'); + $this->assertEquals('foo', $this->service->getTarget()); + } + + public function testTargetAccessorsShouldNormalizeToString() + { + $this->testTargetShouldBeNullInitially(); + $this->service->setTarget(123); + $value = $this->service->getTarget(); + $this->assertTrue(is_string($value)); + $this->assertEquals((string) 123, $value); + } + + public function testEnvelopeShouldBeJSONRpc1CompliantByDefault() + { + $this->assertEquals(Server\Smd::ENV_JSONRPC_1, $this->service->getEnvelope()); + } + + public function testEnvelopeShouldOnlyComplyWithJSONRpc1And2() + { + $this->testEnvelopeShouldBeJSONRpc1CompliantByDefault(); + $this->service->setEnvelope(Server\Smd::ENV_JSONRPC_2); + $this->assertEquals(Server\Smd::ENV_JSONRPC_2, $this->service->getEnvelope()); + $this->service->setEnvelope(Server\Smd::ENV_JSONRPC_1); + $this->assertEquals(Server\Smd::ENV_JSONRPC_1, $this->service->getEnvelope()); + try { + $this->service->setEnvelope('JSON-P'); + $this->fail('Should not be able to set non-JSON-RPC spec envelopes'); + } catch (Server\Exception\InvalidArgumentException $e) { + $this->assertContains('Invalid envelope', $e->getMessage()); + } + } + + public function testShouldHaveNoParamsByDefault() + { + $params = $this->service->getParams(); + $this->assertTrue(empty($params)); + } + + public function testShouldBeAbleToAddParamsByTypeOnly() + { + $this->service->addParam('integer'); + $params = $this->service->getParams(); + $this->assertEquals(1, count($params)); + $param = array_shift($params); + $this->assertEquals('integer', $param['type']); + } + + public function testParamsShouldAcceptArrayOfTypes() + { + $type = array('integer', 'string'); + $this->service->addParam($type); + $params = $this->service->getParams(); + $param = array_shift($params); + $test = $param['type']; + $this->assertTrue(is_array($test)); + $this->assertEquals($type, $test); + } + + public function testInvalidParamTypeShouldThrowException() + { + $this->setExpectedException('Zend\Json\Server\Exception\InvalidArgumentException', 'Invalid param type'); + $this->service->addParam(new \stdClass); + } + + public function testShouldBeAbleToOrderParams() + { + $this->service->addParam('integer', array(), 4) + ->addParam('string') + ->addParam('boolean', array(), 3); + $params = $this->service->getParams(); + + $this->assertEquals(3, count($params)); + + $param = array_shift($params); + $this->assertEquals('string', $param['type'], var_export($params, 1)); + $param = array_shift($params); + $this->assertEquals('boolean', $param['type'], var_export($params, 1)); + $param = array_shift($params); + $this->assertEquals('integer', $param['type'], var_export($params, 1)); + } + + public function testShouldBeAbleToAddArbitraryParamOptions() + { + $this->service->addParam( + 'integer', + array( + 'name' => 'foo', + 'optional' => false, + 'default' => 1, + 'description' => 'Foo parameter', + ) + ); + $params = $this->service->getParams(); + $param = array_shift($params); + $this->assertEquals('foo', $param['name']); + $this->assertFalse($param['optional']); + $this->assertEquals(1, $param['default']); + $this->assertEquals('Foo parameter', $param['description']); + } + + public function testShouldBeAbleToAddMultipleParamsAtOnce() + { + $this->service->addParams(array( + array('type' => 'integer', 'order' => 4), + array('type' => 'string', 'name' => 'foo'), + array('type' => 'boolean', 'order' => 3), + )); + $params = $this->service->getParams(); + + $this->assertEquals(3, count($params)); + $param = array_shift($params); + $this->assertEquals('string', $param['type']); + $this->assertEquals('foo', $param['name']); + + $param = array_shift($params); + $this->assertEquals('boolean', $param['type']); + + $param = array_shift($params); + $this->assertEquals('integer', $param['type']); + } + + public function testSetparamsShouldOverwriteExistingParams() + { + $this->testShouldBeAbleToAddMultipleParamsAtOnce(); + $params = $this->service->getParams(); + $this->assertEquals(3, count($params)); + + $this->service->setParams(array( + array('type' => 'string'), + array('type' => 'integer'), + )); + $test = $this->service->getParams(); + $this->assertNotEquals($params, $test); + $this->assertEquals(2, count($test)); + } + + public function testReturnShouldBeNullByDefault() + { + $this->assertNull($this->service->getReturn()); + } + + public function testReturnAccessorsShouldWorkWithNormalInput() + { + $this->testReturnShouldBeNullByDefault(); + $this->service->setReturn('integer'); + $this->assertEquals('integer', $this->service->getReturn()); + } + + public function testReturnAccessorsShouldAllowArrayOfTypes() + { + $this->testReturnShouldBeNullByDefault(); + $type = array('integer', 'string'); + $this->service->setReturn($type); + $this->assertEquals($type, $this->service->getReturn()); + } + + public function testInvalidReturnTypeShouldThrowException() + { + $this->setExpectedException('Zend\Json\Server\Exception\InvalidArgumentException', 'Invalid param type'); + $this->service->setReturn(new \stdClass); + } + + public function testToArrayShouldCreateSmdCompatibleHash() + { + $this->setupSmdValidationObject(); + $smd = $this->service->toArray(); + $this->validateSmdArray($smd); + } + + public function testTojsonShouldEmitJSON() + { + $this->setupSmdValidationObject(); + $json = $this->service->toJSON(); + $smd = \Zend\Json\Json::decode($json, \Zend\Json\Json::TYPE_ARRAY); + + $this->assertTrue(array_key_exists('foo', $smd)); + $this->assertTrue(is_array($smd['foo'])); + + $this->validateSmdArray($smd['foo']); + } + + public function setupSmdValidationObject() + { + $this->service->setName('foo') + ->setTransport('POST') + ->setTarget('/foo') + ->setEnvelope(Server\Smd::ENV_JSONRPC_2) + ->addParam('boolean') + ->addParam('array') + ->addParam('object') + ->setReturn('boolean'); + } + + public function validateSmdArray(array $smd) + { + $this->assertTrue(array_key_exists('transport', $smd)); + $this->assertEquals('POST', $smd['transport']); + + $this->assertTrue(array_key_exists('envelope', $smd)); + $this->assertEquals(Server\Smd::ENV_JSONRPC_2, $smd['envelope']); + + $this->assertTrue(array_key_exists('parameters', $smd)); + $params = $smd['parameters']; + $this->assertEquals(3, count($params)); + $param = array_shift($params); + $this->assertEquals('boolean', $param['type']); + $param = array_shift($params); + $this->assertEquals('array', $param['type']); + $param = array_shift($params); + $this->assertEquals('object', $param['type']); + + $this->assertTrue(array_key_exists('returns', $smd)); + $this->assertEquals('boolean', $smd['returns']); + } +} diff --git a/test/Server/SmdTest.php b/test/Server/SmdTest.php new file mode 100644 index 000000000..e01cd6aeb --- /dev/null +++ b/test/Server/SmdTest.php @@ -0,0 +1,388 @@ +smd = new SMD(); + } + + public function testTransportShouldDefaultToPost() + { + $this->assertEquals('POST', $this->smd->getTransport()); + } + + public function testTransportAccessorsShouldWorkUnderNormalInput() + { + $this->smd->setTransport('POST'); + $this->assertEquals('POST', $this->smd->getTransport()); + } + + public function testTransportShouldBeLimitedToPost() + { + foreach (array('GET', 'REST') as $transport) { + try { + $this->smd->setTransport($transport); + $this->fail('Invalid transport should throw exception'); + } catch (InvalidArgumentException $e) { + $this->assertContains('Invalid transport', $e->getMessage()); + } + } + } + + public function testEnvelopeShouldDefaultToJSONRpcVersion1() + { + $this->assertEquals(Smd::ENV_JSONRPC_1, $this->smd->getEnvelope()); + } + + public function testEnvelopeAccessorsShouldWorkUnderNormalInput() + { + $this->testEnvelopeShouldDefaultToJSONRpcVersion1(); + $this->smd->setEnvelope(Smd::ENV_JSONRPC_2); + $this->assertEquals(Smd::ENV_JSONRPC_2, $this->smd->getEnvelope()); + $this->smd->setEnvelope(Smd::ENV_JSONRPC_1); + $this->assertEquals(Smd::ENV_JSONRPC_1, $this->smd->getEnvelope()); + } + + public function testEnvelopeShouldBeLimitedToJSONRpcVersions() + { + foreach (array('URL', 'PATH', 'JSON') as $env) { + try { + $this->smd->setEnvelope($env); + $this->fail('Invalid envelope type should throw exception'); + } catch (InvalidArgumentException $e) { + $this->assertContains('Invalid envelope', $e->getMessage()); + } + } + } + + public function testContentTypeShouldDefaultToApplicationJSON() + { + $this->assertEquals('application/json', $this->smd->getContentType()); + } + + public function testContentTypeAccessorsShouldWorkUnderNormalInput() + { + foreach (array('text/json', 'text/plain', 'application/x-json') as $type) { + $this->smd->setContentType($type); + $this->assertEquals($type, $this->smd->getContentType()); + } + } + + public function testContentTypeShouldBeLimitedToMimeFormatStrings() + { + foreach (array('plain', 'json', 'foobar') as $type) { + try { + $this->smd->setContentType($type); + $this->fail('Invalid content type should raise exception'); + } catch (InvalidArgumentException $e) { + $this->assertContains('Invalid content type', $e->getMessage()); + } + } + } + + public function testTargetShouldDefaultToNull() + { + $this->assertNull($this->smd->getTarget()); + } + + public function testTargetAccessorsShouldWorkUnderNormalInput() + { + $this->testTargetShouldDefaultToNull(); + $this->smd->setTarget('foo'); + $this->assertEquals('foo', $this->smd->getTarget()); + } + + public function testIdShouldDefaultToNull() + { + $this->assertNull($this->smd->getId()); + } + + public function testIdAccessorsShouldWorkUnderNormalInput() + { + $this->testIdShouldDefaultToNull(); + $this->smd->setId('foo'); + $this->assertEquals('foo', $this->smd->getId()); + } + + public function testDescriptionShouldDefaultToNull() + { + $this->assertNull($this->smd->getDescription()); + } + + public function testDescriptionAccessorsShouldWorkUnderNormalInput() + { + $this->testDescriptionShouldDefaultToNull(); + $this->smd->setDescription('foo'); + $this->assertEquals('foo', $this->smd->getDescription()); + } + + public function testDojoCompatibilityShouldBeDisabledByDefault() + { + $this->assertFalse($this->smd->isDojoCompatible()); + } + + public function testDojoCompatibilityFlagShouldBeMutable() + { + $this->testDojoCompatibilityShouldBeDisabledByDefault(); + $this->smd->setDojoCompatible(true); + $this->assertTrue($this->smd->isDojoCompatible()); + $this->smd->setDojoCompatible(false); + $this->assertFalse($this->smd->isDojoCompatible()); + } + + public function testServicesShouldBeEmptyByDefault() + { + $services = $this->smd->getServices(); + $this->assertTrue(is_array($services)); + $this->assertTrue(empty($services)); + } + + public function testShouldBeAbleToUseServiceObjectToAddService() + { + $service = new Smd\Service('foo'); + $this->smd->addService($service); + $this->assertSame($service, $this->smd->getService('foo')); + } + + public function testShouldBeAbleToUseArrayToAddService() + { + $service = array( + 'name' => 'foo', + ); + $this->smd->addService($service); + $foo = $this->smd->getService('foo'); + $this->assertTrue($foo instanceof Smd\Service); + $this->assertEquals('foo', $foo->getName()); + } + + public function testAddingServiceWithExistingServiceNameShouldThrowException() + { + $service = new Smd\Service('foo'); + $this->smd->addService($service); + $test = new Smd\Service('foo'); + try { + $this->smd->addService($test); + $this->fail('Adding service with existing service name should throw exception'); + } catch (RuntimeException $e) { + $this->assertContains('already register', $e->getMessage()); + } + } + + public function testAttemptingToRegisterInvalidServiceShouldThrowException() + { + foreach (array('foo', false, 1, 1.0) as $service) { + try { + $this->smd->addService($service); + $this->fail('Attempt to register invalid service should throw exception'); + } catch (InvalidArgumentException $e) { + $this->assertContains('Invalid service', $e->getMessage()); + } + } + } + + public function testShouldBeAbleToAddManyServicesAtOnceWithArrayOfServiceObjects() + { + $one = new Smd\Service('one'); + $two = new Smd\Service('two'); + $three = new Smd\Service('three'); + $services = array($one, $two, $three); + $this->smd->addServices($services); + $test = $this->smd->getServices(); + $this->assertSame($services, array_values($test)); + } + + public function testShouldBeAbleToAddManyServicesAtOnceWithArrayOfArrays() + { + $services = array( + array('name' => 'one'), + array('name' => 'two'), + array('name' => 'three'), + ); + $this->smd->addServices($services); + $test = $this->smd->getServices(); + $this->assertSame(array('one', 'two', 'three'), array_keys($test)); + } + + public function testShouldBeAbleToAddManyServicesAtOnceWithMixedArrayOfObjectsAndArrays() + { + $two = new Smd\Service('two'); + $services = array( + array('name' => 'one'), + $two, + array('name' => 'three'), + ); + $this->smd->addServices($services); + $test = $this->smd->getServices(); + $this->assertSame(array('one', 'two', 'three'), array_keys($test)); + $this->assertEquals($two, $test['two']); + } + + public function testSetServicesShouldOverwriteExistingServices() + { + $this->testShouldBeAbleToAddManyServicesAtOnceWithMixedArrayOfObjectsAndArrays(); + $five = new Smd\Service('five'); + $services = array( + array('name' => 'four'), + $five, + array('name' => 'six'), + ); + $this->smd->setServices($services); + $test = $this->smd->getServices(); + $this->assertSame(array('four', 'five', 'six'), array_keys($test)); + $this->assertEquals($five, $test['five']); + } + + public function testShouldBeAbleToRetrieveServiceByName() + { + $this->testShouldBeAbleToUseServiceObjectToAddService(); + } + + public function testShouldBeAbleToRemoveServiceByName() + { + $this->testShouldBeAbleToUseServiceObjectToAddService(); + $this->assertTrue($this->smd->removeService('foo')); + $this->assertFalse($this->smd->getService('foo')); + } + + public function testShouldBeAbleToCastToArray() + { + $options = $this->getOptions(); + $this->smd->setOptions($options); + $service = $this->smd->toArray(); + $this->validateServiceArray($service, $options); + } + + public function testShouldBeAbleToCastToDojoArray() + { + $options = $this->getOptions(); + $this->smd->setOptions($options); + $smd = $this->smd->toDojoArray(); + + $this->assertTrue(is_array($smd)); + + $this->assertTrue(array_key_exists('SMDVersion', $smd)); + $this->assertTrue(array_key_exists('serviceType', $smd)); + $this->assertTrue(array_key_exists('methods', $smd)); + + $this->assertEquals('.1', $smd['SMDVersion']); + $this->assertEquals('JSON-RPC', $smd['serviceType']); + $methods = $smd['methods']; + $this->assertEquals(2, count($methods)); + + $foo = array_shift($methods); + $this->assertTrue(array_key_exists('name', $foo)); + $this->assertTrue(array_key_exists('serviceURL', $foo)); + $this->assertTrue(array_key_exists('parameters', $foo)); + $this->assertEquals('foo', $foo['name']); + $this->assertEquals($this->smd->getTarget(), $foo['serviceURL']); + $this->assertTrue(is_array($foo['parameters'])); + $this->assertEquals(1, count($foo['parameters'])); + + $bar = array_shift($methods); + $this->assertTrue(array_key_exists('name', $bar)); + $this->assertTrue(array_key_exists('serviceURL', $bar)); + $this->assertTrue(array_key_exists('parameters', $bar)); + $this->assertEquals('bar', $bar['name']); + $this->assertEquals($this->smd->getTarget(), $bar['serviceURL']); + $this->assertTrue(is_array($bar['parameters'])); + $this->assertEquals(1, count($bar['parameters'])); + } + + public function testShouldBeAbleToRenderAsJSON() + { + $options = $this->getOptions(); + $this->smd->setOptions($options); + $json = $this->smd->toJSON(); + $smd = Json\Json::decode($json, Json\Json::TYPE_ARRAY); + $this->validateServiceArray($smd, $options); + } + + public function testToStringImplementationShouldProxyToJSON() + { + $options = $this->getOptions(); + $this->smd->setOptions($options); + $json = $this->smd->__toString(); + $smd = Json\Json::decode($json, Json\Json::TYPE_ARRAY); + $this->validateServiceArray($smd, $options); + } + + public function getOptions() + { + return array( + 'target' => '/test/me', + 'id' => '/test/me', + 'services' => array( + array( + 'name' => 'foo', + 'params' => array( + array('type' => 'boolean'), + ), + 'return' => 'boolean', + ), + array( + 'name' => 'bar', + 'params' => array( + array('type' => 'integer'), + ), + 'return' => 'string', + ), + ) + ); + } + + public function validateServiceArray(array $smd, array $options) + { + $this->assertTrue(is_array($smd)); + + $this->assertTrue(array_key_exists('SMDVersion', $smd)); + $this->assertTrue(array_key_exists('target', $smd)); + $this->assertTrue(array_key_exists('id', $smd)); + $this->assertTrue(array_key_exists('transport', $smd)); + $this->assertTrue(array_key_exists('envelope', $smd)); + $this->assertTrue(array_key_exists('contentType', $smd)); + $this->assertTrue(array_key_exists('services', $smd)); + + $this->assertEquals(Smd::SMD_VERSION, $smd['SMDVersion']); + $this->assertEquals($options['target'], $smd['target']); + $this->assertEquals($options['id'], $smd['id']); + $this->assertEquals($this->smd->getTransport(), $smd['transport']); + $this->assertEquals($this->smd->getEnvelope(), $smd['envelope']); + $this->assertEquals($this->smd->getContentType(), $smd['contentType']); + $services = $smd['services']; + $this->assertEquals(2, count($services)); + $this->assertTrue(array_key_exists('foo', $services)); + $this->assertTrue(array_key_exists('bar', $services)); + } +} diff --git a/test/ServerTest.php b/test/ServerTest.php new file mode 100644 index 000000000..7efbd02f6 --- /dev/null +++ b/test/ServerTest.php @@ -0,0 +1,476 @@ +server = new Server\Server(); + } + + /** + * Tears down the fixture, for example, close a network connection. + * This method is called after a test is executed. + * + * @return void + */ + public function tearDown() + { + } + + public function testShouldBeAbleToBindFunctionToServer() + { + $this->server->addFunction('strtolower'); + $methods = $this->server->getFunctions(); + $this->assertTrue($methods->hasMethod('strtolower')); + } + + public function testShouldBeAbleToBindCallbackToServer() + { + try { + $this->server->addFunction(array($this, 'setUp')); + } catch (\Zend\Server\Reflection\Exception\RuntimeException $e) { + $this->markTestIncomplete('PHPUnit docblocks may be incorrect'); + } + $methods = $this->server->getFunctions(); + $this->assertTrue($methods->hasMethod('setUp')); + } + + public function testShouldBeAbleToBindClassToServer() + { + $this->server->setClass('Zend\Json\Server\Server'); + $test = $this->server->getFunctions(); + $this->assertTrue(0 < count($test)); + } + + public function testBindingClassToServerShouldRegisterAllPublicMethods() + { + $this->server->setClass('Zend\Json\Server\Server'); + $test = $this->server->getFunctions(); + $methods = get_class_methods('Zend\Json\Server\Server'); + foreach ($methods as $method) { + if ('_' == $method[0]) { + continue; + } + $this->assertTrue($test->hasMethod($method), 'Testing for method ' . $method . ' against ' . var_export($test, 1)); + } + } + + public function testShouldBeAbleToBindObjectToServer() + { + $object = new Server\Server(); + $this->server->setClass($object); + $test = $this->server->getFunctions(); + $this->assertTrue(0 < count($test)); + } + + public function testBindingObjectToServerShouldRegisterAllPublicMethods() + { + $object = new Server\Server(); + $this->server->setClass($object); + $test = $this->server->getFunctions(); + $methods = get_class_methods($object); + foreach ($methods as $method) { + if ('_' == $method[0]) { + continue; + } + $this->assertTrue($test->hasMethod($method), 'Testing for method ' . $method . ' against ' . var_export($test, 1)); + } + } + + public function testShouldBeAbleToBindMultipleClassesAndObjectsToServer() + { + $this->server->setClass('Zend\Json\Server\Server') + ->setClass(new Json\Json()); + $methods = $this->server->getFunctions(); + $zjsMethods = get_class_methods('Zend\Json\Server\Server'); + $zjMethods = get_class_methods('Zend_JSON'); + $this->assertTrue(count($zjsMethods) < count($methods)); + $this->assertTrue(count($zjMethods) < count($methods)); + } + + public function testNamingCollisionsShouldResolveToLastRegisteredMethod() + { + $this->server->setClass('Zend\Json\Server\Request') + ->setClass('Zend\Json\Server\Response'); + $methods = $this->server->getFunctions(); + $this->assertTrue($methods->hasMethod('toJson')); + $toJSON = $methods->getMethod('toJson'); + $this->assertEquals('Zend\Json\Server\Response', $toJSON->getCallback()->getClass()); + } + + public function testGetRequestShouldInstantiateRequestObjectByDefault() + { + $request = $this->server->getRequest(); + $this->assertTrue($request instanceof Request); + } + + public function testShouldAllowSettingRequestObjectManually() + { + $orig = $this->server->getRequest(); + $new = new Request(); + $this->server->setRequest($new); + $test = $this->server->getRequest(); + $this->assertSame($new, $test); + $this->assertNotSame($orig, $test); + } + + public function testGetResponseShouldInstantiateResponseObjectByDefault() + { + $response = $this->server->getResponse(); + $this->assertTrue($response instanceof Response); + } + + public function testShouldAllowSettingResponseObjectManually() + { + $orig = $this->server->getResponse(); + $new = new Response(); + $this->server->setResponse($new); + $test = $this->server->getResponse(); + $this->assertSame($new, $test); + $this->assertNotSame($orig, $test); + } + + public function testFaultShouldCreateErrorResponse() + { + $response = $this->server->getResponse(); + $this->assertFalse($response->isError()); + $this->server->fault('error condition', -32000); + $this->assertTrue($response->isError()); + $error = $response->getError(); + $this->assertEquals(-32000, $error->getCode()); + $this->assertEquals('error condition', $error->getMessage()); + } + + public function testResponseShouldBeEmittedAutomaticallyByDefault() + { + $this->assertFalse($this->server->getReturnResponse()); + } + + public function testShouldBeAbleToDisableAutomaticResponseEmission() + { + $this->testResponseShouldBeEmittedAutomaticallyByDefault(); + $this->server->setReturnResponse(true); + $this->assertTrue($this->server->getReturnResponse()); + } + + public function testShouldBeAbleToRetrieveSmdObject() + { + $smd = $this->server->getServiceMap(); + $this->assertTrue($smd instanceof \Zend\Json\Server\Smd); + } + + public function testShouldBeAbleToSetArbitrarySmdMetadata() + { + $this->server->setTransport('POST') + ->setEnvelope('JSON-RPC-1.0') + ->setContentType('application/x-json') + ->setTarget('/foo/bar') + ->setId('foobar') + ->setDescription('This is a test service'); + + $this->assertEquals('POST', $this->server->getTransport()); + $this->assertEquals('JSON-RPC-1.0', $this->server->getEnvelope()); + $this->assertEquals('application/x-json', $this->server->getContentType()); + $this->assertEquals('/foo/bar', $this->server->getTarget()); + $this->assertEquals('foobar', $this->server->getId()); + $this->assertEquals('This is a test service', $this->server->getDescription()); + } + + public function testSmdObjectRetrievedFromServerShouldReflectServerState() + { + $this->server->addFunction('strtolower') + ->setClass('Zend\Json\Server\Server') + ->setTransport('POST') + ->setEnvelope('JSON-RPC-1.0') + ->setContentType('application/x-json') + ->setTarget('/foo/bar') + ->setId('foobar') + ->setDescription('This is a test service'); + $smd = $this->server->getServiceMap(); + $this->assertEquals('POST', $this->server->getTransport()); + $this->assertEquals('JSON-RPC-1.0', $this->server->getEnvelope()); + $this->assertEquals('application/x-json', $this->server->getContentType()); + $this->assertEquals('/foo/bar', $this->server->getTarget()); + $this->assertEquals('foobar', $this->server->getId()); + $this->assertEquals('This is a test service', $this->server->getDescription()); + + $services = $smd->getServices(); + $this->assertTrue(is_array($services)); + $this->assertTrue(0 < count($services)); + $this->assertTrue(array_key_exists('strtolower', $services)); + $methods = get_class_methods('Zend\Json\Server\Server'); + foreach ($methods as $method) { + if ('_' == $method[0]) { + continue; + } + $this->assertTrue(array_key_exists($method, $services)); + } + } + + public function testHandleValidMethodShouldWork() + { + $this->server->setClass('ZendTest\\Json\\Foo') + ->addFunction('ZendTest\\Json\\FooFunc') + ->setReturnResponse(true); + $request = $this->server->getRequest(); + $request->setMethod('bar') + ->setParams(array(true, 'foo', 'bar')) + ->setId('foo'); + $response = $this->server->handle(); + $this->assertTrue($response instanceof Response); + $this->assertFalse($response->isError()); + + + $request->setMethod('ZendTest\\Json\\FooFunc') + ->setId('foo'); + $response = $this->server->handle(); + $this->assertTrue($response instanceof Response); + $this->assertFalse($response->isError()); + } + + public function testHandleValidMethodWithTooFewParamsShouldPassDefaultsOrNullsForMissingParams() + { + $this->server->setClass('ZendTest\Json\Foo') + ->setReturnResponse(true); + $request = $this->server->getRequest(); + $request->setMethod('bar') + ->setParams(array(true)) + ->setId('foo'); + $response = $this->server->handle(); + $this->assertTrue($response instanceof Response); + $this->assertFalse($response->isError()); + $result = $response->getResult(); + $this->assertTrue(is_array($result)); + $this->assertTrue(3 == count($result)); + $this->assertEquals('two', $result[1], var_export($result, 1)); + $this->assertNull($result[2]); + } + + public function testHandleValidMethodWithTooManyParamsShouldWork() + { + $this->server->setClass('ZendTest\Json\Foo') + ->setReturnResponse(true); + $request = $this->server->getRequest(); + $request->setMethod('bar') + ->setParams(array(true, 'foo', 'bar', 'baz')) + ->setId('foo'); + $response = $this->server->handle(); + $this->assertTrue($response instanceof Response); + $this->assertFalse($response->isError()); + $result = $response->getResult(); + $this->assertTrue(is_array($result)); + $this->assertTrue(3 == count($result)); + $this->assertEquals('foo', $result[1]); + $this->assertEquals('bar', $result[2]); + } + + public function testHandleShouldAllowNamedParamsInAnyOrder1() + { + $this->server->setClass('ZendTest\Json\Foo') + ->setReturnResponse(true); + $request = $this->server->getRequest(); + $request->setMethod('bar') + ->setParams( array( + 'three' => 3, + 'two' => 2, + 'one' => 1 + )) + ->setId( 'foo' ); + $response = $this->server->handle(); + $result = $response->getResult(); + + $this->assertTrue( is_array( $result ) ); + $this->assertEquals( 1, $result[0] ); + $this->assertEquals( 2, $result[1] ); + $this->assertEquals( 3, $result[2] ); + } + + public function testHandleShouldAllowNamedParamsInAnyOrder2() + { + $this->server->setClass('ZendTest\Json\Foo') + ->setReturnResponse(true); + $request = $this->server->getRequest(); + $request->setMethod('bar') + ->setParams( array( + 'three' => 3, + 'one' => 1, + 'two' => 2, + ) ) + ->setId( 'foo' ); + $response = $this->server->handle(); + $result = $response->getResult(); + + $this->assertTrue( is_array( $result ) ); + $this->assertEquals( 1, $result[0] ); + $this->assertEquals( 2, $result[1] ); + $this->assertEquals( 3, $result[2] ); + } + + public function testHandleValidWithoutRequiredParamShouldReturnError() + { + $this->server->setClass('ZendTest\Json\Foo') + ->setReturnResponse(true); + $request = $this->server->getRequest(); + $request->setMethod('bar') + ->setParams( array( + 'three' => 3, + 'two' => 2, + ) ) + ->setId( 'foo' ); + $response = $this->server->handle(); + + $this->assertTrue($response instanceof Response); + $this->assertTrue($response->isError()); + $this->assertEquals(Server\Error::ERROR_INVALID_PARAMS, $response->getError()->getCode()); + } + + public function testHandleRequestWithErrorsShouldReturnErrorResponse() + { + $this->server->setClass('ZendTest\Json\Foo') + ->setReturnResponse(true); + $response = $this->server->handle(); + $this->assertTrue($response instanceof Response); + $this->assertTrue($response->isError()); + $this->assertEquals(Server\Error::ERROR_INVALID_REQUEST, $response->getError()->getCode()); + } + + public function testHandleRequestWithInvalidMethodShouldReturnErrorResponse() + { + $this->server->setClass('ZendTest\Json\Foo') + ->setReturnResponse(true); + $request = $this->server->getRequest(); + $request->setMethod('bogus') + ->setId('foo'); + $response = $this->server->handle(); + $this->assertTrue($response instanceof Response); + $this->assertTrue($response->isError()); + $this->assertEquals(Server\Error::ERROR_INVALID_METHOD, $response->getError()->getCode()); + } + + public function testHandleRequestWithExceptionShouldReturnErrorResponse() + { + $this->server->setClass('ZendTest\Json\Foo') + ->setReturnResponse(true); + $request = $this->server->getRequest(); + $request->setMethod('baz') + ->setId('foo'); + $response = $this->server->handle(); + $this->assertTrue($response instanceof Response); + $this->assertTrue($response->isError()); + $this->assertEquals(Server\Error::ERROR_OTHER, $response->getError()->getCode()); + $this->assertEquals('application error', $response->getError()->getMessage()); + } + + public function testHandleShouldEmitResponseByDefault() + { + $this->server->setClass('ZendTest\Json\Foo'); + $request = $this->server->getRequest(); + $request->setMethod('bar') + ->setParams(array(true, 'foo', 'bar')) + ->setId('foo'); + ob_start(); + $this->server->handle(); + $buffer = ob_get_clean(); + + $decoded = Json\Json::decode($buffer, Json\Json::TYPE_ARRAY); + $this->assertTrue(is_array($decoded)); + $this->assertTrue(array_key_exists('result', $decoded)); + $this->assertTrue(array_key_exists('id', $decoded)); + + $response = $this->server->getResponse(); + $this->assertEquals($response->getResult(), $decoded['result']); + $this->assertEquals($response->getId(), $decoded['id']); + } + + public function testResponseShouldBeEmptyWhenRequestHasNoId() + { + $this->server->setClass('ZendTest\Json\Foo'); + $request = $this->server->getRequest(); + $request->setMethod('bar') + ->setParams(array(true, 'foo', 'bar')); + ob_start(); + $this->server->handle(); + $buffer = ob_get_clean(); + + $this->assertTrue(empty($buffer)); + } + + public function testLoadFunctionsShouldLoadResultOfGetFunctions() + { + $this->server->setClass('ZendTest\Json\Foo'); + $functions = $this->server->getFunctions(); + $server = new Server\Server(); + $server->loadFunctions($functions); + $this->assertEquals($functions->toArray(), $server->getFunctions()->toArray()); + } +} + +/** + * Class for testing JSON-RPC server + */ +class Foo +{ + /** + * Bar + * + * @param bool $one + * @param string $two + * @param mixed $three + * @return array + */ + public function bar($one, $two = 'two', $three = null) + { + return array($one, $two, $three); + } + + /** + * Baz + * + * @return void + */ + public function baz() + { + throw new \Exception('application error'); + } +} + +/** + * Test function for JSON-RPC server + * + * @return bool + */ +function FooFunc() +{ + return true; +} diff --git a/test/TestAsset/TestIteratorAggregate.php b/test/TestAsset/TestIteratorAggregate.php new file mode 100644 index 000000000..742b9a993 --- /dev/null +++ b/test/TestAsset/TestIteratorAggregate.php @@ -0,0 +1,27 @@ + 'bar', + 'baz' => 5 + ); + + public function getIterator() + { + return new \ArrayIterator($this->array); + } +} diff --git a/test/bootstrap.php b/test/bootstrap.php new file mode 100644 index 000000000..095cc0c66 --- /dev/null +++ b/test/bootstrap.php @@ -0,0 +1,34 @@ +