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
Push — master ( 02328d...d51b89 )
by Freek
02:13
created

Client::listFolderContinue()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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