{$flash->message}
25 | {/foreach} 26 | 27 | {include #content} 28 |├── test-suite ├── jsonTestModule │ ├── templates │ │ ├── Prototypes │ │ │ ├── submenu.latte │ │ │ ├── submenu-methods.latte │ │ │ ├── form-input.latte │ │ │ ├── form-array.latte │ │ │ ├── form-select.latte │ │ │ ├── form-checkbox.latte │ │ │ ├── form.latte │ │ │ ├── form-abstract.latte │ │ │ └── form-object.latte │ │ ├── Base │ │ │ ├── moduleDefault.latte │ │ │ ├── listModules.latte │ │ │ ├── moduleMethod.latte │ │ │ └── jsonAcessLogin.latte │ │ └── @layout.latte │ └── presenters │ │ ├── UserPresenter.php │ │ ├── AccessPresenter.php │ │ └── BasePresenter.php ├── jsonTest.css ├── README.rst └── JsonTestSuitePresenter.php ├── lib ├── exceptions.php ├── Client.php └── Server.php ├── composer.json ├── tests ├── clientTest.php ├── serverTest.php └── consoleTest.php ├── LICENSE.txt ├── README.md └── example └── nette.presenter.php /test-suite/jsonTestModule/templates/Prototypes/submenu.latte: -------------------------------------------------------------------------------- 1 |
2 | {foreach $modules as $module} 3 | {$module['name']} 4 | {sep} | {/sep} 5 | {/foreach} 6 |
-------------------------------------------------------------------------------- /test-suite/jsonTestModule/templates/Prototypes/submenu-methods.latte: -------------------------------------------------------------------------------- 1 |2 | {foreach $methods as $method} 3 | {$method['name']} 4 | {sep} | {/sep} 5 | {/foreach} 6 |
-------------------------------------------------------------------------------- /test-suite/jsonTestModule/templates/Base/moduleDefault.latte: -------------------------------------------------------------------------------- 1 | {block title}{$currentModule}{/block} 2 | {block content} 3 |{$currentModule}:
4 | 5 | {foreach $methods as $method} 6 | {first}Within the json API test suite, we have following modules available:
4 | 5 | {foreach $modules as $module} 6 | {first}
9 | request:
10 | {$jsonRequest}
11 |
12 |
13 | response:
14 | {$jsonResponse}
15 |
16 | {/if}
17 | Welcome to the test suite of the <projectname> API.
4 |Before you can proceed, you need to log-in. Note that these credetials are 5 | not to the API itself, it's just for to the Test Suite.
6 | 7 | 16 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "foglcz/jsonrpc2", 3 | "description": "jsonrpc2 implementation for generic use. Test suite for Nette Framework included.", 4 | "keywords": ["json", "rpc", "nette"], 5 | "homepage": "http://github.com/foglcz/JSONRpc2", 6 | "license": ["New BSD"], 7 | "authors":[ 8 | { 9 | "name": "Pavel Ptáček", 10 | "homepage": "http://animalgroup.cz" 11 | } 12 | ], 13 | "support": { 14 | "issues": "https://github.com/foglcz/JSONRpc2/issues" 15 | }, 16 | "require":{ 17 | "php": ">=5.3.2" 18 | }, 19 | "autoload": { 20 | "classmap": ["lib/"] 21 | } 22 | } -------------------------------------------------------------------------------- /test-suite/jsonTestModule/templates/Prototypes/form.latte: -------------------------------------------------------------------------------- 1 |2 | debug = true; 15 | 16 | echo 'call $client->notify();
'; 17 | var_dump($client->notify()); 18 | 19 | echo '
'; 20 | echo 'call $client->echo("something");
'; 21 | var_dump($client->echo('something')); 22 | 23 | echo '
'; 24 | echo 'call $client->math->sum(25, 30, 45);
'; 25 | var_dump($client->math->sum(25, 30, 45)); 26 | 27 | echo '
'; 28 | echo 'call $client->math2->special->substract(25, 5, 10);
'; 29 | var_dump($client->math2->special->substract(25, 5, 10)); 30 | 31 | echo '
'; 32 | echo 'call $client->__call("math2.special.substract", array(25, 5, 10));
'; 33 | var_dump($client->__call('math2.special.substract', array(25, 5, 10))); -------------------------------------------------------------------------------- /tests/serverTest.php: -------------------------------------------------------------------------------- 1 | notify = function() { 17 | return 'notify called'; 18 | }; 19 | 20 | $server->echo = function($what) { 21 | return $what; 22 | }; 23 | 24 | class Math { 25 | public function sum() { 26 | $args = func_get_args(); 27 | $result = 0; 28 | foreach($args as $one) { 29 | $result += $one; 30 | } 31 | 32 | return $result; 33 | } 34 | } 35 | $server->math = new Math; 36 | 37 | $server->{'math2.special.substract'} = function() { 38 | $args = func_get_args(); 39 | $result = array_shift($args); 40 | foreach($args as $one) { 41 | $result -= $one; 42 | } 43 | 44 | return $result; 45 | }; 46 | 47 | // Handle! 48 | $server->handle(); -------------------------------------------------------------------------------- /test-suite/jsonTestModule/templates/@layout.latte: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |{include #title} 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |14 | 21 | 22 |JSON API Test suite
15 | 16 | {if $presenter->isLoggedIn()} 17 | {include 'Prototypes/submenu.latte'} 18 | {if !empty($methods)}{include 'Prototypes/submenu-methods.latte' methods => $methods}{/if} 19 | {/if} 20 |23 | {foreach $flashes as $flash} 24 |29 | 30 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /test-suite/jsonTest.css: -------------------------------------------------------------------------------- 1 | h1,h2,h3,h4,h5,h6,form,body,html,p { margin:0; padding:0; list-style-type:none; border:none; } 2 | 3 | body { text-align: left; font: .9em Arial; padding-bottom: 40px; } 4 | 5 | .clear { clear: both; } 6 | 7 | h2 { margin-top: 25px; font: 1em "Courier New"; font-weight: bold; } 8 | 9 | #footer { position: fixed; bottom: 0; width: 100%; height: 40px; background-color: #657e20; color: #fff; z-index: 100; opacity: .8; } 10 | #footer a { color: #fff; } 11 | #footer a:hover { color: #aaa; } 12 | #footer .left { float: left; margin: 14px 0 0 40px; } 13 | #footer .right { float: right; margin: 14px 40px 0 0; } 14 | 15 | header, #content { padding: 10px; } 16 | 17 | form { margin-top: 10px; margin-bottom: 10px; } 18 | form fieldset { float: left; margin-left: 10px; margin-top: 10px; border: 1px solid black; } 19 | form fieldset input, form fieldset select { border-color: #ccc; } 20 | form label { font: .9em "Courier New"; display: block; float: left; width: 150px; padding: 5px; padding-top: 8px; text-align: right; clear: left; } 21 | form input, form select { font: .9em "Courier New"; display: block; float: left; margin-left: 10px; margin-top: 5px; padding: 2px 5px; border: 1px solid black; width: 230px; } 22 | form button { display: block; width: 400px; border: 1px solid black; background-color: #aaa; clear: left; float: left; margin: 10px; } 23 | 24 | /* generic test page */ 25 | #request { border: 1px dashed black; padding: 10px; float: left; clear: left; background-color: #F4CDBE; } 26 | #response { border: 1px dashed black; margin-left: 25px; float: left; padding: 10px; background-color: #ff9; } 27 | code { font: .9em "Courier New"; } 28 | input.chk { width: auto; margin-top: 8px; } -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | This package is released under New BSD License. 2 | 3 | New BSD License 4 | --------------- 5 | 6 | Copyright (c) 2011 Pavel Ptacek (birdie at animalgroup dot cz) 7 | All rights reserved. 8 | 9 | Redistribution and use in source and binary forms, with or without modification, 10 | are permitted provided that the following conditions are met: 11 | 12 | * Redistributions of source code must retain the above copyright notice, 13 | this list of conditions and the following disclaimer. 14 | 15 | * Redistributions in binary form must reproduce the above copyright notice, 16 | this list of conditions and the following disclaimer in the documentation 17 | and/or other materials provided with the distribution. 18 | 19 | * Neither the name of "Lightbulb Project" nor the names of its contributors 20 | may be used to endorse or promote products derived from this software 21 | without specific prior written permission. 22 | 23 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 24 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 25 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 26 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 27 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 28 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 29 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 30 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 31 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 32 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JSON-RPC client and server libraries 2 | 3 | This library contains a PHP implementation of JSON-RPC version 2, both client and server. 4 | 5 | ## Installation 6 | 7 | Download the contents of the `lib` folder to your project. Then simply include the library: 8 | 9 | ``` 10 | include "lib/Server.php"; 11 | ``` 12 | ### Server methods 13 | 14 | $server = new Lightbulb\Json\Rpc2\Server; 15 | 16 | // Class based where all the methods in myClass are exposed as user.method 17 | $server->user = new MyClass; 18 | 19 | // Anything that is "callable", either built in PHP functions or your own 20 | $server->upper = 'strtoupper'; 21 | $server->userClean = 'userClean'; 22 | 23 | // Anonymous functions work too 24 | $server->firstTwo = function($str) { return substr($str,0,2); }; 25 | 26 | // Force a namespace to map to an object method 27 | $server->{'mytesthandler.myfunc'} = array($myObject, 'myMethod'); 28 | 29 | // Static method calls work 30 | $server->myStaticHandler = 'MyStaticClass::theStaticFunction'; 31 | 32 | The methods, which are given to the server, can be then called via numbered 33 | or named parameters (see json-rpc specification here: http://groups.google.com/group/json-rpc/web/json-rpc-2-0?pli=1 ) 34 | 35 | The server class respects binding of event methods: 36 | 37 | // Bind events 38 | $server->onBeforeCall[] = function($server) {}; 39 | $server->onBeforeCall[] = function($server) {}; 40 | $server->onSuccess[] = function($server) {}; 41 | $server->onError[] = function($server) {}; 42 | 43 | For detailed usage see comments with the server and clients class. 44 | For detailed tests see tests folder. 45 | 46 | ### Client calls 47 | 48 | $client = new Lightbulb\Json\Rpc2\Client('http://api.domain.com/endpoint'); 49 | $client->upper("kitten"); 50 | $client->firstTwo("Hello"); 51 | 52 | #### Client supports class chaining to call nested methods 53 | 54 | $ok = $client->user->login($user, $pass); 55 | 56 | will actually result in following json call: 57 | 58 | {... method: "user.login" ...} 59 | 60 | ## License 61 | Licensed under the New BSD License. Copyright 2011 Pavel Ptacek. All rights reserved. 62 | -------------------------------------------------------------------------------- /test-suite/jsonTestModule/presenters/UserPresenter.php: -------------------------------------------------------------------------------- 1 | 8 | * @copyright Pavel Ptacek and Animal Group{$flash->message}
25 | {/foreach} 26 | 27 | {include #content} 28 |9 | * @license New BSD License 10 | */ 11 | 12 | namespace jsonTestModule; 13 | 14 | /** 15 | * Class for testing user.* methods 16 | * 17 | * @author Pavel Ptacek 18 | */ 19 | class UserPresenter extends BasePresenter { 20 | /** 21 | * user.getInfo 22 | */ 23 | public function renderGetInfo() { 24 | $this->template->formData = array( 25 | 'method' => 'user.getInfo', 26 | 'params' => array( 27 | 'access_token' => $this->getToken(), 28 | 'last_update' => 'timestamp:optional', 29 | ), 30 | ); 31 | } 32 | 33 | /** 34 | * user.store 35 | */ 36 | public function renderStore() { 37 | // Create user object for the request generator 38 | $obj = new \stdClass; 39 | $obj->_objectName = 'user_object'; 40 | $obj->id = null; 41 | $obj->language = array('czech', 'english', 'german'); 42 | $obj->name = null; 43 | $obj->surname = null; 44 | $obj->gender = array('male', 'female'); 45 | $obj->year_of_birth = null; 46 | $obj->email = null; 47 | $obj->password = null; 48 | $obj->last_logins = '~array~'; 49 | $obj->registered = false; // checkbox 50 | 51 | // Create sub-data object 52 | $sub = new \stdClass; 53 | $sub->_objectName = 'sub_object'; 54 | $sub->enum = array('one', 'two', 'three'); 55 | $sub->something = null; 56 | $sub->def = 'default value'; 57 | 58 | // And assign the sub object to the user object 59 | $obj->subobject = $sub; 60 | 61 | // And append the formdata 62 | $this->template->formData = array( 63 | 'method' => 'user.store', 64 | 'params' => array( 65 | 'access_token' => $this->getToken(), 66 | 'user' => $obj, 67 | ), 68 | ); 69 | } 70 | } -------------------------------------------------------------------------------- /test-suite/jsonTestModule/presenters/AccessPresenter.php: -------------------------------------------------------------------------------- 1 | 8 | * @copyright Pavel Ptacek and Animal Group 9 | * @license New BSD License 10 | */ 11 | 12 | namespace jsonTestModule; 13 | 14 | /** 15 | * The test presenter for access.* functions 16 | * 17 | * @author Pavel Ptacek 18 | */ 19 | class AccessPresenter extends BasePresenter { 20 | /** 21 | * Prepare login function 22 | */ 23 | public function renderLogin() { 24 | if(!empty($_POST['sha1enc'])) { 25 | $this->template->encrypted = sha1($_POST['sha1enc']); 26 | } 27 | 28 | if(!empty($this->template->parsedResponse) && !empty($this->template->parsedResponse->result)) { 29 | $this->getSession('Access')->token = $this->template->parsedResponse->result->token; 30 | $this->getSession('Access')->secret = $this->template->parsedResponse->result->secret; 31 | } 32 | 33 | $this->template->formData = array( 34 | 'method' => 'access.login', 35 | 'params' => array( 36 | 'email' => null, 37 | 'password' => null, 38 | 'language' => array('czech', 'english', 'slovak'), 39 | ), 40 | ); 41 | } 42 | 43 | /** 44 | * Prepare logout function 45 | */ 46 | public function renderLogout() { 47 | $sess = $this->getSession('Access'); 48 | $token = isset($sess->token) ? $sess->token : null; 49 | 50 | if(!empty($this->template->parsedResponse)) { 51 | $s = $this->getSession('Access'); 52 | unset($s->token); 53 | unset($s->secret); 54 | } 55 | 56 | $this->template->formData = array( 57 | 'method' => 'access.logout', 58 | 'params' => array( 59 | 'access_token' => $token, 60 | ), 61 | ); 62 | } 63 | 64 | /** 65 | * Prepare extend token function 66 | */ 67 | public function renderExtendToken() { 68 | $sess = $this->getSession('Access'); 69 | $token = isset($sess->token) ? $sess->token : null; 70 | $secret = isset($sess->secret) ? $sess->secret : null; 71 | 72 | $this->template->formData = array( 73 | 'method' => 'access.extendToken', 74 | 'params' => array( 75 | 'access_token' => $token, 76 | 'secret' => $secret, 77 | 'language' => array('czech', 'english', 'german'), 78 | ), 79 | ); 80 | } 81 | } -------------------------------------------------------------------------------- /example/nette.presenter.php: -------------------------------------------------------------------------------- 1 | 6 | * @license New BSD License 7 | */ 8 | 9 | /** 10 | * This is example implementation under Nette Framework (TM) 11 | * 12 | * @author Pavel Ptacek 13 | */ 14 | final class JsonPresenter extends BasePresenter { 15 | public function renderDefault() { 16 | // Get server 17 | $server = new Lightbulb\Json\Rpc2\Server; 18 | 19 | // Bind available functions 20 | $server->myTest = new MyTestHandler; // contains mytest.* methods 21 | $server->myFunction = function($param1, $param2) { /* ... */ }; 22 | $server->{'mytesthandler.myfunc'} = array($myObject, 'myMethod'); 23 | $server->myStaticHandler = 'MyStaticClass::theStaticFunction'; 24 | 25 | // Bind events 26 | $server->onBeforeCall[] = function($server) {}; 27 | $server->onBeforeCall[] = function($server) {}; 28 | $server->onSuccess[] = function($server) {}; 29 | $server->onError[] = function($server) {}; 30 | 31 | // Another way of in-binding the events; it does *not* remove the last one 32 | $server->onError = function($server) {}; 33 | 34 | // Supress handling of output, as we are sending it differently 35 | $server->supressOutput(); 36 | 37 | // When running the request, we need to check whether there has been anything returned. If not, 38 | // then we have to send an empty text response, as Nette\...\JsonResponse throws exception 39 | // on empty responses. 40 | $response = $server->handle(); 41 | if(!empty($response) || is_array($response)) { // is_array in order to send [] 42 | $this->sendResponse(new \Nette\Application\Responses\JsonResponse($response)); 43 | } 44 | else { 45 | $this->sendResponse(new \Nette\Application\Responses\TextResponse('')); 46 | } 47 | } 48 | 49 | /* test usage within the same presenter */ 50 | public function renderTest() { 51 | $client = new Lightbulb\Json\Rpc2\Client('http://localhost/nette/endpoint'); 52 | 53 | // Call functions that are binded in renderDefault method 54 | $result = $client->myTest->someFunc($param1, $param2); 55 | $result = $client->myFunction($param1, $param2); 56 | $result = $client->myTestHandler->myFunc(); 57 | $result = $client->myStaticHandler($param); 58 | 59 | // $result always contain as per the RFC either: 60 | var_dump($result->result); 61 | 62 | // Or an error if there has been some 63 | var_dump($result->error); 64 | } 65 | } -------------------------------------------------------------------------------- /test-suite/README.rst: -------------------------------------------------------------------------------- 1 | ====================== 2 | NETTE API TEST SUITE 3 | ====================== 4 | This is an example (base module) for creating a test suite on top of Nette 5 | Framework. 6 | 7 | We used the test-suite in order to develop easily a web-based UI to test all 8 | of the methods, with the required / optional arguments. Note that this is 9 | just a test suite, which connects to your server endpoint and therefore, 10 | it is not a "documentation-generator." We used common Microsoft Word in order 11 | to do that. 12 | 13 | Installation & fast start 14 | ========================= 15 | 1) Copy contents of "lib" folder to your Nette Framework project 16 | 2) Put the "jsonTestModule" folder into the app/ folder of your Nette Framework project. 17 | 3) Copy the jsonTest.css file into your www/css/ folder of Nette Framework project. 18 | 4) Append following routes to your bootstrap.php file:: 19 | 20 | $router[] = new Route('rpc/test', 'jsonTest:Base:jsonAcessLogin'); 21 | $router[] = new Route('rpc/test/ [/ ]', array('module' => 'jsonTest')); 22 | 23 | 5) Copy JsonTestSuitePresenter.php from test-suite/ folder & create following route:: 24 | 25 | $router[] = new Route('rpc/json', 'JsonTestSuite:default'); 26 | 27 | 6) Edit jsonTestModule/presenters/BasePresenter.php:63 & set-up credentials 28 | 7) Go to "your-project-url/rpc/test", hack around and enjoy the class! 29 | 30 | Should you want to use different endpoint from the "JsonTestSuite:default", which 31 | is for example only, edit the endpoint within jsonTestModule/presenters/BasePresenter.php 32 | class, in the startup() method. 33 | 34 | Example set-up 35 | ============== 36 | The example set-up is based on our real-world implementation. We used the "module" 37 | features of the JsonRPC Server ("the dot-magic") in following manner: 38 | 39 | - Every application login requires to call access.* methods first, in order to obtain 40 | an access-token. That is a string, which is then used subsequently in order to 41 | authenticate user against application & no credentials are actually beign sent 42 | with every request towards the API. 43 | - Every token has limited time-validity (in our case, that's 14 days) after which 44 | it needs to be extended via access.extendToken method 45 | - When user logs-out, the token is destroyed. 46 | 47 | Hence, prior to calling any methods of the Json API, even in test suite, you need 48 | to call access.login which will return the token. That token is subsequently saved 49 | in session and you can see how it's provided as default value. 50 | 51 | Keep in mind 52 | ============ 53 | - The modules (access.*) are loaded automatically based on classes within "jsonTestModule" namespace 54 | - The methods (*.something) are loaded automatically from those classes, based on 55 | "render*" methods. 56 | 57 | Implementation of custom methods 58 | ================================ 59 | Everything is custom-generated based on arrays provided to the template. The forms 60 | are automatically proccessed and every parameter can be un-checked in order not to 61 | send it within request towards the API (hence you can test optional arguments.) 62 | 63 | The request / response forms are auto-generated. All you need to do is implement 64 | respective render*() method within respective presenter, and pass "$formData" 65 | parameter to the template. 66 | 67 | The structure of the formData array is as follows:: 68 | 69 | array( 70 | 'method' => 'the name of json method, eg user.getInfo', 71 | 'params' => array( 72 | // the array of parameters, eg: 73 | 'first' => null, 74 | 'second' => 'parameterName', 75 | 'third' => 'parameterName:datatype', 76 | 'fourth' => array('a', 'b', 'c'), // this is "select box variable" 77 | 'fifth' => false, // to generate a checkbox 78 | 'sixth' => '~array~', // pass "~array~" in order to generate 5 inputs for parameter 79 | 'seventh' => $object, // in order to pass an object 80 | ) 81 | ) 82 | 83 | When passing an object, it has to be an instance of a "stdClass". Also, it has to have 84 | one property set, which is "_objectName" - that is used as title to the object. 85 | 86 | Other properties of the object will be generated respectively to the structure of params. 87 | 88 | Support 89 | ======= 90 | Contact me directly using e-mail at or on twitter 91 | @foglcz , or of course, in here. 92 | 93 | License 94 | ======= 95 | Licensed under the New BSD License. Copyright 2011 Pavel Ptacek. All rights reserved. 96 | -------------------------------------------------------------------------------- /test-suite/JsonTestSuitePresenter.php: -------------------------------------------------------------------------------- 1 | 6 | * @license New BSD License 7 | */ 8 | 9 | /** 10 | * This is example implementation under Nette Framework (TM) 11 | * 12 | * @author Pavel Ptacek 13 | */ 14 | final class JsonTestSuitePresenter extends BasePresenter { 15 | public function renderDefault() { 16 | // Get server 17 | $server = new Lightbulb\Json\Rpc2\Server; 18 | $server->access = new AccessHandler; 19 | $server->user = new UserHandler; 20 | 21 | // Supress handling of output, as we are sending it differently 22 | $server->supressOutput(); 23 | 24 | // When running the request, we need to check whether there has been anything returned. If not, 25 | // then we have to send an empty text response, as Nette\...\JsonResponse throws exception 26 | // on empty responses. 27 | $response = $server->handle(); 28 | if(!empty($response) || is_array($response)) { // is_array in order to send [] 29 | $this->sendResponse(new \Nette\Application\Responses\JsonResponse($response)); 30 | } 31 | else { 32 | $this->sendResponse(new \Nette\Application\Responses\TextResponse('')); 33 | } 34 | } 35 | } 36 | 37 | /** 38 | * Access handler used for examples 39 | */ 40 | class AccessHandler { 41 | const TOKEN = '123'; 42 | const SECRET = '456'; 43 | 44 | /** 45 | * Log-in the user into API 46 | * @param string $email 47 | * @param string $password sha1 encoded 48 | * @param string $language czech|english|german 49 | */ 50 | public function login($email, $password, $language) { 51 | // Handle the login function with database 52 | 53 | // Prepare the token: 54 | $output = new \stdClass; 55 | $output->token = self::TOKEN; 56 | $output->secret = self::SECRET; 57 | $output->valid = date('Y-m-d H:i:s', strtotime('+14 days')); 58 | return $output; 59 | } 60 | 61 | /** 62 | * Log-out user 63 | * @param string $access_token 64 | */ 65 | public function logout($access_token) { 66 | self::validate($access_token); 67 | 68 | // .. remove the token from database .. 69 | 70 | return true; 71 | } 72 | 73 | /** 74 | * Extend the token 75 | * @param string $access_token 76 | * @param string $secret 77 | * @param string $language 78 | */ 79 | public function extendToken($access_token, $secret, $language) { 80 | self::validate($access_token); 81 | 82 | // Check secret 83 | if($secret != self::SECRET) { 84 | throw new \Exception('Invalid token secret', -31002); 85 | } 86 | 87 | // .. extend token within database & return new one .. 88 | $output = new \stdClass; 89 | $output->token = self::TOKEN; 90 | $output->secret = self::SECRET; 91 | $output->valid = date('Y-m-d H:i:s', strtotime('+14 days')); 92 | return $output; 93 | } 94 | 95 | /** 96 | * Static function for the sake of the example 97 | * 98 | * @param string $access_token 99 | * @throws Exception 100 | * @return bool 101 | */ 102 | public static function validate($access_token) { 103 | if($access_token != self::TOKEN) { 104 | throw new \Exception('Invalid access token', -31001); // note the error code 105 | } 106 | 107 | return true; 108 | } 109 | } 110 | 111 | 112 | /** 113 | * User handler used for examples 114 | */ 115 | class UserHandler { 116 | /** 117 | * Returns generic user information 118 | * @param string $access_token 119 | * @param mixed $last_update unix timestamp 120 | */ 121 | public function getInfo($access_token, $last_update = null) { 122 | AccessHandler::validate($access_token); 123 | 124 | // Prepare the return object 125 | $ret = new \stdClass; 126 | $ret->user_id = 1; 127 | $ret->username = 'example user'; 128 | $ret->roles = array('admin', 'user'); 129 | 130 | // Check whether the last update has been provided or not 131 | // !! WARNING: you CANNOT use "===" operator, as the PHP is .. the way it is 132 | if($last_update != null) { // intentional == 133 | $ret->requested_with_lastupdate = true; 134 | } 135 | else { 136 | $ret->requested_with_lastupdate = false; 137 | } 138 | 139 | return $ret; 140 | } 141 | 142 | /** 143 | * Stores given user object within database 144 | * @param string $access_token 145 | * @param stdClass $user 146 | */ 147 | public function store($access_token, $user) { 148 | AccessHandler::validate($access_token); 149 | 150 | // parse the user object, which is given as stdClass: 151 | // eg: $user = MyUserClass::fromJson($user); 152 | 153 | // And for the sake of example, return the sent user object 154 | $user->recieved = date('Y-m-d H:i:s'); 155 | return $user; 156 | } 157 | } -------------------------------------------------------------------------------- /tests/consoleTest.php: -------------------------------------------------------------------------------- 1 | 8 | * @copyright Pavel Ptacek and Animal Group 9 | * @license New BSD License 10 | */ 11 | 12 | /** 13 | * The JSON-RPCv2 test suite: http://groups.google.com/group/json-rpc/web/json-rpc-2-0?pli=1 14 | */ 15 | $requests = array( 16 | // RPC with positional parameters: 17 | '{"jsonrpc": "2.0", "method": "substract", "params": [42, 23], "id": 1}', 18 | '{"jsonrpc": "2.0", "method": "substract", "params": [23, 42], "id": 2}', 19 | 20 | // RPC with named parameters 21 | '{"jsonrpc": "2.0", "method": "substract", "params": {"subtrahend": 23, "minuend": 42}, "id": 3}', 22 | '{"jsonrpc": "2.0", "method": "substract", "params": {"minuend": 42, "subtrahend": 23}, "id": 4}', 23 | 24 | // Notifications: 25 | '{"jsonrpc": "2.0", "method": "update", "params": [1,2,3,4,5]}', 26 | '{"jsonrpc": "2.0", "method": "notificator.test"}', 27 | 28 | // Non existent method: 29 | '{"jsonrpc": "2.0", "method": "foobar", "id": "1"}', 30 | 31 | // RPC call with invalid request object: 32 | '{"jsonrpc": "2.0", "method": 1, "params": "bar"}', 33 | 34 | // RPC batch, invalid json: 35 | '[ {"jsonrpc": "2.0", "method": "sum", "params": [1,2,4], "id": "1"},{"jsonrpc": "2.0", "method" ]', 36 | 37 | // RPC call with empty Array 38 | '[]', 39 | 40 | // RPC call with an invalid Batch (but not empty) 41 | '[1]', 42 | 43 | // rpc call with an invalid Batch 44 | '[1,2,3]', 45 | 46 | // RPC call batch: 47 | '[ 48 | {"jsonrpc": "2.0", "method": "math.sum", "params": [1,2,4], "id": "1"}, 49 | {"jsonrpc": "2.0", "method": "notify_hello", "params": [7]}, 50 | {"jsonrpc": "2.0", "method": "substract", "params": [42,23], "id": "2"}, 51 | {"foo": "boo"}, 52 | {"jsonrpc": "2.0", "method": "foo.get", "params": {"name": "myself"}, "id": "5"}, 53 | {"jsonrpc": "2.0", "method": "get_data", "id": "9"} 54 | ]', 55 | 56 | // RPC call batch (all notifications) 57 | '[ 58 | {"jsonrpc": "2.0", "method": "notify_sum", "params": [1,2,4]}, 59 | {"jsonrpc": "2.0", "method": "notify_hello", "params": [7]} 60 | ]', 61 | ); 62 | 63 | $responses = array( 64 | // Positional parameters: 65 | '{"jsonrpc": "2.0", "result": 19, "id": 1}', 66 | '{"jsonrpc": "2.0", "result": -19, "id": 2}', 67 | 68 | // Named parameters: 69 | '{"jsonrpc": "2.0", "result": -19, "id": 3}', 70 | '{"jsonrpc": "2.0", "result": -19, "id": 4}', 71 | 72 | // Notifications: 73 | '', 74 | '', 75 | 76 | // Non-existent method: 77 | '{"jsonrpc": "2.0", "error": {"code": -32601, "message": "Procedure not found."}, "id": "1"}', 78 | 79 | // RPC call with invalid request object: 80 | '{"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request."}, "id": null}', 81 | 82 | // RPC batch, invalid json: 83 | '{"jsonrpc": "2.0", "error": {"code": -32700, "message": "Parse error."}, "id": null}', 84 | 85 | // RPC call with empty Array 86 | '{"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request."}, "id": null}', 87 | 88 | // RPC call with an invalid batch (but not empty( 89 | '[ 90 | {"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request."}, "id": null} 91 | ]', 92 | 93 | // RPC call with an invalid batch 94 | '[ 95 | {"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request."}, "id": null}, 96 | {"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request."}, "id": null}, 97 | {"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request."}, "id": null} 98 | ]', 99 | 100 | // RPC call batch: 101 | '[ 102 | {"jsonrpc": "2.0", "result": 7, "id": "1"}, 103 | {"jsonrpc": "2.0", "result": 19, "id": "2"}, 104 | {"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request."}, "id": null}, 105 | {"jsonrpc": "2.0", "error": {"code": -32601, "message": "Method not found."}, "id": "5"}, 106 | {"jsonrpc": "2.0", "result": ["hello", 5], "id": "9"} 107 | ]', 108 | 109 | // RPC call batch (all notifications) 110 | '', // nothing is returned for notifications 111 | ); 112 | 113 | /** 114 | * Test suite 115 | */ 116 | require_once __DIR__ . '/../lib/Server.php'; 117 | $server = new Lightbulb\Json\Rpc2\Server; 118 | 119 | // Bind on* methods 120 | //$server->onBeforeCall[] = function() { 121 | // echo 'first onBeforeCall called
'; 122 | //}; 123 | //$server->onBeforeCall = function() { 124 | // echo 'second onBeforeCall called
'; 125 | //}; 126 | //$server->addCallback('onSuccess', function() { echo 'the action has been successfull'; }); 127 | //$server->onError = function() { 128 | // echo 'theres been an error'; 129 | //}; 130 | 131 | // Bind request inline method 132 | $server->substract = function($subtrahend, $minuend) { 133 | return $subtrahend - $minuend; 134 | }; 135 | 136 | // Bind request callback 137 | function updateFunction() { 138 | echo 'update function has been called with following parameters:
'; 139 | var_dump(func_get_args()); 140 | } 141 | $server->update = 'updateFunction'; 142 | 143 | // Bind object callback 144 | class Notificator { 145 | public function test() { 146 | echo 'notificator.test called
'; 147 | } 148 | } 149 | $server->notificator = new Notificator; 150 | 151 | // Bind functions for the batch 152 | $server->{'math.sum'} = function() { 153 | $params = func_get_args(); 154 | $result = 0; 155 | foreach($params as $arg) { 156 | $result += $arg; 157 | } 158 | return $result; 159 | }; 160 | $server->notify_hello = function() { 161 | $params = func_get_args(); 162 | $result = array(); 163 | foreach($params as $par) { 164 | $result[] = 'Hello, ' . $par; 165 | } 166 | echo 'notify_hello called:
- '; 167 | echo implode('
- ', $result); 168 | }; 169 | $server->notify_sum = function() { 170 | $params = func_get_args(); 171 | $result = 0; 172 | foreach($params as $arg) { 173 | $result += $arg; 174 | } 175 | echo 'notify_sum called, result: ' . $result; 176 | }; 177 | $server->get_data = function() { 178 | return array('hello', 5); 179 | }; 180 | 181 | /** 182 | * The test suite! 183 | */ 184 | $server->supressOutput(); 185 | $pass = $fail = $total = 0; 186 | 187 | echo ''; 188 | foreach($requests as $key => $request) { 189 | $total++; 190 | 191 | echo '--> ' . $request . ''; 192 | $output = $server->handle($request); 193 | $rawOutput = $server->getRawOutput(); 194 | 195 | // Check the output 196 | $wanted = json_decode($responses[$key]); 197 | if($wanted == $output) { 198 | $pass++; 199 | $color = '#00aa00'; 200 | } 201 | else { 202 | $fail++; 203 | $color = '#ff0000'; 204 | } 205 | 206 | // Format raw output for the tests 207 | if($rawOutput[0] == '[') { 208 | $rawOutput = str_replace('[{', '[
{', $rawOutput); 209 | $rawOutput = str_replace('},{', '},
{', $rawOutput ); 210 | $rawOutput = str_replace('}]', '}
]', $rawOutput); 211 | } 212 | 213 | echo ''; 214 | echo '<-- ' . $rawOutput . ''; 216 | 217 | echo '
'; 215 | echo 'sup:' . $responses[$key] . ''; 218 | echo '
'; 219 | } 220 | 221 | echo '
'; 222 | echo 'total tests: ' . $total . '
'; 223 | echo 'passed tests: ' . $pass . '
'; 224 | echo 'failed tests: ' . $fail . ''; -------------------------------------------------------------------------------- /test-suite/jsonTestModule/presenters/BasePresenter.php: -------------------------------------------------------------------------------- 1 | 8 | * @copyright Pavel Ptacek and Animal Group9 | * @license New BSD License 10 | */ 11 | 12 | namespace jsonTestModule; 13 | 14 | /** 15 | * The base presenter 16 | * 17 | * @author Pavel Ptacek 18 | */ 19 | class BasePresenter extends \Nette\Application\UI\Presenter { 20 | /** @var string absolute URL to your endpoint of json server */ 21 | private $endpoint; 22 | 23 | public function startup() { 24 | parent::startup(); 25 | 26 | if(!$this->isLoggedIn() && $this->getAction() != 'jsonAcessLogin') { 27 | $this->redirect('Base:jsonAcessLogin'); 28 | } 29 | 30 | // Setup endpoint 31 | $this->endpoint = $this->link('//:JsonTestSuite:default'); 32 | } 33 | 34 | /** 35 | * Gets token from session 36 | */ 37 | public function getToken() { 38 | $sess = $this->getSession('Access'); 39 | if(!empty($sess->token)) { 40 | return $sess->token; 41 | } 42 | 43 | return ''; 44 | } 45 | 46 | /** 47 | * Determines whether the user has logged in into the test suite api 48 | */ 49 | public function isLoggedIn() { 50 | $sess = $this->getSession('ApiTest'); 51 | if(!isset($sess->auth)) { 52 | return false; 53 | } 54 | 55 | return (bool)$sess->auth; 56 | } 57 | 58 | /** 59 | * Renders the login action of the user 60 | */ 61 | public function handleLogin() { 62 | $users = array( 63 | 'test' => sha1('test'), 64 | ); 65 | 66 | if(empty($_POST['username']) || empty($_POST['pass'])) { 67 | $this->flashMessage('Invalid username or password'); 68 | return; 69 | } 70 | 71 | // validate 72 | $username = strtolower(trim($_POST['username'])); 73 | $pass = sha1($_POST['pass']); 74 | 75 | if(!isset($users[$username])) { 76 | $this->flashMessage('Invalid username or password'); 77 | return; 78 | } 79 | 80 | if($users[$username] === $pass) { 81 | $this->getSession('ApiTest')->auth = true; 82 | $this->redirect('Base:listModules'); 83 | return; 84 | } 85 | 86 | $this->flashMessage('Invalid username or password'); 87 | return; 88 | } 89 | 90 | /** 91 | * Handles generic send of the payload 92 | */ 93 | public function handleSendJson($method) { 94 | // Prepare the request 95 | $args = $this->_mapJson($_POST, $_POST); 96 | 97 | // Showtime! 98 | $client = new \Lightbulb\Json\Rpc2\Client($this->endpoint); 99 | $client->_debug(); 100 | $return = $client->__call($method, $args); 101 | 102 | // Assign into the template 103 | $this->template->jsonRequest = \Lightbulb\Json\Rpc2\Client::formatJson($client->_getRequest()); 104 | $this->template->jsonResponse = \Lightbulb\Json\Rpc2\Client::formatJson($client->_getResponse()); 105 | $this->template->parsedResponse = $return; 106 | } 107 | 108 | /** 109 | * Maps POST-ed data onto json data class 110 | */ 111 | private function _mapJson($data, $sendArr) { 112 | $args = array(); 113 | foreach($data as $key => $val) { 114 | if(strpos($key, 'json_') !== 0) { 115 | continue; 116 | } 117 | $jsonKey = substr($key, 5); 118 | 119 | // Check for skipping of the value --> if the val is array, we check 120 | // for object. If it's not, then we simply check the $sendArr 121 | if(strpos($jsonKey, '_checkbox_') !== 0 && !is_array($val) && !isset($sendArr['send_' . $jsonKey])) { 122 | continue; 123 | } 124 | elseif(strpos($jsonKey, '_checkbox_') !== 0 && is_array($val) && !isset($sendArr['send_' . $jsonKey . '__object'])) { 125 | continue; 126 | } 127 | 128 | if(is_array($val)) { 129 | $arr = $this->_mapJson($val, $sendArr['send_' . $jsonKey], true); 130 | $args[$jsonKey] = $this->_mapArrToObj($arr); 131 | } 132 | elseif(strpos($jsonKey, '_checkbox_') === 0) { 133 | $jsonKey = substr($jsonKey, strlen('_checkbox_')); 134 | if(isset($data['json_' . $jsonKey])) { 135 | $args[$jsonKey] = true; 136 | } 137 | else { 138 | $args[$jsonKey] = false; 139 | } 140 | } 141 | else { 142 | $args[$jsonKey] = $val; 143 | } 144 | } 145 | 146 | return $args; 147 | } 148 | 149 | /** 150 | * Map array to stdClass 151 | */ 152 | private function _mapArrToObj($arr) { 153 | // check for numeric keys only 154 | $keys = array_keys($arr); 155 | $return = true; 156 | $copy = array(); 157 | foreach($keys as $one) { 158 | if(!is_int($one)) { 159 | $return = false; 160 | break; 161 | } 162 | 163 | if(!empty($arr[$one])) { 164 | $copy[] = $arr[$one]; 165 | } 166 | } 167 | 168 | if($return === true) { 169 | return $copy; 170 | } 171 | 172 | // OK! Map to the stdclass 173 | $out = new \stdClass; 174 | foreach($arr as $key => $val) { 175 | if(is_array($val)) { 176 | $out->{$key} = $this->_mapArrToObj($val); 177 | } 178 | else { 179 | $out->{$key} = $val; 180 | } 181 | } 182 | return $out; 183 | } 184 | 185 | /** 186 | * Formats view template file names. 187 | * @return array 188 | */ 189 | public function formatTemplateFiles() { 190 | $name = $this->getName(); 191 | $presenter = substr($name, strrpos(':' . $name, ':')); 192 | $dir = dirname(dirname($this->getReflection()->getFileName())); 193 | $tpl = array( 194 | "$dir/templates/$presenter/$this->view.latte", 195 | "$dir/templates/$presenter.$this->view.latte", 196 | "$dir/templates/$presenter/$this->view.phtml", 197 | "$dir/templates/$presenter.$this->view.phtml", 198 | ); 199 | 200 | // Append template based on moduleFunction / moduleList 201 | if($this->getAction() == 'default') { 202 | $tpl[] = "$dir/templates/Base/moduleDefault.latte"; 203 | } 204 | else { 205 | $tpl[] = "$dir/templates/Base/moduleMethod.latte"; 206 | } 207 | 208 | return $tpl; 209 | } 210 | 211 | /** 212 | * Loads list of modules within the namespace 213 | */ 214 | public function beforeRender() { 215 | // Get the presenters from this module 216 | $files = \Nette\Utils\Finder::findFiles('*Presenter.php')->from(APP_DIR . '/' . __NAMESPACE__); 217 | $out = array(); 218 | 219 | foreach($files as $file) { 220 | $presenter = substr($file->getFilename(), 0, strlen('Presenter.php')*-1); 221 | if($presenter == 'Base') { 222 | continue; 223 | } 224 | 225 | $out[] = array( 226 | 'link' => $presenter . ':default', 227 | 'name' => strtolower($presenter) . '.*', 228 | ); 229 | } 230 | 231 | $this->template->modules = $out; 232 | 233 | // Load the current module if applicable 234 | if($this->getName() != 'jsonTest:Base') { 235 | $current = explode(':', $this->getName()); 236 | $this->template->currentModule = strtolower(end($current)); 237 | 238 | // & load methods of the class 239 | $reflection = new \ReflectionObject($this); 240 | $out = array(); 241 | foreach($reflection->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) { 242 | if(strpos($method->getName(), 'render') !== 0) { 243 | continue; 244 | } 245 | 246 | $name = substr($method->getName(), strlen('render')); 247 | $name = strtolower($name); 248 | $out[] = array( 249 | 'name' => $this->template->currentModule . '.' . $name, 250 | 'link' => $this->template->currentModule . ':' . $name, 251 | ); 252 | } 253 | 254 | $this->template->methods = $out; 255 | } 256 | } 257 | 258 | } -------------------------------------------------------------------------------- /lib/Client.php: -------------------------------------------------------------------------------- 1 | 8 | * @copyright Pavel Ptacek and Animal Group 9 | * @license New BSD License 10 | */ 11 | 12 | namespace Lightbulb\Json\Rpc2; 13 | 14 | 15 | /** 16 | * This is JSON-RPC version 2 client 17 | * 18 | * Conforms http://groups.google.com/group/json-rpc/web/json-rpc-2-0?pli=1 19 | * 20 | * Usage: 21 | * 22 | * $client = new Lightbulb\Json\Rpc2\Client('http://endpoint'); 23 | * $return = $client->method(arg1, arg2, ...); 24 | * 25 | * // optionally add custom curl headers (for example: timeout) 26 | * $client->setOption(CURLOPT_TIMEOUT, 400); 27 | * 28 | * // Results in method "math.sum" 29 | * $return = $client->math->sum(arg1, arg2, arg3, ...); 30 | * 31 | * // Any level of nesting is possible, hence following is valid: 32 | * $return = $client->strings->hash->encode("string"); 33 | * 34 | * // You can also use 35 | * $return = $client->__call('strings.hash.encode', 'string'); 36 | *37 | * 38 | * Currently, the implementation does not support batch calls from the client side 39 | * -> the server however, does. 40 | * 41 | * @author Pavel Ptacek 42 | */ 43 | class Client { 44 | /** 45 | * Determines indentation for debugging (self::formatJsonString) 46 | */ 47 | const DEBUG_INDENT = 4; 48 | 49 | /** @var string */ 50 | protected $_endpoint; 51 | 52 | /** @var bool */ 53 | protected $_debug; 54 | 55 | /** @var array */ 56 | private $_callstack; 57 | 58 | /** @var int */ 59 | private $_id; 60 | 61 | /** @var string */ 62 | protected $_debugRequest; 63 | 64 | // curl additonal options 65 | /** @var array */ 66 | private $options = array(); 67 | 68 | /** @var string */ 69 | protected $_debugResponse; 70 | 71 | /** 72 | * Creates json-conforming request 73 | * 74 | * @param type $method 75 | * @param type $args 76 | * @return string 77 | */ 78 | protected function _requestFactory($method, $args) { 79 | $request = new \stdClass; 80 | $request->jsonrpc = '2.0'; 81 | $request->method = $method; 82 | $request->params = $args; 83 | $request->id = $this->_id++; 84 | return json_encode($request); 85 | } 86 | 87 | /** 88 | * add CURL option - see https://www.php.net/manual/en/function.curl-setopt.php 89 | * 90 | * @param int $option 91 | * @param mixed $value 92 | * @return void 93 | */ 94 | public function setOption($option, $value) { 95 | $this->options[$option] = $value; 96 | } 97 | 98 | /** 99 | * Creates new cURL handle 100 | */ 101 | protected function &_curlFactory($data) { 102 | $options = array( 103 | CURLOPT_FRESH_CONNECT => false, 104 | CURLOPT_POST => true, 105 | CURLOPT_RETURNTRANSFER => true, 106 | CURLOPT_POSTFIELDS => $data, 107 | ); 108 | foreach ($this->options as $option => $value) { 109 | $options[$option] = $value; 110 | } 111 | 112 | $curl = curl_init($this->_endpoint); 113 | curl_setopt_array($curl, $options); 114 | return $curl; 115 | } 116 | 117 | /** 118 | * The magic getter in order to make class.method calls possible 119 | */ 120 | public function __get($name) { 121 | $this->_callstack[] = $name; 122 | return $this; 123 | } 124 | 125 | /** 126 | * The RPC actual caller 127 | */ 128 | public function __call($method, $args) { 129 | // use callstack or not? 130 | if(strpos($method, '.') === false && count($this->_callstack) > 0) { 131 | $method = implode('.', $this->_callstack) . '.' . $method; 132 | } 133 | 134 | // Empty callstack, construct cURL object, call and return 135 | $this->_callstack = array(); 136 | $request = $this->_requestFactory($method, $args); 137 | $curl = $this->_curlFactory(json_encode($request)); 138 | $raw = curl_exec($curl); 139 | $return = json_decode($raw); 140 | curl_close($curl); 141 | 142 | // Debugging? 143 | if($this->_debug === true) { 144 | $this->_debugRequest = $request; 145 | $this->_debugResponse = $raw; 146 | } 147 | 148 | return $return; 149 | } 150 | 151 | /** 152 | * Create new client to an endpoint 153 | * 154 | * @param string $endpointUrl 155 | */ 156 | public function __construct($endpointUrl) { 157 | $this->_endpoint = $endpointUrl; 158 | $this->_callstack = array(); 159 | $this->_id = 0; 160 | $this->_debug = false; 161 | } 162 | 163 | /** 164 | * Send batch of requests into the server and return the response 165 | * 166 | * @param array $batch 167 | * @return array|null 168 | */ 169 | public function _batchRequest($method, array $batch = array()) { 170 | $data = array(); 171 | foreach($batch as $one) { 172 | $data[] = $this->_requestFactory($method, $one); 173 | } 174 | 175 | // Build the curl, execute and return 176 | $curl = $this->_curlFactory($data); 177 | $raw = curl_exec($curl); 178 | $return = json_decode($raw); 179 | curl_close($curl); 180 | 181 | // Debug! 182 | if($this->_debug === true) { 183 | $this->_debugRequest = $data; 184 | $this->_debugResponse = $raw; 185 | } 186 | 187 | return $return; 188 | } 189 | 190 | /** 191 | * Enable or disable debug 192 | */ 193 | public function _debug($enable = true) { 194 | $this->_debug = (bool)$enable; 195 | } 196 | 197 | /** 198 | * Get raw request string 199 | * 200 | * @return string|array (array if batch request) 201 | */ 202 | public function _getRequest() { 203 | return $this->_debugRequest; 204 | } 205 | 206 | /** 207 | * Get raw response output 208 | * 209 | * @return string 210 | */ 211 | public function _getResponse() { 212 | return $this->_debugResponse; 213 | } 214 | 215 | /** 216 | * Format the json string from debugging functions & return it 217 | * 218 | * @param string|array $jsonData 219 | * @return string|array 220 | */ 221 | public static function formatJson($jsonData) { 222 | if(is_array($jsonData)) { 223 | $out = array(); 224 | foreach($jsonData as $one) { 225 | $out[] = self::_formatJsonActual($one); 226 | } 227 | return $out; 228 | } 229 | 230 | // Or return the formatted string 231 | return self::_formatJsonActual($jsonData); 232 | } 233 | 234 | /** 235 | * Actually formats the data 236 | * 237 | * @param string $jsonData 238 | * @return string 239 | */ 240 | private static function _formatJsonActual($jsonData) { 241 | $len = strlen($jsonData); 242 | $out = ''; 243 | $level = 0; 244 | 245 | // Char-by-char 246 | $actionChars = array('{', '}', ':', ',', '"'); 247 | $inQuotes = false; 248 | for($i = 0; $i < $len; ++$i) { 249 | $c = $jsonData[$i]; 250 | 251 | if(!in_array($c, $actionChars)) { 252 | $out .= $c; 253 | continue; 254 | } 255 | 256 | // If we have ":", then we just add space before & after 257 | if($c == ':' && $inQuotes == false) { 258 | $out .= ' : '; 259 | continue; 260 | } 261 | elseif($c == ':') { 262 | $out .= ':'; 263 | continue; 264 | } 265 | 266 | // If we have {, increment the nesting 267 | if($c == '{') { 268 | $level++; 269 | $out .= $c; 270 | $out .= "\n"; 271 | 272 | if($level > 0) { 273 | $out .= str_repeat(' ', $level * self::DEBUG_INDENT); 274 | } 275 | 276 | continue; 277 | } 278 | 279 | // If we have , -> newline 280 | if($c == ',') { 281 | $out .= ','; 282 | $out .= "\n"; 283 | 284 | if($level > 0) { 285 | $out .= str_repeat(' ', $level * self::DEBUG_INDENT); 286 | } 287 | 288 | continue; 289 | } 290 | 291 | // If we have }, then decrement nesting 292 | if($c == '}') { 293 | $appendIndent = false; 294 | 295 | // Check if next character is comma 296 | if($c == '}' && ($i+1) < $len && $jsonData[$i+1] == ',') { 297 | $c .= ','; 298 | $i++; 299 | $appendIndent = true; 300 | } 301 | 302 | $level--; 303 | 304 | $indentLevel = $level * self::DEBUG_INDENT; 305 | $out .= "\n"; 306 | if($indentLevel > 0) { 307 | $out .= str_repeat(' ', $level * self::DEBUG_INDENT); 308 | } 309 | $out .= $c; 310 | $out .= "\n"; 311 | 312 | if($appendIndent === true && $indentLevel > 0) { 313 | $out .= str_repeat(' ', $level * self::DEBUG_INDENT);; 314 | } 315 | 316 | continue; 317 | } 318 | 319 | // If we have quote, mark it 320 | if($c == '"') { 321 | $inQuotes = !$inQuotes; 322 | } 323 | } 324 | 325 | return $out; 326 | } 327 | } 328 | -------------------------------------------------------------------------------- /lib/Server.php: -------------------------------------------------------------------------------- 1 | 8 | * @copyright Pavel Ptacek and Animal Group9 | * @license New BSD License 10 | */ 11 | 12 | namespace Lightbulb\Json\Rpc2; 13 | 14 | require_once __DIR__ . '/exceptions.php'; 15 | 16 | /** 17 | * This is JSON-RPCv2 server handler class 18 | * 19 | * Usage: 20 | * 21 | * $server = new \Lightbulb\Json\Rpc\Server; 22 | * 23 | * // Bind the classes for "inst.method" calls 24 | * $server->inst = new MyHandler; 25 | * 26 | * // Bind functions for "method" calls 27 | * $server->method1 = function(param1, param2, ...) { } 28 | * $server->method2 = array($obj, 'method'); 29 | * $server->method3 = 'some_php_method'; 30 | * $server->method4 = 'SomeClass::staticMethod'; 31 | * 32 | * // Bind classes with inline methods 33 | * $server->inst2 = new stdClass; 34 | * $server->inst2->some = function() {} // call with "inst2.some" 35 | * 36 | * // Clear binded method / class 37 | * unset($server->inst2->some); 38 | * unset($server->method2); 39 | *40 | * 41 | * The server can be used as stand-alone: 42 | *43 | * $server = new \Lightbulb\Json\Rpc\Server; 44 | * 45 | * // set the stuff here 46 | * 47 | * $server->run(); 48 | *49 | * 50 | * Or embedded into any framework like this: 51 | *52 | * $server = new \Lightbulb\Json\Rpc\Server; 53 | * 54 | * // set the stuff here 55 | * 56 | * // First way: server loads the input directly from raw_post_data 57 | * $server->supressOutput(); 58 | * $output = $server->handle(); 59 | * $myFramework->sendResponse($structuredOutput); 60 | * 61 | * // Second way: you give the server either raw json string, or structured 62 | * // parameters of the request 63 | * $server->supressOutput(); 64 | * $output = $server->handle($incommingJsonOrParsedData); 65 | * $myFramework->sendResponse($output); 66 | * 67 | * // Another option: you can get raw output like this: 68 | * $server->supressOutput(); 69 | * $server->handle(); 70 | * $rawOutput = $server->getRawOutput(); 71 | * echo $rawOutput; 72 | * exit; // this one is pretty much nasty solution 73 | *74 | * 75 | * The class support following callbacks: 76 | * - onBeforeCall($server) -> called before calling the actual method 77 | * - onSuccess($server) -> called after calling the actual method 78 | * - onError($server) -> called on errors 79 | * 80 | * Bind callbacks this way: 81 | *82 | * // Both will *append* the callback into stack 83 | * $server->onBeforeCall[] = function() {} 84 | * $server->onBeforeCall = function() {} 85 | * $server->addOnBeforeCall(function() {}); 86 | * $server->addOnBeforeCall(array($obj, 'someMethod')); 87 | * 88 | * // clear the stack 89 | * unset($server->onSuccess); 90 | * $server->clearOnBeforeCall(); 91 | *92 | * 93 | * Use $server->supressOutput() in callbacks, in order to push your output 94 | * instead of server's 95 | * 96 | * Throw exceptions in order to have the server return standard error. 97 | * Use exception code in order to show the exception's code in the json err output. 98 | * 99 | * Use $server->setOutput($output) in order to send that output instead of 100 | * the output of the standard server-generated format 101 | * 102 | * @author Pavel Ptacek 103 | */ 104 | 105 | #[\AllowDynamicProperties] 106 | final class Server { 107 | /** @var mixed structured output sent to browser */ 108 | private $_output; 109 | 110 | /** @var string raw output sent to browser */ 111 | private $_rawOutput; 112 | 113 | /** @var bool */ 114 | private $_supressOutput; 115 | 116 | /** @var bool */ 117 | private $_isError; 118 | 119 | /** @var array of on* callbacks */ 120 | private $_callbacks; 121 | 122 | /** @var array of server callbacks */ 123 | private $_server; 124 | 125 | /** @var array of errors during execution outside scope of the server */ 126 | private $_errors = array(); 127 | 128 | /** @var int Error level to be reported when handling server callbacks, @see http://www.php.net/manual/en/function.error-reporting.php */ 129 | private $_error_handling_level = ''; 130 | 131 | /** Allow calls to be made via GET in addition to POST. Disabled by default, but can be enabled at runtime */ 132 | public $_allow_get_calls = 0; 133 | 134 | /** 135 | * Get reflection for the function 136 | * 137 | * @param mixed $method 138 | */ 139 | private function _getReflection($callback) { 140 | if(is_array($callback)) { 141 | $last = array_pop($callback); 142 | $current = $this; 143 | foreach($callback as $one) { 144 | $current = $current->$one; 145 | } 146 | 147 | return array( 148 | 'reflection' => new \ReflectionMethod($current, $last), 149 | 'object' => $last, 150 | ); 151 | } 152 | 153 | // class::method 154 | if(is_string($callback) && strpos($callback, '::') !== false) { 155 | $ex = explode('::', $callback); // php 5.4 compatibility 156 | return array( 157 | 'reflection' => new \ReflectionMethod($ex[0], $ex[1]), 158 | 'object' => null, 159 | ); 160 | } 161 | 162 | // objects as functions 163 | if(method_exists($callback, '__invoke') && is_object($callback)) { 164 | return array( 165 | 'reflection' => new \ReflectionMethod($callback, '__invoke'), 166 | 'object' => $callback, 167 | ); 168 | } 169 | 170 | // closures & functions 171 | return array( 172 | 'reflection' => new \ReflectionFunction($callback), 173 | 'object' => false, 174 | ); 175 | } 176 | 177 | /** 178 | * Check validity of the request 179 | * 180 | * @return void 181 | * @throws Exception 182 | */ 183 | private function _checkRequest($request) { 184 | // If batch request, everything is ok 185 | if(!$request instanceof \stdClass) { 186 | throw new \Exception('Invalid Request. Internal error: request is not a stdClass instance.', -32600); 187 | } 188 | 189 | // Check that it's a valid 1.0 or 2.0 request 190 | if(!$this->get_request_version($request)) { 191 | throw new \Exception('Invalid Request. Request does not contain version number in "jsonrpc" parameter for 2.0, or "request_version" for 1.x', -32600); 192 | } 193 | if(!isset($request->method) || !is_string($request->method)) { 194 | throw new \Exception('Invalid Request. Request does not contain method.', -32600); 195 | } 196 | if(substr($request->method, 0, 4) == 'rpc.') { 197 | throw new \Exception('Method not found.', -32601); 198 | } 199 | if(isset($request->id) && !is_int($request->id) && !is_string($request->id)) { 200 | throw new \Exception('Invalid Request. Request contains ID, but it\'s not string or integer.', -32600); 201 | } 202 | } 203 | 204 | /** 205 | * Handle function 206 | */ 207 | private function _handle($request) { 208 | $batch = $responses = array(); 209 | 210 | // To simplify stuff, make everything as batch 211 | if(is_array($request)) { 212 | $batch = $request; 213 | } 214 | else { 215 | $batch[] = $request; 216 | } 217 | 218 | // Empty? 219 | if(empty($batch)) { 220 | $error = new \stdClass; 221 | $error->jsonrpc = '2.0'; 222 | $error->error = new \stdClass; 223 | $error->error->code = -32600; 224 | $error->error->message = 'Invalid Request.'; 225 | $error->id = null; 226 | 227 | return $error; 228 | } 229 | 230 | // Loop through the batch & execute each 231 | foreach($batch as $one) { 232 | try { 233 | $this->_checkRequest($one); 234 | 235 | if(!isset($one->params)) { 236 | $one->params = null; 237 | } 238 | 239 | $this->last->method = $one->method; 240 | $this->last->params = $one->params; 241 | 242 | // call_user_func_array() wants an array 243 | if (!is_array($one->params)) { 244 | $one->params = array($one->params); 245 | } 246 | 247 | // All the functions should be stored inside of $this so we need to call them 248 | $func = $this->{$one->method}; // Method: $s->foo = array($obj,$method); 249 | if (!is_callable($func)) { 250 | $func = array($this,$one->method); // Class: $s->foo = new myClass; 251 | } 252 | 253 | $return = call_user_func_array($func, $one->params); 254 | 255 | // No response for no id -> it's a notification 256 | if(!isset($one->id)) { 257 | continue; 258 | } 259 | 260 | // Build the response 261 | $response = new \stdClass; 262 | $response->jsonrpc = '2.0'; 263 | $response->result = $return; 264 | $response->id = $one->id; 265 | $responses[] = $response; 266 | } 267 | 268 | // Build error reponse on wrong requests 269 | catch(\Exception $e) { 270 | $error = new \stdClass; 271 | $error->jsonrpc = '2.0'; 272 | $error->error = new \stdClass; 273 | $error->error->code = $e->getCode(); 274 | $error->error->message = $e->getMessage(); 275 | 276 | // If there is an error calling that method trickle it up so we catch onError(). 277 | if ($error->error->code == -32601) { 278 | throw new \Exception("Bad parameters or method '{$one->method}' not found", -32601); 279 | } 280 | 281 | if(isset($one->id)) { 282 | $error->id = $one->id; 283 | } 284 | else { 285 | $error->id = null; 286 | } 287 | 288 | $responses[] = $error; 289 | } 290 | } 291 | 292 | // If the request is batch, return response batch 293 | if(is_array($request)) { 294 | return $responses; 295 | } 296 | 297 | // Return first response if the request is stdClass with id 298 | elseif($request instanceof \stdClass && isset($request->id)) { 299 | return $responses[0]; 300 | } 301 | 302 | // Or return nothing 303 | else { 304 | return null; 305 | } 306 | } 307 | 308 | 309 | /** 310 | * End the execution with given response object 311 | * 312 | * Outputs the data into browser or not - depending on the setup. 313 | * Also, prepares $this->_output and $this->_rawOutput variables. 314 | * 315 | * @param stdClass $response 316 | * @return stdClass the response object 317 | */ 318 | private function _end($response) { 319 | // Version 1.x doesn't understand the JSONRPC in the response 320 | if ($this->request_version < 2) { 321 | unset($response->jsonrpc); 322 | } 323 | 324 | $this->_output = $response; 325 | $this->_rawOutput = json_encode($response); 326 | 327 | // Output to browser if needed 328 | if($this->_supressOutput === false) { 329 | echo $this->_rawOutput; 330 | } 331 | 332 | return $this->_output; 333 | } 334 | 335 | /** 336 | * Construct the class 337 | */ 338 | public function __construct() { 339 | $this->_output = array(); 340 | $this->_rawOutput = ''; 341 | $this->_supressOutput = false; 342 | $this->_isError = false; 343 | $this->_callbacks = array( 344 | 'onbeforecall' => array(), 345 | 'onsuccess' => array(), 346 | 'onerror' => array(), 347 | ); 348 | $this->_server = new \stdClass; 349 | 350 | /** Defaults to the same level as the calling code **/ 351 | $current_error_level = error_reporting(); 352 | $this->_error_handling_level = $current_error_level; 353 | } 354 | 355 | /** 356 | * The setter -> used for the dot magic 357 | * 358 | * @param type $name 359 | * @param type $val 360 | */ 361 | public function __set($name, $val) { 362 | // on* method? 363 | $lower = strtolower($name); 364 | if(isset($this->_callbacks[$lower])) { 365 | $this->addCallback($lower, $val); 366 | return; 367 | } 368 | 369 | // The dot magic 370 | $exploded = explode('.', $name); 371 | $method = array_pop($exploded); 372 | $current = $this; 373 | foreach($exploded as $one) { 374 | if(!isset($current->$one)) { 375 | $current->$one = new \stdClass; 376 | } 377 | 378 | $current = $current->$one; 379 | } 380 | 381 | // Append the variable / function 382 | $current->$method = $val; 383 | } 384 | 385 | /** 386 | * Getter for the dot magic 387 | */ 388 | public function &__get($name) { 389 | // on* method? 390 | $lower = strtolower($name); 391 | if(isset($this->_callbacks[$lower])) { 392 | throw new \Exception('Getting the callback via __get is forbidden'); 393 | } 394 | 395 | // The dot magic 396 | $exploded = explode('.', $name); 397 | $method = array_pop($exploded); 398 | $current = $this; 399 | foreach($exploded as $one) { 400 | if(!isset($current->$one)) { 401 | throw new \Exception('Method not found.', -32601); 402 | } 403 | 404 | $current = $current->$one; 405 | } 406 | 407 | // Append the variable / function 408 | $function = &$current->$method; 409 | return $function; 410 | } 411 | 412 | /** 413 | * Caller with the dot magic 414 | */ 415 | public function __call($methodName, $args) { 416 | // on* method? 417 | $lower = strtolower($methodName); 418 | if(isset($this->_callbacks[$lower])) { 419 | foreach($this->_callbacks[$lower] as $one) { 420 | call_user_func($one, $this); 421 | } 422 | return; 423 | } 424 | 425 | // The dot magic 426 | $exploded = explode('.', $methodName); 427 | $function = array_pop($exploded); 428 | $current = $this; 429 | foreach($exploded as $one) { 430 | if(!isset($current->$one)) { 431 | throw new \Exception("Method not found. ($methodName)", -32601); 432 | } 433 | 434 | $current = $current->$one; 435 | } 436 | 437 | // Get the reflection 438 | try { 439 | if(isset($current->$function)) { 440 | $method = $this->_getReflection($current->$function); 441 | } 442 | else { 443 | $method = array( 444 | 'reflection' => new \ReflectionMethod($current, $function), 445 | 'object' => $current, 446 | ); 447 | } 448 | } 449 | catch(\Exception $e) { 450 | throw new \Exception("Procedure not found. ($methodName)", -32601); 451 | } 452 | 453 | // Call with named arguments 454 | if($args instanceof \stdClass) { 455 | $pass = array(); 456 | foreach($method['reflection']->getParameters() as $param) { 457 | if(isset($args->{$param->getName()})) { 458 | $pass[] = $args->{$param->getName()}; 459 | } 460 | else { 461 | if(!$param->isOptional()) { 462 | throw new \Exception('Invalid params', -32602); 463 | } 464 | 465 | $pass[] = $param->getDefaultValue(); 466 | } 467 | } 468 | 469 | $args = $pass; 470 | } 471 | 472 | // No arguments? 473 | if(empty($args)) { 474 | $args = array(); 475 | } 476 | 477 | // Check the ammount of arguments 478 | $wanted = $method['reflection']->getNumberOfRequiredParameters(); 479 | if($wanted > count($args)) { 480 | throw new \Exception('Invalid params', -32602); 481 | } 482 | 483 | // Invoke 484 | if($method['object'] === false) { 485 | return $method['reflection']->invokeArgs($args); 486 | } 487 | else { 488 | return $method['reflection']->invokeArgs($method['object'], $args); 489 | } 490 | } 491 | 492 | /** 493 | * Unsetter 494 | */ 495 | public function __unset($name) { 496 | // The dot magic 497 | $exploded = explode('.', $name); 498 | $method = array_pop($exploded); 499 | $current = $this; 500 | foreach($exploded as $class) { 501 | $current = $current->$class; 502 | } 503 | 504 | unset($current->$method); 505 | } 506 | 507 | /** 508 | * Issetter 509 | */ 510 | public function __isset($name) { 511 | // The dot magic 512 | $exploded = explode('.', $name); 513 | $method = array_pop($exploded); 514 | $current = $this; 515 | foreach($exploded as $class) { 516 | $current = $current->$class; 517 | } 518 | 519 | return isset($current->$method); 520 | } 521 | 522 | /** 523 | * Add onBeforeCall, onSuccess, onError callbacks 524 | * 525 | * @param string $name onBeforeCall || onSuccess || onError 526 | * @param callable $callback 527 | * @return Server fluent interface 528 | * @throws InvalidArgumentException 529 | */ 530 | public function addCallback($name, $callback) { 531 | $name = strtolower($name); 532 | if(!isset($this->_callbacks[$name])) { 533 | throw new \InvalidArgumentException('Callback "' . $name . '" is not a valid callback. Use onBeforeCall, onSuccess or onError callbacks.'); 534 | } 535 | if(!is_callable($callback)) { 536 | throw new \InvalidArgumentException('Callback "' . print_r($callback, true) . '" is not a valid callback'); 537 | } 538 | 539 | $this->_callbacks[$name][] = $callback; 540 | return $this; 541 | } 542 | 543 | /** 544 | * Clear callback entirely 545 | * 546 | * @param string $name onBeforeCall || onSuccess || onError 547 | * @return Server fluent interface 548 | * @throws InvalidArgumentException 549 | */ 550 | public function clearCallback($name) { 551 | $name = strtolower($name); 552 | if(!isset($this->_callbacks[$name])) { 553 | throw new \InvalidArgumentException('Callback "' . $name . '" is not a valid callback. Use onBeforeCall, onSuccess or onError callbacks.'); 554 | } 555 | 556 | $this->_callbacks[$name] = array(); 557 | return $this; 558 | } 559 | 560 | /** 561 | * TRUE if the server is not supposed to send anything to browser 562 | * If so, use $this->getOutput or $this->getRawOutput after handle() 563 | * 564 | * @param bool $supress 565 | * @return Server fluent interface 566 | */ 567 | public function supressOutput($supress = true) { 568 | $this->_supressOutput = (bool)$supress; 569 | return $this; 570 | } 571 | 572 | /** 573 | * The magic happens here 574 | * 575 | * @param mixed $params either JSON string or the parameters directly 576 | */ 577 | public function handle($params = null) { 578 | header("Content-Type: application/json", true); 579 | 580 | // Setup error handler 581 | $handler = set_error_handler(array($this, '_errorHandler'), $this->_error_handling_level); 582 | 583 | // Prepare current output -> in case of wrong json string 584 | $error = new \stdClass; 585 | $error->jsonrpc = '2.0'; 586 | $error->error = new \stdClass; 587 | $error->error->code = -32700; 588 | $error->error->message = 'Parse error.'; 589 | $error->id = null; 590 | 591 | // Build the "last" object so we can reference it later 592 | $this->last = new \stdClass; 593 | 594 | // Callback time! 595 | try { 596 | $this->onBeforeCall($this); 597 | } catch (\Exception $e) { 598 | // Wrap the exception into request 599 | $error->error->code = $e->getCode(); 600 | $error->error->message = get_class($e) . ': ' . $e->getMessage(); 601 | $this->onError($this); 602 | return $this->_end($error); 603 | } 604 | 605 | // Raw json string? 606 | if(is_string($params)) { 607 | $input = json_decode($params); 608 | 609 | // End immidiatelly 610 | if($input === null) { 611 | $this->onError($this); 612 | return $this->_end($error); 613 | } 614 | // Already a set of parameters? 615 | } elseif($params !== null) { 616 | $input = $params; 617 | // Attemping a GET call but they're not enabled 618 | } elseif(!$this->_allow_get_calls && isset($_GET['method'])) { 619 | $error->error->message = 'GET method calls are not allowed'; 620 | $this->onError($this); 621 | return $this->_end($error); 622 | // A valid GET call 623 | } elseif($this->_allow_get_calls && isset($_GET['method'])) { 624 | $method = $_GET['method']; 625 | $p_str = $_GET['params'] ?? ""; 626 | 627 | $params = array(); 628 | if ($p_str) { 629 | $params = preg_split("/,/",$p_str); 630 | } 631 | 632 | $input = new \stdClass; 633 | $input->method = $method; 634 | $input->params = $params; 635 | $input->id = 4; 636 | $input->jsonrpc = '2.0'; 637 | 638 | $this->method = $method; 639 | $this->params = $params; 640 | 641 | $this->request_version = $input->jsonrpc; 642 | // From raw post data 643 | } else { 644 | $rawPost = trim(file_get_contents('php://input')); 645 | $input = json_decode($rawPost); 646 | 647 | // Some weird stuff going on here 648 | if(is_string($input)) { 649 | $input = json_decode($input); 650 | } 651 | 652 | // Set the JSONRPC version for later 653 | $ver = $this->get_request_version($input); 654 | 655 | $this->last->method = ''; 656 | $this->last->params = array(); 657 | 658 | // Store the request version so we can respond properly 659 | $this->request_version = $ver; 660 | $this->last->request_version = $ver; 661 | 662 | // End immidiatelly? 663 | if($input === null) { 664 | $error->error->message = 'Null input'; 665 | $this->onError($this); 666 | return $this->_end($error); 667 | } 668 | } 669 | 670 | // ------------------------- Execution time ---------------------------- 671 | try { 672 | $output = $this->_handle($input); 673 | $this->onSuccess($this); 674 | 675 | if($handler) { 676 | set_error_handler($handler); 677 | } 678 | 679 | return $this->_end($output); 680 | } 681 | catch(\Exception $e) { 682 | // restore error handler 683 | if($handler) { 684 | set_error_handler($handler); 685 | } 686 | 687 | // Wrap the exception into request 688 | $error->error->code = $e->getCode(); 689 | $error->error->message = get_class($e) . ': ' . $e->getMessage(); 690 | $this->onError($this); 691 | return $this->_end($error); 692 | } 693 | } 694 | 695 | /** 696 | * Gets structured output from the server 697 | * 698 | * @return stdClass 699 | * @throws InvalidStateException 700 | */ 701 | public function getOutput() { 702 | if(empty($this->_output)) { 703 | throw new \InvalidStateException('You are requesting output from server while the handle() function has not been called'); 704 | } 705 | 706 | return $this->_output; 707 | } 708 | 709 | /** 710 | * Get raw output 711 | * 712 | * @return string 713 | * @throws InvalidStateException 714 | */ 715 | public function getRawOutput() { 716 | if(empty($this->_rawOutput)) { 717 | throw new \InvalidStateException('You are requesting output from server while the handle() function has not been called'); 718 | } 719 | 720 | return $this->_rawOutput; 721 | } 722 | 723 | /** 724 | * Returns true if there has been an error 725 | */ 726 | public function isError() { 727 | return $this->_isError; 728 | } 729 | 730 | /** 731 | * Error handler 732 | */ 733 | public function _errorHandler($severity, $message, $file = null, $line = null, $context = null) { 734 | // ignore error messages from expressions prepended the at sign (@) see https://www.php.net/manual/en/language.operators.errorcontrol.php 735 | if (error_reporting() === 0) { 736 | return false; 737 | } 738 | throw new \ErrorException($message . "\n" . 'in file ' . $file . "\n" . 'on line ' . $line, 0, $severity, $file, $line); 739 | } 740 | 741 | /** 742 | * Sets the error level to be reported when handling server callbacks 743 | * 744 | * @param int $level 745 | * @see http://www.php.net/manual/en/function.error-reporting.php 746 | */ 747 | public function setErrorHandlingLevel($level) { 748 | $this->_error_handling_level = $level; 749 | } 750 | 751 | private function get_request_version($i) { 752 | if (isset($i->jsonrpc)) { 753 | $ver = $i->jsonrpc; 754 | } elseif (isset($i->version)) { 755 | $ver = $i->version; 756 | } elseif (isset($i->method)) { 757 | $ver = 1.0; 758 | } else { 759 | $ver = false; 760 | } 761 | 762 | return $ver; 763 | } 764 | 765 | private function log($str) { 766 | $str = trim($str); 767 | $time = date("H:i:s"); 768 | $str = "$time $str"; 769 | $file = "/tmp/test.log"; 770 | 771 | if (is_writable($file)) { 772 | file_put_contents($file,$str,FILE_APPEND); 773 | } 774 | } 775 | } 776 | 777 | // vim: tabstop=4 shiftwidth=4 expandtab autoindent softtabstop=4 778 | --------------------------------------------------------------------------------