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 (#30)
by
unknown
02:53
created

Client::upload()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 19
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 19
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 10
nc 2
nop 3
1
<?php
2
3
namespace Spatie\Dropbox;
4
5
use Exception;
6
use GuzzleHttp\Psr7;
7
use GuzzleHttp\Psr7\StreamWrapper;
8
use GuzzleHttp\Client as GuzzleClient;
9
use Psr\Http\Message\ResponseInterface;
10
use GuzzleHttp\Exception\ClientException;
11
use Psr\Http\Message\StreamInterface;
12
use Spatie\Dropbox\Exceptions\BadRequest;
13
14
class Client
15
{
16
    const THUMBNAIL_FORMAT_JPEG = 'jpeg';
17
    const THUMBNAIL_FORMAT_PNG = 'png';
18
19
    const THUMBNAIL_SIZE_XS = 'w32h32';
20
    const THUMBNAIL_SIZE_S = 'w64h64';
21
    const THUMBNAIL_SIZE_M = 'w128h128';
22
    const THUMBNAIL_SIZE_L = 'w640h480';
23
    const THUMBNAIL_SIZE_XL = 'w1024h768';
24
25
    const MAX_CHUNK_SIZE = 150 * 1024 * 1024;
26
27
    /** @var string */
28
    protected $accessToken;
29
30
    /** @var \GuzzleHttp\Client */
31
    protected $client;
32
33
    /** @var int */
34
    protected $maxChunkSize;
35
36
37
    /**
38
     * @param string            $accessToken
39
     * @param GuzzleClient|null $client
40
     * @param int               $maxChunkSize This is also the amount of memory that will be used for storing data
41
     */
42
    public function __construct(string $accessToken, GuzzleClient $client = null, int $maxChunkSize = self::MAX_CHUNK_SIZE /*4 * 1024 * 1024*/)
43
    {
44
        $this->accessToken = $accessToken;
45
46
        $this->client = $client ?? new GuzzleClient([
47
                'headers' => [
48
                    'Authorization' => "Bearer {$this->accessToken}",
49
                ],
50
            ]);
51
52
        $this->maxChunkSize = ($maxChunkSize < self::MAX_CHUNK_SIZE ? $maxChunkSize : self::MAX_CHUNK_SIZE);
53
    }
54
55
    /**
56
     * Copy a file or folder to a different location in the user's Dropbox.
57
     *
58
     * If the source path is a folder all its contents will be copied.
59
     *
60
     * @link https://www.dropbox.com/developers/documentation/http/documentation#files-copy_v2
61
     */
62 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...
63
    {
64
        $parameters = [
65
            'from_path' => $this->normalizePath($fromPath),
66
            'to_path' => $this->normalizePath($toPath),
67
        ];
68
69
        return $this->rpcEndpointRequest('files/copy_v2', $parameters);
70
    }
71
72
    /**
73
     * Create a folder at a given path.
74
     *
75
     * @link https://www.dropbox.com/developers/documentation/http/documentation#files-create_folder
76
     */
77
    public function createFolder(string $path): array
78
    {
79
        $parameters = [
80
            'path' => $this->normalizePath($path),
81
        ];
82
83
        $object = $this->rpcEndpointRequest('files/create_folder', $parameters);
84
85
        $object['.tag'] = 'folder';
86
87
        return $object;
88
    }
89
90
    /**
91
     * Create a shared link with custom settings.
92
     *
93
     * If no settings are given then the default visibility is RequestedVisibility.public.
94
     * The resolved visibility, though, may depend on other aspects such as team and
95
     * shared folder settings). Only for paid users.
96
     *
97
     * @link https://www.dropbox.com/developers/documentation/http/documentation#sharing-create_shared_link_with_settings
98
     */
99 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...
100
    {
101
        $parameters = [
102
            'path' => $this->normalizePath($path),
103
            'settings' => $settings,
104
        ];
105
106
        return $this->rpcEndpointRequest('sharing/create_shared_link_with_settings', $parameters);
107
    }
108
109
    /**
110
     * List shared links.
111
     *
112
     * For empty path returns a list of all shared links. For non-empty path
113
     * returns a list of all shared links with access to the given path.
114
     *
115
     * If direct_only is set true, only direct links to the path will be returned, otherwise
116
     * it may return link to the path itself and parent folders as described on docs.
117
     *
118
     * @link https://www.dropbox.com/developers/documentation/http/documentation#sharing-list_shared_links
119
     */
120
    public function listSharedLinks(string $path = null, bool $direct_only = false, string $cursor = null): array
121
    {
122
        $parameters = [
123
            'path' => $path ? $this->normalizePath($path) : null,
124
            'cursor' => $cursor,
125
            'direct_only' => $direct_only,
126
        ];
127
128
        $body = $this->rpcEndpointRequest('sharing/list_shared_links', $parameters);
129
130
        return $body['links'];
131
    }
132
133
    /**
134
     * Delete the file or folder at a given path.
135
     *
136
     * If the path is a folder, all its contents will be deleted too.
137
     * A successful response indicates that the file or folder was deleted.
138
     *
139
     * @link https://www.dropbox.com/developers/documentation/http/documentation#files-delete
140
     */
141
    public function delete(string $path): array
142
    {
143
        $parameters = [
144
            'path' => $this->normalizePath($path),
145
        ];
146
147
        return $this->rpcEndpointRequest('files/delete', $parameters);
148
    }
149
150
    /**
151
     * Download a file from a user's Dropbox.
152
     *
153
     * @param string $path
154
     *
155
     * @return resource
156
     *
157
     * @link https://www.dropbox.com/developers/documentation/http/documentation#files-download
158
     */
159
    public function download(string $path)
160
    {
161
        $arguments = [
162
            'path' => $this->normalizePath($path),
163
        ];
164
165
        $response = $this->contentEndpointRequest('files/download', $arguments);
166
167
        return StreamWrapper::getResource($response->getBody());
168
    }
169
170
    /**
171
     * Returns the metadata for a file or folder.
172
     *
173
     * Note: Metadata for the root folder is unsupported.
174
     *
175
     * @link https://www.dropbox.com/developers/documentation/http/documentation#files-get_metadata
176
     */
177
    public function getMetadata(string $path): array
178
    {
179
        $parameters = [
180
            'path' => $this->normalizePath($path),
181
        ];
182
183
        return $this->rpcEndpointRequest('files/get_metadata', $parameters);
184
    }
185
186
    /**
187
     * Get a temporary link to stream content of a file.
188
     *
189
     * This link will expire in four hours and afterwards you will get 410 Gone.
190
     * Content-Type of the link is determined automatically by the file's mime type.
191
     *
192
     * @link https://www.dropbox.com/developers/documentation/http/documentation#files-get_temporary_link
193
     */
194 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...
195
    {
196
        $parameters = [
197
            'path' => $this->normalizePath($path),
198
        ];
199
200
        $body = $this->rpcEndpointRequest('files/get_temporary_link', $parameters);
201
202
        return $body['link'];
203
    }
204
205
    /**
206
     * Get a thumbnail for an image.
207
     *
208
     * This method currently supports files with the following file extensions:
209
     * jpg, jpeg, png, tiff, tif, gif and bmp.
210
     *
211
     * Photos that are larger than 20MB in size won't be converted to a thumbnail.
212
     *
213
     * @link https://www.dropbox.com/developers/documentation/http/documentation#files-get_thumbnail
214
     */
215
    public function getThumbnail(string $path, string $format = 'jpeg', string $size = 'w64h64'): string
216
    {
217
        $arguments = [
218
            'path' => $this->normalizePath($path),
219
            'format' => $format,
220
            'size' => $size,
221
        ];
222
223
        $response = $this->contentEndpointRequest('files/get_thumbnail', $arguments);
224
225
        return (string) $response->getBody();
226
    }
227
228
    /**
229
     * Starts returning the contents of a folder.
230
     *
231
     * If the result's ListFolderResult.has_more field is true, call
232
     * list_folder/continue with the returned ListFolderResult.cursor to retrieve more entries.
233
     *
234
     * Note: auth.RateLimitError may be returned if multiple list_folder or list_folder/continue calls
235
     * with same parameters are made simultaneously by same API app for same user. If your app implements
236
     * retry logic, please hold off the retry until the previous request finishes.
237
     *
238
     * @link https://www.dropbox.com/developers/documentation/http/documentation#files-list_folder
239
     */
240 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...
241
    {
242
        $parameters = [
243
            'path' => $this->normalizePath($path),
244
            'recursive' => $recursive,
245
        ];
246
247
        return $this->rpcEndpointRequest('files/list_folder', $parameters);
248
    }
249
250
    /**
251
     * Once a cursor has been retrieved from list_folder, use this to paginate through all files and
252
     * retrieve updates to the folder, following the same rules as documented for list_folder.
253
     *
254
     * @link https://www.dropbox.com/developers/documentation/http/documentation#files-list_folder-continue
255
     */
256
    public function listFolderContinue(string $cursor = ''): array
257
    {
258
        return $this->rpcEndpointRequest('files/list_folder/continue', compact('cursor'));
259
    }
260
261
    /**
262
     * Move a file or folder to a different location in the user's Dropbox.
263
     *
264
     * If the source path is a folder all its contents will be moved.
265
     *
266
     * @link https://www.dropbox.com/developers/documentation/http/documentation#files-move_v2
267
     */
268 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...
269
    {
270
        $parameters = [
271
            'from_path' => $this->normalizePath($fromPath),
272
            'to_path' => $this->normalizePath($toPath),
273
        ];
274
275
        return $this->rpcEndpointRequest('files/move_v2', $parameters);
276
    }
277
278
    /**
279
     * The file should be uploaded in chunks if it size exceeds the 150 MB threshold
280
     * or if the resource size could not be determined (eg. a popen() stream).
281
     *
282
     * @param string|resource $contents
283
     *
284
     * @return bool
285
     */
286
    protected function shouldUploadChunked($contents): bool
287
    {
288
        $size = is_string($contents) ? strlen($contents) : fstat($contents)['size'];
289
290
        if ($this->isPipe($contents)) {
291
            return true;
292
        }
293
294
        if ($size === null) {
295
            return true;
296
        }
297
298
        return $size > $this->maxChunkSize;
299
    }
300
301
    /**
302
     * Check if the contents is a pipe stream (not seekable, no size defined).
303
     *
304
     * @param string|resource $contents
305
     *
306
     * @return bool
307
     */
308
    protected function isPipe($contents): bool
309
    {
310
        return is_resource($contents) ? (fstat($contents)['mode'] & 010000) != 0 : false;
311
    }
312
313
    /**
314
     * Create a new file with the contents provided in the request.
315
     *
316
     * Do not use this to upload a file larger than 150 MB. Instead, create an upload session with upload_session/start.
317
     *
318
     * @link https://www.dropbox.com/developers/documentation/http/documentation#files-upload
319
     *
320
     * @param string $path
321
     * @param string|resource $contents
322
     * @param string|array $mode
323
     *
324
     * @return array
325
     */
326
    public function upload(string $path, $contents, $mode = 'add'): array
327
    {
328
        if ($this->shouldUploadChunked($contents)) {
329
            return $this->uploadChunked($path, $contents, $mode);
0 ignored issues
show
Bug introduced by
It seems like $mode defined by parameter $mode on line 326 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...
330
        }
331
332
        $arguments = [
333
            'path' => $this->normalizePath($path),
334
            'mode' => $mode,
335
        ];
336
337
        $response = $this->contentEndpointRequest('files/upload', $arguments, $contents);
338
339
        $metadata = json_decode($response->getBody(), true);
340
341
        $metadata['.tag'] = 'file';
342
343
        return $metadata;
344
    }
345
346
    /**
347
     * Upload file split in chunks. This allows uploading large files, since
348
     * Dropbox API v2 limits the content size to 150MB.
349
     *
350
     * The chunk size will affect directly the memory usage, so be careful.
351
     * Large chunks tends to speed up the upload, while smaller optimizes memory usage.
352
     *
353
     * @param string          $path
354
     * @param string|resource $contents
355
     * @param string          $mode
356
     * @param int             $chunkSize
357
     *
358
     * @return array
359
     */
360
    public function uploadChunked(string $path, $contents, $mode = 'add', $chunkSize = null): array
361
    {
362
        if ($chunkSize === null || $chunkSize > $this->maxChunkSize)
363
            $chunkSize = $this->maxChunkSize;
364
365
        $cursor = null;
366
        $stream = Psr7\stream_for($contents);
367
        while (!$stream->eof()) {
368
            $chunk_stream = new Psr7\LimitStream($stream, $chunkSize, $stream->tell());
369
            // Start upload session on first iteration, then just append on subsequent iterations
370
            if(isset($cursor))
371
                $cursor = $this->uploadSessionAppend($chunk_stream, $cursor);
372
            else
373
                $cursor = $this->uploadSessionStart($chunk_stream);
374
        }
375
        return $this->uploadSessionFinish('', $cursor, $path, $mode);
0 ignored issues
show
Bug introduced by
It seems like $cursor defined by null on line 365 can be null; however, Spatie\Dropbox\Client::uploadSessionFinish() 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...
376
    }
377
378
    /**
379
     * Upload sessions allow you to upload a single file in one or more requests,
380
     * for example where the size of the file is greater than 150 MB.
381
     * This call starts a new upload session with the given data.
382
     *
383
     * @link https://www.dropbox.com/developers/documentation/http/documentation#files-upload_session-start
384
     *
385
     * @param string|StreamInterface $contents
386
     * @param bool   $close
387
     *
388
     * @return UploadSessionCursor
389
     */
390
    public function uploadSessionStart($contents, bool $close = false): UploadSessionCursor
391
    {
392
        $arguments = compact('close');
393
394
        $response = json_decode(
395
            $this->contentEndpointRequest('files/upload_session/start', $arguments, $contents)->getBody(),
396
            true
397
        );
398
399
        return new UploadSessionCursor($response['session_id'], ($contents instanceof StreamInterface ? $contents->tell() : strlen($contents)));
400
    }
401
402
    /**
403
     * Append more data to an upload session.
404
     * When the parameter close is set, this call will close the session.
405
     * A single request should not upload more than 150 MB.
406
     *
407
     * @link https://www.dropbox.com/developers/documentation/http/documentation#files-upload_session-append_v2
408
     *
409
     * @param string|StreamInterface $contents
410
     * @param UploadSessionCursor $cursor
411
     * @param bool                $close
412
     *
413
     * @return \Spatie\Dropbox\UploadSessionCursor
414
     */
415
    public function uploadSessionAppend($contents, UploadSessionCursor $cursor, bool $close = false): UploadSessionCursor
416
    {
417
        $arguments = compact('cursor', 'close');
418
419
        $pos = $contents instanceof StreamInterface ? $contents->tell() : 0;
420
        $this->contentEndpointRequest('files/upload_session/append_v2', $arguments, $contents);
421
422
        $cursor->offset += $contents instanceof StreamInterface ? ($contents->tell() - $pos) : strlen($contents);
423
424
        return $cursor;
425
    }
426
427
    /**
428
     * Finish an upload session and save the uploaded data to the given file path.
429
     * A single request should not upload more than 150 MB.
430
     *
431
     * @link https://www.dropbox.com/developers/documentation/http/documentation#files-upload_session-finish
432
     *
433
     * @param string|StreamInterface              $contents
434
     * @param \Spatie\Dropbox\UploadSessionCursor $cursor
435
     * @param string                              $path
436
     * @param string|array                        $mode
437
     * @param bool                                $autorename
438
     * @param bool                                $mute
439
     *
440
     * @return array
441
     */
442
    public function uploadSessionFinish($contents, UploadSessionCursor $cursor, string $path, $mode = 'add', $autorename = false, $mute = false): array
443
    {
444
        $arguments = compact('cursor');
445
        $arguments['commit'] = compact('path', 'mode', 'autorename', 'mute');
446
447
        $response = $this->contentEndpointRequest(
448
            'files/upload_session/finish',
449
            $arguments,
450
            ($contents == '') ? null : $contents
451
        );
452
453
        $metadata = json_decode($response->getBody(), true);
454
455
        $metadata['.tag'] = 'file';
456
457
        return $metadata;
458
    }
459
460
    /**
461
     * Get Account Info for current authenticated user.
462
     *
463
     * @link https://www.dropbox.com/developers/documentation/http/documentation#users-get_current_account
464
     *
465
     * @return array
466
     */
467
    public function getAccountInfo(): array
468
    {
469
        return $this->rpcEndpointRequest('users/get_current_account');
470
    }
471
472
    /**
473
     * Revoke current access token.
474
     *
475
     * @link https://www.dropbox.com/developers/documentation/http/documentation#auth-token-revoke
476
     */
477
    public function revokeToken()
478
    {
479
        $this->rpcEndpointRequest('auth/token/revoke');
480
    }
481
482
    protected function normalizePath(string $path): string
483
    {
484
        $path = trim($path, '/');
485
486
        return ($path === '') ? '' : '/'.$path;
487
    }
488
489
    /**
490
     * @param string $endpoint
491
     * @param array $arguments
492
     * @param string|resource|StreamInterface $body
493
     *
494
     * @return \Psr\Http\Message\ResponseInterface
495
     *
496
     * @throws \Exception
497
     */
498
    public function contentEndpointRequest(string $endpoint, array $arguments, $body = ''): ResponseInterface
499
    {
500
        $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...
501
502
        if ($body !== '') {
503
            $headers['Content-Type'] = 'application/octet-stream';
504
        }
505
506
        try {
507
            $response = $this->client->post("https://content.dropboxapi.com/2/{$endpoint}", [
508
                'headers' => $headers,
509
                'body' => $body,
510
            ]);
511
        } catch (ClientException $exception) {
512
            throw $this->determineException($exception);
513
        }
514
515
        return $response;
516
    }
517
518
    public function rpcEndpointRequest(string $endpoint, array $parameters = null): array
519
    {
520
        try {
521
            $options = [];
522
523
            if ($parameters) {
524
                $options['json'] = $parameters;
525
            }
526
527
            $response = $this->client->post("https://api.dropboxapi.com/2/{$endpoint}", $options);
528
        } catch (ClientException $exception) {
529
            throw $this->determineException($exception);
530
        }
531
532
        $response = json_decode($response->getBody(), true);
533
534
        return $response ?? [];
535
    }
536
537
    protected function determineException(ClientException $exception): Exception
538
    {
539
        if (in_array($exception->getResponse()->getStatusCode(), [400, 409])) {
540
            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...
541
        }
542
543
        return $exception;
544
    }
545
}
546