JsonRpcHandler::isAppropriate()   A
last analyzed

Complexity

Conditions 6
Paths 3

Size

Total Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 6

Importance

Changes 0
Metric Value
cc 6
nc 3
nop 4
dl 0
loc 19
ccs 9
cts 9
cp 1
crap 6
rs 9.0111
c 0
b 0
f 0
1
<?php
2
3
namespace Vectorface\SnappyRouter\Handler;
4
5
use \stdClass;
6
use \Exception;
7
use Vectorface\SnappyRouter\Encoder\JsonEncoder;
8
use Vectorface\SnappyRouter\Exception\ResourceNotFoundException;
9
use Vectorface\SnappyRouter\Handler\AbstractRequestHandler;
10
use Vectorface\SnappyRouter\Request\HttpRequest;
11
use Vectorface\SnappyRouter\Request\JsonRpcRequest;
12
use Vectorface\SnappyRouter\Response\Response;
13
use Vectorface\SnappyRouter\Response\JsonRpcResponse;
14
15
/**
16
 * Handle JSON-RPC 1/2 requests
17
 *
18
 * @copyright Copyright (c) 2014, VectorFace, Inc.
19
 */
20
class JsonRpcHandler extends AbstractRequestHandler implements BatchRequestHandlerInterface
21
{
22
    /**
23
     * Option key for a base path
24
     */
25
    const KEY_BASE_PATH = 'basePath';
26
27
    /**
28
     * Error constants from the JSON-RPC 2.0 spec.
29
     */
30
    const ERR_PARSE_ERROR = -32700;
31
    const ERR_INVALID_REQUEST = -32600;
32
    const ERR_METHOD_NOT_FOUND = -32601;
33
    const ERR_INVALID_PARAMS = -32602;
34
    const ERR_INTERNAL_ERROR = -32603;
35
36
    /**
37
     * The path to the stdin stream. This can be set for testing.
38
     *
39
     * @var string
40
     */
41
    private $stdin = 'php://input';
42
43
    /**
44
     * An array of HttpRequest objects.
45
     *
46
     * @var array
47
     */
48
    private $requests;
49
50
51
    /**
52
     * A flag indicating whether the request is a batch request.
53
     *
54
     * @var boolean
55
     */
56
    private $batch;
57
58
    /**
59
     * The encoder instance to be used to encode responses.
60
     *
61
     * @var \Vectorface\SnappyRouter\Encoder\EncoderInterface|\Vectorface\SnappyRouter\Encoder\JsonEncoder
62
     */
63
    private $encoder;
64
65
    /**
66
     * Returns true if the handler determines it should handle this request and false otherwise.
67
     *
68
     * @param string $path The URL path for the request.
69
     * @param array $query The query parameters.
70
     * @param array $post The post data.
71
     * @param string $verb The HTTP verb used in the request.
72
     * @return boolean Returns true if this handler will handle the request and false otherwise.
73
     */
74 5
    public function isAppropriate($path, $query, $post, $verb)
75
    {
76
        /* JSON-RPC is POST-only. */
77 5
        if ($verb !== 'POST' && $verb !== 'OPTIONS') {
78 1
            return false;
79
        }
80
81
        /* Try decoding and validating POST data, and skip if it doesn't look like JSON-RPC */
82 5
        $post = json_decode(file_get_contents($this->stdin));
83 5
        if ($verb === 'POST' && !is_object($post) && !is_array($post)) {
84 2
            return false;
85
        }
86
87
        // extract the list of requests from the payload and tell the router
88
        // we'll handle this request
89 5
        $service = $this->getServiceFromPath($path);
90 5
        $this->processPayload($service, $verb, $post);
91 5
        return true;
92
    }
93
94
    /**
95
     * Determine the service to load via the ServiceProvider based on path
96
     *
97
     * @param string $path The raw path (URI) used to determine the service.
98
     * @return string The name of the service that we should attempt to load.
99
     */
100 5
    private function getServiceFromPath($path)
101
    {
102
        /* Ensure the path is in a standard form, removing empty elements. */
103 5
        $path = implode('/', array_filter(array_map('trim', explode('/', $path)), 'strlen'));
104
105
        /* If using the basePath option, strip the basePath. Otherwise, the path becomes the basename of the URI. */
106 5
        if (isset($this->options[self::KEY_BASE_PATH])) {
107 1
            $basePathPosition = strpos($path, $this->options[self::KEY_BASE_PATH]);
108 1 View Code Duplication
            if (false !== $basePathPosition) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
109 1
                $path = substr($path, $basePathPosition + strlen($this->options[self::KEY_BASE_PATH]));
110
            }
111
112 1
            return trim(dirname($path), "/") . '/' .  basename($path, '.php'); /* For example, x/y/z/FooService */
113
        } else {
114 4
            return basename($path, '.php'); /* For example: FooService, from /x/y/z/FooService.php */
115
        }
116
    }
117
118
    /**
119
     * Processes the payload POST data and sets up the array of requests.
120
     * @param string $service The service being requested.
121
     * @param string $verb The HTTP verb being used.
122
     * @param array|object $post The raw POST data.
123
     */
124 5
    private function processPayload($service, $verb, $post)
125
    {
126 5
        $this->batch = is_array($post);
127 5
        if (false === $this->batch) {
128 4
            $post = array($post);
129
        }
130 5
        $this->requests = array_map(function ($payload) use ($service, $verb) {
131 4
            return new JsonRpcRequest($service, $payload, $verb);
132 5
        }, $post);
133 5
    }
134
135
    /**
136
     * Returns a request object extracted from the request details (path, query, etc). The method
137
     * isAppropriate() must have returned true, otherwise this method should return null.
138
     * @return HttpRequest Returns a Request object or null if this handler is not appropriate.
139
     */
140 4
    public function getRequest()
141
    {
142 4
        return (!empty($this->requests)) ? $this->requests[0] : null;
143
    }
144
145
    /**
146
     * Returns an array of batched requests.
147
     * @return array An array of batched requests.
148
     */
149 2
    public function getRequests()
150
    {
151 2
        return $this->requests;
152
    }
153
154
    /**
155
     * Performs the actual routing.
156
     *
157
     * @return string Returns the result of the route.
158
     */
159 3
    public function performRoute()
160
    {
161 3
        $this->invokePluginsHook(
162 3
            'beforeServiceSelected',
163 3
            array($this, $this->getRequest())
164
        );
165
166
        /* Check if we can get the service. */
167
        try {
168 3
            $service = $this->getServiceProvider()->getServiceInstance(
169 3
                $this->getRequest()->getController()
170
            );
171 1
        } catch (Exception $e) {
172 1
            throw new ResourceNotFoundException(
173 1
                'No such service: '.$this->getRequest()->getController()
174
            );
175
        }
176
177
        /* Loop through each call in the possible batch. */
178 2
        $response = array();
179 2
        foreach ($this->requests as $request) {
180 2
            $callResponse = $this->invokeMethod($service, $request);
181
182
            /* Stop here if this isn't a batch. There is only one response. */
183 2
            if (false === $this->batch) {
184 2
                $response = $callResponse->getResponseObject();
185 2
                break;
186
            }
187
188
            /* Omit empty responses from the batch response. */
189 1
            if ($callResponse = $callResponse->getResponseObject()) {
190 1
                $response[] = $callResponse;
191
            }
192
        }
193
194 2
        @header('Content-type: application/json');
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
195 2
        $response = new Response($response);
196 2
        return $this->getEncoder()->encode($response);
197
    }
198
199
    /**
200
     * Invokes a method on a service class, based on the raw JSON-RPC request.
201
     *
202
     * @param mixed $service The service being invoked.
203
     * @param Vectorface\SnappyRouter\Request\JsonRpcRequest $request The request
204
     *        to invoke.
205
     * @return JsonRpcResponse A response based on the result of the procedure call.
206
     */
207 2
    private function invokeMethod($service, JsonRpcRequest $request)
208
    {
209 2
        if (false === $request->isValid()) {
210
            /* Note: Method isn't known, so invocation hooks aren't called. */
211 1
            return new JsonRpcResponse(
212 1
                null,
213 1
                new Exception(
214 1
                    'The JSON sent is not a valid Request object',
215 1
                    self::ERR_INVALID_REQUEST
216
                )
217
            );
218
        }
219
220 2
        $action = $request->getAction();
221 2
        $this->invokePluginsHook(
222 2
            'afterServiceSelected',
223 2
            array($this, $request, $service, $action)
224
        );
225
226 2
        $this->invokePluginsHook(
227 2
            'beforeMethodInvoked',
228 2
            array($this, $request, $service, $action)
229
        );
230
231
        try {
232 2
            $response = new JsonRpcResponse(
233 2
                call_user_func_array(array($service, $action), $request->getParameters()),
234 1
                null,
235 1
                $request
236
            );
237 1
        } catch (Exception $e) {
238 1
            $error = new Exception($e->getMessage(), self::ERR_INTERNAL_ERROR);
239 1
            $response = new JsonRpcResponse(null, $error, $request);
240
        }
241
242 2
        $this->invokePluginsHook(
243 2
            'afterMethodInvoked',
244 2
            array($this, $request, $service, $action, $response)
245
        );
246
247 2
        return $response;
248
    }
249
250
    /**
251
     * Returns the active response encoder.
252
     *
253
     * @return \Vectorface\SnappyRouter\Encoder\EncoderInterface Returns the response encoder.
254
     */
255 2
    public function getEncoder()
256
    {
257 2
        if (!isset($this->encoder)) {
258 2
            $this->encoder = new JsonEncoder();
259
        }
260 2
        return $this->encoder;
261
    }
262
263
    /**
264
     * Provides the handler with an opportunity to perform any last minute
265
     * error handling logic. The returned value will be serialized by the
266
     * handler's encoder.
267
     *
268
     * @param Exception $e The exception that was thrown.
269
     * @return mixed Returns a serializable value that will be encoded and returned
270
     *         to the client.
271
     */
272 1
    public function handleException(Exception $e)
273
    {
274 1
        if ($e->getCode() > -32000) {
275
            /* Don't pass through internal errors in case there's something sensitive. */
276 1
            $response = new JsonRpcResponse(null, new Exception("Internal Error", self::ERR_INTERNAL_ERROR));
277
        } else {
278
            /* JSON-RPC errors (<= -32000) can be passed on. */
279 1
            $response = new JsonRpcResponse(null, $e, null);
280
        }
281
282 1
        return $response->getResponseObject();
283
    }
284
}
285