1: <?php
2: /**
3: * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
4: * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
5: *
6: * Licensed under The MIT License
7: * For full copyright and license information, please see the LICENSE.txt
8: * Redistributions of files must retain the above copyright notice.
9: *
10: * @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
11: * @link http://cakephp.org CakePHP(tm) Project
12: * @since 3.6.0
13: * @license http://www.opensource.org/licenses/mit-license.php MIT License
14: */
15: namespace Cake\Http\Middleware;
16:
17: use Cake\Http\Exception\BadRequestException;
18: use Cake\Utility\Exception\XmlException;
19: use Cake\Utility\Xml;
20: use Psr\Http\Message\ResponseInterface;
21: use Psr\Http\Message\ServerRequestInterface;
22:
23: /**
24: * Parse encoded request body data.
25: *
26: * Enables JSON and XML request payloads to be parsed into the request's
27: * Provides CSRF protection & validation.
28: *
29: * You can also add your own request body parsers using the `addParser()` method.
30: */
31: class BodyParserMiddleware
32: {
33: /**
34: * Registered Parsers
35: *
36: * @var array
37: */
38: protected $parsers = [];
39:
40: /**
41: * The HTTP methods to parse data on.
42: *
43: * @var array
44: */
45: protected $methods = ['PUT', 'POST', 'PATCH', 'DELETE'];
46:
47: /**
48: * Constructor
49: *
50: * ### Options
51: *
52: * - `json` Set to false to disable json body parsing.
53: * - `xml` Set to true to enable XML parsing. Defaults to false, as XML
54: * handling requires more care than JSON does.
55: * - `methods` The HTTP methods to parse on. Defaults to PUT, POST, PATCH DELETE.
56: *
57: * @param array $options The options to use. See above.
58: */
59: public function __construct(array $options = [])
60: {
61: $options += ['json' => true, 'xml' => false, 'methods' => null];
62: if ($options['json']) {
63: $this->addParser(
64: ['application/json', 'text/json'],
65: [$this, 'decodeJson']
66: );
67: }
68: if ($options['xml']) {
69: $this->addParser(
70: ['application/xml', 'text/xml'],
71: [$this, 'decodeXml']
72: );
73: }
74: if ($options['methods']) {
75: $this->setMethods($options['methods']);
76: }
77: }
78:
79: /**
80: * Set the HTTP methods to parse request bodies on.
81: *
82: * @param array $methods The methods to parse data on.
83: * @return $this
84: */
85: public function setMethods(array $methods)
86: {
87: $this->methods = $methods;
88:
89: return $this;
90: }
91:
92: /**
93: * Add a parser.
94: *
95: * Map a set of content-type header values to be parsed by the $parser.
96: *
97: * ### Example
98: *
99: * An naive CSV request body parser could be built like so:
100: *
101: * ```
102: * $parser->addParser(['text/csv'], function ($body) {
103: * return str_getcsv($body);
104: * });
105: * ```
106: *
107: * @param array $types An array of content-type header values to match. eg. application/json
108: * @param callable $parser The parser function. Must return an array of data to be inserted
109: * into the request.
110: * @return $this
111: */
112: public function addParser(array $types, callable $parser)
113: {
114: foreach ($types as $type) {
115: $type = strtolower($type);
116: $this->parsers[$type] = $parser;
117: }
118:
119: return $this;
120: }
121:
122: /**
123: * Apply the middleware.
124: *
125: * Will modify the request adding a parsed body if the content-type is known.
126: *
127: * @param \Psr\Http\Message\ServerRequestInterface $request The request.
128: * @param \Psr\Http\Message\ResponseInterface $response The response.
129: * @param callable $next Callback to invoke the next middleware.
130: * @return \Cake\Http\Response A response
131: */
132: public function __invoke(ServerRequestInterface $request, ResponseInterface $response, $next)
133: {
134: if (!in_array($request->getMethod(), $this->methods)) {
135: return $next($request, $response);
136: }
137: list($type) = explode(';', $request->getHeaderLine('Content-Type'));
138: $type = strtolower($type);
139: if (!isset($this->parsers[$type])) {
140: return $next($request, $response);
141: }
142:
143: $parser = $this->parsers[$type];
144: $result = $parser($request->getBody()->getContents());
145: if (!is_array($result)) {
146: throw new BadRequestException();
147: }
148: $request = $request->withParsedBody($result);
149:
150: return $next($request, $response);
151: }
152:
153: /**
154: * Decode JSON into an array.
155: *
156: * @param string $body The request body to decode
157: * @return array
158: */
159: protected function decodeJson($body)
160: {
161: return json_decode($body, true);
162: }
163:
164: /**
165: * Decode XML into an array.
166: *
167: * @param string $body The request body to decode
168: * @return array
169: */
170: protected function decodeXml($body)
171: {
172: try {
173: $xml = Xml::build($body, ['return' => 'domdocument', 'readFile' => false]);
174: // We might not get child nodes if there are nested inline entities.
175: if ((int)$xml->childNodes->length > 0) {
176: return Xml::toArray($xml);
177: }
178:
179: return [];
180: } catch (XmlException $e) {
181: return [];
182: }
183: }
184: }
185: