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 ( 338dbf...e20631 )
by Freek
16s queued 11s
created

Client::search()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

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