Passed
Push — developer ( 2a4a55...bf97c6 )
by Radosław
18:45
created

Response::send()   F

Complexity

Conditions 20
Paths 336

Size

Total Lines 62
Code Lines 46

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 420

Importance

Changes 0
Metric Value
eloc 46
dl 0
loc 62
ccs 0
cts 0
cp 0
rs 1.6333
c 0
b 0
f 0
cc 20
nc 336
nop 0
crap 420

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 5.0 (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
	 * @var string Reason content type
65
	 */
66
	protected $contentType;
67
68
	/**
69
	 * Get instance.
70
	 *
71
	 * @return self
72
	 */
73
	public static function getInstance(): self
74
	{
75
		if (!static::$instance) {
76
			static::$instance = new self();
77
		}
78
		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...
79
	}
80
81
	/**
82
	 * Add header.
83
	 *
84
	 * @param string $key
85
	 * @param mixed  $value
86
	 *
87
	 * @return void
88
	 */
89
	public function addHeader(string $key, $value): void
90
	{
91
		$this->headers[$key] = $value;
92
	}
93
94
	/**
95
	 * Set status code.
96
	 *
97
	 * @param int $status
98
	 *
99
	 * @return void
100
	 */
101
	public function setStatus(int $status): void
102
	{
103
		if (is_numeric($status)) {
0 ignored issues
show
introduced by
The condition is_numeric($status) is always true.
Loading history...
104
			$this->status = $status;
105
		}
106
	}
107
108
	/**
109
	 * Set reason phrase.
110
	 *
111
	 * @param string $reasonPhrase
112
	 *
113
	 * @return void
114
	 */
115
	public function setReasonPhrase(string $reasonPhrase): void
116
	{
117
		$this->reasonPhrase = $reasonPhrase;
118
	}
119
120
	/**
121
	 * Set body data.
122
	 *
123
	 * @param array $body
124
	 *
125
	 * @return void
126
	 */
127
	public function setBody(array $body): void
128
	{
129
		$this->body = $body;
130
		$this->responseType = 'data';
131
	}
132
133
	/**
134
	 * Set file instance.
135
	 *
136
	 * @param \App\Fields\File $file
137
	 *
138
	 * @return void
139
	 */
140
	public function setFile(\App\Fields\File $file): void
141
	{
142
		$this->file = $file;
143
		$this->responseType = 'file';
144
	}
145
146
	/**
147
	 * Set request.
148
	 *
149
	 * @param \Api\Core\Request $request
150
	 *
151
	 * @return void
152
	 */
153
	public function setRequest(Request $request): void
154
	{
155
		$this->request = $request;
156
	}
157
158
	/**
159
	 * Set acceptable methods.
160
	 *
161
	 * @param string[] $methods
162
	 *
163
	 * @return void
164
	 */
165
	public function setAcceptableMethods(array $methods)
166
	{
167
		$this->acceptableMethods = array_merge($this->acceptableMethods, $methods);
168
	}
169
170
	/**
171
	 * Set acceptable headers.
172
	 *
173
	 * @param string[] $headers
174
	 *
175
	 * @return void
176
	 */
177
	public function setAcceptableHeaders(array $headers)
178
	{
179
		$this->acceptableHeaders = array_merge($this->acceptableHeaders, $headers);
180
	}
181
182
	/**
183
	 * Set acceptable headers.
184
	 *
185
	 * @param string $type
186
	 *
187
	 * @return void
188
	 */
189
	public function setContentType(string $type): void
190
	{
191
		$this->contentType = $type;
192
	}
193
194
	/**
195
	 * Get reason phrase.
196
	 *
197
	 * @return string
198
	 */
199
	private function getReasonPhrase(): string
200
	{
201
		if (isset($this->reasonPhrase)) {
202
			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...
203
		}
204
		$statusCodes = [
205
			200 => 'OK',
206
			401 => 'Unauthorized',
207
			403 => 'Forbidden',
208
			404 => 'Not Found',
209
			405 => 'Method Not Allowed',
210
			500 => 'Internal Server Error',
211
		];
212
		return $statusCodes[$this->status] ?? $statusCodes[500];
213
	}
214
215
	public function send()
216
	{
217
		$encryptDataTransfer = \App\Config::api('ENCRYPT_DATA_TRANSFER') ? 1 : 0;
218
		if (200 !== $this->status || 'data' !== $this->responseType) {
219
			$encryptDataTransfer = 0;
220
		}
221
		if (empty($this->contentType)) {
222
			$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

222
			$requestContentType = strtolower(\App\Request::/** @scrutinizer ignore-call */ _getServer('HTTP_ACCEPT'));
Loading history...
223
			if (empty($requestContentType) || '*/*' === $requestContentType) {
224
				$this->contentType = $this->request->contentType;
225
			} else {
226
				$this->contentType = $requestContentType;
227
			}
228
		}
229
		$headersSent = headers_sent();
230
		if (!$headersSent) {
231
			header('Access-Control-Allow-Origin: *');
232
			header('Access-Control-Allow-Methods: ' . implode(', ', $this->acceptableMethods));
233
			header('Access-Control-Allow-Headers: Origin, X-Requested-With, Content-Type, Accept, Authorization, ' . implode(', ', $this->acceptableHeaders));
234
			header(\App\Request::_getServer('SERVER_PROTOCOL') . ' ' . $this->status . ' ' . $this->getReasonPhrase());
235
			header('Encrypted: ' . $encryptDataTransfer);
236
			foreach ($this->headers as $key => $header) {
237
				header(\strtolower($key) . ': ' . $header);
238
			}
239
		}
240
		if ($encryptDataTransfer) {
241
			header('Content-disposition: attachment; filename="api.json"');
242
			if (!empty($this->body)) {
243
				echo $this->encryptData($this->body);
244
			}
245
		} else {
246
			switch ($this->responseType) {
247
				case 'data':
248
					if (!empty($this->body)) {
249
						if (!$headersSent) {
250
							header("Content-type: {$this->contentType}");
251
						}
252
						if (false !== strpos($this->contentType, 'application/xml')) {
253
							if (!$headersSent) {
254
								header('Content-disposition: attachment; filename="api.xml"');
255
							}
256
							echo $this->encodeXml($this->body);
257
						} else {
258
							if (!$headersSent) {
259
								header('Content-disposition: attachment; filename="api.json"');
260
							}
261
							echo $this->encodeJson($this->body);
262
						}
263
					}
264
					break;
265
				case 'file':
266
					if (isset($this->file) && file_exists($this->file->getPath())) {
267
						header('Content-type: ' . $this->file->getMimeType());
268
						header('Content-transfer-encoding: binary');
269
						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.

5 paths for user data to reach this point

  1. Path: Read from $_FILES, and File::loadFromRequest() is called in modules/Settings/Picklist/actions/SaveAjax.php on line 59
  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. $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
  2. Path: Read from $_FILES, and File::loadFromRequest() is called in modules/Products/actions/StocktakingModal.php on line 48
  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
  3. Path: Read from $_FILES, and File::loadFromRequest() is called in modules/Settings/Companies/models/Record.php on line 228
  1. Read from $_FILES, and File::loadFromRequest() is called
    in modules/Settings/Companies/models/Record.php on line 228
  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
  4. Path: Read from $_FILES, and File::loadFromRequest() is called in modules/Settings/PDF/views/Import.php on line 19
  1. Read from $_FILES, and File::loadFromRequest() is called
    in modules/Settings/PDF/views/Import.php on line 19
  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
  5. Path: Read from $_FILES, and File::loadFromRequest() is called in modules/Settings/Roles/actions/UploadLogo.php on line 19
  1. Read from $_FILES, and File::loadFromRequest() is called
    in modules/Settings/Roles/actions/UploadLogo.php on line 19
  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...
270
						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.

2 paths for user data to reach this point

  1. Path: Read from $_FILES, and File::loadFromRequest() is called in modules/Settings/Companies/models/Record.php on line 228
  1. Read from $_FILES, and File::loadFromRequest() is called
    in modules/Settings/Companies/models/Record.php on line 228
  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
  2. Path: Read from $_FILES, and File::loadFromRequest() is called in modules/Settings/Picklist/actions/SaveAjax.php on line 59
  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...
271
						readfile($this->file->getPath());
272
					}
273
					break;
274
			}
275
		}
276
		$this->debugResponse();
277
	}
278
279
	public function encryptData($data)
280
	{
281
		openssl_public_encrypt($data, $encrypted, 'file://' . ROOT_DIRECTORY . \DIRECTORY_SEPARATOR . \App\Config::api('PUBLIC_KEY'), OPENSSL_PKCS1_OAEP_PADDING);
282
		return $encrypted;
283
	}
284
285
	/**
286
	 * Debug response function.
287
	 *
288
	 * @return void
289
	 */
290
	public function debugResponse()
291
	{
292
		if (\App\Config::debug('apiLogAllRequests')) {
293
			$log = '============ Request ' . \App\RequestUtil::requestId() . ' (Response) ======  ' . date('Y-m-d H:i:s') . "  ======\n";
294
			$log .= 'REQUEST_METHOD: ' . \App\Request::getRequestMethod() . PHP_EOL;
295
			$log .= 'REQUEST_URI: ' . $_SERVER['REQUEST_URI'] . PHP_EOL;
296
			$log .= 'QUERY_STRING: ' . $_SERVER['QUERY_STRING'] . PHP_EOL;
297
			$log .= 'PATH_INFO: ' . ($_SERVER['PATH_INFO'] ?? '') . PHP_EOL;
298
			$log .= 'IP: ' . $_SERVER['REMOTE_ADDR'] . PHP_EOL;
299
			if ($this->body) {
300
				$log .= "----------- Response data -----------\n";
301
				$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

301
				$log .= /** @scrutinizer ignore-type */ print_r($this->body, true) . PHP_EOL;
Loading history...
302
			}
303
			$path = ROOT_DIRECTORY . '/cache/logs/webserviceDebug.log';
304
			if (isset(\Api\Controller::$container)) {
305
				$path = ROOT_DIRECTORY . '/cache/logs/webservice' . \Api\Controller::$container . 'Debug.log';
306
			}
307
			file_put_contents($path, $log, FILE_APPEND);
0 ignored issues
show
Security File Manipulation introduced by
$path can contain request data and is used in file manipulation context(s) leading to a potential security vulnerability.

1 path for user data to reach this point

  1. Read from $_GET, and $_GET['_container'] is assigned to $container
    in api/webservice/Controller.php on line 65
  2. $container is assigned to property Controller::$container
    in api/webservice/Controller.php on line 69
  3. Read from property Controller::$container, and Api\Core\ROOT_DIRECTORY . '/cache/logs/webservice' . Api\Controller::container . 'Debug.log' is assigned to $path
    in api/webservice/Core/Response.php on line 305

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...
308
		}
309
	}
310
311
	/**
312
	 * Encode json data output.
313
	 *
314
	 * @param array $responseData
315
	 *
316
	 * @return string
317
	 */
318
	public function encodeJson($responseData): string
319
	{
320
		return json_encode($responseData, JSON_FORCE_OBJECT | JSON_UNESCAPED_UNICODE);
321
	}
322
323
	public function encodeXml($responseData)
324
	{
325
		$xml = new \SimpleXMLElement('<?xml version="1.0"?><data></data>');
326
		$this->toXml($responseData, $xml);
327
		return $xml->asXML();
328
	}
329
330
	public function toXml($data, \SimpleXMLElement &$xmlData)
331
	{
332
		foreach ($data as $key => $value) {
333
			if (is_numeric($key)) {
334
				$key = 'item' . $key;
335
			}
336
			if (\is_array($value)) {
337
				$subnode = $xmlData->addChild($key);
338
				$this->toXml($value, $subnode);
339
			} else {
340
				$xmlData->addChild("$key", htmlspecialchars("$value"));
341
			}
342
		}
343
	}
344
}
345