Response::send()   F
last analyzed

Complexity

Conditions 19
Paths 224

Size

Total Lines 58
Code Lines 43

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 380

Importance

Changes 0
Metric Value
eloc 43
dl 0
loc 58
ccs 0
cts 0
cp 0
rs 3.3833
c 0
b 0
f 0
cc 19
nc 224
nop 0
crap 380

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * Web service response file.
4
 *
5
 * @package API
6
 *
7
 * @copyright YetiForce S.A.
8
 * @license   YetiForce Public License 6.5 (licenses/LicenseEN.txt or yetiforce.com)
9
 * @author    Mariusz Krzaczkowski <[email protected]>
10
 */
11
12
namespace Api\Core;
13
14
/**
15
 * Web service response class.
16
 */
17
class Response
18
{
19
	/**
20
	 * Access control allow headers.
21
	 *
22
	 * @var string[]
23
	 */
24
	protected $acceptableHeaders = ['x-api-key', 'x-encrypted', 'x-token'];
25
	/**
26
	 * Access control allow methods.
27
	 *
28
	 * @var string[]
29
	 */
30
	protected $acceptableMethods = [];
31
	/**
32
	 * Request instance.
33
	 *
34
	 * @var \Api\Core\Request
35
	 */
36
	protected $request;
37
	protected static $instance = false;
38
	protected $body;
39
	/**
40
	 * File instance.
41
	 *
42
	 * @var \App\Fields\File
43
	 */
44
	protected $file;
45
	/**
46
	 * Headers.
47
	 *
48
	 * @var array
49
	 */
50
	protected $headers = [];
51
	/**
52
	 * @var int Response status code.
53
	 */
54
	protected $status = 200;
55
	/**
56
	 * @var string Response data type.
57
	 */
58
	protected $responseType;
59
	/**
60
	 * @var string Reason phrase.
61
	 */
62
	protected $reasonPhrase;
63
64
	/**
65
	 * Get instance.
66
	 *
67
	 * @return self
68
	 */
69
	public static function getInstance(): self
70
	{
71
		if (!static::$instance) {
72
			static::$instance = new self();
73
		}
74
		return static::$instance;
0 ignored issues
show
Bug Best Practice introduced by
The expression return static::instance could return the type true which is incompatible with the type-hinted return Api\Core\Response. Consider adding an additional type-check to rule them out.
Loading history...
75
	}
76
77
	/**
78
	 * Add header.
79
	 *
80
	 * @param string $key
81
	 * @param mixed  $value
82
	 *
83
	 * @return void
84
	 */
85
	public function addHeader(string $key, $value): void
86
	{
87
		$this->headers[$key] = $value;
88
	}
89
90
	/**
91
	 * Set status code.
92
	 *
93
	 * @param int $status
94
	 *
95
	 * @return void
96
	 */
97
	public function setStatus(int $status): void
98
	{
99
		$this->status = $status;
100
	}
101
102
	/**
103
	 * Set reason phrase.
104
	 *
105
	 * @param string $reasonPhrase
106
	 *
107
	 * @return void
108
	 */
109
	public function setReasonPhrase(string $reasonPhrase): void
110
	{
111
		$this->reasonPhrase = $reasonPhrase;
112
	}
113
114
	/**
115
	 * Set body data.
116
	 *
117
	 * @param array $body
118
	 *
119
	 * @return void
120
	 */
121
	public function setBody(array $body): void
122
	{
123
		$this->body = $body;
124
		$this->responseType = 'data';
125
	}
126
127
	/**
128
	 * Set file instance.
129
	 *
130
	 * @param \App\Fields\File $file
131
	 *
132
	 * @return void
133
	 */
134
	public function setFile(\App\Fields\File $file): void
135
	{
136
		$this->file = $file;
137
		$this->responseType = 'file';
138
	}
139
140
	/**
141
	 * Set request.
142
	 *
143
	 * @param \Api\Core\Request $request
144
	 *
145
	 * @return void
146
	 */
147
	public function setRequest(Request $request): void
148
	{
149
		$this->request = $request;
150
	}
151
152
	/**
153
	 * Set acceptable methods.
154
	 *
155
	 * @param string[] $methods
156
	 *
157
	 * @return void
158
	 */
159
	public function setAcceptableMethods(array $methods)
160
	{
161
		$this->acceptableMethods = array_merge($this->acceptableMethods, $methods);
162
	}
163
164
	/**
165
	 * Set acceptable headers.
166
	 *
167
	 * @param string[] $headers
168
	 *
169
	 * @return void
170
	 */
171
	public function setAcceptableHeaders(array $headers)
172
	{
173
		$this->acceptableHeaders = array_merge($this->acceptableHeaders, $headers);
174
	}
175
176
	/**
177
	 * Get reason phrase.
178
	 *
179
	 * @return string
180
	 */
181
	private function getReasonPhrase(): string
182
	{
183
		if (isset($this->reasonPhrase)) {
184
			return str_ireplace(["\r\n", "\r", "\n"], ' ', $this->reasonPhrase);
0 ignored issues
show
Bug Best Practice introduced by
The expression return str_ireplace(arra...', $this->reasonPhrase) could return the type array which is incompatible with the type-hinted return string. Consider adding an additional type-check to rule them out.
Loading history...
185
		}
186
		$statusCodes = [
187
			200 => 'OK',
188
			401 => 'Unauthorized',
189
			403 => 'Forbidden',
190
			404 => 'Not Found',
191
			405 => 'Method Not Allowed',
192
			500 => 'Internal Server Error',
193
		];
194
		return $statusCodes[$this->status] ?? $statusCodes[500];
195
	}
196
197
	public function send()
198
	{
199
		$encryptDataTransfer = \App\Config::api('ENCRYPT_DATA_TRANSFER') ? 1 : 0;
200
		if (200 !== $this->status || 'data' !== $this->responseType) {
201
			$encryptDataTransfer = 0;
202
		}
203
		$requestContentType = strtolower(\App\Request::_getServer('HTTP_ACCEPT'));
0 ignored issues
show
Bug introduced by
The method _getServer() does not exist on App\Request. Since you implemented __callStatic, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

203
		$requestContentType = strtolower(\App\Request::/** @scrutinizer ignore-call */ _getServer('HTTP_ACCEPT'));
Loading history...
204
		if (empty($requestContentType) || '*/*' === $requestContentType) {
205
			$requestContentType = $this->request->contentType;
206
		}
207
		$headersSent = headers_sent();
208
		if (!$headersSent) {
209
			header('Access-Control-Allow-Origin: *');
210
			header('Access-Control-Allow-Methods: ' . implode(', ', $this->acceptableMethods));
211
			header('Access-Control-Allow-Headers: Origin, X-Requested-With, Content-Type, Accept, Authorization, ' . implode(', ', $this->acceptableHeaders));
212
			header(\App\Request::_getServer('SERVER_PROTOCOL') . ' ' . $this->status . ' ' . $this->getReasonPhrase());
213
			header('Encrypted: ' . $encryptDataTransfer);
214
			foreach ($this->headers as $key => $header) {
215
				header(\strtolower($key) . ': ' . $header);
216
			}
217
		}
218
		if ($encryptDataTransfer) {
219
			header('Content-disposition: attachment; filename="api.json"');
220
			if (!empty($this->body)) {
221
				echo $this->encryptData($this->body);
222
			}
223
		} else {
224
			switch ($this->responseType) {
225
				case 'data':
226
					if (!empty($this->body)) {
227
						if (!$headersSent) {
228
							header("Content-type: $requestContentType");
229
						}
230
						if (false !== strpos($requestContentType, 'application/xml')) {
231
							if (!$headersSent) {
232
								header('Content-disposition: attachment; filename="api.xml"');
233
							}
234
							echo $this->encodeXml($this->body);
235
						} else {
236
							if (!$headersSent) {
237
								header('Content-disposition: attachment; filename="api.json"');
238
							}
239
							echo $this->encodeJson($this->body);
240
						}
241
					}
242
					break;
243
				case 'file':
244
					if (isset($this->file) && file_exists($this->file->getPath())) {
245
						header('Content-type: ' . $this->file->getMimeType());
246
						header('Content-transfer-encoding: binary');
247
						header('Content-length: ' . $this->file->getSize());
0 ignored issues
show
Security Response Splitting introduced by
'Content-length: ' . $this->file->getSize() can contain request data and is used in response header context(s) leading to a potential security vulnerability.

1 path for user data to reach this point

  1. Read from $_FILES, and File::loadFromRequest() is called
    in modules/Products/actions/StocktakingModal.php on line 48
  2. Enters via parameter $file
    in app/Fields/File.php on line 149
  3. $file['size'] is assigned to property File::$size
    in app/Fields/File.php on line 154
  4. Read from property File::$size, and $this->size is returned
    in app/Fields/File.php on line 307

Response Splitting Attacks

Allowing an attacker to set a response header, opens your application to response splitting attacks; effectively allowing an attacker to send any response, he would like.

General Strategies to prevent injection

In general, it is advisable to prevent any user-data to reach this point. This can be done by white-listing certain values:

if ( ! in_array($value, array('this-is-allowed', 'and-this-too'), true)) {
    throw new \InvalidArgumentException('This input is not allowed.');
}

For numeric data, we recommend to explicitly cast the data:

$sanitized = (integer) $tainted;
Loading history...
248
						header('Content-disposition: attachment; filename="' . $this->file->getName() . '"');
0 ignored issues
show
Security Response Splitting introduced by
'Content-disposition: at...->file->getName() . '"' can contain request data and is used in response header context(s) leading to a potential security vulnerability.

1 path for user data to reach this point

  1. Read from $_FILES, and File::loadFromRequest() is called
    in modules/Settings/Picklist/actions/SaveAjax.php on line 59
  2. Enters via parameter $file
    in app/Fields/File.php on line 149
  3. Data is passed through purify(), and Data is passed through trim(), and trim(App\Purifier::purify($file['name'])) is assigned to property File::$name
    in app/Fields/File.php on line 152
  4. Read from property File::$name, and $decode ? App\Purifier::decodeHtml($this->name) : $this->name is returned
    in app/Fields/File.php on line 329

Response Splitting Attacks

Allowing an attacker to set a response header, opens your application to response splitting attacks; effectively allowing an attacker to send any response, he would like.

General Strategies to prevent injection

In general, it is advisable to prevent any user-data to reach this point. This can be done by white-listing certain values:

if ( ! in_array($value, array('this-is-allowed', 'and-this-too'), true)) {
    throw new \InvalidArgumentException('This input is not allowed.');
}

For numeric data, we recommend to explicitly cast the data:

$sanitized = (integer) $tainted;
Loading history...
249
						readfile($this->file->getPath());
250
					}
251
					break;
252
			}
253
		}
254
		$this->debugResponse();
255
	}
256
257
	public function encryptData($data)
258
	{
259
		openssl_public_encrypt($data, $encrypted, 'file://' . ROOT_DIRECTORY . \DIRECTORY_SEPARATOR . \App\Config::api('PUBLIC_KEY'), OPENSSL_PKCS1_OAEP_PADDING);
260
		return $encrypted;
261
	}
262
263
	/**
264
	 * Debug response function.
265
	 *
266
	 * @return void
267
	 */
268
	public function debugResponse()
269
	{
270
		if (\App\Config::debug('apiLogAllRequests')) {
271
			$log = '============ Request ' . \App\RequestUtil::requestId() . ' (Response) ======  ' . date('Y-m-d H:i:s') . "  ======\n";
272
			$log .= 'REQUEST_METHOD: ' . \App\Request::getRequestMethod() . PHP_EOL;
273
			$log .= 'REQUEST_URI: ' . $_SERVER['REQUEST_URI'] . PHP_EOL;
274
			$log .= 'QUERY_STRING: ' . $_SERVER['QUERY_STRING'] . PHP_EOL;
275
			$log .= 'PATH_INFO: ' . ($_SERVER['PATH_INFO'] ?? '') . PHP_EOL;
276
			$log .= 'IP: ' . $_SERVER['REMOTE_ADDR'] . PHP_EOL;
277
			if ($this->body) {
278
				$log .= "----------- Response data -----------\n";
279
				$log .= print_r($this->body, true) . PHP_EOL;
0 ignored issues
show
Bug introduced by
Are you sure print_r($this->body, true) of type string|true can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

279
				$log .= /** @scrutinizer ignore-type */ print_r($this->body, true) . PHP_EOL;
Loading history...
280
			}
281
			file_put_contents('cache/logs/webserviceDebug.log', $log, FILE_APPEND);
282
		}
283
	}
284
285
	/**
286
	 * Encode json data output.
287
	 *
288
	 * @param array $responseData
289
	 *
290
	 * @return string
291
	 */
292
	public function encodeJson($responseData): string
293
	{
294
		return json_encode($responseData, JSON_FORCE_OBJECT | JSON_UNESCAPED_UNICODE);
295
	}
296
297
	public function encodeXml($responseData)
298
	{
299
		$xml = new \SimpleXMLElement('<?xml version="1.0"?><data></data>');
300
		$this->toXml($responseData, $xml);
301
		return $xml->asXML();
302
	}
303
304
	public function toXml($data, \SimpleXMLElement &$xmlData)
305
	{
306
		foreach ($data as $key => $value) {
307
			if (is_numeric($key)) {
308
				$key = 'item' . $key;
309
			}
310
			if (\is_array($value)) {
311
				$subnode = $xmlData->addChild($key);
312
				$this->toXml($value, $subnode);
313
			} else {
314
				$xmlData->addChild("$key", htmlspecialchars("$value"));
315
			}
316
		}
317
	}
318
}
319