Passed
Push — master ( 52e05d...6f0859 )
by
unknown
08:31 queued 03:15
created

SeafileApi::ping()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 5
c 1
b 0
f 0
nc 1
nop 0
dl 0
loc 7
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Datamate\SeafileApi;
6
7
use Datamate\SeafileApi\Exception\ConnectionException;
8
use Datamate\SeafileApi\Exception\InvalidArgumentException;
9
use Datamate\SeafileApi\Exception\InvalidResponseException;
10
use Datamate\SeafileApi\Exception\UnexpectedJsonTextResponseException as JsonDecodeException;
11
12
/**
13
 * Seafile API.
14
 *
15
 * Interact with the Seafile API with the curl extension.
16
 *
17
 * @see https://manual.seafile.com/develop/web_api_v2.1.html
18
 */
19
final class SeafileApi {
20
	public const USER_PREFIX_AUTH_TOKEN = '@auth:token:';
21
22
	public const TYPE_DIR = 'dir';
23
	public const TYPE_FILE = 'file';
24
	public const TYPE_REPO = 'repo';
25
	public const TYPE_SREPO = 'srepo';
26
	public const TYPE_GREPO = 'grepo';
27
28
	public const TYPES = self::TYPES_FILE + self::TYPES_DIR_LIKE;
29
	public const TYPES_DIR_LIKE = self::TYPES_DIR + self::TYPES_REPO;
30
	public const TYPES_DIR = [self::TYPE_DIR => self::TYPE_DIR];
31
	public const TYPES_FILE = [self::TYPE_FILE => self::TYPE_FILE];
32
	public const TYPES_REPO = [self::TYPE_REPO => self::TYPE_REPO, self::TYPE_SREPO => self::TYPE_SREPO, self::TYPE_GREPO => self::TYPE_GREPO];
33
34
	/**
35
	 * @const string
36
	 */
37
	public const STRING_SUCCESS = 'success';
38
39
	/**
40
	 * Error codes.
41
	 */
42
	public const ERROR_CODE_FEATURES = 802;
43
	public const ERROR_CODE_NO_CURL = 803;
44
	public const ERROR_CODE_FILE_IO = 808;
45
46
	/**
47
	 * default curl options.
48
	 */
49
	private const CURL_OPTION_DEFAULTS = [
50
		CURLOPT_AUTOREFERER => true,
51
		CURLOPT_TIMEOUT => 10,
52
		CURLOPT_RETURNTRANSFER => true,
53
		CURLOPT_FOLLOWLOCATION => false,
54
	];
55
56
	/**
57
	 * @var array shared curl options between all requests
58
	 */
59
	private array $curlSharedOptions = self::CURL_OPTION_DEFAULTS;
60
61
	/**
62
	 * jsonDecode accept flags.
63
	 *
64
	 * @see jsonDecode
65
	 */
66
	private const JSON_DECODE_ACCEPT_MASK = 31;                         # 1 1111 accept bitmask (five bits with the msb flags)
67
	private const JSON_DECODE_ACCEPT_JSON = 16;                         # 1 0000 JSON text
68
	private const JSON_DECODE_ACCEPT_DEFAULT = 23;                      # 1 0111 default: string, array or object
69
	private const JSON_DECODE_ACCEPT_OBJECT = 17;                       # 1 0001 object
70
	private const JSON_DECODE_ACCEPT_ARRAY = 18;                        # 1 0010 array
71
	private const JSON_DECODE_ACCEPT_STRING = 20;                       # 1 0100 string
72
	private const JSON_DECODE_ACCEPT_ARRAY_OF_OBJECTS = 24;             # 1 1000 array with only objects (incl. none)
73
	private const JSON_DECODE_ACCEPT_ARRAY_SINGLE_OBJECT = 25;          # 1 1001 array with one single object, return that item
74
	private const JSON_DECODE_ACCEPT_ARRAY_SINGLE_OBJECT_NULLABLE = 26; # 1 1010 array with one single object, return that item, or empty array, return null
75
	private const JSON_DECODE_ACCEPT_SUCCESS_STRING = 28;               # 1 1100 string "success"
76
	private const JSON_DECODE_ACCEPT_SUCCESS_OBJECT = 29;               # 1 1101 object with single "success" property and value true
77
78
	/**
79
	 * @const string ASCII upper-case characters part of a hexit
80
	 */
81
	private const HEX_ALPHA_UPPER = 'ABCDEF';
82
83
	/**
84
	 * @const string ASCII lower-case characters part of a hexit
85
	 */
86
	private const HEX_ALPHA_LOWER = 'abcdef';
87
88
	private $handle;
89
90
	private string $token = '';
91
92
	/**
93
	 * constructor.
94
	 */
95
	public function __construct(/**
96
	 * @var string Server base URL
97
	 */
98
		private readonly string $baseurl, /**
99
	 * @var string Username
100
	 */
101
		private string $user, /**
102
	 * @var string Password
103
	 */
104
		private readonly string $pass,
105
		?string $otp = null
106
	) {
107
		if (!function_exists('curl_version')) {
108
			throw new ConnectionException('PHP-CURL not installed', self::ERROR_CODE_NO_CURL);
109
		}
110
		// trigger_error(sprintf("ctor: %s:%s", $user, $pass), E_USER_NOTICE);
111
112
		$this->setTokenByUsernameAndPassword($otp);
113
	}
114
115
	/**
116
	 * ping (with authentication).
117
	 *
118
	 * @see https://download.seafile.com/published/web-api/home.md#user-content-Quick%20Start
119
	 *
120
	 * @return string "pong"
121
	 */
122
	public function ping(): string {
123
		return $this->jsonDecode(
124
			$this->get(
0 ignored issues
show
Bug introduced by
It seems like $this->get($this->baseur...Token '.$this->token))) can also be of type true; however, parameter $jsonText of Datamate\SeafileApi\SeafileApi::jsonDecode() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

124
			/** @scrutinizer ignore-type */ $this->get(
Loading history...
125
				"{$this->baseurl}/api2/ping/",
126
				[CURLOPT_HTTPHEADER => ["Authorization: Token {$this->token}"]],
127
			),
128
			self::JSON_DECODE_ACCEPT_STRING,
129
		);
130
	}
131
132
	/**
133
	 * get server version.
134
	 *
135
	 * @throws Exception
136
	 */
137
	public function getServerVersion(): string {
138
		$serverInfo = $this->getServerInformation();
139
		if (
140
			!is_string($serverInfo->version ?? null) ||
141
			!is_array($serverInfo->features ?? null)
142
		) {
143
			throw new InvalidResponseException('We could not retrieve list of server features.', self::ERROR_CODE_FEATURES);
144
		}
145
146
		$isSeafilePro = in_array('seafile-pro', $serverInfo->features, true);
147
		$edition = $isSeafilePro ? 'Professional' : 'Community';
148
149
		return "{$serverInfo->version} ({$edition})";
150
	}
151
152
	/**
153
	 * get server information.
154
	 *
155
	 * @see https://download.seafile.com/published/web-api/v2.1/server-info.md#user-content-Get%20Server%20Information
156
	 *
157
	 * @throws Exception
158
	 */
159
	public function getServerInformation(): object {
160
		return $this->jsonDecode(
161
			$this->get(
0 ignored issues
show
Bug introduced by
It seems like $this->get($this->baseur...Token '.$this->token))) can also be of type true; however, parameter $jsonText of Datamate\SeafileApi\SeafileApi::jsonDecode() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

161
			/** @scrutinizer ignore-type */ $this->get(
Loading history...
162
				"{$this->baseurl}/api2/server-info/",
163
				[CURLOPT_HTTPHEADER => ["Authorization: Token {$this->token}"]],
164
			),
165
			self::JSON_DECODE_ACCEPT_OBJECT,
166
		);
167
	}
168
169
	/**
170
	 * check account info.
171
	 *
172
	 * @see https://download.seafile.com/published/web-api/v2.1/account.md#user-content-Check%20Account%20Info
173
	 */
174
	public function checkAccountInfo(): object {
175
		return $this->jsonDecode(
176
			$this->get(
0 ignored issues
show
Bug introduced by
It seems like $this->get($this->baseur...Token '.$this->token))) can also be of type true; however, parameter $jsonText of Datamate\SeafileApi\SeafileApi::jsonDecode() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

176
			/** @scrutinizer ignore-type */ $this->get(
Loading history...
177
				"{$this->baseurl}/api2/account/info/",
178
				[CURLOPT_HTTPHEADER => ["Authorization: Token {$this->token}"]]
179
			),
180
			self::JSON_DECODE_ACCEPT_OBJECT,
181
		);
182
	}
183
184
	/**
185
	 * list seafile server libraries.
186
	 *
187
	 * that are all libraries a user can access.
188
	 *
189
	 * @see https://download.seafile.com/published/web-api/v2.1/libraries.md#user-content-List%20Libraries
190
	 *
191
	 * @return object[]
192
	 *
193
	 * @throws Exception
194
	 */
195
	public function listLibraries(): array {
196
		return $this->listLibrariesCached(true);
197
	}
198
199
	/**
200
	 * get default library.
201
	 *
202
	 * @see https://download.seafile.com/published/web-api/v2.1/libraries.md#user-content-Get%20Default%20Library
203
	 *
204
	 * @return object{exists: bool, repo_id: string}
205
	 */
206
	public function getDefaultLibrary(): object {
207
		return $this->jsonDecode(
208
			$this->get(
0 ignored issues
show
Bug introduced by
It seems like $this->get($this->baseur...Token '.$this->token))) can also be of type true; however, parameter $jsonText of Datamate\SeafileApi\SeafileApi::jsonDecode() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

208
			/** @scrutinizer ignore-type */ $this->get(
Loading history...
209
				"{$this->baseurl}/api2/default-repo/",
210
				[CURLOPT_HTTPHEADER => ["Authorization: Token {$this->token}"]],
211
			),
212
			self::JSON_DECODE_ACCEPT_OBJECT,
213
		);
214
	}
215
216
	/**
217
	 * create a library.
218
	 *
219
	 * @see https://download.seafile.com/published/web-api/v2.1/libraries.md#user-content-Create%20Library
220
	 *
221
	 * @param string $name of library
222
	 */
223
	public function createLibrary(string $name): object {
224
		$name = trim($name, "/");
225
226
		return $this->jsonDecode(
227
			$this->post(
0 ignored issues
show
Bug introduced by
It seems like $this->post($this->baseu...Token '.$this->token))) can also be of type true; however, parameter $jsonText of Datamate\SeafileApi\SeafileApi::jsonDecode() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

227
			/** @scrutinizer ignore-type */ $this->post(
Loading history...
228
				"{$this->baseurl}/api2/repos/",
229
				['name' => $name],
230
				[CURLOPT_HTTPHEADER => ["Authorization: Token {$this->token}"]],
231
			),
232
			self::JSON_DECODE_ACCEPT_OBJECT,
233
		);
234
	}
235
236
	/**
237
	 * name a library.
238
	 *
239
	 * @see https://download.seafile.com/published/web-api/v2.1/libraries.md#user-content-Rename%20Library
240
	 */
241
	public function nameLibrary(string $lib, string $name): void {
242
		$lib = $this->verifyLib($lib);
243
		$fields = ['repo_name' => $name];
244
		$_ = $this->jsonDecode(
245
			$this->post(
0 ignored issues
show
Bug introduced by
It seems like $this->post($this->baseu...Token '.$this->token))) can also be of type true; however, parameter $jsonText of Datamate\SeafileApi\SeafileApi::jsonDecode() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

245
			/** @scrutinizer ignore-type */ $this->post(
Loading history...
246
				"{$this->baseurl}/api2/repos/{$lib}/?op=rename",
247
				$fields,
248
				[CURLOPT_HTTPHEADER => ["Authorization: Token {$this->token}"]],
249
			),
250
			self::JSON_DECODE_ACCEPT_SUCCESS_STRING,
251
		);
252
		unset($_);
253
	}
254
255
	/**
256
	 * rename a library.
257
	 *
258
	 * do nothing if the library can not be found
259
	 *
260
	 * @see https://download.seafile.com/published/web-api/v2.1/libraries.md#user-content-Rename%20Library
261
	 */
262
	public function renameLibrary(string $oldName, string $newName): void {
263
		$lib = $this->getLibraryIdByLibraryName($oldName);
264
		if ($lib === null) {
265
			return; // no library to rename
266
		}
267
268
		$this->nameLibrary($lib, $newName);
269
	}
270
271
	/**
272
	 * delete a library by name.
273
	 *
274
	 * @see https://download.seafile.com/published/web-api/v2.1/libraries.md#user-content-Delete%20Library
275
	 */
276
	public function deleteLibraryByName(string $name): void {
277
		$id = $this->getLibraryIdByLibraryName($name);
278
		if ($id === null) {
279
			return; // library already gone
280
		}
281
282
		$this->deleteLibraryById($id);
283
	}
284
285
	/**
286
	 * delete a library by id.
287
	 *
288
	 * @see https://download.seafile.com/published/web-api/v2.1/libraries.md#user-content-Delete%20Library
289
	 */
290
	public function deleteLibraryById(string $id): void {
291
		$lib = $this->verifyLib($id);
292
293
		$_ = $this->jsonDecode(
294
			$this->delete(
0 ignored issues
show
Bug introduced by
It seems like $this->delete($this->bas...Token '.$this->token))) can also be of type true; however, parameter $jsonText of Datamate\SeafileApi\SeafileApi::jsonDecode() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

294
			/** @scrutinizer ignore-type */ $this->delete(
Loading history...
295
				"{$this->baseurl}/api2/repos/{$lib}/",
296
				[CURLOPT_HTTPHEADER => ["Authorization: Token {$this->token}"]],
297
			),
298
			self::JSON_DECODE_ACCEPT_SUCCESS_STRING,
299
		);
300
		unset($_);
301
	}
302
303
	/**
304
	 * get library info array from path.
305
	 *
306
	 * @param string $libNamedPath with the library name as first component
307
	 *
308
	 * @return object with 'id' and 'name' of library, both NULL if not found
309
	 *
310
	 * @throws Exception
311
	 * @throws InvalidResponseException
312
	 */
313
	public function getLibraryFromPath(string $libNamedPath): object {
314
		$libraries = $this->listLibrariesCached();
315
		$libraries = array_column($libraries, null, 'name');
316
317
		$name = explode('/', ltrim($this->normalizePath($libNamedPath), '/'), 2)[0];
318
319
		return (object) [
320
			'id' => $libraries[$name]->id ?? null,
321
			'name' => $libraries[$name]->name ?? null,
322
		];
323
	}
324
325
	/**
326
	 * list all share links.
327
	 *
328
	 * all folder/file download share links in all libraries created by user.
329
	 *
330
	 * @see https://download.seafile.com/published/web-api/v2.1/share-links.md#user-content-List%20all%20Share%20Links
331
	 *
332
	 * @return object[]
333
	 *
334
	 * @throws Exception
335
	 * @throws InvalidResponseException
336
	 *
337
	 * @noinspection PhpUnused
338
	 */
339
	public function listAllShareLinks(): array {
340
		return $this->jsonDecode(
341
			$this->get(
0 ignored issues
show
Bug introduced by
It seems like $this->get($this->baseur...Token '.$this->token))) can also be of type true; however, parameter $jsonText of Datamate\SeafileApi\SeafileApi::jsonDecode() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

341
			/** @scrutinizer ignore-type */ $this->get(
Loading history...
342
				"{$this->baseurl}/api/v2.1/share-links/",
343
				[CURLOPT_HTTPHEADER => ["Authorization: Token {$this->token}"]],
344
			),
345
			self::JSON_DECODE_ACCEPT_ARRAY_OF_OBJECTS,
346
		);
347
	}
348
349
	/**
350
	 * Create Share Link.
351
	 *
352
	 * @see https://download.seafile.com/published/web-api/v2.1/share-links.md#user-content-Create%20Share%20Link
353
	 *
354
	 * @param ?string                 $password    [optional]
355
	 * @param \DateTimeInterface|?int $expire      [optional] number of days to expire (int) or DateTime to expire
356
	 * @param ?array                  $permissions [optional] see seafile api docs
357
	 *
358
	 * @throws Exception
359
	 * @throws InvalidArgumentException
360
	 * @throws InvalidResponseException
361
	 */
362
	public function createShareLink(string $lib, string $path, ?string $password = null, $expire = null, ?array $permissions = null): object {
363
		$lib = $this->verifyLib($lib);
364
		$path = $this->normalizePath($path);
365
366
		$fields = [
367
			'repo_id' => $lib,
368
			'path' => $path,
369
		];
370
		if ($password !== null) {
371
			$fields['password'] = $password;
372
		}
373
		if ($expire !== null) {
374
			$expireTime = $expire;
375
			if (is_int($expire)) {
376
				$expireDays = max(1, min(365, (int) $expire));
377
				$expireTime = (new \DateTimeImmutable())->add(
378
					new \DateInterval("P{$expireDays}D")
379
				);
380
			}
381
			if (!$expireTime instanceof \DateTimeInterface) {
382
				throw new InvalidArgumentException('Expire type mismatch: ' . \gettype($expireTime));
383
			}
384
			$fields['expiration_time'] = $expireTime->format(\DATE_ATOM);
385
		}
386
		if ($permissions !== null) {
387
			try {
388
				$fields['permissions'] = json_encode($permissions, JSON_THROW_ON_ERROR);
389
			} /* @noinspection PhpMultipleClassDeclarationsInspection */ catch (\JsonException) {
390
				throw new InvalidArgumentException('permissions');
391
			}
392
		}
393
394
		return $this->jsonDecode(
395
			$this->post(
0 ignored issues
show
Bug introduced by
It seems like $this->post($this->baseu...Token '.$this->token))) can also be of type true; however, parameter $jsonText of Datamate\SeafileApi\SeafileApi::jsonDecode() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

395
			/** @scrutinizer ignore-type */ $this->post(
Loading history...
396
				"{$this->baseurl}/api/v2.1/share-links/",
397
				$fields,
398
				[CURLOPT_HTTPHEADER => ["Authorization: Token {$this->token}"]],
399
			),
400
			self::JSON_DECODE_ACCEPT_OBJECT,
401
		);
402
	}
403
404
	/**
405
	 * List Share Links of a Library.
406
	 *
407
	 * @see https://download.seafile.com/published/web-api/v2.1/share-links.md#user-content-List%20all%20Share%20Links
408
	 *
409
	 * @param ?string $lib [optional] library id (guid), default/null for all libraries
410
	 *
411
	 * @return object[]
412
	 *
413
	 * @throws Exception
414
	 */
415
	public function listShareLinksOfALibrary(?string $lib = null): array {
416
		$lib = $this->verifyLib($lib ?? '', true);
417
418
		return $this->jsonDecode(
419
			$this->get(
0 ignored issues
show
Bug introduced by
It seems like $this->get($this->baseur...Token '.$this->token))) can also be of type true; however, parameter $jsonText of Datamate\SeafileApi\SeafileApi::jsonDecode() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

419
			/** @scrutinizer ignore-type */ $this->get(
Loading history...
420
				"{$this->baseurl}/api/v2.1/share-links/?repo_id={$lib}",
421
				[CURLOPT_HTTPHEADER => ["Authorization: Token {$this->token}"]],
422
			),
423
			self::JSON_DECODE_ACCEPT_ARRAY_OF_OBJECTS,
424
		);
425
	}
426
427
	/**
428
	 * check password (of a share link by token).
429
	 *
430
	 * @see https://download.seafile.com/published/web-api/v2.1-admin/share-links.md#user-content-Check%20Password
431
	 *
432
	 * @param string $token    of share link
433
	 * @param string $password in plain
434
	 *
435
	 * @throws Exception
436
	 * @throws InvalidResponseException
437
	 *
438
	 * @noinspection PhpUnused
439
	 */
440
	public function checkShareLinkPassword(string $token, string $password): object {
441
		$tokenEncoded = rawurlencode($token);
442
443
		return $this->jsonDecode(
444
			$this->post(
0 ignored issues
show
Bug introduced by
It seems like $this->post($this->baseu...Token '.$this->token))) can also be of type true; however, parameter $jsonText of Datamate\SeafileApi\SeafileApi::jsonDecode() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

444
			/** @scrutinizer ignore-type */ $this->post(
Loading history...
445
				"{$this->baseurl}/api/v2.1/admin/share-links/{$tokenEncoded}/check-password/",
446
				['password' => $password],
447
				[CURLOPT_HTTPHEADER => ["Authorization: Token {$this->token}"]],
448
			),
449
			self::JSON_DECODE_ACCEPT_SUCCESS_OBJECT,
450
		);
451
	}
452
453
	/**
454
	 * share Link of a folder (or file).
455
	 *
456
	 * @see https://download.seafile.com/published/web-api/v2.1/share-links.md#user-content-List%20Share%20Link%20of%20a%20Folder%20(File)
457
	 *
458
	 * @return object the share link
459
	 *
460
	 * @throws Exception
461
	 * @throws InvalidArgumentException
462
	 * @throws InvalidResponseException
463
	 *
464
	 * @noinspection PhpUnused
465
	 */
466
	public function listShareLinksOfAFolder(string $lib, string $path): ?object {
467
		$lib = $this->verifyLib($lib);
468
		$path = $this->normalizePath($path);
469
		$pathEncoded = rawurlencode($path);
470
471
		return $this->jsonDecode(
472
			$this->get(
0 ignored issues
show
Bug introduced by
It seems like $this->get($this->baseur...Token '.$this->token))) can also be of type true; however, parameter $jsonText of Datamate\SeafileApi\SeafileApi::jsonDecode() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

472
			/** @scrutinizer ignore-type */ $this->get(
Loading history...
473
				"{$this->baseurl}/api/v2.1/share-links/?repo_id={$lib}&path={$pathEncoded}",
474
				[CURLOPT_HTTPHEADER => ["Authorization: Token {$this->token}"]],
475
			),
476
			self::JSON_DECODE_ACCEPT_ARRAY_SINGLE_OBJECT_NULLABLE,
477
		);
478
	}
479
480
	/**
481
	 * Delete Share Link.
482
	 *
483
	 * @see https://download.seafile.com/published/web-api/v2.1/share-links.md#user-content-Delete%20Share%20Link
484
	 *
485
	 * @return object success {"success": true}
486
	 *
487
	 * @throws Exception
488
	 */
489
	public function deleteShareLink(string $token): object {
490
		$token = $this->verifyToken($token);
491
492
		return $this->jsonDecode(
493
			$this->delete(
0 ignored issues
show
Bug introduced by
It seems like $this->delete($this->bas...Token '.$this->token))) can also be of type true; however, parameter $jsonText of Datamate\SeafileApi\SeafileApi::jsonDecode() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

493
			/** @scrutinizer ignore-type */ $this->delete(
Loading history...
494
				"{$this->baseurl}/api/v2.1/share-links/{$token}/",
495
				[CURLOPT_HTTPHEADER => ["Authorization: Token {$this->token}"]],
496
			),
497
			self::JSON_DECODE_ACCEPT_SUCCESS_OBJECT,
498
		);
499
	}
500
501
	/**
502
	 * search user.
503
	 *
504
	 * @see https://download.seafile.com/published/web-api/v2.1/user-search.md#user-content-Search%20User
505
	 *
506
	 * @throws Exception
507
	 */
508
	public function searchUser(string $search): object {
509
		$searchEncoded = rawurlencode($search);
510
511
		return $this->jsonDecode(
512
			$this->get(
0 ignored issues
show
Bug introduced by
It seems like $this->get($this->baseur...ken ' . $this->token))) can also be of type true; however, parameter $jsonText of Datamate\SeafileApi\SeafileApi::jsonDecode() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

512
			/** @scrutinizer ignore-type */ $this->get(
Loading history...
513
				"{$this->baseurl}/api2/search-user/?q={$searchEncoded}",
514
				[CURLOPT_HTTPHEADER => ['Authorization: Token ' . $this->token]],
515
			),
516
			self::JSON_DECODE_ACCEPT_OBJECT,
517
		);
518
	}
519
520
	/**
521
	 * list groups for user sharing.
522
	 *
523
	 * @return array|object|string
524
	 *
525
	 * @throws Exception
526
	 * @throws InvalidResponseException
527
	 *
528
	 * @see (undocumented)
529
	 */
530
	public function shareableGroups(): array {
531
		return $this->jsonDecode(
532
			$this->get(
0 ignored issues
show
Bug introduced by
It seems like $this->get($this->baseur...Token '.$this->token))) can also be of type true; however, parameter $jsonText of Datamate\SeafileApi\SeafileApi::jsonDecode() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

532
			/** @scrutinizer ignore-type */ $this->get(
Loading history...
533
				"{$this->baseurl}/api/v2.1/shareable-groups/",
534
				[CURLOPT_HTTPHEADER => ["Authorization: Token {$this->token}"]],
535
			),
536
			self::JSON_DECODE_ACCEPT_ARRAY_OF_OBJECTS,
537
		);
538
	}
539
540
	/**
541
	 * Share a Library to Group.
542
	 *
543
	 * @see https://download.seafile.com/published/web-api/v2.1/share.md#user-content-Share%20a%20Library%20to%20Group
544
	 *
545
	 * @param int|int[]   $group
546
	 * @param null|string $permission [optional] r, rw, admin (default: r)
547
	 *
548
	 * @return array
549
	 *
550
	 * @throws Exception
551
	 * @throws InvalidArgumentException
552
	 */
553
	public function shareLibraryPathToGroup(string $lib, string $path, $group, ?string $permission = null) {
554
		$lib = $this->verifyLib($lib);
555
		$path = $this->normalizePath($path);
556
		$pathEncoded = rawurlencode($path);
557
558
		$fields = [
559
			'share_type' => 'group',
560
			'group_id' => $group,
561
			'permission' => $permission ?? 'r',
562
		];
563
564
		return $this->jsonDecode(
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->jsonDecode...N_DECODE_ACCEPT_OBJECT) also could return the type object|string which is incompatible with the documented return type array.
Loading history...
565
			$this->put(
0 ignored issues
show
Bug introduced by
It seems like $this->put($this->baseur...Token '.$this->token))) can also be of type true; however, parameter $jsonText of Datamate\SeafileApi\SeafileApi::jsonDecode() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

565
			/** @scrutinizer ignore-type */ $this->put(
Loading history...
566
				"{$this->baseurl}/api2/repos/{$lib}/dir/shared_items/?p={$pathEncoded}",
567
				$fields,
568
				[CURLOPT_HTTPHEADER => ["Authorization: Token {$this->token}"]],
569
			),
570
			self::JSON_DECODE_ACCEPT_OBJECT,
571
		);
572
	}
573
574
	/**
575
	 * Share a Library to User.
576
	 *
577
	 * @see https://download.seafile.com/published/web-api/v2.1/share.md#user-content-Share%20a%20Library%20to%20User
578
	 *
579
	 * @param null|string $permission [optional] r, rw, admin (default: r)
580
	 *
581
	 * @return array
582
	 *
583
	 * @throws Exception
584
	 * @throws InvalidArgumentException
585
	 */
586
	public function shareLibraryPathToUser(string $lib, string $path, string $user, ?string $permission = null) {
587
		$lib = $this->verifyLib($lib);
588
		$path = $this->normalizePath($path);
589
		$pathEncoded = rawurlencode($path);
590
591
		$fields = [
592
			'share_type' => 'user',
593
			'username' => $user,
594
			'permission' => $permission ?? 'r',
595
		];
596
597
		return $this->jsonDecode(
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->jsonDecode...N_DECODE_ACCEPT_OBJECT) also could return the type object|string which is incompatible with the documented return type array.
Loading history...
598
			$this->put(
0 ignored issues
show
Bug introduced by
It seems like $this->put($this->baseur...Token '.$this->token))) can also be of type true; however, parameter $jsonText of Datamate\SeafileApi\SeafileApi::jsonDecode() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

598
			/** @scrutinizer ignore-type */ $this->put(
Loading history...
599
				"{$this->baseurl}/api2/repos/{$lib}/dir/shared_items/?p={$pathEncoded}",
600
				$fields,
601
				[CURLOPT_HTTPHEADER => ["Authorization: Token {$this->token}"]],
602
			),
603
			// either array of objects -or- failure object
604
			self::JSON_DECODE_ACCEPT_ARRAY | self::JSON_DECODE_ACCEPT_OBJECT,
605
		);
606
	}
607
608
	/**
609
	 * Unshare a Library to Group.
610
	 *
611
	 * @see https://download.seafile.com/published/web-api/v2.1/share.md#user-content-Unshare%20a%20Library%20from%20Group
612
	 *
613
	 * @throws Exception
614
	 * @throws InvalidArgumentException
615
	 */
616
	public function unshareLibraryPathToGroup(string $lib, string $path, int $group): object {
617
		$lib = $this->verifyLib($lib);
618
		$path = $this->normalizePath($path);
619
		$pathEncoded = rawurlencode($path);
620
621
		return $this->jsonDecode(
622
			$this->delete(
0 ignored issues
show
Bug introduced by
It seems like $this->delete($this->bas...Token '.$this->token))) can also be of type true; however, parameter $jsonText of Datamate\SeafileApi\SeafileApi::jsonDecode() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

622
			/** @scrutinizer ignore-type */ $this->delete(
Loading history...
623
				"{$this->baseurl}/api2/repos/{$lib}/dir/shared_items/?p={$pathEncoded}&share_type=group&group_id={$group}",
624
				[CURLOPT_HTTPHEADER => ["Authorization: Token {$this->token}"]],
625
			),
626
			self::JSON_DECODE_ACCEPT_SUCCESS_OBJECT,
627
		);
628
	}
629
630
	/**
631
	 * Unshare a Library to User.
632
	 *
633
	 * @see https://download.seafile.com/published/web-api/v2.1/share.md#user-content-Unshare%20a%20Library%20from%20User
634
	 *
635
	 * @param string|string[] $user
636
	 *
637
	 * @throws Exception
638
	 * @throws InvalidArgumentException
639
	 */
640
	public function unshareLibraryPathToUser(string $lib, string $path, $user): object {
641
		$lib = $this->verifyLib($lib);
642
		$path = $this->normalizePath($path);
643
		$pathEncoded = rawurlencode($path);
644
		$userEncoded = rawurlencode($user);
0 ignored issues
show
Bug introduced by
It seems like $user can also be of type string[]; however, parameter $string of rawurlencode() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

644
		$userEncoded = rawurlencode(/** @scrutinizer ignore-type */ $user);
Loading history...
645
646
		return $this->jsonDecode(
647
			$this->delete(
0 ignored issues
show
Bug introduced by
It seems like $this->delete($this->bas...Token '.$this->token))) can also be of type true; however, parameter $jsonText of Datamate\SeafileApi\SeafileApi::jsonDecode() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

647
			/** @scrutinizer ignore-type */ $this->delete(
Loading history...
648
				"{$this->baseurl}/api2/repos/{$lib}/dir/shared_items/?p={$pathEncoded}&share_type=user&username={$userEncoded}",
649
				[CURLOPT_HTTPHEADER => ["Authorization: Token {$this->token}"]],
650
			),
651
			self::JSON_DECODE_ACCEPT_SUCCESS_OBJECT,
652
		);
653
	}
654
655
	/**
656
	 * list user shares for a library path.
657
	 *
658
	 * @see https://download.seafile.com/published/web-api/v2.1/share.md#user-content-List%20Shared%20Users%20of%20a%20Library
659
	 *
660
	 * @return array<int, object>
661
	 *
662
	 * @throws Exception
663
	 * @throws InvalidArgumentException
664
	 * @throws InvalidResponseException
665
	 */
666
	public function listSharesOfLibraryPath(string $lib, string $path): array {
667
		$lib = $this->verifyLib($lib);
668
		$path = $this->normalizePath($path);
669
		$pathEncoded = rawurlencode($path);
670
671
		return $this->jsonDecode(
672
			$this->get(
0 ignored issues
show
Bug introduced by
It seems like $this->get($this->baseur...Token '.$this->token))) can also be of type true; however, parameter $jsonText of Datamate\SeafileApi\SeafileApi::jsonDecode() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

672
			/** @scrutinizer ignore-type */ $this->get(
Loading history...
673
				"{$this->baseurl}/api2/repos/{$lib}/dir/shared_items/?p={$pathEncoded}",
674
				[CURLOPT_HTTPHEADER => ["Authorization: Token {$this->token}"]],
675
			),
676
			self::JSON_DECODE_ACCEPT_ARRAY_OF_OBJECTS,
677
		);
678
	}
679
680
	/**
681
	 * create a new directory.
682
	 *
683
	 * @see https://download.seafile.com/published/web-api/v2.1/directories.md#user-content-Create%20New%20Directory
684
	 *
685
	 * @param string $lib  library id (guid)
686
	 * @param string $path of the directory to create (e.g.: "/path/to/new-directory", leading and trailing slashes can be omitted)
687
	 *
688
	 * @return object|string the common "success" or the object with error_msg property
689
	 *
690
	 * @throws Exception|InvalidArgumentException
691
	 */
692
	public function createNewDirectory(string $lib, string $path) {
693
		$lib = $this->verifyLib($lib);
694
		$path = $this->normalizePath($path);
695
		$pathEncoded = rawurlencode($path);
696
697
		return $this->jsonDecode(
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->jsonDecode...N_DECODE_ACCEPT_OBJECT) also could return the type array which is incompatible with the documented return type object|string.
Loading history...
698
			$this->post(
0 ignored issues
show
Bug introduced by
It seems like $this->post($this->baseu...Token '.$this->token))) can also be of type true; however, parameter $jsonText of Datamate\SeafileApi\SeafileApi::jsonDecode() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

698
			/** @scrutinizer ignore-type */ $this->post(
Loading history...
699
				"{$this->baseurl}/api2/repos/{$lib}/dir/?p={$pathEncoded}",
700
				['operation' => 'mkdir'],
701
				[CURLOPT_HTTPHEADER => ["Authorization: Token {$this->token}"]],
702
			),
703
			self::JSON_DECODE_ACCEPT_STRING | self::JSON_DECODE_ACCEPT_OBJECT,
704
		);
705
	}
706
707
	/**
708
	 * delete a file.
709
	 *
710
	 * @see https://download.seafile.com/published/web-api/v2.1/file.md#user-content-Delete%20File
711
	 *
712
	 * @param string $lib  library id (guid)
713
	 * @param string $path of the fle to delete (e.g.: "/path/to/file-to-delete", leading and trailing slashes can be omitted)
714
	 *
715
	 * @return object|string the common "success" or the known object with error_msg property
716
	 *
717
	 * @throws Exception|InvalidArgumentException
718
	 */
719
	public function deleteFile(string $lib, string $path) {
720
		$lib = $this->verifyLib($lib);
721
		$path = $this->normalizePath($path);
722
		$pathEncoded = rawurlencode($path);
723
724
		return $this->jsonDecode(
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->jsonDecode...N_DECODE_ACCEPT_OBJECT) also could return the type array which is incompatible with the documented return type object|string.
Loading history...
725
			$this->delete(
0 ignored issues
show
Bug introduced by
It seems like $this->delete($this->bas...Token '.$this->token))) can also be of type true; however, parameter $jsonText of Datamate\SeafileApi\SeafileApi::jsonDecode() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

725
			/** @scrutinizer ignore-type */ $this->delete(
Loading history...
726
				"{$this->baseurl}/api2/repos/{$lib}/file/?p={$pathEncoded}",
727
				[CURLOPT_HTTPHEADER => ["Authorization: Token {$this->token}"]],
728
			),
729
			self::JSON_DECODE_ACCEPT_STRING | self::JSON_DECODE_ACCEPT_OBJECT,
730
		);
731
	}
732
733
	/**
734
	 * get a file download URL.
735
	 *
736
	 * @see https://download.seafile.com/published/web-api/v2.1/file.md#user-content-Download%20File
737
	 *
738
	 * @return string download URL (http/s)
739
	 *
740
	 * @throws Exception|InvalidArgumentException
741
	 */
742
	public function downloadFile(string $lib, string $path): string {
743
		$lib = $this->verifyLib($lib);
744
		$path = $this->normalizePath($path);
745
		$pathEncoded = rawurlencode($path);
746
747
		return $this->jsonDecode(
748
			$this->get(
0 ignored issues
show
Bug introduced by
It seems like $this->get($this->baseur...Token '.$this->token))) can also be of type true; however, parameter $jsonText of Datamate\SeafileApi\SeafileApi::jsonDecode() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

748
			/** @scrutinizer ignore-type */ $this->get(
Loading history...
749
				"{$this->baseurl}/api2/repos/{$lib}/file/?p={$pathEncoded}",
750
				[CURLOPT_HTTPHEADER => ["Authorization: Token {$this->token}"]],
751
			),
752
			self::JSON_DECODE_ACCEPT_STRING,
753
		);
754
	}
755
756
	/**
757
	 * download a file.
758
	 *
759
	 * get file contents of a file in a library
760
	 *
761
	 * @return false|string on failure
762
	 *
763
	 * @throws Exception|InvalidArgumentException
764
	 */
765
	public function downloadFileAsBuffer(string $lib, string $path) {
766
		$url = $this->downloadFile($lib, $path);
767
768
		return $this->get($url);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->get($url) also could return the type true which is incompatible with the documented return type false|string.
Loading history...
769
	}
770
771
	/**
772
	 * download a file to a local file.
773
	 *
774
	 * @param string $localPath path to a file - existing or not - on the local file-system
775
	 *
776
	 * @return bool success/failure
777
	 *
778
	 * @throws Exception|InvalidArgumentException
779
	 */
780
	public function downloadFileToFile(string $lib, string $path, string $localPath): bool {
781
		$handle = fopen($localPath, 'wb');
782
		if ($handle === false) {
783
			throw new Exception('failed to open local path for writing', self::ERROR_CODE_FILE_IO);
784
		}
785
786
		try {
787
			$result = $this->downloadFileToStream($lib, $path, $handle);
788
		}
789
		finally {
790
			$close = fclose($handle);
791
		}
792
793
		if (!$close) {
794
			throw new Exception('failed to close local path handle', self::ERROR_CODE_FILE_IO);
795
		}
796
797
		return $result;
798
	}
799
800
	/**
801
	 * download a file to a stream handle.
802
	 *
803
	 * @param resource $handle stream handle
804
	 *
805
	 * @return bool success/failure
806
	 *
807
	 * @throws Exception|InvalidArgumentException
808
	 */
809
	public function downloadFileToStream(string $lib, string $path, $handle): bool {
810
		$url = $this->downloadFile($lib, $path);
811
812
		return $this->get($url, [CURLOPT_RETURNTRANSFER => true, CURLOPT_FILE => $handle]);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->get($url, ...RLOPT_FILE => $handle)) could return the type string which is incompatible with the type-hinted return boolean. Consider adding an additional type-check to rule them out.
Loading history...
813
	}
814
815
	/**
816
	 * list items in directory.
817
	 *
818
	 * @see https://download.seafile.com/published/web-api/v2.1/directories.md#user-content-List%20Items%20in%20Directory
819
	 *
820
	 * @throws Exception
821
	 * @throws InvalidArgumentException
822
	 * @throws InvalidResponseException
823
	 */
824
	public function listItemsInDirectory(string $lib, string $path): array {
825
		$lib = $this->verifyLib($lib);
826
		$path = $this->normalizePath($path);
827
		$pathEncoded = rawurlencode($path);
828
829
		$result = $this->jsonDecode(
830
			$this->get(
0 ignored issues
show
Bug introduced by
It seems like $this->get($this->baseur...ken ' . $this->token))) can also be of type true; however, parameter $jsonText of Datamate\SeafileApi\SeafileApi::jsonDecode() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

830
			/** @scrutinizer ignore-type */ $this->get(
Loading history...
831
				"{$this->baseurl}/api2/repos/{$lib}/dir/?p={$pathEncoded}",
832
				[CURLOPT_HTTPHEADER => ['Authorization: Token ' . $this->token]],
833
			),
834
		);
835
836
		if (is_object($result)) {
837
			// likely a folder not found.
838
			$result = [];
839
		}
840
841
		return $result;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $result could return the type string which is incompatible with the type-hinted return array. Consider adding an additional type-check to rule them out.
Loading history...
842
	}
843
844
	/**
845
	 * move a file.
846
	 *
847
	 * @see https://download.seafile.com/published/web-api/v2.1/file.md#user-content-Move%20File
848
	 *
849
	 * @throws Exception|InvalidArgumentException
850
	 */
851
	public function moveFile(string $lib, string $path, string $dstLib, string $dstDir): object {
852
		$lib = $this->verifyLib($lib);
853
		$path = $this->normalizePath($path);
854
		$pathEncoded = rawurlencode($path);
855
		$dstLib = $this->verifyLib($dstLib);
856
		$dstDir = $this->normalizePath($dstDir);
857
858
		return $this->jsonDecode(
859
			$this->post(
0 ignored issues
show
Bug introduced by
It seems like $this->post($this->baseu...Token '.$this->token))) can also be of type true; however, parameter $jsonText of Datamate\SeafileApi\SeafileApi::jsonDecode() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

859
			/** @scrutinizer ignore-type */ $this->post(
Loading history...
860
				"{$this->baseurl}/api2/repos/{$lib}/file/?p={$pathEncoded}",
861
				['operation' => 'move', 'dst_repo' => $dstLib, 'dst_dir' => $dstDir],
862
				[CURLOPT_HTTPHEADER => ["Authorization: Token {$this->token}"]],
863
			),
864
			self::JSON_DECODE_ACCEPT_OBJECT,
865
		);
866
	}
867
868
	/**
869
	 * rename a file.
870
	 *
871
	 * @see https://download.seafile.com/published/web-api/v2.1/file.md#user-content-Move%20File
872
	 *
873
	 * @param string $lib     library id (guid)
874
	 * @param string $path    of the file to rename (e.g. "/path/to/file-to-rename")
875
	 * @param string $newName new basename for the basename of $path (e.g. "new-file-name")
876
	 *
877
	 * @return object|string the common "success" or the known object with error_msg property
878
	 *
879
	 * @throws Exception
880
	 */
881
	public function renameFile(string $lib, string $path, string $newName) {
882
		$lib = $this->verifyLib($lib);
883
		$path = $this->normalizePath($path);
884
		$pathEncoded = rawurlencode($path);
885
886
		return $this->jsonDecode(
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->jsonDecode...N_DECODE_ACCEPT_OBJECT) also could return the type array which is incompatible with the documented return type object|string.
Loading history...
887
			$this->post(
0 ignored issues
show
Bug introduced by
It seems like $this->post($this->baseu...Token '.$this->token))) can also be of type true; however, parameter $jsonText of Datamate\SeafileApi\SeafileApi::jsonDecode() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

887
			/** @scrutinizer ignore-type */ $this->post(
Loading history...
888
				"{$this->baseurl}/api2/repos/{$lib}/file/?p={$pathEncoded}",
889
				['operation' => 'rename', 'newname' => $newName],
890
				[CURLOPT_HTTPHEADER => ["Authorization: Token {$this->token}"]],
891
			),
892
			self::JSON_DECODE_ACCEPT_STRING | self::JSON_DECODE_ACCEPT_OBJECT,
893
		);
894
	}
895
896
	/**
897
	 * simplified file upload routine for string buffer.
898
	 *
899
	 * @throws InvalidArgumentException
900
	 * @throws Exception
901
	 */
902
	public function uploadBuffer(string $lib, string $path, string $buffer): object {
903
		$lib = $this->verifyLib($lib);
904
		$path = $this->normalizePath($path);
905
906
		$parentDir = dirname($path);
907
		$uploadLink = $this->uploadGetLink($lib, $parentDir);
908
		$fileName = basename($path);
909
910
		return $this->uploadFileBuffer($uploadLink, $parentDir, '', $buffer, $fileName);
911
	}
912
913
	/**
914
	 * simplified file upload routine for standard file.
915
	 *
916
	 * @param string $path path in seafile to upload the file as
917
	 * @param string $file path of file to upload
918
	 *
919
	 * @throws InvalidArgumentException
920
	 * @throws Exception
921
	 */
922
	public function uploadFile(string $lib, string $path, string $file): object {
923
		$lib = $this->verifyLib($lib);
924
		$path = $this->normalizePath($path);
925
		if (!is_file($file) && !is_readable($file)) {
926
			throw new InvalidArgumentException(sprintf('Not a readable file: %s', $file));
927
		}
928
929
		$parentDir = dirname($path);
930
		$uploadLink = $this->uploadGetLink($lib, $parentDir);
931
		$fileName = basename($path);
932
933
		return $this->uploadFilePath($uploadLink, $parentDir, '', $file, $fileName);
934
	}
935
936
	/**
937
	 * upload string buffer as a file.
938
	 *
939
	 * same as {@see SeafileApi::uploadFile()} with the option to upload without a
940
	 * concrete file on the system. the temporary file to upload is created
941
	 * from the string buffer.
942
	 *
943
	 * @param string $uploadLink   from {@see uploadGetLink}
944
	 * @param string $parentDir    the parent directory to upload the file to
945
	 * @param string $relativePath the name of the file, subdirectories possible (e.g. uploading a folder)
946
	 * @param string $buffer       file contents to upload as string (not the file-name)
947
	 * @param string $fileName     to use as basename for the data
948
	 *
949
	 * @throws Exception
950
	 */
951
	public function uploadFileBuffer(
952
		string $uploadLink,
953
		string $parentDir,
954
		string $relativePath,
955
		string $buffer,
956
		string $fileName = 'upload.dat',
957
		bool $replace = false
958
	): object {
959
		$tmpHandle = tmpfile();
960
		if ($tmpHandle === false) {
961
			throw new Exception('Upload data rejected: Unable to open temporary stream.');
962
		}
963
964
		$meta = stream_get_meta_data($tmpHandle);
965
		$tmpFile = $meta['uri'];
966
		if (!is_file($tmpFile)) {
967
			fclose($tmpHandle);
968
969
			throw new Exception('Upload data rejected: No file with temporary stream.');
970
		}
971
972
		$bytes = fwrite($tmpHandle, $buffer);
973
		if ($bytes === false) {
974
			fclose($tmpHandle);
975
976
			throw new Exception('Upload data rejected: Failed to write to temporary stream.');
977
		}
978
979
		$diff = strlen($buffer) - $bytes;
980
		if ($diff !== 0) {
981
			fclose($tmpHandle);
982
983
			throw new Exception(sprintf("Upload data rejected: Unexpected difference writing to temporary stream: %d bytes", $diff));
984
		}
985
986
		$result = rewind($tmpHandle);
987
		if ($result === false) {
988
			fclose($tmpHandle);
989
990
			throw new Exception('Upload data rejected: Failed to rewind temporary stream.');
991
		}
992
993
		$result = $this->uploadFilePath($uploadLink, $parentDir, $relativePath, $tmpFile, $fileName, $replace);
994
		fclose($tmpHandle);
995
996
		return $result;
997
	}
998
999
	/**
1000
	 * upload file.
1001
	 *
1002
	 * @see https://download.seafile.com/published/web-api/v2.1/file-upload.md#user-content-Upload%20File
1003
	 *
1004
	 * @param string  $uploadLink   from {@see uploadGetLink}
1005
	 * @param string  $parentDir    the parent directory to upload a file to
1006
	 * @param string  $relativePath to place the file in under $uploadPath (can include subdirectories)
1007
	 * @param string  $path         path of the file to upload
1008
	 * @param ?string $fileName     to use as basename for the file (the name used in Seafile)
1009
	 *
1010
	 * @throws Exception
1011
	 */
1012
	public function uploadFilePath(
1013
		string $uploadLink,
1014
		string $parentDir,
1015
		string $relativePath,
1016
		string $path,
1017
		?string $fileName = null,
1018
		bool $replace = false
1019
	): object {
1020
		$parentDir = $this->normalizePath($parentDir);
1021
		$relativePath = ltrim('/', $this->normalizePath($relativePath));
1022
		$fileName ??= basename($path);
1023
1024
		$fields = [
1025
			'file' => new \CURLFile($path, 'application/octet-stream', $fileName),
1026
			'parent_dir' => $parentDir,
1027
			'relative_path' => $relativePath,
1028
			'replace' => $replace ? '1' : '0',
1029
		];
1030
1031
		return $this->jsonDecode(
1032
			$this->post(
0 ignored issues
show
Bug introduced by
It seems like $this->post($uploadLink....Token '.$this->token))) can also be of type true; however, parameter $jsonText of Datamate\SeafileApi\SeafileApi::jsonDecode() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1032
			/** @scrutinizer ignore-type */ $this->post(
Loading history...
1033
				"{$uploadLink}?ret-json=1",
1034
				$fields,
1035
				[CURLOPT_HTTPHEADER => ["Authorization: Token {$this->token}"]],
1036
			),
1037
			self::JSON_DECODE_ACCEPT_ARRAY_SINGLE_OBJECT,
1038
		);
1039
	}
1040
1041
	/**
1042
	 * get file upload link.
1043
	 *
1044
	 * @see https://download.seafile.com/published/web-api/v2.1/file-upload.md#user-content-Get%20Upload%20Link
1045
	 *
1046
	 * @param string $uploadDir the directory to upload file(s) to
1047
	 *
1048
	 * @return string upload link (https?://...)
1049
	 *
1050
	 * @throws Exception|InvalidArgumentException
1051
	 */
1052
	public function uploadGetLink(string $lib, string $uploadDir): string {
1053
		$lib = $this->verifyLib($lib);
1054
		$uploadDir = $this->normalizePath($uploadDir);
1055
		$pathEncoded = rawurlencode($uploadDir);
1056
1057
		return $this->jsonDecode(
1058
			$this->get(
0 ignored issues
show
Bug introduced by
It seems like $this->get($this->baseur...Token '.$this->token))) can also be of type true; however, parameter $jsonText of Datamate\SeafileApi\SeafileApi::jsonDecode() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1058
			/** @scrutinizer ignore-type */ $this->get(
Loading history...
1059
				"{$this->baseurl}/api2/repos/{$lib}/upload-link/?p={$pathEncoded}",
1060
				[CURLOPT_HTTPHEADER => ["Authorization: Token {$this->token}"]],
1061
			),
1062
			self::JSON_DECODE_ACCEPT_STRING,
1063
		);
1064
	}
1065
1066
	public function generateUserAuthToken(string $email): object {
1067
		return $this->jsonDecode(
1068
			$this->post(
0 ignored issues
show
Bug introduced by
It seems like $this->post($this->baseu...Token '.$this->token))) can also be of type true; however, parameter $jsonText of Datamate\SeafileApi\SeafileApi::jsonDecode() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1068
			/** @scrutinizer ignore-type */ $this->post(
Loading history...
1069
				"{$this->baseurl}/api/v2.1/admin/generate-user-auth-token/",
1070
				['email' => $email],
1071
				[CURLOPT_HTTPHEADER => ["Authorization: Token {$this->token}"]],
1072
			),
1073
			self::JSON_DECODE_ACCEPT_OBJECT,
1074
		);
1075
	}
1076
1077
	public function getUserActivity(string $email, int $page = 1, int $perPage = 25): object {
1078
		return $this->jsonDecode(
1079
			$this->get(
0 ignored issues
show
Bug introduced by
It seems like $this->get($this->baseur...Token '.$this->token))) can also be of type true; however, parameter $jsonText of Datamate\SeafileApi\SeafileApi::jsonDecode() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1079
			/** @scrutinizer ignore-type */ $this->get(
Loading history...
1080
				"{$this->baseurl}/api/v2.1/admin/user-activities/?user={$email}&page={$page}&per_page={$perPage}",
1081
				[CURLOPT_HTTPHEADER => ["Authorization: Token {$this->token}"]],
1082
			),
1083
			self::JSON_DECODE_ACCEPT_OBJECT,
1084
		);
1085
	}
1086
1087
	/**
1088
	 * set authorization token.
1089
	 */
1090
	public function setToken(string $token): void {
1091
		$this->token = $token;
1092
	}
1093
1094
	public function listDevices(): array {
1095
		return $this->jsonDecode(
1096
			$this->get(
0 ignored issues
show
Bug introduced by
It seems like $this->get($this->baseur...Token '.$this->token))) can also be of type true; however, parameter $jsonText of Datamate\SeafileApi\SeafileApi::jsonDecode() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1096
			/** @scrutinizer ignore-type */ $this->get(
Loading history...
1097
				"{$this->baseurl}/api2/devices/",
1098
				[CURLOPT_HTTPHEADER => ["Authorization: Token {$this->token}"]],
1099
			),
1100
		);
1101
	}
1102
1103
	public function listStarredItems(): object {
1104
		return $this->jsonDecode(
1105
			$this->get(
0 ignored issues
show
Bug introduced by
It seems like $this->get($this->baseur...Token '.$this->token))) can also be of type true; however, parameter $jsonText of Datamate\SeafileApi\SeafileApi::jsonDecode() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1105
			/** @scrutinizer ignore-type */ $this->get(
Loading history...
1106
				"{$this->baseurl}/api/v2.1/starred-items/",
1107
				[CURLOPT_HTTPHEADER => ["Authorization: Token {$this->token}"]],
1108
			),
1109
		);
1110
	}
1111
1112
	public function listGroups(): object {
1113
		return $this->jsonDecode(
1114
			$this->get(
0 ignored issues
show
Bug introduced by
It seems like $this->get($this->baseur...Token '.$this->token))) can also be of type true; however, parameter $jsonText of Datamate\SeafileApi\SeafileApi::jsonDecode() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1114
			/** @scrutinizer ignore-type */ $this->get(
Loading history...
1115
				"{$this->baseurl}/api2/groups/",
1116
				[CURLOPT_HTTPHEADER => ["Authorization: Token {$this->token}"]],
1117
			),
1118
		);
1119
	}
1120
1121
	public function listInvitations(): array {
1122
		return $this->jsonDecode(
1123
			$this->get(
0 ignored issues
show
Bug introduced by
It seems like $this->get($this->baseur...Token '.$this->token))) can also be of type true; however, parameter $jsonText of Datamate\SeafileApi\SeafileApi::jsonDecode() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1123
			/** @scrutinizer ignore-type */ $this->get(
Loading history...
1124
				"{$this->baseurl}/api/v2.1/invitations/",
1125
				[CURLOPT_HTTPHEADER => ["Authorization: Token {$this->token}"]],
1126
			),
1127
		);
1128
	}
1129
1130
	/**
1131
	 * @param string $start e.g. date('Y-m-d', time() - 7776000 ); // 3 months
1132
	 * @param string $end   e.g. date('Y-m-d', time());
1133
	 */
1134
	public function getLoginLog(string $start, string $end): array {
1135
		$start = rawurlencode($start);
1136
		$end = rawurlencode($end);
1137
1138
		return $this->jsonDecode(
1139
			$this->get(
0 ignored issues
show
Bug introduced by
It seems like $this->get($this->baseur...Token '.$this->token))) can also be of type true; however, parameter $jsonText of Datamate\SeafileApi\SeafileApi::jsonDecode() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1139
			/** @scrutinizer ignore-type */ $this->get(
Loading history...
1140
				"{$this->baseurl}/api/v2.1/admin/logs/login/?start={$start}&end={$end}",
1141
				[CURLOPT_HTTPHEADER => ["Authorization: Token {$this->token}"]],
1142
			),
1143
		);
1144
	}
1145
1146
	public function listUploadLinks(): array {
1147
		return $this->jsonDecode(
1148
			$this->get(
0 ignored issues
show
Bug introduced by
It seems like $this->get($this->baseur...Token '.$this->token))) can also be of type true; however, parameter $jsonText of Datamate\SeafileApi\SeafileApi::jsonDecode() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1148
			/** @scrutinizer ignore-type */ $this->get(
Loading history...
1149
				"{$this->baseurl}/api/v2.1/upload-links/",
1150
				[CURLOPT_HTTPHEADER => ["Authorization: Token {$this->token}"]],
1151
			),
1152
		);
1153
	}
1154
1155
	public function listRepoApiTokens(string $lib): object {
1156
		$lib = $this->verifyLib($lib);
1157
1158
		return $this->jsonDecode(
1159
			$this->get(
0 ignored issues
show
Bug introduced by
It seems like $this->get($this->baseur...Token '.$this->token))) can also be of type true; however, parameter $jsonText of Datamate\SeafileApi\SeafileApi::jsonDecode() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1159
			/** @scrutinizer ignore-type */ $this->get(
Loading history...
1160
				"{$this->baseurl}/api/v2.1/repos/{$lib}/repo-api-tokens/",
1161
				[CURLOPT_HTTPHEADER => ["Authorization: Token {$this->token}"]],
1162
			),
1163
		);
1164
	}
1165
1166
	/**
1167
	 * internal api implementation to get library by name.
1168
	 *
1169
	 * @return ?string id (guid) of the library, null if library does not exist
1170
	 *
1171
	 * @throws Exception
1172
	 */
1173
	private function getLibraryIdByLibraryName(string $name): ?string {
1174
		$name = explode('/', $this->normalizePath($name), 2)[1];
1175
1176
		$libraries = $this->listLibrariesCached();
1177
		$libraries = array_column($libraries, null, 'name');
1178
1179
		return $libraries[$name]->id ?? null;
1180
	}
1181
1182
	/**
1183
	 * List libraries a user can access.
1184
	 *
1185
	 * internal api implementation for {@see listLibrariesCached()} and {@see listLibraries()}
1186
	 *
1187
	 * @see https://download.seafile.com/published/web-api/v2.1/libraries.md#user-content-List%20Libraries
1188
	 *
1189
	 * @return object[]
1190
	 *
1191
	 * @throws Exception
1192
	 */
1193
	private function listLibrariesDo(): array {
1194
		return $this->jsonDecode(
1195
			$this->get(
0 ignored issues
show
Bug introduced by
It seems like $this->get($this->baseur...Token '.$this->token))) can also be of type true; however, parameter $jsonText of Datamate\SeafileApi\SeafileApi::jsonDecode() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1195
			/** @scrutinizer ignore-type */ $this->get(
Loading history...
1196
				"{$this->baseurl}/api2/repos/",
1197
				[CURLOPT_HTTPHEADER => ["Authorization: Token {$this->token}"]],
1198
			),
1199
			self::JSON_DECODE_ACCEPT_ARRAY_OF_OBJECTS,
1200
		);
1201
	}
1202
1203
	/**
1204
	 * like {@see listLibraries()} but cached.
1205
	 *
1206
	 * @throws Exception
1207
	 * @throws InvalidResponseException
1208
	 */
1209
	private function listLibrariesCached(bool $invalidate = false): array {
1210
		static $librariesCache;
1211
1212
		return $librariesCache = ($invalidate ? null : $librariesCache) ?? $this->listLibrariesDo();
1213
	}
1214
1215
	/**
1216
	 * normalize path.
1217
	 *
1218
	 * normalizes the path component separator <slash> "/" <U002F> U+002F SOLIDUS
1219
	 * with first character being the slash, no consecutive slashes within the
1220
	 * path and no terminating slash.
1221
	 */
1222
	private function normalizePath(string $path): string {
1223
		$buffer = rtrim($path, '/');
1224
		$buffer = preg_replace('~/{2,}~', '/', $buffer);
1225
		$buffer === '' && $buffer = '/';
1226
		$buffer[0] === '/' || $buffer = "/{$buffer}";
1227
1228
		return $buffer;
1229
	}
1230
1231
	/**
1232
	 * verify library id.
1233
	 *
1234
	 * verifies the format of a library id. can be used in URLs
1235
	 * afterwards. case normalization to lowercase.
1236
	 *
1237
	 * example library id strings:
1238
	 *  - 21b941c2-5411-4372-a514-00b62ab99ef2 (from the docs)
1239
	 *  - 79144b25-f772-42b6-a1c0-60e6359f5884 (from a server)
1240
	 *
1241
	 * @return string library id
1242
	 */
1243
	private function verifyLib(string $lib, bool $allowEmpty = false): string {
1244
		if ($allowEmpty && ($lib === '')) {
1245
			return $lib;
1246
		}
1247
1248
		$buffer = \strtr($lib, self::HEX_ALPHA_UPPER, self::HEX_ALPHA_LOWER);
1249
		$format = '%04x%04x-%04x-%04x-%04x-%04x%04x%04x';
1250
		$values = sscanf($buffer, $format);
1251
		$result = vsprintf($format, $values);
0 ignored issues
show
Bug introduced by
It seems like $values can also be of type integer and null; however, parameter $values of vsprintf() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1251
		$result = vsprintf($format, /** @scrutinizer ignore-type */ $values);
Loading history...
1252
1253
		if ($buffer !== $result) {
1254
			throw new InvalidArgumentException(sprintf('Not a library id: "%s"', $lib));
1255
		}
1256
1257
		return $result;
1258
	}
1259
1260
	/**
1261
	 * verify share link token.
1262
	 *
1263
	 * verifies the format of a share link token. can be used in URLs
1264
	 * afterwards. case normalization to lowercase.
1265
	 *
1266
	 * @param string $token e.g. "0a29ff44dc0b4b56be74"
1267
	 */
1268
	private function verifyToken(string $token): string {
1269
		$buffer = \strtr($token, self::HEX_ALPHA_UPPER, self::HEX_ALPHA_LOWER);
1270
		$format = '%04x%04x%04x%04x%04x';
1271
		$values = sscanf($buffer, $format);
1272
		$result = vsprintf($format, $values);
0 ignored issues
show
Bug introduced by
It seems like $values can also be of type integer and null; however, parameter $values of vsprintf() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1272
		$result = vsprintf($format, /** @scrutinizer ignore-type */ $values);
Loading history...
1273
1274
		if ($buffer !== $result) {
1275
			throw new InvalidArgumentException(sprintf('Not a token: "%s"', $token));
1276
		}
1277
1278
		return $result;
1279
	}
1280
1281
	/**
1282
	 * authenticate class against seafile api.
1283
	 *
1284
	 * @see https://download.seafile.com/published/web-api/home.md#user-content-Quick%20Start
1285
	 *
1286
	 * @param null|string $otp (optional) Seafile OTP (if user uses OTP access)
1287
	 */
1288
	private function setTokenByUsernameAndPassword(?string $otp = null): void {
1289
		// @auth:token:<email> : password is auth token
1290
		if (str_starts_with($this->user, $needle = self::USER_PREFIX_AUTH_TOKEN)) {
1291
			$this->user = \substr($this->user, strlen($needle)) ?: '';
1292
			$this->token = $this->pass;
1293
			if ($this->ping() !== 'pong') {
1294
				throw new ConnectionException('token authentication failure');
1295
			}
1296
1297
			return;
1298
		}
1299
1300
		$data = $this->jsonDecode(
1301
			$this->post(
0 ignored issues
show
Bug introduced by
It seems like $this->post($this->baseu...TP: '.$otp)) : array()) can also be of type true; however, parameter $jsonText of Datamate\SeafileApi\SeafileApi::jsonDecode() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1301
			/** @scrutinizer ignore-type */ $this->post(
Loading history...
1302
				"{$this->baseurl}/api2/auth-token/",
1303
				['username' => $this->user, 'password' => $this->pass],
1304
				$otp ? [CURLOPT_HTTPHEADER => ["X-SEAFILE-OTP: {$otp}"]] : [],
1305
			),
1306
			self::JSON_DECODE_ACCEPT_OBJECT,
1307
		);
1308
		$this->token = (string) $data->token;
1309
	}
1310
1311
	/**
1312
	 * http request with get method.
1313
	 *
1314
	 * @return bool|string
1315
	 */
1316
	private function get(string $url, array $curlOptions = []) {
1317
		$curlOptions += $this->curlSharedOptions;
1318
1319
		return $this->curlExec($url, $curlOptions);
1320
	}
1321
1322
	/**
1323
	 * http request with post method.
1324
	 *
1325
	 * @return bool|string
1326
	 */
1327
	private function post(string $url, array $fields = [], array $curlOptions = []) {
1328
		$curlOptions += $this->curlSharedOptions;
1329
		$curlOptions[CURLOPT_POST] = true;
1330
		$curlOptions[CURLOPT_POSTFIELDS] = $fields;
1331
1332
		return $this->curlExec($url, $curlOptions);
1333
	}
1334
1335
	/**
1336
	 * http request with put method.
1337
	 *
1338
	 * @return bool|string
1339
	 */
1340
	public function put(string $url, array $fields = [], array $curlOptions = []) {
1341
		$curlOptions += $this->curlSharedOptions;
1342
		$curlOptions[CURLOPT_CUSTOMREQUEST] = 'PUT';
1343
		$curlOptions[CURLOPT_POSTFIELDS] = $fields;
1344
1345
		return $this->curlExec($url, $curlOptions);
1346
	}
1347
1348
	/**
1349
	 * http request with delete method.
1350
	 *
1351
	 * @return bool|string
1352
	 */
1353
	public function delete(string $url, array $curlOptions = []) {
1354
		$curlOptions += $this->curlSharedOptions;
1355
		$curlOptions[CURLOPT_CUSTOMREQUEST] = 'DELETE';
1356
1357
		return $this->curlExec($url, $curlOptions);
1358
	}
1359
1360
	/**
1361
	 * json decode handler.
1362
	 *
1363
	 * decode json with structural acceptance
1364
	 *
1365
	 * @param int $flags decode accept flag
1366
	 *
1367
	 * @return array|object|string
1368
	 *
1369
	 * @throws InvalidResponseException
1370
	 */
1371
	private function jsonDecode(string $jsonText, int $flags = self::JSON_DECODE_ACCEPT_DEFAULT) {
1372
		$accept = $flags & self::JSON_DECODE_ACCEPT_MASK;
1373
		if ($accept === 0) {
1374
			return $jsonText;
1375
		}
1376
1377
		try {
1378
			$result = json_decode($jsonText, false, 512, JSON_THROW_ON_ERROR);
1379
		} /* @noinspection PhpMultipleClassDeclarationsInspection */ catch (\JsonException $e) {
1380
			throw JsonDecodeException::create(sprintf('json decode error of %s', JsonDecodeException::shorten($jsonText)), $jsonText, $e);
1381
		}
1382
1383
		if ($accept === self::JSON_DECODE_ACCEPT_JSON) {
1384
			return $result;
1385
		}
1386
1387
		if ($accept === self::JSON_DECODE_ACCEPT_ARRAY_OF_OBJECTS) {
1388
			if (is_array($result) && $result === array_filter($result, 'is_object')) {
1389
				return $result;
1390
			}
1391
1392
			throw JsonDecodeException::create(sprintf('json decode accept %5d error [%s] of %s', decbin($accept), \gettype($result), JsonDecodeException::shorten($jsonText)), $jsonText);
1393
		}
1394
1395
		if ($accept === self::JSON_DECODE_ACCEPT_ARRAY_SINGLE_OBJECT_NULLABLE) {
1396
			if (is_array($result) &&
1397
				(
1398
					(\count($result) === 1 && is_object($result[0] ?? null)) ||
1399
					(\count($result) === 0)
1400
				)
1401
			) {
1402
				return $result[0] ?? null;
1403
			}
1404
1405
			throw JsonDecodeException::create(sprintf('json decode accept %5d error [%s] of %s', decbin($accept), \gettype($result), JsonDecodeException::shorten($jsonText)), $jsonText);
1406
		}
1407
1408
		if ($accept === self::JSON_DECODE_ACCEPT_ARRAY_SINGLE_OBJECT) {
1409
			if (is_array($result) && is_object($result[0] ?? null) && \count($result) === 1) {
1410
				return $result[0];
1411
			}
1412
1413
			throw JsonDecodeException::create(sprintf('json decode accept %5d error [%s] of %s', decbin($accept), \gettype($result), JsonDecodeException::shorten($jsonText)), $jsonText);
1414
		}
1415
1416
		if ($accept === self::JSON_DECODE_ACCEPT_SUCCESS_OBJECT) {
1417
			if (is_object($result) && (array) $result === ['success' => true]) {
1418
				return $result;
1419
			}
1420
1421
			throw JsonDecodeException::create(sprintf('json decode accept %5d error [%s] of %s', decbin($accept), \gettype($result), JsonDecodeException::shorten($jsonText)), $jsonText);
1422
		}
1423
1424
		if ($accept === self::JSON_DECODE_ACCEPT_SUCCESS_STRING) {
1425
			if ($result === self::STRING_SUCCESS) {
1426
				return $result;
1427
			}
1428
1429
			throw JsonDecodeException::create(sprintf('json decode accept %5d error [%s] of %s', decbin($accept), \gettype($result), JsonDecodeException::shorten($jsonText)), $jsonText);
1430
		}
1431
1432
		if (is_string($result) && (self::JSON_DECODE_ACCEPT_STRING !== ($accept & self::JSON_DECODE_ACCEPT_STRING))) {
1433
			throw JsonDecodeException::create(sprintf('json decode type %s not accepted; of %s', \gettype($result), JsonDecodeException::shorten($jsonText)), $jsonText);
1434
		}
1435
1436
		if (is_array($result) && (self::JSON_DECODE_ACCEPT_ARRAY !== ($accept & self::JSON_DECODE_ACCEPT_ARRAY))) {
1437
			throw JsonDecodeException::create(sprintf('json decode type %s not accepted; of %s', \gettype($result), JsonDecodeException::shorten($jsonText)), $jsonText);
1438
		}
1439
1440
		if (is_object($result) && (self::JSON_DECODE_ACCEPT_OBJECT !== ($accept & self::JSON_DECODE_ACCEPT_OBJECT))) {
1441
			throw JsonDecodeException::create(sprintf('json decode type %s not accepted; of %s', \gettype($result), JsonDecodeException::shorten($jsonText)), $jsonText);
1442
		}
1443
1444
		return $result;
1445
	}
1446
1447
	/**
1448
	 * execute curl with url and options.
1449
	 *
1450
	 * @return bool|string
1451
	 */
1452
	private function curlExec(string $url, array $options) {
1453
		$handle = curl_init($url);
1454
		if (!$handle instanceof \CurlHandle) {
1455
			throw new ConnectionException('Unable to initialise cURL session.', self::ERROR_CODE_NO_CURL);
1456
		}
1457
1458
		$this->handle = $handle;
1459
1460
		if (!curl_setopt_array($this->handle, $options)) {
1461
			throw new Exception("Error setting cURL request options.");
1462
		}
1463
		$result = curl_exec($this->handle);
1464
		$this->curlExecHandleResult($result);
1465
		curl_close($this->handle);
1466
1467
		return $result;
1468
	}
1469
1470
	/**
1471
	 * internal handling of curl_exec() return.
1472
	 *
1473
	 * {@see curlExec()}
1474
	 *
1475
	 * @param bool|string $curlResult return value from curl_exec();
1476
	 *
1477
	 * @throws ConnectionException
1478
	 */
1479
	private function curlExecHandleResult($curlResult): void {
1480
		if (empty($curlResult)) {
1481
			throw new ConnectionException(curl_error($this->handle), -1);
1482
		}
1483
1484
		$code = (int) curl_getinfo($this->handle)['http_code'];
1485
1486
		$codeIsInErrorRange = $code >= 400 && $code <= 600;
1487
		$codeIsNotInNonErrorCodes = !in_array($code, [200, 201, 202, 203, 204, 205, 206, 207, 301], true);
1488
1489
		if ($codeIsInErrorRange || $codeIsNotInNonErrorCodes) {
1490
			ConnectionException::throwCurlResult($code, $curlResult);
1491
		}
1492
	}
1493
}
1494