GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.
Completed
Pull Request — master (#21)
by
unknown
01:11
created

Client::uploadChunked()   B

Complexity

Conditions 6
Paths 12

Size

Total Lines 29
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 29
rs 8.439
c 0
b 0
f 0
cc 6
eloc 16
nc 12
nop 4
1
<?php
2
3
namespace Spatie\Dropbox;
4
5
use Exception;
6
use GuzzleHttp\Psr7\StreamWrapper;
7
use GuzzleHttp\Client as GuzzleClient;
8
use Psr\Http\Message\ResponseInterface;
9
use GuzzleHttp\Exception\ClientException;
10
use Spatie\Dropbox\Exceptions\BadRequest;
11
12
class Client
13
{
14
    const THUMBNAIL_FORMAT_JPEG = 'jpeg';
15
    const THUMBNAIL_FORMAT_PNG = 'png';
16
17
    const THUMBNAIL_SIZE_XS = 'w32h32';
18
    const THUMBNAIL_SIZE_S = 'w64h64';
19
    const THUMBNAIL_SIZE_M = 'w128h128';
20
    const THUMBNAIL_SIZE_L = 'w640h480';
21
    const THUMBNAIL_SIZE_XL = 'w1024h768';
22
23
    const AUTO_CHUNKED_UPLOAD_THRESHOLD = 157286400;
24
    const DEFAULT_CHUNK_SIZE = 4194304;
25
26
    /** @var string */
27
    protected $accessToken;
28
29
    /** @var \GuzzleHttp\Client */
30
    protected $client;
31
32
    public function __construct(string $accessToken, GuzzleClient $client = null)
33
    {
34
        $this->accessToken = $accessToken;
35
36
        $this->client = $client ?? new GuzzleClient([
37
                'headers' => [
38
                    'Authorization' => "Bearer {$this->accessToken}",
39
                ],
40
            ]);
41
    }
42
43
    /**
44
     * Copy a file or folder to a different location in the user's Dropbox.
45
     *
46
     * If the source path is a folder all its contents will be copied.
47
     *
48
     * @link https://www.dropbox.com/developers/documentation/http/documentation#files-copy
49
     */
50 View Code Duplication
    public function copy(string $fromPath, string $toPath): array
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
51
    {
52
        $parameters = [
53
            'from_path' => $this->normalizePath($fromPath),
54
            'to_path' => $this->normalizePath($toPath),
55
        ];
56
57
        return $this->rpcEndpointRequest('files/copy', $parameters);
58
    }
59
60
    /**
61
     * Create a folder at a given path.
62
     *
63
     * @link https://www.dropbox.com/developers/documentation/http/documentation#files-create_folder
64
     */
65
    public function createFolder(string $path): array
66
    {
67
        $parameters = [
68
            'path' => $this->normalizePath($path),
69
        ];
70
71
        $object = $this->rpcEndpointRequest('files/create_folder', $parameters);
72
73
        $object['.tag'] = 'folder';
74
75
        return $object;
76
    }
77
78
    /**
79
     * Create a shared link with custom settings.
80
     *
81
     * If no settings are given then the default visibility is RequestedVisibility.public.
82
     * The resolved visibility, though, may depend on other aspects such as team and
83
     * shared folder settings). Only for paid users.
84
     *
85
     * @link https://www.dropbox.com/developers/documentation/http/documentation#sharing-create_shared_link_with_settings
86
     */
87 View Code Duplication
    public function createSharedLinkWithSettings(string $path, array $settings = [])
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
88
    {
89
        $parameters = [
90
            'path' => $this->normalizePath($path),
91
            'settings' => $settings,
92
        ];
93
94
        return $this->rpcEndpointRequest('sharing/create_shared_link_with_settings', $parameters);
95
    }
96
97
    /**
98
     * List shared links.
99
     *
100
     * For empty path returns a list of all shared links. For non-empty path
101
     * returns a list of all shared links with access to the given path.
102
     *
103
     * If direct_only is set true, only direct links to the path will be returned, otherwise
104
     * it may return link to the path itself and parent folders as described on docs.
105
     *
106
     * @link https://www.dropbox.com/developers/documentation/http/documentation#sharing-list_shared_links
107
     */
108
    public function listSharedLinks(string $path = null, bool $direct_only = false, string $cursor = null): array
109
    {
110
        $parameters = [
111
            'path' => $path ? $this->normalizePath($path) : null,
112
            'cursor' => $cursor,
113
            'direct_only' => $direct_only,
114
        ];
115
116
        $body = $this->rpcEndpointRequest('sharing/list_shared_links', $parameters);
117
118
        return $body['links'];
119
    }
120
121
    /**
122
     * Delete the file or folder at a given path.
123
     *
124
     * If the path is a folder, all its contents will be deleted too.
125
     * A successful response indicates that the file or folder was deleted.
126
     *
127
     * @link https://www.dropbox.com/developers/documentation/http/documentation#files-delete
128
     */
129
    public function delete(string $path): array
130
    {
131
        $parameters = [
132
            'path' => $this->normalizePath($path),
133
        ];
134
135
        return $this->rpcEndpointRequest('files/delete', $parameters);
136
    }
137
138
    /**
139
     * Download a file from a user's Dropbox.
140
     *
141
     * @param string $path
142
     *
143
     * @return resource
144
     *
145
     * @link https://www.dropbox.com/developers/documentation/http/documentation#files-download
146
     */
147
    public function download(string $path)
148
    {
149
        $arguments = [
150
            'path' => $this->normalizePath($path),
151
        ];
152
153
        $response = $this->contentEndpointRequest('files/download', $arguments);
154
155
        return StreamWrapper::getResource($response->getBody());
156
    }
157
158
    /**
159
     * Returns the metadata for a file or folder.
160
     *
161
     * Note: Metadata for the root folder is unsupported.
162
     *
163
     * @link https://www.dropbox.com/developers/documentation/http/documentation#files-get_metadata
164
     */
165
    public function getMetadata(string $path): array
166
    {
167
        $parameters = [
168
            'path' => $this->normalizePath($path),
169
        ];
170
171
        return $this->rpcEndpointRequest('files/get_metadata', $parameters);
172
    }
173
174
    /**
175
     * Get a temporary link to stream content of a file.
176
     *
177
     * This link will expire in four hours and afterwards you will get 410 Gone.
178
     * Content-Type of the link is determined automatically by the file's mime type.
179
     *
180
     * @link https://www.dropbox.com/developers/documentation/http/documentation#files-get_temporary_link
181
     */
182 View Code Duplication
    public function getTemporaryLink(string $path): string
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
183
    {
184
        $parameters = [
185
            'path' => $this->normalizePath($path),
186
        ];
187
188
        $body = $this->rpcEndpointRequest('files/get_temporary_link', $parameters);
189
190
        return $body['link'];
191
    }
192
193
    /**
194
     * Get a thumbnail for an image.
195
     *
196
     * This method currently supports files with the following file extensions:
197
     * jpg, jpeg, png, tiff, tif, gif and bmp.
198
     *
199
     * Photos that are larger than 20MB in size won't be converted to a thumbnail.
200
     *
201
     * @link https://www.dropbox.com/developers/documentation/http/documentation#files-get_thumbnail
202
     */
203
    public function getThumbnail(string $path, string $format = 'jpeg', string $size = 'w64h64'): string
204
    {
205
        $arguments = [
206
            'path' => $this->normalizePath($path),
207
            'format' => $format,
208
            'size' => $size,
209
        ];
210
211
        $response = $this->contentEndpointRequest('files/get_thumbnail', $arguments);
212
213
        return (string) $response->getBody();
214
    }
215
216
    /**
217
     * Starts returning the contents of a folder.
218
     *
219
     * If the result's ListFolderResult.has_more field is true, call
220
     * list_folder/continue with the returned ListFolderResult.cursor to retrieve more entries.
221
     *
222
     * Note: auth.RateLimitError may be returned if multiple list_folder or list_folder/continue calls
223
     * with same parameters are made simultaneously by same API app for same user. If your app implements
224
     * retry logic, please hold off the retry until the previous request finishes.
225
     *
226
     * @link https://www.dropbox.com/developers/documentation/http/documentation#files-list_folder
227
     */
228 View Code Duplication
    public function listFolder(string $path = '', bool $recursive = false): array
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
229
    {
230
        $parameters = [
231
            'path' => $this->normalizePath($path),
232
            'recursive' => $recursive,
233
        ];
234
235
        return $this->rpcEndpointRequest('files/list_folder', $parameters);
236
    }
237
238
    /**
239
     * Once a cursor has been retrieved from list_folder, use this to paginate through all files and
240
     * retrieve updates to the folder, following the same rules as documented for list_folder.
241
     *
242
     * @link https://www.dropbox.com/developers/documentation/http/documentation#files-list_folder-continue
243
     */
244
    public function listFolderContinue(string $cursor = ''): array
245
    {
246
        return $this->rpcEndpointRequest('files/list_folder/continue', compact('cursor'));
247
    }
248
249
    /**
250
     * Move a file or folder to a different location in the user's Dropbox.
251
     *
252
     * If the source path is a folder all its contents will be moved.
253
     *
254
     * @link https://www.dropbox.com/developers/documentation/http/documentation#files-move
255
     */
256 View Code Duplication
    public function move(string $fromPath, string $toPath): array
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
257
    {
258
        $parameters = [
259
            'from_path' => $this->normalizePath($fromPath),
260
            'to_path' => $this->normalizePath($toPath),
261
        ];
262
263
        return $this->rpcEndpointRequest('files/move', $parameters);
264
    }
265
266
    /**
267
     * The file should be uploaded in chunks if it size exceeds the 150 MB threshold
268
     * or if the resource size could not be determined. (eg. a popen() stream)
269
     *
270
     * @param string|resource $contents
271
     *
272
     * @return bool
273
     */
274
    protected function shouldUploadChunked($contents): boolean
275
    {
276
        $size = is_string($contents) ? strlen($contents) : fstat($contents)['size'];
277
278
        return ($size === null || $size > self::AUTO_CHUNKED_UPLOAD_THRESHOLD);
279
    }
280
281
    /**
282
     * Create a new file with the contents provided in the request.
283
     *
284
     * Do not use this to upload a file larger than 150 MB. Instead, create an upload session with upload_session/start.
285
     *
286
     * @link https://www.dropbox.com/developers/documentation/http/documentation#files-upload
287
     *
288
     * @param string $path
289
     * @param string|resource $contents
290
     * @param string|array $mode
291
     *
292
     * @return array
293
     */
294
    public function upload(string $path, $contents, $mode = 'add'): array
295
    {
296
        if ($this->shouldUploadChunked($contents)) {
297
            return $this->uploadChunked($path, $contents, $mode);
0 ignored issues
show
Bug introduced by
It seems like $mode defined by parameter $mode on line 294 can also be of type array; however, Spatie\Dropbox\Client::uploadChunked() does only seem to accept string, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
298
        }
299
300
        $arguments = [
301
            'path' => $this->normalizePath($path),
302
            'mode' => $mode,
303
        ];
304
305
        $response = $this->contentEndpointRequest('files/upload', $arguments, $contents);
306
307
        $metadata = json_decode($response->getBody(), true);
308
309
        $metadata['.tag'] = 'file';
310
311
        return $metadata;
312
    }
313
314
    /**
315
     * Upload file split in chunks. This allows uploading large files, since
316
     * Dropbox API v2 limits the content size to 150MB.
317
     *
318
     * The chunk size will affect directly the memory usage, so be careful.
319
     * Large chunks tends to speed up the upload, while smaller optimizes memory usage.
320
     *
321
     * @param string          $path
322
     * @param string|resource $contents
323
     * @param string          $mode
324
     * @param int             $chunkSize
325
     *
326
     * @return array
327
     */
328
    public function uploadChunked(string $path, $contents, $mode = 'add', $chunkSize = null): array
329
    {
330
        $chunkSize = $chunkSize ?? static::DEFAULT_CHUNK_SIZE;
331
        $stream = $contents;
332
333
        // This method relies on resources, so we need to convert strings to resource
334
        if (is_string($contents)) {
335
            $stream = fopen('php://memory', 'r+');
336
            fwrite($stream, $contents);
337
            rewind($stream);
338
        }
339
340
        $data = self::readChunk($stream, $chunkSize);
0 ignored issues
show
Bug introduced by
It seems like $stream defined by $contents on line 331 can also be of type string; however, Spatie\Dropbox\Client::readChunk() does only seem to accept resource, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
341
        $cursor = null;
342
343
        while (! ((strlen($data) < $chunkSize) || feof($stream))) {
344
            // Start upload session on first iteration, then just append on subsequent iterations
345
            $cursor = isset($cursor) ? $this->uploadSessionAppend($data, $cursor) : $this->uploadSessionStart($data);
346
            $data = self::readChunk($stream, $chunkSize);
0 ignored issues
show
Bug introduced by
It seems like $stream defined by $contents on line 331 can also be of type string; however, Spatie\Dropbox\Client::readChunk() does only seem to accept resource, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
347
        }
348
349
        // If there's no cursor here, our stream is small enough to a single request
350
        if (! isset($cursor)) {
351
            $cursor = $this->uploadSessionStart($data);
352
            $data = '';
353
        }
354
355
        return $this->uploadSessionFinish($data, $cursor, $path, $mode);
356
    }
357
358
    /**
359
     * Upload sessions allow you to upload a single file in one or more requests,
360
     * for example where the size of the file is greater than 150 MB.
361
     * This call starts a new upload session with the given data.
362
     *
363
     * @link https://www.dropbox.com/developers/documentation/http/documentation#files-upload_session-start
364
     *
365
     * @param string $contents
366
     * @param bool   $close
367
     *
368
     * @return UploadSessionCursor
369
     */
370
    public function uploadSessionStart($contents, bool $close = false): UploadSessionCursor
371
    {
372
        $arguments = compact('close');
373
374
        $response = json_decode(
375
            $this->contentEndpointRequest('files/upload_session/start', $arguments, $contents)->getBody(),
376
            true
377
        );
378
379
        return new UploadSessionCursor($response['session_id'], strlen($contents));
380
    }
381
382
    /**
383
     * Append more data to an upload session.
384
     * When the parameter close is set, this call will close the session.
385
     * A single request should not upload more than 150 MB.
386
     *
387
     * @link https://www.dropbox.com/developers/documentation/http/documentation#files-upload_session-append_v2
388
     *
389
     * @param string              $contents
390
     * @param UploadSessionCursor $cursor
391
     * @param bool                $close
392
     *
393
     * @return \Spatie\Dropbox\UploadSessionCursor
394
     */
395
    public function uploadSessionAppend($contents, UploadSessionCursor $cursor, bool $close = false): UploadSessionCursor
396
    {
397
        $arguments = compact('cursor', 'close');
398
399
        $this->contentEndpointRequest('files/upload_session/append_v2', $arguments, $contents);
400
401
        $cursor->offset += strlen($contents);
402
403
        return $cursor;
404
    }
405
406
    /**
407
     * Finish an upload session and save the uploaded data to the given file path.
408
     * A single request should not upload more than 150 MB.
409
     *
410
     * @link https://www.dropbox.com/developers/documentation/http/documentation#files-upload_session-finish
411
     *
412
     * @param string                              $contents
413
     * @param \Spatie\Dropbox\UploadSessionCursor $cursor
414
     * @param string                              $path
415
     * @param string|array                        $mode
416
     * @param bool                                $autorename
417
     * @param bool                                $mute
418
     *
419
     * @return array
420
     */
421
    public function uploadSessionFinish($contents, UploadSessionCursor $cursor, string $path, $mode = 'add', $autorename = false, $mute = false): array
422
    {
423
        $arguments = compact('cursor');
424
        $arguments['commit'] = compact('path', 'mode', 'autorename', 'mute');
425
426
        $response = $this->contentEndpointRequest(
427
            'files/upload_session/finish',
428
            $arguments,
429
            ($contents == '') ? null : $contents
430
        );
431
432
        return json_decode($response->getBody(), true);
433
    }
434
435
    /**
436
     * Sometimes fread() returns less than the request number of bytes (for example, when reading
437
     * from network streams).  This function repeatedly calls fread until the requested number of
438
     * bytes have been read or we've reached EOF.
439
     *
440
     * @param resource $stream
441
     * @param int      $chunkSize
442
     *
443
     * @throws Exception
444
     * @return string
445
     */
446
    protected static function readChunk($stream, int $chunkSize)
447
    {
448
        $chunk = '';
449
        while (! feof($stream) && $chunkSize > 0) {
450
            $part = fread($stream, $chunkSize);
451
            if ($part === false) {
452
                throw new Exception('Error reading from $stream.');
453
            }
454
            $chunk .= $part;
455
            $chunkSize -= strlen($part);
456
        }
457
458
        return $chunk;
459
    }
460
461
    /**
462
     * Get Account Info for current authenticated user.
463
     *
464
     * @link https://www.dropbox.com/developers/documentation/http/documentation#users-get_current_account
465
     *
466
     * @return array
467
     */
468
    public function getAccountInfo(): array
469
    {
470
        return $this->rpcEndpointRequest('users/get_current_account');
471
    }
472
473
    /**
474
     * Revoke current access token.
475
     *
476
     * @link https://www.dropbox.com/developers/documentation/http/documentation#auth-token-revoke
477
     */
478
    public function revokeToken()
479
    {
480
        $this->rpcEndpointRequest('auth/token/revoke');
481
    }
482
483
    protected function normalizePath(string $path): string
484
    {
485
        $path = trim($path, '/');
486
487
        return ($path === '') ? '' : '/'.$path;
488
    }
489
490
    /**
491
     * @param string $endpoint
492
     * @param array $arguments
493
     * @param string|resource $body
494
     *
495
     * @return \Psr\Http\Message\ResponseInterface
496
     *
497
     * @throws \Exception
498
     */
499
    public function contentEndpointRequest(string $endpoint, array $arguments, $body = ''): ResponseInterface
500
    {
501
        $headers['Dropbox-API-Arg'] = json_encode($arguments);
0 ignored issues
show
Coding Style Comprehensibility introduced by
$headers was never initialized. Although not strictly required by PHP, it is generally a good practice to add $headers = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
502
503
        if ($body !== '') {
504
            $headers['Content-Type'] = 'application/octet-stream';
505
        }
506
507
        try {
508
            $response = $this->client->post("https://content.dropboxapi.com/2/{$endpoint}", [
509
                'headers' => $headers,
510
                'body' => $body,
511
            ]);
512
        } catch (ClientException $exception) {
513
            throw $this->determineException($exception);
514
        }
515
516
        return $response;
517
    }
518
519
    public function rpcEndpointRequest(string $endpoint, array $parameters = null): array
520
    {
521
        try {
522
            $options = [];
523
524
            if ($parameters) {
525
                $options['json'] = $parameters;
526
            }
527
528
            $response = $this->client->post("https://api.dropboxapi.com/2/{$endpoint}", $options);
529
        } catch (ClientException $exception) {
530
            throw $this->determineException($exception);
531
        }
532
533
        $response = json_decode($response->getBody(), true);
534
535
        return $response ?? [];
536
    }
537
538
    protected function determineException(ClientException $exception): Exception
539
    {
540
        if (in_array($exception->getResponse()->getStatusCode(), [400, 409])) {
541
            return new BadRequest($exception->getResponse());
0 ignored issues
show
Bug introduced by
It seems like $exception->getResponse() can be null; however, __construct() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
542
        }
543
544
        return $exception;
545
    }
546
}
547