Test Setup Failed
Pull Request — master (#13)
by
unknown
13:33
created

Client::getItemBreadcrumbs()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 1
c 0
b 0
f 0
dl 0
loc 3
ccs 0
cts 0
cp 0
rs 10
cc 1
nc 1
nop 1
crap 2
1
<?php
2
declare(strict_types=1);
3
4
namespace Kapersoft\ShareFile;
5
6
use Exception;
7
use GuzzleHttp\Client as HttpClient;
8
use GuzzleHttp\HandlerStack;
9
use GuzzleHttp\Handler\MockHandler;
10
use GuzzleHttp\Exception\ClientException;
11
use Kapersoft\Sharefile\Exceptions\BadRequest;
12
use Slacker775\OAuth2\Client\Provider\ShareFile as AuthProvider;
13
use League\OAuth2\Client\Provider\AbstractProvider;
14
use League\OAuth2\Client\Token\AccessToken;
15
use OAuth\Client\TokenStorage\TokenStorageInterface;
16
use OAuth\Client\Exception\TokenNotFoundException;
17
18
/**
19
 * Class Client.
20
 *
21
 * @author   Jan Willem Kaper <[email protected]>
22
 * @license  MIT (see License.txt)
23
 *
24
 * @link     http://github.com/kapersoft/sharefile-api
25
 */
26
class Client
27
{
28
    /**
29
     * ShareFile token.
30
     *
31
     * @var array
32
     */
33
    public $token;
34
35
    /**
36
     *
37
     * @var AbstractProvider
38
     */
39
    protected $authProvider;
40
41
    /**
42
     *
43
     * @var AccessToken
44
     */
45
    protected $accessToken;
46
47
    /**
48
     *
49
     * @var TokenStorageInterface
50
     */
51
    protected $tokenRepository;
52
53
    /**
54
     *
55
     * @var array
56
     */
57
    protected $options;
58
59
    /**
60
     * Thumbnail size.
61
     */
62
    const THUMBNAIL_SIZE_M = 75;
63
    const THUMBNAIL_SIZE_L = 600;
64
65
    /*
66
     * ShareFile Folder
67
     */
68 75
    const FOLDER_TOP = 'top';
69
    const FOLDER_HOME = 'home';
70 75
    const FOLDER_FAVORITES = 'favorites';
71
    const FOLDER_ALLSHARED = 'allshared';
72 69
73 3
    /*
74
    * Default Chunk Size for uploading files
75
    */
76 66
    const DEFAULT_CHUNK_SIZE = 8 * 1024 * 1024;
77 66
78
    // 8 megabytes
79 66
80
    /**
81 66
     * Client constructor.
82
     *
83
     * @param string $hostname
84
     *            ShareFile hostname
85 66
     * @param string $client_id
86
     *            OAuth2 client_id
87
     * @param string $client_secret
88
     *            OAuth2 client_secret
89
     * @param string $username
90
     *            ShareFile username
91
     * @param string $password
92
     *            ShareFile password
93
     * @param MockHandler|HandlerStack $handler
94
     *            Guzzle Handler
95
     *
96
     * @throws Exception
97
     */
98
    public function __construct(string $hostname, string $client_id, string $client_secret, string $username, string $password, $handler = null, TokenStorageInterface $tokenRepository = null)
99
    {
100
        $this->tokenRepository = $tokenRepository;
101 75
102
        $client = new HttpClient([
103 75
            'handler' => $handler
104
        ]);
105
106 75
        $this->authProvider = new AuthProvider([
107 75
            'clientId' => $client_id,
108 75
            'clientSecret' => $client_secret
109 75
        ], [
110 75
            'httpClient' => $client
111
        ]);
112
113
        $this->options = [
114 75
            'username' => $username,
115 75
            'password' => $password,
116 75
            'baseUrl' => $hostname
117 75
        ];
118
    }
119 3
120 3
    /**
121
     * Get user details.
122
     *
123 72
     * @param string $userId ShareFile user id (optional)
124 69
     *
125
     * @return array
126 3
     */
127
    public function getUser(string $userId = '') : array
128
    {
129
        return $this->get("Users({$userId})");
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->get('Users('.$userId.')') could return the type string which is incompatible with the type-hinted return array. Consider adding an additional type-check to rule them out.
Loading history...
130
    }
131
132
    public function updateUser(string $userId, array $data) : array
133
    {
134
        return $this->patch("Users({$userId})", $data);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->patch('Users('.$userId.')', $data) could return the type string which is incompatible with the type-hinted return array. Consider adding an additional type-check to rule them out.
Loading history...
135
    }
136
137 3
    /**
138
     * Create a folder.
139 3
     *
140
     * @param string $parentId    Id of the parent folder
141
     * @param string $name        Name
142
     * @param string $description Description
143
     * @param bool   $overwrite   Overwrite folder
144
     *
145
     * @return array
146
     */
147
    public function createFolder(string $parentId, string $name, string $description = '', bool $overwrite = false) : array
148
    {
149
        $parameters = $this->buildHttpQuery(
150
            [
151
                'overwrite'   => $overwrite,
152 6
                'passthrough' => false,
153
            ]
154 6
        );
155
156 6
        $data = [
157
            'name'        => $name,
158
            'description' => $description,
159
        ];
160
161
        return $this->post("Items({$parentId})/Folder?{$parameters}", $data);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->post('Item...r?'.$parameters, $data) could return the type string which is incompatible with the type-hinted return array. Consider adding an additional type-check to rule them out.
Loading history...
162 6
    }
163 6
164
    /**
165
     * Get Folder/File using Id.
166 6
     *
167
     * @param string $itemId      Item id
168
     * @param bool   $getChildren Include children
169
     *
170
     * @return array
171
     */
172
    public function getItemById(string $itemId, bool $getChildren = false) : array
173
    {
174
        $parameters = $getChildren === true ? '$expand=Children' : '';
175
176
        return $this->get("Items({$itemId})?{$parameters}");
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->get('Items...temId.')?'.$parameters) could return the type string which is incompatible with the type-hinted return array. Consider adding an additional type-check to rule them out.
Loading history...
177 6
    }
178
179 6
    /**
180
     * Get Folder/File using path.
181 6
     *
182
     * @param string $path   Path
183
     * @param string $itemId Id of the root folder (optional)
184
     *
185
     * @return array
186
     */
187
    public function getItemByPath(string $path, string $itemId = '') : array
188
    {
189
        if (empty($itemId)) {
190
            return $this->get("Items/ByPath?Path={$path}");
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->get('Items/ByPath?Path='.$path) could return the type string which is incompatible with the type-hinted return array. Consider adding an additional type-check to rule them out.
Loading history...
191
        } else {
192 3
            return $this->get("Items({$itemId})/ByPath?Path={$path}");
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->get('Items...')/ByPath?Path='.$path) could return the type string which is incompatible with the type-hinted return array. Consider adding an additional type-check to rule them out.
Loading history...
193
        }
194 3
    }
195 3
196
    /**
197
     * Get breadcrumbs of an item.
198
     *
199
     * @param string $itemId Item Id
200
     *
201
     * @return array
202
     */
203
    public function getItemBreadcrumbs(string $itemId) : array
204
    {
205
        return $this->get("Items({$itemId})/Breadcrumbs");
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->get('Items...itemId.')/Breadcrumbs') could return the type string which is incompatible with the type-hinted return array. Consider adding an additional type-check to rule them out.
Loading history...
206
    }
207
208 3
    /**
209
     * Copy an item.
210 3
     *
211
     * @param string $targetId  Id of the target folder
212
     * @param string $itemId    Id of the copied item
213
     * @param bool   $overwrite Indicates whether items with the same name will be overwritten or not (optional)
214
     *
215
     * @return array
216
     */
217
    public function copyItem(string $targetId, string $itemId, bool $overwrite = false) : array
218
    {
219
        $parameters = $this->buildHttpQuery(
220
            [
221
                'targetid'  => $targetId,
222 6
                'overwrite' => $overwrite,
223
            ]
224 6
        );
225
226 6
        return $this->post("Items({$itemId})/Copy?{$parameters}");
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->post('Item....')/Copy?'.$parameters) could return the type string which is incompatible with the type-hinted return array. Consider adding an additional type-check to rule them out.
Loading history...
227 6
    }
228
229
    /**
230
     * Update an item.
231 6
     *
232
     * @param string $itemId    Id of the item
233
     * @param array  $data      New data
234
     * @param bool   $forceSync Indicates whether operation is to be executed synchronously (optional)
235
     * @param bool   $notify    Indicates whether an email should be sent to users subscribed to Upload Notifications (optional)
236
     *
237
     * @return array
238
     */
239
    public function updateItem(string $itemId, array $data, bool $forceSync = true, bool $notify = true) : array
240
    {
241
        $parameters = $this->buildHttpQuery(
242
            [
243
                'forceSync' => $forceSync,
244 3
                'notify'    => $notify,
245
            ]
246 3
        );
247
248 3
        return $this->patch("Items({$itemId})?{$parameters}", $data);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->patch('Ite...)?'.$parameters, $data) could return the type string which is incompatible with the type-hinted return array. Consider adding an additional type-check to rule them out.
Loading history...
249 3
    }
250
251
    /**
252
     * Delete an item.
253 3
     *
254
     * @param string $itemId        Item id
255
     * @param bool   $singleversion True it will delete only the specified version rather than all sibling files with the same filename (optional)
256
     * @param bool   $forceSync     True will block the operation from taking place asynchronously (optional)
257
     *
258
     * @return string
259
     */
260
    public function deleteItem(string $itemId, bool $singleversion = false, bool $forceSync = false) : string
261
    {
262
        $parameters = $this->buildHttpQuery(
263
            [
264
                'singleversion' => $singleversion,
265 3
                'forceSync'     => $forceSync,
266
            ]
267 3
        );
268
269 3
        return $this->delete("Items({$itemId})?{$parameters}");
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->delete('It...temId.')?'.$parameters) 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...
270 3
    }
271
272
    /**
273
     * Get temporary download URL for an item.
274 3
     *
275
     * @param string $itemId             Item id
276
     * @param bool   $includeallversions For folder downloads only, includes old versions of files in the folder in the zip when true, current versions only when false (default)
277
     *
278
     * @return array
279
     */
280
    public function getItemDownloadUrl(string $itemId, bool $includeallversions = false):array
281
    {
282
        $parameters = $this->buildHttpQuery(
283
            [
284
                'includeallversions' => $includeallversions,
285 3
                'redirect'           => false,
286
            ]
287 3
        );
288
289 3
        return $this->get("Items({$itemId})/Download?{$parameters}");
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->get('Items...Download?'.$parameters) could return the type string which is incompatible with the type-hinted return array. Consider adding an additional type-check to rule them out.
Loading history...
290
    }
291
292
    /**
293
     * Get contents of and item.
294 3
     *
295
     * @param string $itemId             Item id
296
     * @param bool   $includeallversions $includeallversions For folder downloads only, includes old versions of files in the folder in the zip when true, current versions only when false (default)
297
     *
298
     * @return mixed
299
     */
300
    public function getItemContents(string $itemId, bool $includeallversions = false)
301
    {
302
        $parameters = $this->buildHttpQuery(
303
            [
304
                'includeallversions' => $includeallversions,
305 3
                'redirect'           => true,
306
            ]
307 3
        );
308
309 3
        return $this->get("Items({$itemId})/Download?{$parameters}");
310
    }
311
312
    /**
313
     * Get the Chunk Uri to start a file-upload.
314 3
     *
315
     * @param string   $method    Upload method (Standard or Streamed)
316
     * @param string   $filename  Name of file
317
     * @param string   $folderId  Id of the parent folder
318
     * @param bool     $unzip     Indicates that the upload is a Zip file, and contents must be extracted at the end of upload. The resulting files and directories will be placed in the target folder. If set to false, the ZIP file is uploaded as a single file. Default is false (optional)
319
     * @param bool     $overwrite Indicates whether items with the same name will be overwritten or not (optional)
320
     * @param bool     $notify    Indicates whether users will be notified of this upload - based on folder preferences (optional)
321
     * @param bool     $raw       Send contents contents directly in the POST body (=true) or send contents in MIME format (=false) (optional)
322
     * @param resource $stream    Resource stream of the contents (optional)
323
     *
324
     * @return array
325
     */
326
    public function getChunkUri(string $method, string $filename, string $folderId, bool $unzip = false, $overwrite = true, bool $notify = true, bool $raw = false, $stream = null):array
327
    {
328
        $parameters = $this->buildHttpQuery(
329
            [
330
                'method'                => $method,
331 9
                'raw'                   => $raw,
332
                'fileName'              => basename($filename),
333 9
                'fileSize'              => $stream == null ? filesize($filename) : fstat($stream)['size'],
334
                'canResume'             => false,
335 9
                'startOver'             => false,
336 9
                'unzip'                 => $unzip,
337 9
                'tool'                  => 'apiv3',
338 9
                'overwrite'             => $overwrite,
339
                'title'                 => basename($filename),
340
                'isSend'                => false,
341 9
                'responseFormat'        => 'json',
342 9
                'notify'                => $notify,
343 9
                'clientCreatedDateUTC'  => $stream == null ? filectime($filename) : fstat($stream)['ctime'],
344 9
                'clientModifiedDateUTC' => $stream == null ? filemtime($filename) : fstat($stream)['mtime'],
345
            ]
346 9
        );
347 9
348 9
        return $this->post("Items({$folderId})/Upload?{$parameters}");
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->post('Item...)/Upload?'.$parameters) could return the type string which is incompatible with the type-hinted return array. Consider adding an additional type-check to rule them out.
Loading history...
349 9
    }
350
351
    /**
352
     * Upload a file using a single HTTP POST.
353 9
     *
354
     * @param string $filename  Name of file
355
     * @param string $folderId  Id of the parent folder
356
     * @param bool   $unzip     Indicates that the upload is a Zip file, and contents must be extracted at the end of upload. The resulting files and directories will be placed in the target folder. If set to false, the ZIP file is uploaded as a single file. Default is false (optional)
357
     * @param bool   $overwrite Indicates whether items with the same name will be overwritten or not (optional)
358
     * @param bool   $notify    Indicates whether users will be notified of this upload - based on folder preferences (optional)
359
     *
360
     * @return string
361
     */
362
    public function uploadFileStandard(string $filename, string $folderId, bool $unzip = false, bool $overwrite = true, bool $notify = true):string
363
    {
364
        $chunkUri = $this->getChunkUri('standard', $filename, $folderId, $unzip, $overwrite, $notify);
365
366
        $request = $this->authProvider->getAuthenticatedRequest(
367 3
            'POST',
368
            $chunkUri['ChunkUri'],
369 3
            $this->accessToken,
370
            [
371 3
                'multipart' => [
372 3
                    [
373 3
                        'name'     => 'File1',
374
                        'contents' => fopen($filename, 'r'),
375
                    ],
376
                ],
377 3
            ]
378 3
        );
379
        $response = $this->authProvider->getResponse($request);
380
381
        return (string) $response->getBody();
382
    }
383
384 3
    /**
385
     * Upload a file using multiple HTTP POSTs.
386
     *
387
     * @param mixed    $stream    Stream resource
388
     * @param string   $folderId  Id of the parent folder
389
     * @param string   $filename  Filename (optional)
390
     * @param bool     $unzip     Indicates that the upload is a Zip file, and contents must be extracted at the end of upload. The resulting files and directories will be placed in the target folder. If set to false, the ZIP file is uploaded as a single file. Default is false (optional)
391
     * @param bool     $overwrite Indicates whether items with the same name will be overwritten or not (optional)
392
     * @param bool     $notify    Indicates whether users will be notified of this upload - based on folder preferences (optional)
393
     * @param int      $chunkSize Maximum size of the individual HTTP posts in bytes
394
     *
395
     * @return string
396
     */
397
    public function uploadFileStreamed($stream, string $folderId, string $filename = null, bool $unzip = false, bool $overwrite = true, bool $notify = true, int $chunkSize = null):string
398
    {
399
        $filename = $filename ?? stream_get_meta_data($stream)['uri'];
400 3
        if (empty($filename)) {
401
            return 'Error: no filename';
402 3
        }
403 3
404
        $chunkUri = $this->getChunkUri('streamed', $filename, $folderId, $unzip, $overwrite, $notify, true, $stream);
405
        $chunkSize = $chunkSize ?? SELF::DEFAULT_CHUNK_SIZE;
406
        $index = 0;
407 3
408 3
        // First Chunk
409 3
        $data = $this->readChunk($stream, $chunkSize);
410
        while (! ((strlen($data) < $chunkSize) || feof($stream))) {
411
            $parameters = $this->buildHttpQuery(
412 3
                [
413 3
                    'index'      => $index,
414 3
                    'byteOffset' => $index * $chunkSize,
415
                    'hash'       => md5($data),
416 3
                ]
417 3
            );
418 3
419
            $response = $this->uploadChunk("{$chunkUri['ChunkUri']}&{$parameters}", $data);
420
421
            if ($response != 'true') {
422 3
                return $response;
423
            }
424 3
425
            // Next chunk
426
            $index++;
427
            $data = $this->readChunk($stream, $chunkSize);
428
        }
429 3
430 3
        // Final chunk
431
        $parameters = $this->buildHttpQuery(
432
            [
433
                'index'      => $index,
434 3
                'byteOffset' => $index * $chunkSize,
435
                'hash'       => md5($data),
436 3
                'filehash'   => Psr7\hash(Psr7\stream_for($stream), 'md5'),
0 ignored issues
show
Bug introduced by
The function stream_for was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

436
                'filehash'   => Psr7\hash(/** @scrutinizer ignore-call */ Psr7\stream_for($stream), 'md5'),
Loading history...
437 3
                'finish'    => true,
438 3
            ]
439 3
        );
440
441
        return $this->uploadChunk("{$chunkUri['ChunkUri']}&{$parameters}", $data);
442
    }
443
444 3
    /**
445
     * Get Thumbnail of an item.
446
     *
447
     * @param string $itemId Item id
448
     * @param int    $size   Thumbnail size: THUMBNAIL_SIZE_M or THUMBNAIL_SIZE_L (optional)
449
     *
450
     * @return array
451
     */
452
    public function getThumbnailUrl(string $itemId, int $size = 75):array
453
    {
454
        $parameters = $this->buildHttpQuery(
455 3
            [
456
                'size'     => $size,
457 3
                'redirect' => false,
458
            ]
459 3
        );
460
461
        return $this->get("Items({$itemId})/Thumbnail?{$parameters}");
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->get('Items...humbnail?'.$parameters) could return the type string which is incompatible with the type-hinted return array. Consider adding an additional type-check to rule them out.
Loading history...
462
    }
463
464 3
    /**
465
     * Get browser link for an item.
466
     *
467
     * @param string $itemId Item id
468
     *
469
     * @return array
470
     */
471
    public function getWebAppLink(string $itemId):array
472
    {
473
        return $this->post("Items({$itemId})/WebAppLink");
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->post('Item...$itemId.')/WebAppLink') could return the type string which is incompatible with the type-hinted return array. Consider adding an additional type-check to rule them out.
Loading history...
474 3
    }
475
476 3
    /**
477
     * Share Share for external user.
478
     *
479
     * @param array $options Share options
480
     * @param bool  $notify  Indicates whether user will be notified if item is downloaded (optional)
481
     *
482
     * @return array
483
     */
484
    public function createShare(array $options, $notify = false):array
485
    {
486
        $parameters = $this->buildHttpQuery(
487 3
            [
488
                'notify' => $notify,
489 3
                'direct' => true,
490
            ]
491 3
        );
492
493
        return $this->post("Shares?{$parameters}", $options);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->post('Shar....$parameters, $options) could return the type string which is incompatible with the type-hinted return array. Consider adding an additional type-check to rule them out.
Loading history...
494
    }
495
496 3
    /**
497
     * Get AccessControl List for an item.
498
     *
499
     * @param string $itemId Id of an item
500
     * @param string $userId Id of an user
501
     *
502
     * @return array
503
     */
504
    public function getItemAccessControls(string $itemId, string $userId = ''):array
505
    {
506
        if (! empty($userId)) {
507 6
            return $this->get("AccessControls(principalid={$userId},itemid={$itemId})");
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->get('Acces...',itemid='.$itemId.')') could return the type string which is incompatible with the type-hinted return array. Consider adding an additional type-check to rule them out.
Loading history...
508
        } else {
509 6
            return $this->get("Items({$itemId})/AccessControls");
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->get('Items...mId.')/AccessControls') could return the type string which is incompatible with the type-hinted return array. Consider adding an additional type-check to rule them out.
Loading history...
510 3
        }
511
    }
512 3
513
    protected function getAccessToken(): AccessToken
514
    {
515
        $tokenId = sprintf('sf-%s', $this->options['username']);
516
517
        if ($this->accessToken === null) {
518
            if ($this->tokenRepository !== null) {
519
                try {
520
                $this->accessToken = $this->tokenRepository->loadToken($tokenId);
521
                } catch(TokenNotFoundException $e) {}
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
522
            }
523 63
524
            if ($this->accessToken === null) {
525 63
                $this->accessToken = $this->authProvider->getAccessToken('password', [
526
                    'username' => $this->options['username'],
527
                    'password' => $this->options['password'],
528
                    'baseUrl' => $this->options['baseUrl']
529
                ]);
530
531
                if ($this->tokenRepository !== null) {
532
                    $this->tokenRepository->storeToken($this->accessToken, $tokenId);
533
                }
534
            }
535
        }
536
537
        if ($this->accessToken->hasExpired() === true) {
538
            $this->accessToken = $this->authProvider->getAccessToken('refresh_token', [
539 63
                'refresh_token' => $this->accessToken->getRefreshToken()
540
            ]);
541 63
            if ($this->tokenRepository !== null) {
542 63
                $this->tokenRepository->storeAccessToken($tokenId, $this->accessToken);
0 ignored issues
show
Bug introduced by
The method storeAccessToken() does not exist on OAuth\Client\TokenStorage\TokenStorageInterface. ( Ignorable by Annotation )

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

542
                $this->tokenRepository->/** @scrutinizer ignore-call */ 
543
                                        storeAccessToken($tokenId, $this->accessToken);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
543
            }
544
        }
545 63
        return $this->accessToken;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->accessToken could return the type League\OAuth2\Client\Token\AccessTokenInterface which includes types incompatible with the type-hinted return League\OAuth2\Client\Token\AccessToken. Consider adding an additional type-check to rule them out.
Loading history...
546
    }
547
548
    /**
549
     * Build API uri.
550 63
     *
551
     * @param string $endpoint API endpoint
552 63
     *
553
     * @return string
554
     */
555
    protected function buildUri(string $endpoint): string
556
    {
557
        return  "https://{$this->accessToken->getValues()['subdomain']}.sf-api.com/sf/v3/{$endpoint}";
558
    }
559
560
    /**
561
     * Make a request to the API.
562 30
     *
563
     * @param string             $method   HTTP Method
564 30
     * @param string             $endpoint API endpoint
565
     * @param mixed|string|array $json     POST body (optional)
566
     *
567
     * @throws Exception
568
     *
569
     * @return mixed
570
     */
571
    protected function request(string $method, string $endpoint, $json = null)
572
    {
573
        $accessToken = $this->getAccessToken();
574
575 27
        $uri = $this->buildUri($endpoint);
576
        $options = $json != null ? ['json' => $json] : [];
577 27
578
        try {
579
            $request = $this->authProvider->getAuthenticatedRequest($method, $uri, $accessToken, $options);
580
            $response = $this->authProvider->getResponse($request);
581
        } catch (ClientException $exception) {
582
            throw $this->determineException($exception);
583
        }
584
585
        $body = (string) $response->getBody();
586
587
        return $this->jsonValidator($body) ? json_decode($body, true) : $body;
588 3
    }
589
590 3
    /**
591
     * Shorthand for GET-request.
592
     *
593
     * @param string $endpoint API endpoint
594
     *
595
     * @return mixed
596
     */
597
    protected function get(string $endpoint)
598
    {
599
        return $this->request('GET', $endpoint);
600 3
    }
601
602 3
    /**
603
     * Shorthand for POST-request.
604
     *
605
     * @param string             $endpoint API endpoint
606
     * @param mixed|string|array $json     POST body (optional)
607
     *
608
     * @return mixed
609
     */
610
    protected function post(string $endpoint, $json = null)
611
    {
612
        return $this->request('POST', $endpoint, $json);
613 3
    }
614
615 3
    /**
616 3
     * Shorthand for PATCH-request.
617 3
     *
618
     * @param string             $endpoint API endpoint
619
     * @param mixed|string|array $json     POST body (optional)
620 3
     *
621 3
     * @return mixed
622
     */
623 3
    protected function patch(string $endpoint, $json = null)
624
    {
625
        return $this->request('PATCH', $endpoint, $json);
626
    }
627 3
628
    /**
629
     * Shorthand for DELETE-request.
630
     *
631
     * @param string $endpoint API endpoint
632
     *
633
     * @return string|array
634
     */
635
    protected function delete(string $endpoint)
636
    {
637
        return $this->request('DELETE', $endpoint);
638
    }
639
640
    /**
641 3
     * Upload a chunk of data using HTTP POST body.
642
     *
643 3
     * @param string $uri  Upload URI
644 3
     * @param string $data Contents to upload
645 3
     *
646 3
     * @return string|array
647
     */
648
    protected function uploadChunk($uri, $data)
649 3
    {
650 3
        $request = $this->authProvider->getAuthenticatedRequest(
651
            'POST',
652
            $uri,
653 3
            $this->accessToken,
654
            [
655
                'headers' => [
656
                    'Content-Length' => strlen($data),
657
                    'Content-Type'   => 'application/octet-stream',
658
                ],
659
                'body' => $data,
660
            ]
661
        );
662
        $response = $this->authProvider->getResponse($request);
663
664
        return (string) $response->getBody();
665
    }
666
667
    /**
668
     * Sometimes fread() returns less than the request number of bytes (for example, when reading
669
     * from network streams).  This function repeatedly calls fread until the requested number of
670
     * bytes have been read or we've reached EOF.
671
     *
672
     * @param resource $stream
673
     * @param int      $chunkSize
674
     *
675
     * @throws Exception
676
     * @return string
677
     */
678
    protected function readChunk($stream, int $chunkSize)
679 39
    {
680
        $chunk = '';
681 39
        while (! feof($stream) && $chunkSize > 0) {
682 39
            $part = fread($stream, $chunkSize);
683 39
            if ($part === false) {
684 39
                throw new Exception('Error reading from $stream.');
685 18
            }
686
            $chunk .= $part;
687
            $chunkSize -= strlen($part);
688 39
        }
689 39
690 39
        return $chunk;
691
    }
692
693
    /**
694
     * Handle ClientException.
695
     *
696
     * @param ClientException $exception ClientException
697
     *
698
     * @return Exception
699
     */
700
    protected function determineException(ClientException $exception): Exception
701
    {
702 63
        if (in_array($exception->getResponse()->getStatusCode(), [400, 403, 404, 409])) {
703
            return new BadRequest($exception->getResponse());
704 63
        }
705 60
706
        return $exception;
707 60
    }
708
709
    /**
710 3
     * Build HTTP query.
711
     *
712
     * @param array $parameters Query parameters
713
     *
714
     * @return string
715
     */
716
    protected function buildHttpQuery(array $parameters):string
717
    {
718
        return http_build_query(
719
            array_map(
720
                function ($parameter) {
721
                    if (! is_bool($parameter)) {
722
                        return $parameter;
723
                    }
724
725
                    return $parameter ? 'true' : 'false';
726
                },
727
                $parameters
728
            )
729
        );
730
    }
731
732
    /**
733
     * Validate JSON.
734
     *
735
     * @param mixed $data JSON variable
736
     *
737
     * @return bool
738
     */
739
    protected function jsonValidator($data = null):bool
740
    {
741
        if (! empty($data)) {
742
            @json_decode($data);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for json_decode(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

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

742
            /** @scrutinizer ignore-unhandled */ @json_decode($data);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
743
744
            return json_last_error() === JSON_ERROR_NONE;
745
        }
746
747
        return false;
748
    }
749
}
750