use zend router
[GitHub/Stricted/Domain-Control-Panel.git] / vendor / Zend / Mvc / Controller / AbstractRestfulController.php
1 <?php
2 /**
3 * Zend Framework (http://framework.zend.com/)
4 *
5 * @link http://github.com/zendframework/zf2 for the canonical source repository
6 * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
7 * @license http://framework.zend.com/license/new-bsd New BSD License
8 */
9 namespace Zend\Mvc\Controller;
10
11 use Zend\Http\Request as HttpRequest;
12 use Zend\Json\Json;
13 use Zend\Mvc\Exception;
14 use Zend\Mvc\MvcEvent;
15 use Zend\Stdlib\RequestInterface as Request;
16 use Zend\Stdlib\ResponseInterface as Response;
17
18 /**
19 * Abstract RESTful controller
20 */
21 abstract class AbstractRestfulController extends AbstractController
22 {
23 const CONTENT_TYPE_JSON = 'json';
24
25 /**
26 * {@inheritDoc}
27 */
28 protected $eventIdentifier = __CLASS__;
29
30 /**
31 * @var array
32 */
33 protected $contentTypes = [
34 self::CONTENT_TYPE_JSON => [
35 'application/hal+json',
36 'application/json'
37 ]
38 ];
39
40 /**
41 * Name of request or query parameter containing identifier
42 *
43 * @var string
44 */
45 protected $identifierName = 'id';
46
47 /**
48 * @var int From Zend\Json\Json
49 */
50 protected $jsonDecodeType = Json::TYPE_ARRAY;
51
52 /**
53 * Map of custom HTTP methods and their handlers
54 *
55 * @var array
56 */
57 protected $customHttpMethodsMap = [];
58
59 /**
60 * Set the route match/query parameter name containing the identifier
61 *
62 * @param string $name
63 * @return self
64 */
65 public function setIdentifierName($name)
66 {
67 $this->identifierName = (string) $name;
68 return $this;
69 }
70
71 /**
72 * Retrieve the route match/query parameter name containing the identifier
73 *
74 * @return string
75 */
76 public function getIdentifierName()
77 {
78 return $this->identifierName;
79 }
80
81 /**
82 * Create a new resource
83 *
84 * @param mixed $data
85 * @return mixed
86 */
87 public function create($data)
88 {
89 $this->response->setStatusCode(405);
90
91 return [
92 'content' => 'Method Not Allowed'
93 ];
94 }
95
96 /**
97 * Delete an existing resource
98 *
99 * @param mixed $id
100 * @return mixed
101 */
102 public function delete($id)
103 {
104 $this->response->setStatusCode(405);
105
106 return [
107 'content' => 'Method Not Allowed'
108 ];
109 }
110
111 /**
112 * Delete the entire resource collection
113 *
114 * Not marked as abstract, as that would introduce a BC break
115 * (introduced in 2.1.0); instead, raises an exception if not implemented.
116 *
117 * @return mixed
118 */
119 public function deleteList($data)
120 {
121 $this->response->setStatusCode(405);
122
123 return [
124 'content' => 'Method Not Allowed'
125 ];
126 }
127
128 /**
129 * Return single resource
130 *
131 * @param mixed $id
132 * @return mixed
133 */
134 public function get($id)
135 {
136 $this->response->setStatusCode(405);
137
138 return [
139 'content' => 'Method Not Allowed'
140 ];
141 }
142
143 /**
144 * Return list of resources
145 *
146 * @return mixed
147 */
148 public function getList()
149 {
150 $this->response->setStatusCode(405);
151
152 return [
153 'content' => 'Method Not Allowed'
154 ];
155 }
156
157 /**
158 * Retrieve HEAD metadata for the resource
159 *
160 * Not marked as abstract, as that would introduce a BC break
161 * (introduced in 2.1.0); instead, raises an exception if not implemented.
162 *
163 * @param null|mixed $id
164 * @return mixed
165 */
166 public function head($id = null)
167 {
168 $this->response->setStatusCode(405);
169
170 return [
171 'content' => 'Method Not Allowed'
172 ];
173 }
174
175 /**
176 * Respond to the OPTIONS method
177 *
178 * Typically, set the Allow header with allowed HTTP methods, and
179 * return the response.
180 *
181 * Not marked as abstract, as that would introduce a BC break
182 * (introduced in 2.1.0); instead, raises an exception if not implemented.
183 *
184 * @return mixed
185 */
186 public function options()
187 {
188 $this->response->setStatusCode(405);
189
190 return [
191 'content' => 'Method Not Allowed'
192 ];
193 }
194
195 /**
196 * Respond to the PATCH method
197 *
198 * Not marked as abstract, as that would introduce a BC break
199 * (introduced in 2.1.0); instead, raises an exception if not implemented.
200 *
201 * @param $id
202 * @param $data
203 * @return array
204 */
205 public function patch($id, $data)
206 {
207 $this->response->setStatusCode(405);
208
209 return [
210 'content' => 'Method Not Allowed'
211 ];
212 }
213
214 /**
215 * Replace an entire resource collection
216 *
217 * Not marked as abstract, as that would introduce a BC break
218 * (introduced in 2.1.0); instead, raises an exception if not implemented.
219 *
220 * @param mixed $data
221 * @return mixed
222 */
223 public function replaceList($data)
224 {
225 $this->response->setStatusCode(405);
226
227 return [
228 'content' => 'Method Not Allowed'
229 ];
230 }
231
232 /**
233 * Modify a resource collection without completely replacing it
234 *
235 * Not marked as abstract, as that would introduce a BC break
236 * (introduced in 2.2.0); instead, raises an exception if not implemented.
237 *
238 * @param mixed $data
239 * @return mixed
240 */
241 public function patchList($data)
242 {
243 $this->response->setStatusCode(405);
244
245 return [
246 'content' => 'Method Not Allowed'
247 ];
248 }
249
250 /**
251 * Update an existing resource
252 *
253 * @param mixed $id
254 * @param mixed $data
255 * @return mixed
256 */
257 public function update($id, $data)
258 {
259 $this->response->setStatusCode(405);
260
261 return [
262 'content' => 'Method Not Allowed'
263 ];
264 }
265
266 /**
267 * Basic functionality for when a page is not available
268 *
269 * @return array
270 */
271 public function notFoundAction()
272 {
273 $this->response->setStatusCode(404);
274
275 return [
276 'content' => 'Page not found'
277 ];
278 }
279
280 /**
281 * Dispatch a request
282 *
283 * If the route match includes an "action" key, then this acts basically like
284 * a standard action controller. Otherwise, it introspects the HTTP method
285 * to determine how to handle the request, and which method to delegate to.
286 *
287 * @events dispatch.pre, dispatch.post
288 * @param Request $request
289 * @param null|Response $response
290 * @return mixed|Response
291 * @throws Exception\InvalidArgumentException
292 */
293 public function dispatch(Request $request, Response $response = null)
294 {
295 if (! $request instanceof HttpRequest) {
296 throw new Exception\InvalidArgumentException(
297 'Expected an HTTP request');
298 }
299
300 return parent::dispatch($request, $response);
301 }
302
303 /**
304 * Handle the request
305 *
306 * @todo try-catch in "patch" for patchList should be removed in the future
307 * @param MvcEvent $e
308 * @return mixed
309 * @throws Exception\DomainException if no route matches in event or invalid HTTP method
310 */
311 public function onDispatch(MvcEvent $e)
312 {
313 $routeMatch = $e->getRouteMatch();
314 if (! $routeMatch) {
315 /**
316 * @todo Determine requirements for when route match is missing.
317 * Potentially allow pulling directly from request metadata?
318 */
319 throw new Exception\DomainException(
320 'Missing route matches; unsure how to retrieve action');
321 }
322
323 $request = $e->getRequest();
324
325 // Was an "action" requested?
326 $action = $routeMatch->getParam('action', false);
327 if ($action) {
328 // Handle arbitrary methods, ending in Action
329 $method = static::getMethodFromAction($action);
330 if (! method_exists($this, $method)) {
331 $method = 'notFoundAction';
332 }
333 $return = $this->$method();
334 $e->setResult($return);
335 return $return;
336 }
337
338 // RESTful methods
339 $method = strtolower($request->getMethod());
340 switch ($method) {
341 // Custom HTTP methods (or custom overrides for standard methods)
342 case (isset($this->customHttpMethodsMap[$method])):
343 $callable = $this->customHttpMethodsMap[$method];
344 $action = $method;
345 $return = call_user_func($callable, $e);
346 break;
347 // DELETE
348 case 'delete':
349 $id = $this->getIdentifier($routeMatch, $request);
350 $data = $this->processBodyContent($request);
351
352 if ($id !== false) {
353 $action = 'delete';
354 $return = $this->delete($id);
355 break;
356 }
357
358 $action = 'deleteList';
359 $return = $this->deleteList($data);
360 break;
361 // GET
362 case 'get':
363 $id = $this->getIdentifier($routeMatch, $request);
364 if ($id !== false) {
365 $action = 'get';
366 $return = $this->get($id);
367 break;
368 }
369 $action = 'getList';
370 $return = $this->getList();
371 break;
372 // HEAD
373 case 'head':
374 $id = $this->getIdentifier($routeMatch, $request);
375 if ($id === false) {
376 $id = null;
377 }
378 $action = 'head';
379 $headResult = $this->head($id);
380 $response = ($headResult instanceof Response) ? clone $headResult : $e->getResponse();
381 $response->setContent('');
382 $return = $response;
383 break;
384 // OPTIONS
385 case 'options':
386 $action = 'options';
387 $this->options();
388 $return = $e->getResponse();
389 break;
390 // PATCH
391 case 'patch':
392 $id = $this->getIdentifier($routeMatch, $request);
393 $data = $this->processBodyContent($request);
394
395 if ($id !== false) {
396 $action = 'patch';
397 $return = $this->patch($id, $data);
398 break;
399 }
400
401 // TODO: This try-catch should be removed in the future, but it
402 // will create a BC break for pre-2.2.0 apps that expect a 405
403 // instead of going to patchList
404 try {
405 $action = 'patchList';
406 $return = $this->patchList($data);
407 } catch (Exception\RuntimeException $ex) {
408 $response = $e->getResponse();
409 $response->setStatusCode(405);
410 return $response;
411 }
412 break;
413 // POST
414 case 'post':
415 $action = 'create';
416 $return = $this->processPostData($request);
417 break;
418 // PUT
419 case 'put':
420 $id = $this->getIdentifier($routeMatch, $request);
421 $data = $this->processBodyContent($request);
422
423 if ($id !== false) {
424 $action = 'update';
425 $return = $this->update($id, $data);
426 break;
427 }
428
429 $action = 'replaceList';
430 $return = $this->replaceList($data);
431 break;
432 // All others...
433 default:
434 $response = $e->getResponse();
435 $response->setStatusCode(405);
436 return $response;
437 }
438
439 $routeMatch->setParam('action', $action);
440 $e->setResult($return);
441 return $return;
442 }
443
444 /**
445 * Process post data and call create
446 *
447 * @param Request $request
448 * @return mixed
449 */
450 public function processPostData(Request $request)
451 {
452 if ($this->requestHasContentType($request, self::CONTENT_TYPE_JSON)) {
453 $data = Json::decode($request->getContent(), $this->jsonDecodeType);
454 } else {
455 $data = $request->getPost()->toArray();
456 }
457
458 return $this->create($data);
459 }
460
461 /**
462 * Check if request has certain content type
463 *
464 * @param Request $request
465 * @param string|null $contentType
466 * @return bool
467 */
468 public function requestHasContentType(Request $request, $contentType = '')
469 {
470 /** @var $headerContentType \Zend\Http\Header\ContentType */
471 $headerContentType = $request->getHeaders()->get('content-type');
472 if (!$headerContentType) {
473 return false;
474 }
475
476 $requestedContentType = $headerContentType->getFieldValue();
477 if (strstr($requestedContentType, ';')) {
478 $headerData = explode(';', $requestedContentType);
479 $requestedContentType = array_shift($headerData);
480 }
481 $requestedContentType = trim($requestedContentType);
482 if (array_key_exists($contentType, $this->contentTypes)) {
483 foreach ($this->contentTypes[$contentType] as $contentTypeValue) {
484 if (stripos($contentTypeValue, $requestedContentType) === 0) {
485 return true;
486 }
487 }
488 }
489
490 return false;
491 }
492
493 /**
494 * Register a handler for a custom HTTP method
495 *
496 * This method allows you to handle arbitrary HTTP method types, mapping
497 * them to callables. Typically, these will be methods of the controller
498 * instance: e.g., array($this, 'foobar'). The typical place to register
499 * these is in your constructor.
500 *
501 * Additionally, as this map is checked prior to testing the standard HTTP
502 * methods, this is a way to override what methods will handle the standard
503 * HTTP methods. However, if you do this, you will have to retrieve the
504 * identifier and any request content manually.
505 *
506 * Callbacks will be passed the current MvcEvent instance.
507 *
508 * To retrieve the identifier, you can use "$id =
509 * $this->getIdentifier($routeMatch, $request)",
510 * passing the appropriate objects.
511 *
512 * To retrieve the body content data, use "$data = $this->processBodyContent($request)";
513 * that method will return a string, array, or, in the case of JSON, an object.
514 *
515 * @param string $method
516 * @param Callable $handler
517 * @return AbstractRestfulController
518 */
519 public function addHttpMethodHandler($method, /* Callable */ $handler)
520 {
521 if (!is_callable($handler)) {
522 throw new Exception\InvalidArgumentException(sprintf(
523 'Invalid HTTP method handler: must be a callable; received "%s"',
524 (is_object($handler) ? get_class($handler) : gettype($handler))
525 ));
526 }
527 $method = strtolower($method);
528 $this->customHttpMethodsMap[$method] = $handler;
529 return $this;
530 }
531
532 /**
533 * Retrieve the identifier, if any
534 *
535 * Attempts to see if an identifier was passed in either the URI or the
536 * query string, returning it if found. Otherwise, returns a boolean false.
537 *
538 * @param \Zend\Mvc\Router\RouteMatch $routeMatch
539 * @param Request $request
540 * @return false|mixed
541 */
542 protected function getIdentifier($routeMatch, $request)
543 {
544 $identifier = $this->getIdentifierName();
545 $id = $routeMatch->getParam($identifier, false);
546 if ($id !== false) {
547 return $id;
548 }
549
550 $id = $request->getQuery()->get($identifier, false);
551 if ($id !== false) {
552 return $id;
553 }
554
555 return false;
556 }
557
558 /**
559 * Process the raw body content
560 *
561 * If the content-type indicates a JSON payload, the payload is immediately
562 * decoded and the data returned. Otherwise, the data is passed to
563 * parse_str(). If that function returns a single-member array with a empty
564 * value, the method assumes that we have non-urlencoded content and
565 * returns the raw content; otherwise, the array created is returned.
566 *
567 * @param mixed $request
568 * @return object|string|array
569 */
570 protected function processBodyContent($request)
571 {
572 $content = $request->getContent();
573
574 // JSON content? decode and return it.
575 if ($this->requestHasContentType($request, self::CONTENT_TYPE_JSON)) {
576 return Json::decode($content, $this->jsonDecodeType);
577 }
578
579 parse_str($content, $parsedParams);
580
581 // If parse_str fails to decode, or we have a single element with empty value
582 if (!is_array($parsedParams) || empty($parsedParams)
583 || (1 == count($parsedParams) && '' === reset($parsedParams))
584 ) {
585 return $content;
586 }
587
588 return $parsedParams;
589 }
590 }