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 Elias
02:10
created

Client::upload()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 15
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 15
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 8
nc 1
nop 3
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
use Spatie\Dropbox\Exceptions\StreamReadException;
12
13
class Client
14
{
15
    const THUMBNAIL_FORMAT_JPEG = 'jpeg';
16
    const THUMBNAIL_FORMAT_PNG = 'png';
17
18
    const THUMBNAIL_SIZE_XS = 'w32h32';
19
    const THUMBNAIL_SIZE_S = 'w64h64';
20
    const THUMBNAIL_SIZE_M = 'w128h128';
21
    const THUMBNAIL_SIZE_L = 'w640h480';
22
    const THUMBNAIL_SIZE_XL = 'w1024h768';
23
24
    /** @var string */
25
    protected $accessToken;
26
27
    /** @var \GuzzleHttp\Client */
28
    protected $client;
29
30
    public function __construct(string $accessToken, GuzzleClient $client = null)
31
    {
32
        $this->accessToken = $accessToken;
33
34
        $this->client = $client ?? new GuzzleClient([
35
                'headers' => [
36
                    'Authorization' => "Bearer {$this->accessToken}",
37
                ],
38
            ]);
39
    }
40
41
    /**
42
     * Copy a file or folder to a different location in the user's Dropbox.
43
     *
44
     * If the source path is a folder all its contents will be copied.
45
     *
46
     * @link https://www.dropbox.com/developers/documentation/http/documentation#files-copy
47
     */
48 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...
49
    {
50
        $parameters = [
51
            'from_path' => $this->normalizePath($fromPath),
52
            'to_path' => $this->normalizePath($toPath),
53
        ];
54
55
        return $this->rpcEndpointRequest('files/copy', $parameters);
56
    }
57
58
    /**
59
     * Create a folder at a given path.
60
     *
61
     * @link https://www.dropbox.com/developers/documentation/http/documentation#files-create_folder
62
     */
63
    public function createFolder(string $path): array
64
    {
65
        $parameters = [
66
            'path' => $this->normalizePath($path),
67
        ];
68
69
        $object = $this->rpcEndpointRequest('files/create_folder', $parameters);
70
71
        $object['.tag'] = 'folder';
72
73
        return $object;
74
    }
75
76
    /**
77
     * Create a shared link with custom settings.
78
     *
79
     * If no settings are given then the default visibility is RequestedVisibility.public.
80
     * The resolved visibility, though, may depend on other aspects such as team and
81
     * shared folder settings). Only for paid users.
82
     *
83
     * @link https://www.dropbox.com/developers/documentation/http/documentation#sharing-create_shared_link_with_settings
84
     */
85 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...
86
    {
87
        $parameters = [
88
            'path' => $this->normalizePath($path),
89
            'settings' => $settings,
90
        ];
91
92
        return $this->rpcEndpointRequest('sharing/create_shared_link_with_settings', $parameters);
93
    }
94
95
    /**
96
     * List shared links.
97
     *
98
     * For empty path returns a list of all shared links. For non-empty path
99
     * returns a list of all shared links with access to the given path.
100
     *
101
     * If direct_only is set true, only direct links to the path will be returned, otherwise
102
     * it may return link to the path itself and parent folders as described on docs.
103
     *
104
     * @link https://www.dropbox.com/developers/documentation/http/documentation#sharing-list_shared_links
105
     */
106
    public function listSharedLinks(string $path = null, bool $direct_only = false, string $cursor = null): array
107
    {
108
        $parameters = [
109
            'path' => $path ? $this->normalizePath($path) : null,
110
            'cursor' => $cursor,
111
            'direct_only' => $direct_only,
112
        ];
113
114
        $body = $this->rpcEndpointRequest('sharing/list_shared_links', $parameters);
115
116
        return $body['links'];
117
    }
118
119
    /**
120
     * Delete the file or folder at a given path.
121
     *
122
     * If the path is a folder, all its contents will be deleted too.
123
     * A successful response indicates that the file or folder was deleted.
124
     *
125
     * @link https://www.dropbox.com/developers/documentation/http/documentation#files-delete
126
     */
127
    public function delete(string $path): array
128
    {
129
        $parameters = [
130
            'path' => $this->normalizePath($path),
131
        ];
132
133
        return $this->rpcEndpointRequest('files/delete', $parameters);
134
    }
135
136
    /**
137
     * Download a file from a user's Dropbox.
138
     *
139
     * @param string $path
140
     *
141
     * @return resource
142
     *
143
     * @link https://www.dropbox.com/developers/documentation/http/documentation#files-download
144
     */
145
    public function download(string $path)
146
    {
147
        $arguments = [
148
            'path' => $this->normalizePath($path),
149
        ];
150
151
        $response = $this->contentEndpointRequest('files/download', $arguments);
152
153
        return StreamWrapper::getResource($response->getBody());
154
    }
155
156
    /**
157
     * Returns the metadata for a file or folder.
158
     *
159
     * Note: Metadata for the root folder is unsupported.
160
     *
161
     * @link https://www.dropbox.com/developers/documentation/http/documentation#files-get_metadata
162
     */
163
    public function getMetadata(string $path): array
164
    {
165
        $parameters = [
166
            'path' => $this->normalizePath($path),
167
        ];
168
169
        return $this->rpcEndpointRequest('files/get_metadata', $parameters);
170
    }
171
172
    /**
173
     * Get a temporary link to stream content of a file.
174
     *
175
     * This link will expire in four hours and afterwards you will get 410 Gone.
176
     * Content-Type of the link is determined automatically by the file's mime type.
177
     *
178
     * @link https://www.dropbox.com/developers/documentation/http/documentation#files-get_temporary_link
179
     */
180 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...
181
    {
182
        $parameters = [
183
            'path' => $this->normalizePath($path),
184
        ];
185
186
        $body = $this->rpcEndpointRequest('files/get_temporary_link', $parameters);
187
188
        return $body['link'];
189
    }
190
191
    /**
192
     * Get a thumbnail for an image.
193
     *
194
     * This method currently supports files with the following file extensions:
195
     * jpg, jpeg, png, tiff, tif, gif and bmp.
196
     *
197
     * Photos that are larger than 20MB in size won't be converted to a thumbnail.
198
     *
199
     * @link https://www.dropbox.com/developers/documentation/http/documentation#files-get_thumbnail
200
     */
201
    public function getThumbnail(string $path, string $format = 'jpeg', string $size = 'w64h64'): string
202
    {
203
        $arguments = [
204
            'path' => $this->normalizePath($path),
205
            'format' => $format,
206
            'size' => $size,
207
        ];
208
209
        $response = $this->contentEndpointRequest('files/get_thumbnail', $arguments);
210
211
        return (string) $response->getBody();
212
    }
213
214
    /**
215
     * Starts returning the contents of a folder.
216
     *
217
     * If the result's ListFolderResult.has_more field is true, call
218
     * list_folder/continue with the returned ListFolderResult.cursor to retrieve more entries.
219
     *
220
     * Note: auth.RateLimitError may be returned if multiple list_folder or list_folder/continue calls
221
     * with same parameters are made simultaneously by same API app for same user. If your app implements
222
     * retry logic, please hold off the retry until the previous request finishes.
223
     *
224
     * @link https://www.dropbox.com/developers/documentation/http/documentation#files-list_folder
225
     */
226 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...
227
    {
228
        $parameters = [
229
            'path' => $this->normalizePath($path),
230
            'recursive' => $recursive,
231
        ];
232
233
        return $this->rpcEndpointRequest('files/list_folder', $parameters);
234
    }
235
236
    /**
237
     * Once a cursor has been retrieved from list_folder, use this to paginate through all files and
238
     * retrieve updates to the folder, following the same rules as documented for list_folder.
239
     *
240
     * @link https://www.dropbox.com/developers/documentation/http/documentation#files-list_folder-continue
241
     */
242
    public function listFolderContinue(string $cursor = ''): array
243
    {
244
        return $this->rpcEndpointRequest('files/list_folder/continue', compact('cursor'));
245
    }
246
247
    /**
248
     * Move a file or folder to a different location in the user's Dropbox.
249
     *
250
     * If the source path is a folder all its contents will be moved.
251
     *
252
     * @link https://www.dropbox.com/developers/documentation/http/documentation#files-move
253
     */
254 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...
255
    {
256
        $parameters = [
257
            'from_path' => $this->normalizePath($fromPath),
258
            'to_path' => $this->normalizePath($toPath),
259
        ];
260
261
        return $this->rpcEndpointRequest('files/move', $parameters);
262
    }
263
264
    /**
265
     * Create a new file with the contents provided in the request.
266
     *
267
     * Do not use this to upload a file larger than 150 MB. Instead, create an upload session with upload_session/start.
268
     *
269
     * @link https://www.dropbox.com/developers/documentation/http/documentation#files-upload
270
     *
271
     * @param string $path
272
     * @param string|resource $contents
273
     * @param string|array $mode
274
     *
275
     * @return array
276
     */
277
    public function upload(string $path, $contents, $mode = 'add'): array
278
    {
279
        $arguments = [
280
            'path' => $this->normalizePath($path),
281
            'mode' => $mode,
282
        ];
283
284
        $response = $this->contentEndpointRequest('files/upload', $arguments, $contents);
285
286
        $metadata = json_decode($response->getBody(), true);
287
288
        $metadata['.tag'] = 'file';
289
290
        return $metadata;
291
    }
292
293
    public function uploadChunked(string $path, $contents, $chunkSize, $mode = 'add'): array
294
    {
295
        // This method relies on resources, so we need to convert strings to resource
296
        if (is_string($contents)) {
297
            $stream = fopen('php://memory', 'r+');
298
            fwrite($stream, $contents);
299
            rewind($stream);
300
        } else {
301
            $stream = $contents;
302
        }
303
304
        $data = self::readFully($stream, $chunkSize);
305
        $cursor = null;
306
307
        while (!((strlen($data) < $chunkSize) || feof($stream))) {
308
            // Start upload session on first iteration, then just append on subsequent iterations
309
            if (isset($cursor)) {
310
                $cursor = $this->uploadSessionAppend($data, $cursor);
311
            } else {
312
                $cursor = $this->uploadSessionStart($data);
313
            }
314
315
            $data = self::readFully($stream, $chunkSize);
316
        }
317
318
        // If there's no cursor here, our stream is small enough to a single request
319
        if (!isset($cursor)) {
320
            $cursor = $this->uploadSessionStart($data);
321
            $data = '';
322
        }
323
324
        return $this->uploadSessionFinish($data, $cursor, $path, $mode);
325
    }
326
327
    /**
328
     * Upload sessions allow you to upload a single file in one or more requests,
329
     * for example where the size of the file is greater than 150 MB.
330
     * This call starts a new upload session with the given data.
331
     *
332
     * @link https://www.dropbox.com/developers/documentation/http/documentation#files-upload_session-start
333
     *
334
     * @param string $contents
335
     * @param bool $close
336
     *
337
     * @return UploadSessionCursor
338
     */
339 View Code Duplication
    public function uploadSessionStart($contents, bool $close = false): UploadSessionCursor
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...
340
    {
341
        $arguments = compact('close');
342
343
        $response = json_decode(
344
            $this->contentEndpointRequest('files/upload_session/start', $arguments, $contents)->getBody(),
345
            true
346
        );
347
348
        return new UploadSessionCursor($response['session_id'], strlen($contents));
349
    }
350
351
    /**
352
     * Append more data to an upload session.
353
     * When the parameter close is set, this call will close the session.
354
     * A single request should not upload more than 150 MB.
355
     *
356
     * @link https://www.dropbox.com/developers/documentation/http/documentation#files-upload_session-append_v2
357
     *
358
     * @param string              $contents
359
     * @param UploadSessionCursor $cursor
360
     * @param int|null            $chunkSize
361
     * @param bool                $close
362
     *
363
     * @return \Spatie\Dropbox\UploadSessionCursor
364
     */
365 View Code Duplication
    public function uploadSessionAppend($contents, UploadSessionCursor $cursor, int $chunkSize = null, bool $close = false): UploadSessionCursor
0 ignored issues
show
Unused Code introduced by
The parameter $chunkSize is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
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...
366
    {
367
        $arguments = compact('cursor', 'close');
368
369
        $this->contentEndpointRequest('files/upload_session/append_v2', $arguments, $contents);
370
371
        $cursor->offset += strlen($contents);
372
373
        return $cursor;
374
    }
375
376
    /**
377
     * Finish an upload session and save the uploaded data to the given file path.
378
     * A single request should not upload more than 150 MB.
379
     *
380
     * @link https://www.dropbox.com/developers/documentation/http/documentation#files-upload_session-finish
381
     *
382
     * @param string                              $contents
383
     * @param \Spatie\Dropbox\UploadSessionCursor $cursor
384
     * @param string                              $path
385
     * @param string|array                        $mode
386
     * @param bool                                $autorename
387
     * @param bool                                $mute
388
     *
389
     * @return array
390
     */
391
    public function uploadSessionFinish($contents, UploadSessionCursor $cursor, string $path, $mode = 'add', $autorename = false, $mute = false): array
392
    {
393
        $arguments = compact('cursor');
394
        $arguments['commit'] = compact('path', 'mode', 'autorename', 'mute');
395
396
        $response = $this->contentEndpointRequest('files/upload_session/finish', $arguments, $contents);
397
398
        return json_decode($response->getBody(), true);
399
    }
400
401
    /**
402
     * Sometimes fread() returns less than the request number of bytes (for example, when reading
403
     * from network streams).  This function repeatedly calls fread until the requested number of
404
     * bytes have been read or we've reached EOF.
405
     *
406
     * @param resource $inStream
407
     * @param int $numBytes
408
     * @throws StreamReadException
409
     * @return string
410
     */
411
    private static function readFully($inStream, int $numBytes)
412
    {
413
        $full = '';
414
        $bytesRemaining = $numBytes;
415
        while (!feof($inStream) && $bytesRemaining > 0) {
416
            $part = fread($inStream, $bytesRemaining);
417
            if ($part === false) throw new Exception("Error reading from \$inStream.");
418
            $full .= $part;
419
            $bytesRemaining -= strlen($part);
420
        }
421
        return $full;
422
    }
423
424
    /**
425
     * Get Account Info for current authenticated user.
426
     *
427
     * @link https://www.dropbox.com/developers/documentation/http/documentation#users-get_current_account
428
     *
429
     * @return array
430
     */
431
    public function getAccountInfo(): array
432
    {
433
        return $this->rpcEndpointRequest('users/get_current_account');
434
    }
435
436
    /**
437
     * Revoke current access token.
438
     *
439
     * @link https://www.dropbox.com/developers/documentation/http/documentation#auth-token-revoke
440
     */
441
    public function revokeToken()
442
    {
443
        $this->rpcEndpointRequest('auth/token/revoke');
444
    }
445
446
    protected function normalizePath(string $path): string
447
    {
448
        $path = trim($path, '/');
449
450
        return ($path === '') ? '' : '/'.$path;
451
    }
452
453
    /**
454
     * @param string $endpoint
455
     * @param array $arguments
456
     * @param string|resource $body
457
     *
458
     * @return \Psr\Http\Message\ResponseInterface
459
     *
460
     * @throws \Exception
461
     */
462
    public function contentEndpointRequest(string $endpoint, array $arguments, $body = ''): ResponseInterface
463
    {
464
        $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...
465
466
        if ($body !== '') {
467
            $headers['Content-Type'] = 'application/octet-stream';
468
        }
469
470
        try {
471
            $response = $this->client->post("https://content.dropboxapi.com/2/{$endpoint}", [
472
                'headers' => $headers,
473
                'body' => $body,
474
            ]);
475
        } catch (ClientException $exception) {
476
            throw $this->determineException($exception);
477
        }
478
479
        return $response;
480
    }
481
482
    public function rpcEndpointRequest(string $endpoint, array $parameters = null): array
483
    {
484
        try {
485
            $options = [];
486
487
            if ($parameters) {
488
                $options['json'] = $parameters;
489
            }
490
491
            $response = $this->client->post("https://api.dropboxapi.com/2/{$endpoint}", $options);
492
        } catch (ClientException $exception) {
493
            throw $this->determineException($exception);
494
        }
495
496
        $response = json_decode($response->getBody(), true);
497
498
        return $response ?? [];
499
    }
500
501
    protected function determineException(ClientException $exception): Exception
502
    {
503
        if (in_array($exception->getResponse()->getStatusCode(), [400, 409])) {
504
            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...
505
        }
506
507
        return $exception;
508
    }
509
}