Passed
Pull Request — master (#132)
by Arman
02:54
created

DropboxApp::rpcRequest()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 4
c 1
b 0
f 0
nc 1
nop 2
dl 0
loc 8
rs 10
1
<?php
2
/**
3
 * Quantum PHP Framework
4
 *
5
 * An open source software development framework for PHP
6
 *
7
 * @package Quantum
8
 * @author Arman Ag. <[email protected]>
9
 * @copyright Copyright (c) 2018 Softberg LLC (https://softberg.org)
10
 * @link http://quantum.softberg.org/
11
 * @since 2.9.0
12
 */
13
14
namespace Quantum\Libraries\Storage\Adapters\Dropbox;
15
16
use Quantum\Exceptions\DatabaseException;
17
use Quantum\Exceptions\CryptorException;
18
use Quantum\Libraries\Curl\HttpClient;
19
use Quantum\Exceptions\HttpException;
20
use Quantum\Exceptions\LangException;
21
use Quantum\Exceptions\AppException;
22
use Exception;
23
24
class DropboxApp
25
{
26
    /**
27
     * Authorization URL
28
     */
29
    const AUTH_URL = 'https://dropbox.com/oauth2/authorize';
30
31
    /**
32
     * Token URL
33
     */
34
    const AUTH_TOKEN_URL = 'https://api.dropboxapi.com/oauth2/token';
35
36
    /**
37
     * URL for remote procedure call endpoints
38
     */
39
    const RPC_API_URL = 'https://api.dropboxapi.com/2';
40
41
    /**
42
     * URL for content endpoints
43
     */
44
    const CONTENT_API_URL = 'https://content.dropboxapi.com/2';
45
46
    /**
47
     * Create folder endpoint
48
     */
49
    const ENDPOINT_CREATE_FOLDER = 'files/create_folder_v2';
50
51
    /**
52
     * Delete file endpoint
53
     */
54
    const ENDPOINT_DELETE_FILE = 'files/delete_v2';
55
56
    /**
57
     * Download file endpoint
58
     */
59
    const ENDPOINT_DOWNLOAD_FILE = 'files/download';
60
61
    /**
62
     * Upload file endpoint
63
     */
64
    const ENDPOINT_UPLOAD_FILE = 'files/upload';
65
66
    /**
67
     * Move file endpoint
68
     */
69
    const ENDPOINT_MOVE_FILE = 'files/move_v2';
70
71
    /**
72
     * Copy file endpoint
73
     */
74
    const ENDPOINT_COPY_FILE = 'files/copy_v2';
75
76
    /**
77
     * Get metadata for file endpoint
78
     */
79
    const ENDPOINT_FILE_METADATA = 'files/get_metadata';
80
81
    /**
82
     * List folder endpoint
83
     */
84
    const ENDPOINT_LIST_FOLDER = 'files/list_folder';
85
86
    /**
87
     * Access token status indicating it needs refresh
88
     */
89
    const ACCESS_TOKEN_STATUS = ['invalid_access_token', 'expired_access_token'];
90
91
    /**
92
     * @var HttpClient
93
     */
94
    private $httpClient;
95
96
    /**
97
     * @var string
98
     */
99
    private $appKey = null;
100
101
    /**
102
     * @var string
103
     */
104
    private $appSecret = null;
105
106
    /**
107
     * @var TokenServiceInterface
108
     */
109
    private $tokenService = null;
110
111
    /**
112
     * DropboxApp constructor
113
     * @param string $appKey
114
     * @param string $appSecret
115
     * @param TokenServiceInterface $tokenService
116
     * @param HttpClient $httpClient
117
     */
118
    public function __construct(string $appKey, string $appSecret, TokenServiceInterface $tokenService, HttpClient $httpClient)
119
    {
120
        $this->appKey = $appKey;
121
        $this->appSecret = $appSecret;
122
        $this->tokenService = $tokenService;
123
        $this->httpClient = $httpClient;
124
    }
125
126
    /**
127
     * Gets the auth URL
128
     * @throws CryptorException
129
     * @throws AppException
130
     * @throws DatabaseException
131
     */
132
    public function getAuthUrl(string $redirectUrl, string $tokenAccessType = 'offline'): string
133
    {
134
        $params = [
135
            'client_id' => $this->appKey,
136
            'response_type' => 'code',
137
            'state' => csrf_token(),
138
            'redirect_uri' => $redirectUrl,
139
            'token_access_type' => $tokenAccessType,
140
        ];
141
142
        return self::AUTH_URL . '?' . http_build_query($params, '', '&');
143
    }
144
145
    /**
146
     * Fetch tokens
147
     * @param string $code
148
     * @param string $redirectUrl
149
     * @return object|null
150
     * @throws AppException
151
     * @throws HttpException
152
     * @throws LangException
153
     */
154
    public function fetchTokens(string $code, string $redirectUrl): ?object
155
    {
156
        $params = [
157
            'code' => $code,
158
            'grant_type' => 'authorization_code',
159
            'client_id' => $this->appKey,
160
            'client_secret' => $this->appSecret,
161
            'redirect_uri' => $redirectUrl,
162
        ];
163
164
        $tokenUrl = self::AUTH_TOKEN_URL . '?' . http_build_query($params, '', '&');
165
166
        $response = $this->sendRequest($tokenUrl);
167
168
        $this->tokenService->saveTokens($response->access_token, $response->refresh_token);
169
170
        return $response;
171
    }
172
173
    /**
174
     * Sends rpc request
175
     * @param string $endpoint
176
     * @param array|null $params
177
     * @return mixed|null
178
     * @throws Exception
179
     */
180
    public function rpcRequest(string $endpoint, ?array $params = [])
181
    {
182
        $headers = [
183
            'Authorization' => 'Bearer ' . $this->tokenService->getAccessToken(),
184
            'Content-Type' => 'application/json'
185
        ];
186
187
        return $this->sendRequest(self::RPC_API_URL . '/' . $endpoint, $params, $headers);
188
    }
189
190
    /**
191
     * Sends content request
192
     * @param string $endpoint
193
     * @param array $params
194
     * @param string $content
195
     * @return mixed|null
196
     * @throws Exception
197
     */
198
    public function contentRequest(string $endpoint, array $params, string $content = '')
199
    {
200
        $headers = [
201
            'Authorization' => 'Bearer ' . $this->tokenService->getAccessToken(),
202
            'Dropbox-API-Arg' => json_encode($params),
203
            'Content-Type' => 'application/octet-stream'
204
        ];
205
206
        return $this->sendRequest(self::CONTENT_API_URL . '/' . $endpoint, $content, $headers);
207
    }
208
209
    /**
210
     * Gets the normalized path
211
     * @param string $name
212
     * @return array
213
     */
214
    public function path(string $name): array
215
    {
216
        return ['path' => '/' . trim($name, '/')];
217
    }
218
219
    /**
220
     * Sends request
221
     * @param string $uri
222
     * @param mixed|null $data
223
     * @param array $headers
224
     * @return mixed|null
225
     * @throws AppException
226
     * @throws HttpException
227
     * @throws LangException
228
     * @throws Exception
229
     */
230
    public function sendRequest(string $uri, $data = null, array $headers = [])
231
    {
232
        $this->httpClient
233
            ->createRequest($uri)
234
            ->setMethod('POST')
235
            ->setData($data)
236
            ->setHeaders($headers)
237
            ->start();
238
239
        $errors = $this->httpClient->getErrors();
240
        $responseBody = $this->httpClient->getResponseBody();
241
242
        if ($errors) {
243
            $code = $errors['code'];
244
245
            if ($this->accessTokenNeedsRefresh($code, $responseBody)) {
246
                $prevUrl = $this->httpClient->url();
247
                $prevData = $this->httpClient->getData();
248
                $prevHeaders = $this->httpClient->getRequestHeaders();
249
250
                $refreshToken = $this->tokenService->getRefreshToken();
251
252
                $accessToken = $this->fetchAccessTokenByRefreshToken($refreshToken);
253
                $this->tokenService->saveTokens($accessToken);
254
255
                $prevHeaders['Authorization'] = 'Bearer ' . $accessToken;
256
257
                $responseBody = $this->sendRequest($prevUrl, $prevData, $prevHeaders);
258
259
            } else {
260
                throw new Exception(json_encode($responseBody), E_ERROR);
261
            }
262
        }
263
264
        return $responseBody;
265
    }
266
267
    /**
268
     * Fetches the access token by refresh token
269
     * @param string $refreshToken
270
     * @return string
271
     * @throws AppException
272
     * @throws HttpException
273
     * @throws LangException
274
     */
275
    private function fetchAccessTokenByRefreshToken(string $refreshToken): string
276
    {
277
        $params = [
278
            'refresh_token' => $refreshToken,
279
            'grant_type' => 'refresh_token',
280
            'client_id' => $this->appKey,
281
            'client_secret' => $this->appSecret
282
        ];
283
284
        $tokenUrl = self::AUTH_TOKEN_URL . '?' . http_build_query($params, '', '&');
285
286
        $response = $this->sendRequest($tokenUrl);
287
288
        return $response->access_token;
289
    }
290
291
    /**
292
     * Checks if the access token need refresh
293
     * @param int $code
294
     * @param $message
295
     * @return bool
296
     */
297
    private function accessTokenNeedsRefresh(int $code, $message): bool
298
    {
299
        if ($code != 401) {
300
            return false;
301
        }
302
303
        $error = (array)$message->error;
304
305
        if (!isset($error['.tag']) && !in_array($error['.tag'], self::ACCESS_TOKEN_STATUS)) {
306
            return false;
307
        }
308
309
        return true;
310
    }
311
312
}