Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
84.27% covered (warning)
84.27%
75 / 89
42.86% covered (danger)
42.86%
3 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApiInterface
84.27% covered (warning)
84.27%
75 / 89
42.86% covered (danger)
42.86%
3 / 7
21.56
0.00% covered (danger)
0.00%
0 / 1
 __construct
61.54% covered (warning)
61.54%
8 / 13
0.00% covered (danger)
0.00%
0 / 1
6.42
 call
83.33% covered (warning)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
3.04
 processRequest
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
2
 setHeaders
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 setCredentials
28.57% covered (danger)
28.57%
2 / 7
0.00% covered (danger)
0.00%
0 / 1
3.46
 setTimestamp
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 calulateSignature
93.02% covered (success)
93.02%
40 / 43
0.00% covered (danger)
0.00%
0 / 1
6.01
1<?php
2
3namespace LDC\LDCCRM\Transport;
4
5use LDC\LDCCRM\Enums\ApiStatus;
6use LDC\LDCCRM\Exceptions\ApiErrorException;
7use LDC\LDCCRM\Exceptions\CrmException;
8use LDC\LDCCRM\Utils;
9
10/**
11 * @ignore
12 *
13 * This class contains static api credentials
14 * It handles a request to get a response
15 *
16 * @package LDCCrmClient
17 * @subpackage transport
18 */
19class ApiInterface
20{
21    /**
22     * The api endpoint url
23     * This is the CRM FQDN
24     *
25     * @var string
26     */
27    protected static string $endpoint;
28
29    /**
30     * The bearer token
31     * This is related to a specific brand which allows
32     * the crm to identify the concerned brand
33     *
34     * @var string
35     */
36    protected static string $bearerToken;
37
38    /**
39     * The bearer token password
40     *
41     * @var string
42     */
43    protected static string $bearerPwd;
44
45    /**
46     * HTTP AUTH string
47     *
48     * @var string|null
49     */
50    protected static ?string $authentification = null;
51
52    /**
53     * ApiInterface
54     *
55     * @param string|null $endpoint    The crm endpoint.
56     * @param string|null $bearerToken The crm service token.
57     * @param string|null $bearerPwd   The crm service password.
58     *
59     * @throws \LDC\LDCCRM\Exceptions\CrmException If crm.token, crm.password or
60     * crm.endpoint is missing on .env file or in laravel config.
61     */
62    public function __construct(
63        #[\SensitiveParameter] string $endpoint = null,
64        #[\SensitiveParameter] string $bearerToken = null,
65        #[\SensitiveParameter] string $bearerPwd = null
66    ) {
67        $endpoint    = self::$endpoint ?? $endpoint ?? Utils::getConfig('crm.endpoint');
68        $bearerToken = self::$bearerToken ?? $bearerToken ?? Utils::getConfig('crm.token');
69        $bearerPwd   = self::$bearerPwd ?? $bearerPwd ?? Utils::getConfig('crm.password');
70
71        if (!$bearerToken or !$bearerPwd or !$endpoint) {
72            throw new CrmException(
73                'Config need to have the token var set (crm.token, crm.password, crm.endpoint)'
74            );
75        }
76
77        self::$endpoint    = $endpoint;
78        self::$bearerToken = $bearerToken;
79        self::$bearerPwd   = $bearerPwd;
80
81        // Handle HTTP Auth .
82        if (\strpos(self::$endpoint, '@')) {
83            $vars                   = \parse_url(self::$endpoint);
84            self::$authentification = \base64_encode($vars['user'] . ':' . $vars['pass']);
85        }
86    }
87
88    /**
89     * Make a Flucollector http call to get a reponse
90     *
91     * @param \LDC\LDCCRM\Transport\Request $request
92     * @return \LDC\LDCCRM\Transport\Response
93     * @throws \LDC\LDCCRM\Exceptions\CrmException If an error occurs on server side
94     *                                                              or on getting response.
95     * @throws \LDC\LDCCRM\Exceptions\ApiErrorException If any error or \LDC\LDCCRM\Enums\ApiError occurs.
96     */
97    public function call(Request $request): Response
98    {
99        $response = $this->processRequest($request);
100
101        // * Handle response Status.
102        if ($response->status->equals(ApiStatus::KO)) {
103            if (\is_int($response->apiError)) {
104                throw new CrmException($response->message, $response->apiError);
105            } else {
106                throw new ApiErrorException($response->apiError, $response->httpCode, $response->message);
107            }
108        }
109        return $response;
110    }
111
112    /**
113     * Process request
114     *
115     * @param Request $request
116     * @return \LDC\LDCCRM\Transport\Response
117     */
118    private function processRequest(Request $request): Response
119    {
120        $this->setCredentials($request);
121        $this->setHeaders($request);
122        $this->setTimestamp($request);
123        $request->setEndpoint(self::$endpoint);
124        $request->addData([
125            'signature' => $this->calulateSignature($request)
126        ]);
127
128        /** @var \Psr\Http\Message\ResponseInterface $response */
129        $response = $request->call();
130        $status   = in_array($response->getStatusCode(), [200, 201]) ?
131            ApiStatus::OK : ApiStatus::KO;
132
133        return new Response($status, $response);
134    }
135
136    /**
137     * Add spécific headers to he request
138     *
139     * @param \LDC\LDCCRM\Transport\Request $request
140     * @return void
141     */
142    private function setHeaders(Request &$request): void
143    {
144        $headers = [
145            'User-Agent'   => sprintf('LDC CRM client version %s', Utils::getPackageInfo()['version']),
146            'Accept'       => 'application/json',
147            'Content-Type' => 'application/json'
148        ];
149        $request->addHeaders($headers);
150    }
151
152    /**
153     * Inject bearer credentials into the request
154     *
155     * @param \LDC\LDCCRM\Transport\Request $request
156     * @return void
157     */
158    protected function setCredentials(Request $request)
159    {
160        if (self::$authentification) {
161            $request->addHeaders([
162                'Authorization' => implode(',', [
163                    sprintf('Basic %s', self::$authentification)
164                ])
165            ]);
166        }
167        $request->addData(['bearer_token' => self::$bearerToken]);
168    }
169
170    /**
171     * Add a timestamp (UTC) to the request
172     *
173     * @param Request $request
174     * @return void
175     */
176    protected function setTimestamp(Request $request): void
177    {
178        $request->addData([
179            // ! UTC date time.
180            'timestamp' => \gmdate('Y-m-d H:i:s')
181        ]);
182    }
183
184    /**
185     * Calculates request signature
186     *
187     * @param Request $request
188     * @return string
189     * @throws \LDC\LDCCRM\Exceptions\CrmException In case of failure.
190     */
191    protected function calulateSignature(Request $request): string
192    {
193        $uri        = \parse_url(sprintf('%s/%s', self::$endpoint, $request->uri));
194        $method     = \strtolower($request->method->name);
195        $parameters = $request->data;
196
197        // * Merge query in url with Data
198        $q     = $uri['query'] ?? '';
199        $q     = array_filter(explode('&', $q), fn ($el) => !empty($el));
200        $query = [];
201        foreach ($q as $qParam) {
202            $qParam            = explode('=', $qParam, 2);
203            $query[$qParam[0]] = $qParam[1];
204        }
205        $parameters = array_merge($query, $parameters);
206
207        $signature = [
208            'uri' => sprintf(
209                '%s://%s%s',
210                // Assume https if non provided, security first.
211                $uri['scheme'] ?? 'https',
212                $uri['host'],
213                $uri['path'] ?? '',
214            ),
215            'method' => $method
216        ];
217
218        $flattenRequestArray = function (array $array) use (&$flattenRequestArray) {
219            $out = [];
220            foreach ($array as $key => $value) {
221                if (is_array($value)) {
222                    $subArray = $flattenRequestArray($value);
223                    foreach ($subArray as $subKey => $subValue) {
224                        $out["$key.$subKey"] = $subValue;
225                    }
226                    continue;
227                }
228                $out[$key] = $value;
229            }
230            return $out;
231        };
232
233        $parameters = $flattenRequestArray($parameters);
234
235        // ! Natural sort by keys.
236        if (!uksort($parameters, "strnatcmp")) {
237            throw new CrmException('Natural sorting the request parameters failed.');
238        }
239
240        $parameters = \array_map(function ($key, $value) {
241            return sprintf('%s=%s', \rawurlencode($key), \rawurlencode($value));
242        }, \array_keys($parameters), $parameters);
243
244        $signature[] = \implode('&', $parameters);
245
246        return base64_encode(hash_hmac(
247            'sha256',
248            \implode('\n', $signature),
249            self::$bearerPwd,
250            true
251        ));
252    }
253}