Passed
Push — developer ( 2f6199...306ed3 )
by Mariusz
70:42 queued 35:47
created

Api::addLogs()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 10
rs 9.9332
c 0
b 0
f 0
cc 2
nc 2
nop 4
1
<?php
2
/**
3
 * Basic communication file.
4
 *
5
 * @package App
6
 *
7
 * @copyright YetiForce Sp. z o.o.
8
 * @license   YetiForce Public License 3.0 (licenses/LicenseEN.txt or yetiforce.com))
9
 * @author    Mariusz Krzaczkowski <[email protected]>
10
 * @author    Radosław Skrzypczak <[email protected]>
11
 */
12
13
namespace App;
14
15
class Api
16
{
17
	/** @var self API Instance */
18
	protected static $instance;
19
20
	/** @var array Headers. */
21
	protected $header = [];
22
23
	/** @var \GuzzleHttp\Client Guzzle http client */
24
	protected $httpClient;
25
26
	/** @var array The default configuration of GuzzleHttp. */
27
	protected $options = [];
28
29
	/**
30
	 * Api class constructor.
31
	 *
32
	 * @param array $header
33
	 * @param array $options
34
	 */
35
	public function __construct(array $header, array $options)
36
	{
37
		$this->header = $header;
38
		$this->httpClient = new \GuzzleHttp\Client(array_merge($options, Config::$options));
39
	}
40
41
	/**
42
	 * Initiate API instance.
43
	 *
44
	 * @return self instance
0 ignored issues
show
Documentation introduced by
Should the return type not be \self?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
45
	 */
46
	public static function getInstance(): self
47
	{
48
		if (!isset(self::$instance)) {
49
			$userInstance = User::getUser();
50
			$header = [
51
				'User-Agent' => 'YetiForce Portal',
52
				'X-Encrypted' => Config::$encryptDataTransfer ? 1 : 0,
53
				'X-Api-Key' => Config::$apiKey,
54
				'Content-Type' => 'application/json',
55
				'Accept' => 'application/json',
56
			];
57
			if ($userInstance->has('logged')) {
58
				$header['X-Token'] = $userInstance->get('token');
59
			}
60
			if ($userInstance->has('companyId')) {
61
				$header['X-Parent-Id'] = $userInstance->get('companyId');
62
			}
63
			self::$instance = new self($header, [
64
				'http_errors' => false,
65
				'base_uri' => Config::$apiUrl . 'Portal/',
66
				'auth' => [Config::$serverName, Config::$serverPass],
67
			]);
68
		}
69
		return self::$instance;
70
	}
71
72
	/**
73
	 * Call API method.
74
	 *
75
	 * @param string $method
76
	 * @param array  $data
77
	 * @param string $requestType Default get
78
	 *
79
	 * @return array|false
80
	 */
81
	public function call(string $method, array $data = [], string $requestType = 'get')
82
	{
83
		$rawRequest = $data;
84
		$startTime = microtime(true);
85
		$headers = $this->getHeaders();
86
		try {
87
			if (\in_array($requestType, ['get', 'delete'])) {
88
				$response = $this->httpClient->request($requestType, $method, ['headers' => $headers]);
89
			} else {
90
				$data = Json::encode($data);
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $data. This often makes code more readable.
Loading history...
91
				if (Config::$encryptDataTransfer) {
92
					$data = $this->encryptData($data);
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $data. This often makes code more readable.
Loading history...
Documentation introduced by
$data is of type string, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
93
				}
94
				$response = $this->httpClient->request($requestType, $method, ['headers' => $headers, 'body' => $data]);
95
			}
96
			$contentType = explode(';', $response->getHeaderLine('Content-Type'));
97
			$rawResponse = $response->getBody()->getContents();
98
			if ($headers['Accept'] !== reset($contentType)) {
99
				if (Config::$apiErrorLogs || Config::$apiAllLogs) {
100
					\App\Log::info([
101
						'request' => ['date' => date('Y-m-d H:i:s', $startTime), 'requestType' => strtoupper($requestType), 'method' => $method, 'headers' => $headers, 'rawBody' => $rawRequest, 'body' => $data],
102
						'response' => ['time' => round(microtime(true) - $startTime, 2), 'status' => $response->getStatusCode(), 'reasonPhrase' => $response->getReasonPhrase(), 'error' => "Invalid response content type\n Accept:{$headers['Accept']} <> {$response->getHeaderLine('Content-Type')}", 'headers' => $response->getHeaders(), 'rawBody' => $rawResponse],
103
					], 'Api');
104
				}
105
				throw new Exceptions\AppException('Invalid response content type', 500);
106
			}
107
			$encryptedHeader = $response->getHeader('encrypted');
108
			if ($encryptedHeader && 1 == $response->getHeader('encrypted')[0]) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $encryptedHeader of type string[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
109
				$rawResponse = $this->decryptData($rawResponse);
0 ignored issues
show
Documentation introduced by
$rawResponse is of type string, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
110
			}
111
			$responseBody = Json::decode($rawResponse);
112
		} catch (\Throwable $e) {
113
			if (Config::$apiErrorLogs || Config::$apiAllLogs) {
114
				\App\Log::info([
115
					'request' => ['date' => date('Y-m-d H:i:s', $startTime), 'requestType' => strtoupper($requestType), 'method' => $method, 'headers' => $headers, 'rawBody' => $rawRequest, 'body' => $data],
116
					'response' => ['time' => round(microtime(true) - $startTime, 2), 'status' => (method_exists($response, 'getStatusCode') ? $response->getStatusCode() : '-'), 'reasonPhrase' => (method_exists($response, 'getReasonPhrase') ? $response->getReasonPhrase() : '-'), 'error' => $e->__toString(), 'headers' => (method_exists($response, 'getHeaders') ? $response->getHeaders() : '-'), 'rawBody' => ($rawResponse ?? '')],
0 ignored issues
show
Bug introduced by
The variable $response does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
117
				], 'Api');
118
			}
119
			throw new Exceptions\AppException('An error occurred while communicating with the CRM', 500, $e);
120
		}
121
		if (\App\Config::$debugApi) {
122
			$_SESSION['debugApi'][] = [
123
				'date' => date('Y-m-d H:i:s', $startTime),
124
				'time' => round(microtime(true) - $startTime, 2),
125
				'method' => $method,
126
				'requestType' => strtoupper($requestType),
127
				'requestId' => RequestUtil::requestId(),
128
				'rawRequest' => [$headers, $rawRequest],
129
				'rawResponse' => $rawResponse,
130
				'response' => $responseBody,
131
				'trace' => Debug::getBacktrace(),
132
			];
133
		}
134
		if (Config::$apiAllLogs || (Config::$apiErrorLogs && (isset($responseBody['error']) || empty($responseBody) || 200 !== $response->getStatusCode()))) {
135
			\App\Log::info([
136
				'request' => ['date' => date('Y-m-d H:i:s', $startTime), 'requestType' => strtoupper($requestType), 'method' => $method, 'headers' => $headers, 'rawBody' => $rawRequest, 'body' => $data],
137
				'response' => ['time' => round(microtime(true) - $startTime, 2), 'status' => $response->getStatusCode(), 'reasonPhrase' => $response->getReasonPhrase(), 'headers' => $response->getHeaders(),	'rawBody' => $rawResponse, 'body' => $responseBody],
138
			], 'Api');
139
		}
140
		if (isset($responseBody['error'])) {
141
			$_SESSION['systemError'][] = $responseBody['error'];
142
			throw new Exceptions\AppException($responseBody['error']['message'], $responseBody['error']['code'] ?? 500);
143
		}
144
		if (empty($responseBody) || 200 !== $response->getStatusCode()) {
145
			throw new Exceptions\AppException("API returned an error:\n" . $response->getReasonPhrase(), $response->getStatusCode());
146
		}
147
		if (isset($responseBody['result'])) {
148
			return $responseBody['result'];
149
		}
150
	}
151
152
	/**
153
	 * Headers.
154
	 *
155
	 * @return array
156
	 */
157
	public function getHeaders(): array
158
	{
159
		return $this->header;
160
	}
161
162
	/**
163
	 * @param array $data
164
	 *
165
	 * @return string Encrypted string
166
	 */
167
	public function encryptData($data): string
168
	{
169
		$publicKey = 'file://' . ROOT_DIRECTORY . \DIRECTORY_SEPARATOR . Config::$publicKey;
170
		openssl_public_encrypt(Json::encode($data), $encrypted, $publicKey, OPENSSL_PKCS1_OAEP_PADDING);
171
		return $encrypted;
172
	}
173
174
	/**
175
	 * @param array $data
176
	 *
177
	 * @throws \App\Exceptions\AppException
178
	 *
179
	 * @return array Decrypted string
0 ignored issues
show
Documentation introduced by
Should the return type not be string?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
180
	 */
181
	public function decryptData($data)
182
	{
183
		$privateKey = 'file://' . ROOT_DIRECTORY . \DIRECTORY_SEPARATOR . Config::$privateKey;
184
		if (!$privateKey = openssl_pkey_get_private($privateKey)) {
185
			throw new Exceptions\AppException('Private Key failed');
186
		}
187
		$privateKey = openssl_pkey_get_private($privateKey);
188
		openssl_private_decrypt($data, $decrypted, $privateKey, OPENSSL_PKCS1_OAEP_PADDING);
189
		return $decrypted;
190
	}
191
192
	/**
193
	 * Set custom headers.
194
	 *
195
	 * @param array $headers
196
	 *
197
	 * @return self
198
	 */
199
	public function setCustomHeaders(array $headers): self
200
	{
201
		$this->header = $this->getHeaders();
202
		foreach ($headers as $key => $value) {
203
			$this->header[strtolower($key)] = $value;
204
		}
205
		return $this;
206
	}
207
}
208