1
|
|
|
<?php |
2
|
|
|
declare(strict_types=1); |
3
|
|
|
|
4
|
|
|
namespace Kapersoft\ShareFile; |
5
|
|
|
|
6
|
|
|
use Exception; |
7
|
|
|
use GuzzleHttp\Client as HttpClient; |
8
|
|
|
use GuzzleHttp\HandlerStack; |
9
|
|
|
use GuzzleHttp\Handler\MockHandler; |
10
|
|
|
use GuzzleHttp\Exception\ClientException; |
11
|
|
|
use Kapersoft\ShareFile\Exceptions\BadRequest; |
12
|
|
|
use Slacker775\OAuth2\Client\Provider\ShareFile as AuthProvider; |
13
|
|
|
use League\OAuth2\Client\Provider\AbstractProvider; |
14
|
|
|
use League\OAuth2\Client\Token\AccessToken; |
15
|
|
|
use OAuth\Client\TokenStorage\TokenStorageInterface; |
16
|
|
|
use OAuth\Client\Exception\TokenNotFoundException; |
17
|
|
|
|
18
|
|
|
/** |
19
|
|
|
* Class Client. |
20
|
|
|
* |
21
|
|
|
* @author Jan Willem Kaper <[email protected]> |
22
|
|
|
* @license MIT (see License.txt) |
23
|
|
|
* |
24
|
|
|
* @link http://github.com/kapersoft/sharefile-api |
25
|
|
|
*/ |
26
|
|
|
class Client |
27
|
|
|
{ |
28
|
|
|
/** |
29
|
|
|
* ShareFile token. |
30
|
|
|
* |
31
|
|
|
* @var array |
32
|
|
|
*/ |
33
|
|
|
public $token; |
34
|
|
|
|
35
|
|
|
/** |
36
|
|
|
* |
37
|
|
|
* @var AbstractProvider |
38
|
|
|
*/ |
39
|
|
|
protected $authProvider; |
40
|
|
|
|
41
|
|
|
/** |
42
|
|
|
* |
43
|
|
|
* @var AccessToken |
44
|
|
|
*/ |
45
|
|
|
protected $accessToken; |
46
|
|
|
|
47
|
|
|
/** |
48
|
|
|
* |
49
|
|
|
* @var TokenStorageInterface |
50
|
|
|
*/ |
51
|
|
|
protected $tokenRepository; |
52
|
|
|
|
53
|
|
|
/** |
54
|
|
|
* |
55
|
|
|
* @var array |
56
|
|
|
*/ |
57
|
|
|
protected $options; |
58
|
|
|
|
59
|
|
|
/** |
60
|
|
|
* Thumbnail size. |
61
|
|
|
*/ |
62
|
|
|
const THUMBNAIL_SIZE_M = 75; |
63
|
|
|
const THUMBNAIL_SIZE_L = 600; |
64
|
|
|
|
65
|
|
|
/* |
66
|
|
|
* ShareFile Folder |
67
|
|
|
*/ |
68
|
75 |
|
const FOLDER_TOP = 'top'; |
69
|
|
|
const FOLDER_HOME = 'home'; |
70
|
75 |
|
const FOLDER_FAVORITES = 'favorites'; |
71
|
|
|
const FOLDER_ALLSHARED = 'allshared'; |
72
|
69 |
|
|
73
|
3 |
|
/* |
74
|
|
|
* Default Chunk Size for uploading files |
75
|
|
|
*/ |
76
|
66 |
|
const DEFAULT_CHUNK_SIZE = 8 * 1024 * 1024; |
77
|
66 |
|
|
78
|
|
|
// 8 megabytes |
79
|
66 |
|
|
80
|
|
|
/** |
81
|
66 |
|
* Client constructor. |
82
|
|
|
* |
83
|
|
|
* @param string $hostname |
84
|
|
|
* ShareFile hostname |
85
|
66 |
|
* @param string $client_id |
86
|
|
|
* OAuth2 client_id |
87
|
|
|
* @param string $client_secret |
88
|
|
|
* OAuth2 client_secret |
89
|
|
|
* @param string $username |
90
|
|
|
* ShareFile username |
91
|
|
|
* @param string $password |
92
|
|
|
* ShareFile password |
93
|
|
|
* @param MockHandler|HandlerStack $handler |
94
|
|
|
* Guzzle Handler |
95
|
|
|
* |
96
|
|
|
* @throws Exception |
97
|
|
|
*/ |
98
|
|
|
public function __construct(string $hostname, string $client_id, string $client_secret, string $username, string $password, $handler = null, TokenStorageInterface $tokenRepository = null) |
99
|
|
|
{ |
100
|
|
|
$this->tokenRepository = $tokenRepository; |
101
|
75 |
|
|
102
|
|
|
$client = new HttpClient([ |
103
|
75 |
|
'handler' => $handler |
104
|
|
|
]); |
105
|
|
|
|
106
|
75 |
|
$this->authProvider = new AuthProvider([ |
107
|
75 |
|
'clientId' => $client_id, |
108
|
75 |
|
'clientSecret' => $client_secret |
109
|
75 |
|
], [ |
110
|
75 |
|
'httpClient' => $client |
111
|
|
|
]); |
112
|
|
|
|
113
|
|
|
$this->options = [ |
114
|
75 |
|
'username' => $username, |
115
|
75 |
|
'password' => $password, |
116
|
75 |
|
'baseUrl' => $hostname |
117
|
75 |
|
]; |
118
|
|
|
} |
119
|
3 |
|
|
120
|
3 |
|
/** |
121
|
|
|
* Get user details. |
122
|
|
|
* |
123
|
72 |
|
* @param string $userId ShareFile user id (optional) |
124
|
69 |
|
* |
125
|
|
|
* @return array |
126
|
3 |
|
*/ |
127
|
|
|
public function getUser(string $userId = '') : array |
128
|
|
|
{ |
129
|
|
|
return $this->get("Users({$userId})"); |
|
|
|
|
130
|
|
|
} |
131
|
|
|
|
132
|
|
|
public function updateUser(string $userId, array $data) : array |
133
|
|
|
{ |
134
|
|
|
return $this->patch("Users({$userId})", $data); |
|
|
|
|
135
|
|
|
} |
136
|
|
|
|
137
|
3 |
|
/** |
138
|
|
|
* Create a folder. |
139
|
3 |
|
* |
140
|
|
|
* @param string $parentId Id of the parent folder |
141
|
|
|
* @param string $name Name |
142
|
|
|
* @param string $description Description |
143
|
|
|
* @param bool $overwrite Overwrite folder |
144
|
|
|
* |
145
|
|
|
* @return array |
146
|
|
|
*/ |
147
|
|
|
public function createFolder(string $parentId, string $name, string $description = '', bool $overwrite = false) : array |
148
|
|
|
{ |
149
|
|
|
$parameters = $this->buildHttpQuery( |
150
|
|
|
[ |
151
|
|
|
'overwrite' => $overwrite, |
152
|
6 |
|
'passthrough' => false, |
153
|
|
|
] |
154
|
6 |
|
); |
155
|
|
|
|
156
|
6 |
|
$data = [ |
157
|
|
|
'name' => $name, |
158
|
|
|
'description' => $description, |
159
|
|
|
]; |
160
|
|
|
|
161
|
|
|
return $this->post("Items({$parentId})/Folder?{$parameters}", $data); |
|
|
|
|
162
|
6 |
|
} |
163
|
6 |
|
|
164
|
|
|
/** |
165
|
|
|
* Get Folder/File using Id. |
166
|
6 |
|
* |
167
|
|
|
* @param string $itemId Item id |
168
|
|
|
* @param bool $getChildren Include children |
169
|
|
|
* |
170
|
|
|
* @return array |
171
|
|
|
*/ |
172
|
|
|
public function getItemById(string $itemId, bool $getChildren = false) : array |
173
|
|
|
{ |
174
|
|
|
$parameters = $getChildren === true ? '$expand=Children' : ''; |
175
|
|
|
|
176
|
|
|
return $this->get("Items({$itemId})?{$parameters}"); |
|
|
|
|
177
|
6 |
|
} |
178
|
|
|
|
179
|
6 |
|
/** |
180
|
|
|
* Get Folder/File using path. |
181
|
6 |
|
* |
182
|
|
|
* @param string $path Path |
183
|
|
|
* @param string $itemId Id of the root folder (optional) |
184
|
|
|
* |
185
|
|
|
* @return array |
186
|
|
|
*/ |
187
|
|
|
public function getItemByPath(string $path, string $itemId = '') : array |
188
|
|
|
{ |
189
|
|
|
if (empty($itemId)) { |
190
|
|
|
return $this->get("Items/ByPath?Path={$path}"); |
|
|
|
|
191
|
|
|
} else { |
192
|
3 |
|
return $this->get("Items({$itemId})/ByPath?Path={$path}"); |
|
|
|
|
193
|
|
|
} |
194
|
3 |
|
} |
195
|
3 |
|
|
196
|
|
|
/** |
197
|
|
|
* Get breadcrumbs of an item. |
198
|
|
|
* |
199
|
|
|
* @param string $itemId Item Id |
200
|
|
|
* |
201
|
|
|
* @return array |
202
|
|
|
*/ |
203
|
|
|
public function getItemBreadcrumbs(string $itemId) : array |
204
|
|
|
{ |
205
|
|
|
return $this->get("Items({$itemId})/Breadcrumbs"); |
|
|
|
|
206
|
|
|
} |
207
|
|
|
|
208
|
3 |
|
/** |
209
|
|
|
* Copy an item. |
210
|
3 |
|
* |
211
|
|
|
* @param string $targetId Id of the target folder |
212
|
|
|
* @param string $itemId Id of the copied item |
213
|
|
|
* @param bool $overwrite Indicates whether items with the same name will be overwritten or not (optional) |
214
|
|
|
* |
215
|
|
|
* @return array |
216
|
|
|
*/ |
217
|
|
|
public function copyItem(string $targetId, string $itemId, bool $overwrite = false) : array |
218
|
|
|
{ |
219
|
|
|
$parameters = $this->buildHttpQuery( |
220
|
|
|
[ |
221
|
|
|
'targetid' => $targetId, |
222
|
6 |
|
'overwrite' => $overwrite, |
223
|
|
|
] |
224
|
6 |
|
); |
225
|
|
|
|
226
|
6 |
|
return $this->post("Items({$itemId})/Copy?{$parameters}"); |
|
|
|
|
227
|
6 |
|
} |
228
|
|
|
|
229
|
|
|
/** |
230
|
|
|
* Update an item. |
231
|
6 |
|
* |
232
|
|
|
* @param string $itemId Id of the item |
233
|
|
|
* @param array $data New data |
234
|
|
|
* @param bool $forceSync Indicates whether operation is to be executed synchronously (optional) |
235
|
|
|
* @param bool $notify Indicates whether an email should be sent to users subscribed to Upload Notifications (optional) |
236
|
|
|
* |
237
|
|
|
* @return array |
238
|
|
|
*/ |
239
|
|
|
public function updateItem(string $itemId, array $data, bool $forceSync = true, bool $notify = true) : array |
240
|
|
|
{ |
241
|
|
|
$parameters = $this->buildHttpQuery( |
242
|
|
|
[ |
243
|
|
|
'forceSync' => $forceSync, |
244
|
3 |
|
'notify' => $notify, |
245
|
|
|
] |
246
|
3 |
|
); |
247
|
|
|
|
248
|
3 |
|
return $this->patch("Items({$itemId})?{$parameters}", $data); |
|
|
|
|
249
|
3 |
|
} |
250
|
|
|
|
251
|
|
|
/** |
252
|
|
|
* Delete an item. |
253
|
3 |
|
* |
254
|
|
|
* @param string $itemId Item id |
255
|
|
|
* @param bool $singleversion True it will delete only the specified version rather than all sibling files with the same filename (optional) |
256
|
|
|
* @param bool $forceSync True will block the operation from taking place asynchronously (optional) |
257
|
|
|
* |
258
|
|
|
* @return string |
259
|
|
|
*/ |
260
|
|
|
public function deleteItem(string $itemId, bool $singleversion = false, bool $forceSync = false) : string |
261
|
|
|
{ |
262
|
|
|
$parameters = $this->buildHttpQuery( |
263
|
|
|
[ |
264
|
|
|
'singleversion' => $singleversion, |
265
|
3 |
|
'forceSync' => $forceSync, |
266
|
|
|
] |
267
|
3 |
|
); |
268
|
|
|
|
269
|
3 |
|
return $this->delete("Items({$itemId})?{$parameters}"); |
|
|
|
|
270
|
3 |
|
} |
271
|
|
|
|
272
|
|
|
/** |
273
|
|
|
* Get temporary download URL for an item. |
274
|
3 |
|
* |
275
|
|
|
* @param string $itemId Item id |
276
|
|
|
* @param bool $includeallversions For folder downloads only, includes old versions of files in the folder in the zip when true, current versions only when false (default) |
277
|
|
|
* |
278
|
|
|
* @return array |
279
|
|
|
*/ |
280
|
|
|
public function getItemDownloadUrl(string $itemId, bool $includeallversions = false):array |
281
|
|
|
{ |
282
|
|
|
$parameters = $this->buildHttpQuery( |
283
|
|
|
[ |
284
|
|
|
'includeallversions' => $includeallversions, |
285
|
3 |
|
'redirect' => false, |
286
|
|
|
] |
287
|
3 |
|
); |
288
|
|
|
|
289
|
3 |
|
return $this->get("Items({$itemId})/Download?{$parameters}"); |
|
|
|
|
290
|
|
|
} |
291
|
|
|
|
292
|
|
|
/** |
293
|
|
|
* Get contents of and item. |
294
|
3 |
|
* |
295
|
|
|
* @param string $itemId Item id |
296
|
|
|
* @param bool $includeallversions $includeallversions For folder downloads only, includes old versions of files in the folder in the zip when true, current versions only when false (default) |
297
|
|
|
* |
298
|
|
|
* @return mixed |
299
|
|
|
*/ |
300
|
|
|
public function getItemContents(string $itemId, bool $includeallversions = false) |
301
|
|
|
{ |
302
|
|
|
$parameters = $this->buildHttpQuery( |
303
|
|
|
[ |
304
|
|
|
'includeallversions' => $includeallversions, |
305
|
3 |
|
'redirect' => true, |
306
|
|
|
] |
307
|
3 |
|
); |
308
|
|
|
|
309
|
3 |
|
return $this->get("Items({$itemId})/Download?{$parameters}"); |
310
|
|
|
} |
311
|
|
|
|
312
|
|
|
/** |
313
|
|
|
* Get the Chunk Uri to start a file-upload. |
314
|
3 |
|
* |
315
|
|
|
* @param string $method Upload method (Standard or Streamed) |
316
|
|
|
* @param string $filename Name of file |
317
|
|
|
* @param string $folderId Id of the parent folder |
318
|
|
|
* @param bool $unzip Indicates that the upload is a Zip file, and contents must be extracted at the end of upload. The resulting files and directories will be placed in the target folder. If set to false, the ZIP file is uploaded as a single file. Default is false (optional) |
319
|
|
|
* @param bool $overwrite Indicates whether items with the same name will be overwritten or not (optional) |
320
|
|
|
* @param bool $notify Indicates whether users will be notified of this upload - based on folder preferences (optional) |
321
|
|
|
* @param bool $raw Send contents contents directly in the POST body (=true) or send contents in MIME format (=false) (optional) |
322
|
|
|
* @param resource $stream Resource stream of the contents (optional) |
323
|
|
|
* |
324
|
|
|
* @return array |
325
|
|
|
*/ |
326
|
|
|
public function getChunkUri(string $method, string $filename, string $folderId, bool $unzip = false, $overwrite = true, bool $notify = true, bool $raw = false, $stream = null):array |
327
|
|
|
{ |
328
|
|
|
$parameters = $this->buildHttpQuery( |
329
|
|
|
[ |
330
|
|
|
'method' => $method, |
331
|
9 |
|
'raw' => $raw, |
332
|
|
|
'fileName' => basename($filename), |
333
|
9 |
|
'fileSize' => $stream == null ? filesize($filename) : fstat($stream)['size'], |
334
|
|
|
'canResume' => false, |
335
|
9 |
|
'startOver' => false, |
336
|
9 |
|
'unzip' => $unzip, |
337
|
9 |
|
'tool' => 'apiv3', |
338
|
9 |
|
'overwrite' => $overwrite, |
339
|
|
|
'title' => basename($filename), |
340
|
|
|
'isSend' => false, |
341
|
9 |
|
'responseFormat' => 'json', |
342
|
9 |
|
'notify' => $notify, |
343
|
9 |
|
'clientCreatedDateUTC' => $stream == null ? filectime($filename) : fstat($stream)['ctime'], |
344
|
9 |
|
'clientModifiedDateUTC' => $stream == null ? filemtime($filename) : fstat($stream)['mtime'], |
345
|
|
|
] |
346
|
9 |
|
); |
347
|
9 |
|
|
348
|
9 |
|
return $this->post("Items({$folderId})/Upload?{$parameters}"); |
|
|
|
|
349
|
9 |
|
} |
350
|
|
|
|
351
|
|
|
/** |
352
|
|
|
* Upload a file using a single HTTP POST. |
353
|
9 |
|
* |
354
|
|
|
* @param string $filename Name of file |
355
|
|
|
* @param string $folderId Id of the parent folder |
356
|
|
|
* @param bool $unzip Indicates that the upload is a Zip file, and contents must be extracted at the end of upload. The resulting files and directories will be placed in the target folder. If set to false, the ZIP file is uploaded as a single file. Default is false (optional) |
357
|
|
|
* @param bool $overwrite Indicates whether items with the same name will be overwritten or not (optional) |
358
|
|
|
* @param bool $notify Indicates whether users will be notified of this upload - based on folder preferences (optional) |
359
|
|
|
* |
360
|
|
|
* @return string |
361
|
|
|
*/ |
362
|
|
|
public function uploadFileStandard(string $filename, string $folderId, bool $unzip = false, bool $overwrite = true, bool $notify = true):string |
363
|
|
|
{ |
364
|
|
|
$chunkUri = $this->getChunkUri('standard', $filename, $folderId, $unzip, $overwrite, $notify); |
365
|
|
|
|
366
|
|
|
$request = $this->authProvider->getAuthenticatedRequest( |
367
|
3 |
|
'POST', |
368
|
|
|
$chunkUri['ChunkUri'], |
369
|
3 |
|
$this->accessToken, |
370
|
|
|
[ |
371
|
3 |
|
'multipart' => [ |
372
|
3 |
|
[ |
373
|
3 |
|
'name' => 'File1', |
374
|
|
|
'contents' => fopen($filename, 'r'), |
375
|
|
|
], |
376
|
|
|
], |
377
|
3 |
|
] |
378
|
3 |
|
); |
379
|
|
|
$response = $this->authProvider->getResponse($request); |
380
|
|
|
|
381
|
|
|
return (string) $response->getBody(); |
382
|
|
|
} |
383
|
|
|
|
384
|
3 |
|
/** |
385
|
|
|
* Upload a file using multiple HTTP POSTs. |
386
|
|
|
* |
387
|
|
|
* @param mixed $stream Stream resource |
388
|
|
|
* @param string $folderId Id of the parent folder |
389
|
|
|
* @param string $filename Filename (optional) |
390
|
|
|
* @param bool $unzip Indicates that the upload is a Zip file, and contents must be extracted at the end of upload. The resulting files and directories will be placed in the target folder. If set to false, the ZIP file is uploaded as a single file. Default is false (optional) |
391
|
|
|
* @param bool $overwrite Indicates whether items with the same name will be overwritten or not (optional) |
392
|
|
|
* @param bool $notify Indicates whether users will be notified of this upload - based on folder preferences (optional) |
393
|
|
|
* @param int $chunkSize Maximum size of the individual HTTP posts in bytes |
394
|
|
|
* |
395
|
|
|
* @return string |
396
|
|
|
*/ |
397
|
|
|
public function uploadFileStreamed($stream, string $folderId, string $filename = null, bool $unzip = false, bool $overwrite = true, bool $notify = true, int $chunkSize = null):string |
398
|
|
|
{ |
399
|
|
|
$filename = $filename ?? stream_get_meta_data($stream)['uri']; |
400
|
3 |
|
if (empty($filename)) { |
401
|
|
|
return 'Error: no filename'; |
402
|
3 |
|
} |
403
|
3 |
|
|
404
|
|
|
$chunkUri = $this->getChunkUri('streamed', $filename, $folderId, $unzip, $overwrite, $notify, true, $stream); |
405
|
|
|
$chunkSize = $chunkSize ?? SELF::DEFAULT_CHUNK_SIZE; |
406
|
|
|
$index = 0; |
407
|
3 |
|
|
408
|
3 |
|
// First Chunk |
409
|
3 |
|
$data = $this->readChunk($stream, $chunkSize); |
410
|
|
|
while (! ((strlen($data) < $chunkSize) || feof($stream))) { |
411
|
|
|
$parameters = $this->buildHttpQuery( |
412
|
3 |
|
[ |
413
|
3 |
|
'index' => $index, |
414
|
3 |
|
'byteOffset' => $index * $chunkSize, |
415
|
|
|
'hash' => md5($data), |
416
|
3 |
|
] |
417
|
3 |
|
); |
418
|
3 |
|
|
419
|
|
|
$response = $this->uploadChunk("{$chunkUri['ChunkUri']}&{$parameters}", $data); |
420
|
|
|
|
421
|
|
|
if ($response != 'true') { |
422
|
3 |
|
return $response; |
423
|
|
|
} |
424
|
3 |
|
|
425
|
|
|
// Next chunk |
426
|
|
|
$index++; |
427
|
|
|
$data = $this->readChunk($stream, $chunkSize); |
428
|
|
|
} |
429
|
3 |
|
|
430
|
3 |
|
// Final chunk |
431
|
|
|
$parameters = $this->buildHttpQuery( |
432
|
|
|
[ |
433
|
|
|
'index' => $index, |
434
|
3 |
|
'byteOffset' => $index * $chunkSize, |
435
|
|
|
'hash' => md5($data), |
436
|
3 |
|
'filehash' => \GuzzleHttp\Psr7\hash(\GuzzleHttp\Psr7\stream_for($stream), 'md5'), |
437
|
3 |
|
'finish' => true, |
438
|
3 |
|
] |
439
|
3 |
|
); |
440
|
|
|
|
441
|
|
|
return $this->uploadChunk("{$chunkUri['ChunkUri']}&{$parameters}", $data); |
442
|
|
|
} |
443
|
|
|
|
444
|
3 |
|
/** |
445
|
|
|
* Get Thumbnail of an item. |
446
|
|
|
* |
447
|
|
|
* @param string $itemId Item id |
448
|
|
|
* @param int $size Thumbnail size: THUMBNAIL_SIZE_M or THUMBNAIL_SIZE_L (optional) |
449
|
|
|
* |
450
|
|
|
* @return array |
451
|
|
|
*/ |
452
|
|
|
public function getThumbnailUrl(string $itemId, int $size = 75):array |
453
|
|
|
{ |
454
|
|
|
$parameters = $this->buildHttpQuery( |
455
|
3 |
|
[ |
456
|
|
|
'size' => $size, |
457
|
3 |
|
'redirect' => false, |
458
|
|
|
] |
459
|
3 |
|
); |
460
|
|
|
|
461
|
|
|
return $this->get("Items({$itemId})/Thumbnail?{$parameters}"); |
|
|
|
|
462
|
|
|
} |
463
|
|
|
|
464
|
3 |
|
/** |
465
|
|
|
* Get browser link for an item. |
466
|
|
|
* |
467
|
|
|
* @param string $itemId Item id |
468
|
|
|
* |
469
|
|
|
* @return array |
470
|
|
|
*/ |
471
|
|
|
public function getWebAppLink(string $itemId):array |
472
|
|
|
{ |
473
|
|
|
return $this->post("Items({$itemId})/WebAppLink"); |
|
|
|
|
474
|
3 |
|
} |
475
|
|
|
|
476
|
3 |
|
/** |
477
|
|
|
* Share Share for external user. |
478
|
|
|
* |
479
|
|
|
* @param array $options Share options |
480
|
|
|
* @param bool $notify Indicates whether user will be notified if item is downloaded (optional) |
481
|
|
|
* |
482
|
|
|
* @return array |
483
|
|
|
*/ |
484
|
|
|
public function createShare(array $options, $notify = false):array |
485
|
|
|
{ |
486
|
|
|
$parameters = $this->buildHttpQuery( |
487
|
3 |
|
[ |
488
|
|
|
'notify' => $notify, |
489
|
3 |
|
'direct' => true, |
490
|
|
|
] |
491
|
3 |
|
); |
492
|
|
|
|
493
|
|
|
return $this->post("Shares?{$parameters}", $options); |
|
|
|
|
494
|
|
|
} |
495
|
|
|
|
496
|
3 |
|
/** |
497
|
|
|
* Get AccessControl List for an item. |
498
|
|
|
* |
499
|
|
|
* @param string $itemId Id of an item |
500
|
|
|
* @param string $userId Id of an user |
501
|
|
|
* |
502
|
|
|
* @return array |
503
|
|
|
*/ |
504
|
|
|
public function getItemAccessControls(string $itemId, string $userId = ''):array |
505
|
|
|
{ |
506
|
|
|
if (! empty($userId)) { |
507
|
6 |
|
return $this->get("AccessControls(principalid={$userId},itemid={$itemId})"); |
|
|
|
|
508
|
|
|
} else { |
509
|
6 |
|
return $this->get("Items({$itemId})/AccessControls"); |
|
|
|
|
510
|
3 |
|
} |
511
|
|
|
} |
512
|
3 |
|
|
513
|
|
|
public function getAccessToken(): AccessToken |
514
|
|
|
{ |
515
|
|
|
$tokenId = sprintf('sf-%s', $this->options['username']); |
516
|
|
|
|
517
|
|
|
if ($this->accessToken === null) { |
518
|
|
|
if ($this->tokenRepository !== null) { |
519
|
|
|
try { |
520
|
|
|
$this->accessToken = $this->tokenRepository->loadToken($tokenId); |
521
|
|
|
} catch(TokenNotFoundException $e) {} |
|
|
|
|
522
|
|
|
} |
523
|
63 |
|
|
524
|
|
|
if ($this->accessToken === null) { |
525
|
63 |
|
$this->accessToken = $this->authProvider->getAccessToken('password', [ |
526
|
|
|
'username' => $this->options['username'], |
527
|
|
|
'password' => $this->options['password'], |
528
|
|
|
'baseUrl' => $this->options['baseUrl'] |
529
|
|
|
]); |
530
|
|
|
|
531
|
|
|
if ($this->tokenRepository !== null) { |
532
|
|
|
$this->tokenRepository->storeToken($this->accessToken, $tokenId); |
|
|
|
|
533
|
|
|
} |
534
|
|
|
} |
535
|
|
|
} |
536
|
|
|
|
537
|
|
|
if ($this->accessToken->hasExpired() === true) { |
|
|
|
|
538
|
|
|
$this->accessToken = $this->authProvider->getAccessToken('refresh_token', [ |
539
|
63 |
|
'refresh_token' => $this->accessToken->getRefreshToken() |
540
|
|
|
]); |
541
|
63 |
|
if ($this->tokenRepository !== null) { |
542
|
63 |
|
$this->tokenRepository->storeAccessToken($tokenId, $this->accessToken); |
|
|
|
|
543
|
|
|
} |
544
|
|
|
} |
545
|
63 |
|
return $this->accessToken; |
|
|
|
|
546
|
|
|
} |
547
|
|
|
|
548
|
|
|
/** |
549
|
|
|
* Build API uri. |
550
|
63 |
|
* |
551
|
|
|
* @param string $endpoint API endpoint |
552
|
63 |
|
* |
553
|
|
|
* @return string |
554
|
|
|
*/ |
555
|
|
|
protected function buildUri(string $endpoint): string |
556
|
|
|
{ |
557
|
|
|
return "https://{$this->accessToken->getValues()['subdomain']}.sf-api.com/sf/v3/{$endpoint}"; |
558
|
|
|
} |
559
|
|
|
|
560
|
|
|
/** |
561
|
|
|
* Make a request to the API. |
562
|
30 |
|
* |
563
|
|
|
* @param string $method HTTP Method |
564
|
30 |
|
* @param string $endpoint API endpoint |
565
|
|
|
* @param mixed|string|array $json POST body (optional) |
566
|
|
|
* |
567
|
|
|
* @throws Exception |
568
|
|
|
* |
569
|
|
|
* @return mixed |
570
|
|
|
*/ |
571
|
|
|
protected function request(string $method, string $endpoint, $json = null) |
572
|
|
|
{ |
573
|
|
|
$accessToken = $this->getAccessToken(); |
574
|
|
|
|
575
|
27 |
|
$uri = $this->buildUri($endpoint); |
576
|
|
|
$options = $json != null ? ['json' => $json] : []; |
577
|
27 |
|
|
578
|
|
|
try { |
579
|
|
|
$request = $this->authProvider->getAuthenticatedRequest($method, $uri, $accessToken, $options); |
580
|
|
|
$response = $this->authProvider->getResponse($request); |
581
|
|
|
} catch (ClientException $exception) { |
582
|
|
|
throw $this->determineException($exception); |
583
|
|
|
} |
584
|
|
|
|
585
|
|
|
$body = (string) $response->getBody(); |
586
|
|
|
|
587
|
|
|
return $this->jsonValidator($body) ? json_decode($body, true) : $body; |
588
|
3 |
|
} |
589
|
|
|
|
590
|
3 |
|
/** |
591
|
|
|
* Shorthand for GET-request. |
592
|
|
|
* |
593
|
|
|
* @param string $endpoint API endpoint |
594
|
|
|
* |
595
|
|
|
* @return mixed |
596
|
|
|
*/ |
597
|
|
|
protected function get(string $endpoint) |
598
|
|
|
{ |
599
|
|
|
return $this->request('GET', $endpoint); |
600
|
3 |
|
} |
601
|
|
|
|
602
|
3 |
|
/** |
603
|
|
|
* Shorthand for POST-request. |
604
|
|
|
* |
605
|
|
|
* @param string $endpoint API endpoint |
606
|
|
|
* @param mixed|string|array $json POST body (optional) |
607
|
|
|
* |
608
|
|
|
* @return mixed |
609
|
|
|
*/ |
610
|
|
|
protected function post(string $endpoint, $json = null) |
611
|
|
|
{ |
612
|
|
|
return $this->request('POST', $endpoint, $json); |
613
|
3 |
|
} |
614
|
|
|
|
615
|
3 |
|
/** |
616
|
3 |
|
* Shorthand for PATCH-request. |
617
|
3 |
|
* |
618
|
|
|
* @param string $endpoint API endpoint |
619
|
|
|
* @param mixed|string|array $json POST body (optional) |
620
|
3 |
|
* |
621
|
3 |
|
* @return mixed |
622
|
|
|
*/ |
623
|
3 |
|
protected function patch(string $endpoint, $json = null) |
624
|
|
|
{ |
625
|
|
|
return $this->request('PATCH', $endpoint, $json); |
626
|
|
|
} |
627
|
3 |
|
|
628
|
|
|
/** |
629
|
|
|
* Shorthand for DELETE-request. |
630
|
|
|
* |
631
|
|
|
* @param string $endpoint API endpoint |
632
|
|
|
* |
633
|
|
|
* @return string|array |
634
|
|
|
*/ |
635
|
|
|
protected function delete(string $endpoint) |
636
|
|
|
{ |
637
|
|
|
return $this->request('DELETE', $endpoint); |
638
|
|
|
} |
639
|
|
|
|
640
|
|
|
/** |
641
|
3 |
|
* Upload a chunk of data using HTTP POST body. |
642
|
|
|
* |
643
|
3 |
|
* @param string $uri Upload URI |
644
|
3 |
|
* @param string $data Contents to upload |
645
|
3 |
|
* |
646
|
3 |
|
* @return string|array |
647
|
|
|
*/ |
648
|
|
|
protected function uploadChunk($uri, $data) |
649
|
3 |
|
{ |
650
|
3 |
|
$request = $this->authProvider->getAuthenticatedRequest( |
651
|
|
|
'POST', |
652
|
|
|
$uri, |
653
|
3 |
|
$this->accessToken, |
654
|
|
|
[ |
655
|
|
|
'headers' => [ |
656
|
|
|
'Content-Length' => strlen($data), |
657
|
|
|
'Content-Type' => 'application/octet-stream', |
658
|
|
|
], |
659
|
|
|
'body' => $data, |
660
|
|
|
] |
661
|
|
|
); |
662
|
|
|
$response = $this->authProvider->getResponse($request); |
663
|
|
|
|
664
|
|
|
return (string) $response->getBody(); |
665
|
|
|
} |
666
|
|
|
|
667
|
|
|
/** |
668
|
|
|
* Sometimes fread() returns less than the request number of bytes (for example, when reading |
669
|
|
|
* from network streams). This function repeatedly calls fread until the requested number of |
670
|
|
|
* bytes have been read or we've reached EOF. |
671
|
|
|
* |
672
|
|
|
* @param resource $stream |
673
|
|
|
* @param int $chunkSize |
674
|
|
|
* |
675
|
|
|
* @throws Exception |
676
|
|
|
* @return string |
677
|
|
|
*/ |
678
|
|
|
protected function readChunk($stream, int $chunkSize) |
679
|
39 |
|
{ |
680
|
|
|
$chunk = ''; |
681
|
39 |
|
while (! feof($stream) && $chunkSize > 0) { |
682
|
39 |
|
$part = fread($stream, $chunkSize); |
683
|
39 |
|
if ($part === false) { |
684
|
39 |
|
throw new Exception('Error reading from $stream.'); |
685
|
18 |
|
} |
686
|
|
|
$chunk .= $part; |
687
|
|
|
$chunkSize -= strlen($part); |
688
|
39 |
|
} |
689
|
39 |
|
|
690
|
39 |
|
return $chunk; |
691
|
|
|
} |
692
|
|
|
|
693
|
|
|
/** |
694
|
|
|
* Handle ClientException. |
695
|
|
|
* |
696
|
|
|
* @param ClientException $exception ClientException |
697
|
|
|
* |
698
|
|
|
* @return Exception |
699
|
|
|
*/ |
700
|
|
|
protected function determineException(ClientException $exception): Exception |
701
|
|
|
{ |
702
|
63 |
|
if (in_array($exception->getResponse()->getStatusCode(), [400, 403, 404, 409])) { |
703
|
|
|
return new BadRequest($exception->getResponse()); |
704
|
63 |
|
} |
705
|
60 |
|
|
706
|
|
|
return $exception; |
707
|
60 |
|
} |
708
|
|
|
|
709
|
|
|
/** |
710
|
3 |
|
* Build HTTP query. |
711
|
|
|
* |
712
|
|
|
* @param array $parameters Query parameters |
713
|
|
|
* |
714
|
|
|
* @return string |
715
|
|
|
*/ |
716
|
|
|
protected function buildHttpQuery(array $parameters):string |
717
|
|
|
{ |
718
|
|
|
return http_build_query( |
719
|
|
|
array_map( |
720
|
|
|
function ($parameter) { |
721
|
|
|
if (! is_bool($parameter)) { |
722
|
|
|
return $parameter; |
723
|
|
|
} |
724
|
|
|
|
725
|
|
|
return $parameter ? 'true' : 'false'; |
726
|
|
|
}, |
727
|
|
|
$parameters |
728
|
|
|
) |
729
|
|
|
); |
730
|
|
|
} |
731
|
|
|
|
732
|
|
|
/** |
733
|
|
|
* Validate JSON. |
734
|
|
|
* |
735
|
|
|
* @param mixed $data JSON variable |
736
|
|
|
* |
737
|
|
|
* @return bool |
738
|
|
|
*/ |
739
|
|
|
protected function jsonValidator($data = null):bool |
740
|
|
|
{ |
741
|
|
|
if (! empty($data)) { |
742
|
|
|
@json_decode($data); |
|
|
|
|
743
|
|
|
|
744
|
|
|
return json_last_error() === JSON_ERROR_NONE; |
745
|
|
|
} |
746
|
|
|
|
747
|
|
|
return false; |
748
|
|
|
} |
749
|
|
|
} |
750
|
|
|
|