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 ( 73699d...0df470 )
by Freek
24s queued 11s
created

Client::getHeadersForCredentials()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

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