Passed
Push — master ( 37cafd...a8b392 )
by
unknown
07:18
created
plugins/filesbackendSeafile/php/Model/AccountStoreV1Encoder.php 1 patch
Indentation   +43 added lines, -43 removed lines patch added patch discarded remove patch
@@ -7,48 +7,48 @@
 block discarded – undo
7 7
  */
8 8
 class AccountStoreV1Encoder
9 9
 {
10
-    /**
11
-     * Encode an account setting value
12
-     *
13
-     * {@see AccountStore::encryptBackendConfigProperty()}
14
-     *
15
-     * @param string $value
16
-     * @return string encoded
17
-     */
18
-    public static function encode(string $value): string
19
-    {
20
-        $nonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);
21
-        $encrypted = sodium_crypto_secretbox($value, $nonce, hex2bin(FILES_ACCOUNTSTORE_V1_SECRET_KEY));
22
-        return bin2hex($nonce) . bin2hex($encrypted);
23
-    }
10
+	/**
11
+	 * Encode an account setting value
12
+	 *
13
+	 * {@see AccountStore::encryptBackendConfigProperty()}
14
+	 *
15
+	 * @param string $value
16
+	 * @return string encoded
17
+	 */
18
+	public static function encode(string $value): string
19
+	{
20
+		$nonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);
21
+		$encrypted = sodium_crypto_secretbox($value, $nonce, hex2bin(FILES_ACCOUNTSTORE_V1_SECRET_KEY));
22
+		return bin2hex($nonce) . bin2hex($encrypted);
23
+	}
24 24
 
25
-    /**
26
-     * Decode an encoded account setting value
27
-     *
28
-     * {@see AccountStore::decryptBackendConfigProperty()}
29
-     *
30
-     * @param string $valueInHex
31
-     * @return string decoded
32
-     */
33
-    public static function decode(string $valueInHex): string
34
-    {
35
-        $value = hex2bin($valueInHex);
36
-        if (!is_string($value)) {
37
-            throw new \UnexpectedValueException(sprintf('Not an envelope of an encrypted value. Raw binary length of envelope is %d.', strlen($valueInHex)));
38
-        }
39
-        $nonce = substr($value, 0, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);
40
-        if (!is_string($nonce) || strlen($nonce) !== SODIUM_CRYPTO_SECRETBOX_NONCEBYTES) {
41
-            throw new \UnexpectedValueException(sprintf('Not an encrypted value. Raw binary length is %d which is below %d.', strlen($value), SODIUM_CRYPTO_SECRETBOX_NONCEBYTES));
42
-        }
43
-        $encrypted = substr($value, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES, strlen($value));
44
-        if (!is_string($encrypted)) {
45
-            throw new \UnexpectedValueException(sprintf('Not an encrypted value. Raw binary length is %d.', strlen($value)));
46
-        }
47
-        $result = sodium_crypto_secretbox_open($encrypted, $nonce, hex2bin(FILES_ACCOUNTSTORE_V1_SECRET_KEY));
48
-        // Decryption failed, password might have changed
49
-        if (false === $result) {
50
-            throw new \UnexpectedValueException("invalid password");
51
-        }
52
-        return $result;
53
-    }
25
+	/**
26
+	 * Decode an encoded account setting value
27
+	 *
28
+	 * {@see AccountStore::decryptBackendConfigProperty()}
29
+	 *
30
+	 * @param string $valueInHex
31
+	 * @return string decoded
32
+	 */
33
+	public static function decode(string $valueInHex): string
34
+	{
35
+		$value = hex2bin($valueInHex);
36
+		if (!is_string($value)) {
37
+			throw new \UnexpectedValueException(sprintf('Not an envelope of an encrypted value. Raw binary length of envelope is %d.', strlen($valueInHex)));
38
+		}
39
+		$nonce = substr($value, 0, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);
40
+		if (!is_string($nonce) || strlen($nonce) !== SODIUM_CRYPTO_SECRETBOX_NONCEBYTES) {
41
+			throw new \UnexpectedValueException(sprintf('Not an encrypted value. Raw binary length is %d which is below %d.', strlen($value), SODIUM_CRYPTO_SECRETBOX_NONCEBYTES));
42
+		}
43
+		$encrypted = substr($value, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES, strlen($value));
44
+		if (!is_string($encrypted)) {
45
+			throw new \UnexpectedValueException(sprintf('Not an encrypted value. Raw binary length is %d.', strlen($value)));
46
+		}
47
+		$result = sodium_crypto_secretbox_open($encrypted, $nonce, hex2bin(FILES_ACCOUNTSTORE_V1_SECRET_KEY));
48
+		// Decryption failed, password might have changed
49
+		if (false === $result) {
50
+			throw new \UnexpectedValueException("invalid password");
51
+		}
52
+		return $result;
53
+	}
54 54
 }
Please login to merge, or discard this patch.
plugins/filesbackendSeafile/php/class.pluginseafile.php 1 patch
Indentation   +2 added lines, -2 removed lines patch added patch discarded remove patch
@@ -8,7 +8,7 @@  discard block
 block discarded – undo
8 8
 final class PluginFilesbackendSeafile extends Plugin
9 9
 {
10 10
 
11
-    /**
11
+	/**
12 12
 	 * Called to initialize the plugin and register for hooks.
13 13
 	 *
14 14
 	 * @return void
@@ -30,7 +30,7 @@  discard block
 block discarded – undo
30 30
 			case 'server.core.settings.init.before':
31 31
 				$this->onBeforeSettingsInit($data);
32 32
 				break;
33
-            default:
33
+			default:
34 34
 		}
35 35
 	}
36 36
 
Please login to merge, or discard this patch.
plugins/filesbackendSeafile/php/lib/seafapi/SeafileApi.php 1 patch
Indentation   +1583 added lines, -1583 removed lines patch added patch discarded remove patch
@@ -24,1587 +24,1587 @@
 block discarded – undo
24 24
  */
25 25
 final class SeafileApi
26 26
 {
27
-    public const USER_PREFIX_AUTH_TOKEN = '@auth:token:';
28
-
29
-    public const TYPE_DIR = 'dir';
30
-    public const TYPE_FILE = 'file';
31
-    public const TYPE_REPO = 'repo';
32
-    public const TYPE_SREPO = 'srepo';
33
-    public const TYPE_GREPO = 'grepo';
34
-
35
-    public const TYPES = self::TYPES_FILE + self::TYPES_DIR_LIKE;
36
-    public const TYPES_DIR_LIKE = self::TYPES_DIR + self::TYPES_REPO;
37
-    public const TYPES_DIR = [self::TYPE_DIR => self::TYPE_DIR];
38
-    public const TYPES_FILE = [self::TYPE_FILE => self::TYPE_FILE];
39
-    public const TYPES_REPO = [self::TYPE_REPO => self::TYPE_REPO, self::TYPE_SREPO => self::TYPE_SREPO, self::TYPE_GREPO => self::TYPE_GREPO];
40
-
41
-    /**
42
-     * @const string
43
-     */
44
-    public const STRING_SUCCESS = 'success';
45
-
46
-    /**
47
-     * Error codes
48
-     */
49
-    public const ERROR_CODE_FEATURES = 802;
50
-    public const ERROR_CODE_NO_CURL = 803;
51
-    public const ERROR_CODE_FILE_IO = 808;
52
-
53
-    /**
54
-     * default curl options
55
-     */
56
-    private const CURL_OPTION_DEFAULTS = [
57
-        CURLOPT_AUTOREFERER => true,
58
-        CURLOPT_TIMEOUT => 10,
59
-        CURLOPT_RETURNTRANSFER => true,
60
-        CURLOPT_FOLLOWLOCATION => false,
61
-    ];
62
-
63
-    /**
64
-     * @var array shared curl options between all requests
65
-     */
66
-    private array $curlSharedOptions = self::CURL_OPTION_DEFAULTS;
67
-
68
-    /**
69
-     * jsonDecode accept flags
70
-     *
71
-     * @see jsonDecode
72
-     */
73
-    private const JSON_DECODE_ACCEPT_MASK = 31;                         # 1 1111 accept bitmask (five bits with the msb flags)
74
-    private const JSON_DECODE_ACCEPT_JSON = 16;                         # 1 0000 JSON text
75
-    private const JSON_DECODE_ACCEPT_DEFAULT = 23;                      # 1 0111 default: string, array or object
76
-    private const JSON_DECODE_ACCEPT_OBJECT = 17;                       # 1 0001 object
77
-    private const JSON_DECODE_ACCEPT_ARRAY = 18;                        # 1 0010 array
78
-    private const JSON_DECODE_ACCEPT_STRING = 20;                       # 1 0100 string
79
-    private const JSON_DECODE_ACCEPT_ARRAY_OF_OBJECTS = 24;             # 1 1000 array with only objects (incl. none)
80
-    private const JSON_DECODE_ACCEPT_ARRAY_SINGLE_OBJECT = 25;          # 1 1001 array with one single object, return that item
81
-    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
82
-    private const JSON_DECODE_ACCEPT_SUCCESS_STRING = 28;               # 1 1100 string "success"
83
-    private const JSON_DECODE_ACCEPT_SUCCESS_OBJECT = 29;               # 1 1101 object with single "success" property and value true
84
-
85
-    /**
86
-     * @const string ASCII upper-case characters part of a hexit
87
-     */
88
-    private const HEX_ALPHA_UPPER = 'ABCDEF';
89
-
90
-    /**
91
-     * @const string ASCII lower-case characters part of a hexit
92
-     */
93
-    private const HEX_ALPHA_LOWER = 'abcdef';
94
-
95
-    /**
96
-     * @var string Server base URL
97
-     */
98
-    private string $baseurl;
99
-
100
-    /**
101
-     * @var string Username
102
-     */
103
-    private string $user;
104
-
105
-    /**
106
-     * @var string Password
107
-     */
108
-    private string $pass;
109
-
110
-    private ?CurlHandle $handle = null;
111
-
112
-    /**
113
-     * @var string
114
-     */
115
-    private string $token = '';
116
-
117
-    /**
118
-     * constructor
119
-     *
120
-     * @param string $baseurl
121
-     * @param string $user
122
-     * @param string $pass
123
-     * @param string|null $otp
124
-     */
125
-    public function __construct(string $baseurl, string $user, string $pass, string $otp = null)
126
-    {
127
-        if (!(function_exists('curl_version'))) {
128
-            throw new ConnectionException('PHP-CURL not installed', self::ERROR_CODE_NO_CURL);
129
-        }
130
-
131
-        $this->baseurl = $baseurl;
132
-        $this->user = $user;
133
-        $this->pass = $pass;
134
-        // trigger_error(sprintf("ctor: %s:%s", $user, $pass), E_USER_NOTICE);
135
-
136
-        $this->setTokenByUsernameAndPassword($otp);
137
-    }
138
-
139
-    /**
140
-     * ping (with authentication)
141
-     *
142
-     * @link https://download.seafile.com/published/web-api/home.md#user-content-Quick%20Start
143
-     *
144
-     * @return string "pong"
145
-     */
146
-    public function ping(): string
147
-    {
148
-        return $this->jsonDecode(
149
-            $this->get(
150
-                "$this->baseurl/api2/ping/",
151
-                [CURLOPT_HTTPHEADER => ["Authorization: Token $this->token"]],
152
-            ),
153
-            self::JSON_DECODE_ACCEPT_STRING,
154
-        );
155
-    }
156
-
157
-    /**
158
-     * get server version
159
-     *
160
-     * @throws Exception
161
-     * @return string
162
-     */
163
-    public function getServerVersion(): string
164
-    {
165
-        $serverInfo = $this->getServerInformation();
166
-        if (
167
-            !is_string($serverInfo->version ?? null)
168
-            || !is_array($serverInfo->features ?? null)
169
-        ) {
170
-            throw new InvalidResponseException('We could not retrieve list of server features.', self::ERROR_CODE_FEATURES);
171
-        }
172
-
173
-        $isSeafilePro = in_array('seafile-pro', $serverInfo->features, true);
174
-        $edition = $isSeafilePro ? 'Professional' : 'Community';
175
-
176
-        return "$serverInfo->version ($edition)";
177
-    }
178
-
179
-    /**
180
-     * get server information
181
-     *
182
-     * @link https://download.seafile.com/published/web-api/v2.1/server-info.md#user-content-Get%20Server%20Information
183
-     *
184
-     * @throws Exception
185
-     * @return object
186
-     */
187
-    public function getServerInformation(): object
188
-    {
189
-        return $this->jsonDecode(
190
-            $this->get(
191
-                "$this->baseurl/api2/server-info/",
192
-                [CURLOPT_HTTPHEADER => ["Authorization: Token $this->token"]],
193
-            ),
194
-            self::JSON_DECODE_ACCEPT_OBJECT,
195
-        );
196
-    }
197
-
198
-    /**
199
-     * check account info
200
-     *
201
-     * @link https://download.seafile.com/published/web-api/v2.1/account.md#user-content-Check%20Account%20Info
202
-     * @return object
203
-     */
204
-    public function checkAccountInfo(): object
205
-    {
206
-        return $this->jsonDecode(
207
-            $this->get(
208
-                "$this->baseurl/api2/account/info/",
209
-                [CURLOPT_HTTPHEADER => ["Authorization: Token $this->token"]]
210
-            ),
211
-            self::JSON_DECODE_ACCEPT_OBJECT,
212
-        );
213
-    }
214
-
215
-    /**
216
-     * list seafile server libraries
217
-     *
218
-     * that are all libraries a user can access.
219
-     *
220
-     * @link https://download.seafile.com/published/web-api/v2.1/libraries.md#user-content-List%20Libraries
221
-     *
222
-     * @throws Exception
223
-     * @return object[]
224
-     */
225
-    public function listLibraries(): array
226
-    {
227
-        return $this->listLibrariesCached(true);
228
-    }
229
-
230
-    /**
231
-     * get default library
232
-     *
233
-     * @link https://download.seafile.com/published/web-api/v2.1/libraries.md#user-content-Get%20Default%20Library
234
-     *
235
-     * @return object{exists: bool, repo_id: string}
236
-     */
237
-    public function getDefaultLibrary(): object
238
-    {
239
-        return $this->jsonDecode(
240
-            $this->get(
241
-                "$this->baseurl/api2/default-repo/",
242
-                [CURLOPT_HTTPHEADER => ["Authorization: Token $this->token"]],
243
-            ),
244
-            self::JSON_DECODE_ACCEPT_OBJECT,
245
-        );
246
-    }
247
-
248
-    /**
249
-     * create a library
250
-     *
251
-     * @link https://download.seafile.com/published/web-api/v2.1/libraries.md#user-content-Create%20Library
252
-     *
253
-     * @param string $name of library
254
-     * @return object
255
-     */
256
-    public function createLibrary(string $name): object
257
-    {
258
-        $name = trim($name, "/");
259
-
260
-        return $this->jsonDecode(
261
-            $this->post(
262
-                "$this->baseurl/api2/repos/",
263
-                ['name' => $name],
264
-                [CURLOPT_HTTPHEADER => ["Authorization: Token $this->token"]],
265
-            ),
266
-            self::JSON_DECODE_ACCEPT_OBJECT,
267
-        );
268
-    }
269
-
270
-    /**
271
-     * name a library
272
-     *
273
-     * @link https://download.seafile.com/published/web-api/v2.1/libraries.md#user-content-Rename%20Library
274
-     */
275
-    public function nameLibrary(string $lib, string $name): void
276
-    {
277
-        $lib = $this->verifyLib($lib);
278
-        $fields = ['repo_name' => $name];
279
-        $_ = $this->jsonDecode(
280
-            $this->post(
281
-                "$this->baseurl/api2/repos/$lib/?op=rename",
282
-                $fields,
283
-                [CURLOPT_HTTPHEADER => ["Authorization: Token $this->token"]],
284
-            ),
285
-            self::JSON_DECODE_ACCEPT_SUCCESS_STRING,
286
-        );
287
-        unset($_);
288
-    }
289
-
290
-    /**
291
-     * rename a library
292
-     *
293
-     * do nothing if the library can not be found
294
-     *
295
-     * @link https://download.seafile.com/published/web-api/v2.1/libraries.md#user-content-Rename%20Library
296
-     *
297
-     * @param string $oldName
298
-     * @param string $newName
299
-     * @return void
300
-     */
301
-    public function renameLibrary(string $oldName, string $newName): void
302
-    {
303
-        $lib = $this->getLibraryIdByLibraryName($oldName);
304
-        if (null === $lib) {
305
-            return; // no library to rename
306
-        }
307
-
308
-        $this->nameLibrary($lib, $newName);
309
-    }
310
-
311
-    /**
312
-     * delete a library by name
313
-     *
314
-     * @link https://download.seafile.com/published/web-api/v2.1/libraries.md#user-content-Delete%20Library
315
-     *
316
-     * @param string $name
317
-     * @return void
318
-     */
319
-    public function deleteLibraryByName(string $name): void
320
-    {
321
-        $id = $this->getLibraryIdByLibraryName($name);
322
-        if (null === $id) {
323
-            return; // library already gone
324
-        }
325
-
326
-        $this->deleteLibraryById($id);
327
-    }
328
-
329
-    /**
330
-     * delete a library by id
331
-     *
332
-     * @link https://download.seafile.com/published/web-api/v2.1/libraries.md#user-content-Delete%20Library
333
-     *
334
-     * @param string $id
335
-     * @return void
336
-     */
337
-    public function deleteLibraryById(string $id): void
338
-    {
339
-        $lib = $this->verifyLib($id);
340
-
341
-        $_ = $this->jsonDecode(
342
-            $this->delete(
343
-                "$this->baseurl/api2/repos/$lib/",
344
-                [CURLOPT_HTTPHEADER => ["Authorization: Token $this->token"]],
345
-            ),
346
-            self::JSON_DECODE_ACCEPT_SUCCESS_STRING,
347
-        );
348
-        unset($_);
349
-    }
350
-
351
-    /**
352
-     * get library info array from path
353
-     *
354
-     * @param string $libNamedPath with the library name as first component
355
-     * @throws Exception
356
-     * @throws InvalidResponseException
357
-     * @return object with 'id' and 'name' of library, both NULL if not found
358
-     */
359
-    public function getLibraryFromPath(string $libNamedPath): object
360
-    {
361
-        $libraries = $this->listLibrariesCached();
362
-        $libraries = array_column($libraries, null, 'name');
363
-
364
-        $name = explode('/', ltrim($this->normalizePath($libNamedPath), '/'), 2)[0];
365
-
366
-        return (object)[
367
-            'id' => $libraries[$name]->id ?? null,
368
-            'name' => $libraries[$name]->name ?? null,
369
-        ];
370
-    }
371
-
372
-    /**
373
-     * list all share links
374
-     *
375
-     * all folder/file download share links in all libraries created by user.
376
-     *
377
-     * @link https://download.seafile.com/published/web-api/v2.1/share-links.md#user-content-List%20all%20Share%20Links
378
-     *
379
-     * @return object[]
380
-     * @throws Exception
381
-     * @throws InvalidResponseException
382
-     * @noinspection PhpUnused
383
-     */
384
-    public function listAllShareLinks(): array
385
-    {
386
-        return $this->jsonDecode(
387
-            $this->get(
388
-                "$this->baseurl/api/v2.1/share-links/",
389
-                [CURLOPT_HTTPHEADER => ["Authorization: Token $this->token"]],
390
-            ),
391
-            self::JSON_DECODE_ACCEPT_ARRAY_OF_OBJECTS,
392
-        );
393
-    }
394
-
395
-    /**
396
-     * Create Share Link
397
-     *
398
-     * @link https://download.seafile.com/published/web-api/v2.1/share-links.md#user-content-Create%20Share%20Link
399
-     *
400
-     * @param string $lib
401
-     * @param string $path
402
-     * @param ?string $password [optional]
403
-     * @param ?int|DateTimeInterface $expire [optional] number of days to expire (int) or DateTime to expire
404
-     * @param ?array $permissions [optional] see seafile api docs
405
-     * @return object
406
-     * @throws Exception
407
-     * @throws InvalidArgumentException
408
-     * @throws InvalidResponseException
409
-     */
410
-    public function createShareLink(string $lib, string $path, string $password = null, $expire = null, array $permissions = null): object
411
-    {
412
-        $lib = $this->verifyLib($lib);
413
-        $path = $this->normalizePath($path);
414
-
415
-        $fields = [
416
-            'repo_id' => $lib,
417
-            'path' => $path,
418
-        ];
419
-        if (null !== $password) {
420
-            $fields['password'] = $password;
421
-        }
422
-        if (null !== $expire) {
423
-            $expireTime = $expire;
424
-            if (is_int($expire)) {
425
-                $expireDays = max(1, min(365, (int)$expire));
426
-                $expireTime = (new DateTimeImmutable())->add(
427
-                    new \DateInterval("P{$expireDays}D")
428
-                );
429
-            }
430
-            if (!$expireTime instanceof DateTimeInterface) {
431
-                throw new InvalidArgumentException('Expire type mismatch: ' . gettype($expireTime));
432
-            }
433
-            $fields['expiration_time'] = $expireTime->format(\DATE_ATOM);
434
-        }
435
-        if (null !== $permissions) {
436
-            try {
437
-                $fields['permissions'] = json_encode($permissions, JSON_THROW_ON_ERROR);
438
-            } /** @noinspection PhpMultipleClassDeclarationsInspection */ catch (JsonException $ex) {
439
-                throw new InvalidArgumentException('permissions');
440
-            }
441
-        }
442
-
443
-        return $this->jsonDecode(
444
-            $this->post(
445
-                "$this->baseurl/api/v2.1/share-links/",
446
-                $fields,
447
-                [CURLOPT_HTTPHEADER => ["Authorization: Token $this->token"]],
448
-            ),
449
-            self::JSON_DECODE_ACCEPT_OBJECT,
450
-        );
451
-    }
452
-
453
-    /**
454
-     * List Share Links of a Library
455
-     *
456
-     * @link https://download.seafile.com/published/web-api/v2.1/share-links.md#user-content-List%20all%20Share%20Links
457
-     *
458
-     * @param ?string $lib [optional] library id (guid), default/null for all libraries
459
-     * @throws Exception
460
-     * @return object[]
461
-     */
462
-    public function listShareLinksOfALibrary(string $lib = null): array
463
-    {
464
-        $lib = $this->verifyLib($lib ?? '', true);
465
-
466
-        return $this->jsonDecode(
467
-            $this->get(
468
-                "$this->baseurl/api/v2.1/share-links/?repo_id=$lib",
469
-                [CURLOPT_HTTPHEADER => ["Authorization: Token $this->token"]],
470
-            ),
471
-            self::JSON_DECODE_ACCEPT_ARRAY_OF_OBJECTS,
472
-        );
473
-    }
474
-
475
-    /**
476
-     * check password (of a share link by token)
477
-     *
478
-     * @link https://download.seafile.com/published/web-api/v2.1-admin/share-links.md#user-content-Check%20Password
479
-     *
480
-     * @param string $token of share link
481
-     * @param string $password in plain
482
-     * @return object
483
-     * @throws Exception
484
-     * @throws InvalidResponseException
485
-     * @noinspection PhpUnused
486
-     */
487
-    public function checkShareLinkPassword(string $token, string $password): object
488
-    {
489
-        $tokenEncoded = rawurlencode($token);
490
-
491
-        return $this->jsonDecode(
492
-            $this->post(
493
-                "$this->baseurl/api/v2.1/admin/share-links/$tokenEncoded/check-password/",
494
-                ['password' => $password],
495
-                [CURLOPT_HTTPHEADER => ["Authorization: Token $this->token"]],
496
-            ),
497
-            self::JSON_DECODE_ACCEPT_SUCCESS_OBJECT,
498
-        );
499
-    }
500
-
501
-    /**
502
-     * share Link of a folder (or file)
503
-     *
504
-     * @link https://download.seafile.com/published/web-api/v2.1/share-links.md#user-content-List%20Share%20Link%20of%20a%20Folder%20(File)
505
-     *
506
-     * @param string $lib
507
-     * @param string $path
508
-     * @return object the share link
509
-     * @throws Exception
510
-     * @throws InvalidArgumentException
511
-     * @throws InvalidResponseException
512
-     * @noinspection PhpUnused
513
-     */
514
-    public function listShareLinksOfAFolder(string $lib, string $path): ?object
515
-    {
516
-        $lib = $this->verifyLib($lib);
517
-        $path = $this->normalizePath($path);
518
-        $pathEncoded = rawurlencode($path);
519
-
520
-        return $this->jsonDecode(
521
-            $this->get(
522
-                "$this->baseurl/api/v2.1/share-links/?repo_id=$lib&path=$pathEncoded",
523
-                [CURLOPT_HTTPHEADER => ["Authorization: Token $this->token"]],
524
-            ),
525
-            self::JSON_DECODE_ACCEPT_ARRAY_SINGLE_OBJECT_NULLABLE,
526
-        );
527
-    }
528
-
529
-    /**
530
-     * Delete Share Link
531
-     *
532
-     * @link https://download.seafile.com/published/web-api/v2.1/share-links.md#user-content-Delete%20Share%20Link
533
-     *
534
-     * @param string $token
535
-     * @throws Exception
536
-     * @return object success {"success": true}
537
-     */
538
-    public function deleteShareLink(string $token): object
539
-    {
540
-        $token = $this->verifyToken($token);
541
-
542
-        return $this->jsonDecode(
543
-            $this->delete(
544
-                "$this->baseurl/api/v2.1/share-links/$token/",
545
-                [CURLOPT_HTTPHEADER => ["Authorization: Token $this->token"]],
546
-            ),
547
-            self::JSON_DECODE_ACCEPT_SUCCESS_OBJECT,
548
-        );
549
-    }
550
-
551
-    /**
552
-     * search user
553
-     *
554
-     * @link https://download.seafile.com/published/web-api/v2.1/user-search.md#user-content-Search%20User
555
-     *
556
-     * @param string $search
557
-     * @throws Exception
558
-     * @return object
559
-     */
560
-    public function searchUser(string $search): object
561
-    {
562
-        $searchEncoded = rawurlencode($search);
563
-
564
-        return $this->jsonDecode(
565
-            $this->get(
566
-                "$this->baseurl/api2/search-user/?q=$searchEncoded",
567
-                [CURLOPT_HTTPHEADER => ['Authorization: Token ' . $this->token]],
568
-            ),
569
-            self::JSON_DECODE_ACCEPT_OBJECT,
570
-        );
571
-    }
572
-
573
-    /**
574
-     * list groups for user sharing
575
-     *
576
-     * @throws Exception
577
-     * @throws InvalidResponseException
578
-     * @return array|object|string
579
-     * @link (undocumented)
580
-     *
581
-     */
582
-    public function shareableGroups(): array
583
-    {
584
-        return $this->jsonDecode(
585
-            $this->get(
586
-                "$this->baseurl/api/v2.1/shareable-groups/",
587
-                [CURLOPT_HTTPHEADER => ["Authorization: Token $this->token"]],
588
-            ),
589
-            self::JSON_DECODE_ACCEPT_ARRAY_OF_OBJECTS,
590
-        );
591
-    }
592
-
593
-    /**
594
-     * Share a Library to Group
595
-     *
596
-     * @link https://download.seafile.com/published/web-api/v2.1/share.md#user-content-Share%20a%20Library%20to%20Group
597
-     *
598
-     * @param string $lib
599
-     * @param string $path
600
-     * @param int|int[] $group
601
-     * @param string|null $permission [optional] r, rw, admin (default: r)
602
-     * @throws Exception
603
-     * @throws InvalidArgumentException
604
-     * @return array
605
-     */
606
-    public function shareLibraryPathToGroup(string $lib, string $path, $group, string $permission = null)
607
-    {
608
-        $lib = $this->verifyLib($lib);
609
-        $path = $this->normalizePath($path);
610
-        $pathEncoded = rawurlencode($path);
611
-
612
-        $fields = [
613
-            'share_type' => 'group',
614
-            'group_id' => $group,
615
-            'permission' => $permission ?? 'r',
616
-        ];
617
-        return $this->jsonDecode(
618
-            $this->put(
619
-                "$this->baseurl/api2/repos/$lib/dir/shared_items/?p=$pathEncoded",
620
-                $fields,
621
-                [CURLOPT_HTTPHEADER => ["Authorization: Token $this->token"]],
622
-            ),
623
-            self::JSON_DECODE_ACCEPT_OBJECT,
624
-        );
625
-    }
626
-
627
-    /**
628
-     * Share a Library to User
629
-     *
630
-     * @link https://download.seafile.com/published/web-api/v2.1/share.md#user-content-Share%20a%20Library%20to%20User
631
-     *
632
-     * @param string $lib
633
-     * @param string $path
634
-     * @param string $user
635
-     * @param string|null $permission [optional] r, rw, admin (default: r)
636
-     * @throws Exception
637
-     * @throws InvalidArgumentException
638
-     * @return array
639
-     */
640
-    public function shareLibraryPathToUser(string $lib, string $path, string $user, string $permission = null)
641
-    {
642
-        $lib = $this->verifyLib($lib);
643
-        $path = $this->normalizePath($path);
644
-        $pathEncoded = rawurlencode($path);
645
-
646
-        $fields = [
647
-            'share_type' => 'user',
648
-            'username' => $user,
649
-            'permission' => $permission ?? 'r',
650
-        ];
651
-        return $this->jsonDecode(
652
-            $this->put(
653
-                "$this->baseurl/api2/repos/$lib/dir/shared_items/?p=$pathEncoded",
654
-                $fields,
655
-                [CURLOPT_HTTPHEADER => ["Authorization: Token $this->token"]],
656
-            ),
657
-            // either array of objects -or- failure object
658
-            self::JSON_DECODE_ACCEPT_ARRAY | self::JSON_DECODE_ACCEPT_OBJECT,
659
-        );
660
-    }
661
-
662
-    /**
663
-     * Unshare a Library to Group
664
-     *
665
-     * @link https://download.seafile.com/published/web-api/v2.1/share.md#user-content-Unshare%20a%20Library%20from%20Group
666
-     *
667
-     * @param string $lib
668
-     * @param string $path
669
-     * @param int $group
670
-     * @throws Exception
671
-     * @throws InvalidArgumentException
672
-     * @return object
673
-     */
674
-    public function unshareLibraryPathToGroup(string $lib, string $path, int $group): object
675
-    {
676
-        $lib = $this->verifyLib($lib);
677
-        $path = $this->normalizePath($path);
678
-        $pathEncoded = rawurlencode($path);
679
-
680
-        return $this->jsonDecode(
681
-            $this->delete(
682
-                "$this->baseurl/api2/repos/$lib/dir/shared_items/?p=$pathEncoded&share_type=group&group_id=$group",
683
-                [CURLOPT_HTTPHEADER => ["Authorization: Token $this->token"]],
684
-            ),
685
-            self::JSON_DECODE_ACCEPT_SUCCESS_OBJECT,
686
-        );
687
-    }
688
-
689
-    /**
690
-     * Unshare a Library to User
691
-     *
692
-     * @link https://download.seafile.com/published/web-api/v2.1/share.md#user-content-Unshare%20a%20Library%20from%20User
693
-     *
694
-     * @param string $lib
695
-     * @param string $path
696
-     * @param string|string[] $user
697
-     * @throws Exception
698
-     * @throws InvalidArgumentException
699
-     * @return object
700
-     */
701
-    public function unshareLibraryPathToUser(string $lib, string $path, $user): object
702
-    {
703
-        $lib = $this->verifyLib($lib);
704
-        $path = $this->normalizePath($path);
705
-        $pathEncoded = rawurlencode($path);
706
-        $userEncoded = rawurlencode($user);
707
-
708
-        return $this->jsonDecode(
709
-            $this->delete(
710
-                "$this->baseurl/api2/repos/$lib/dir/shared_items/?p=$pathEncoded&share_type=user&username=$userEncoded",
711
-                [CURLOPT_HTTPHEADER => ["Authorization: Token $this->token"]],
712
-            ),
713
-            self::JSON_DECODE_ACCEPT_SUCCESS_OBJECT,
714
-        );
715
-    }
716
-
717
-    /**
718
-     * list user shares for a library path
719
-     *
720
-     * @link https://download.seafile.com/published/web-api/v2.1/share.md#user-content-List%20Shared%20Users%20of%20a%20Library
721
-     *
722
-     * @param string $lib
723
-     * @param string $path
724
-     * @throws Exception
725
-     * @throws InvalidArgumentException
726
-     * @throws InvalidResponseException
727
-     * @return array<int, object>
728
-     */
729
-    public function listSharesOfLibraryPath(string $lib, string $path): array
730
-    {
731
-        $lib = $this->verifyLib($lib);
732
-        $path = $this->normalizePath($path);
733
-        $pathEncoded = rawurlencode($path);
734
-
735
-        return $this->jsonDecode(
736
-            $this->get(
737
-                "$this->baseurl/api2/repos/$lib/dir/shared_items/?p=$pathEncoded",
738
-                [CURLOPT_HTTPHEADER => ["Authorization: Token $this->token"]],
739
-            ),
740
-            self::JSON_DECODE_ACCEPT_ARRAY_OF_OBJECTS,
741
-        );
742
-    }
743
-
744
-    /**
745
-     * create a new directory
746
-     *
747
-     * @link https://download.seafile.com/published/web-api/v2.1/directories.md#user-content-Create%20New%20Directory
748
-     *
749
-     * @param string $lib library id (guid)
750
-     * @param string $path of the directory to create (e.g.: "/path/to/new-directory", leading and trailing slashes can be omitted)
751
-     * @throws InvalidArgumentException|Exception
752
-     * @return string|object the common "success" or the object with error_msg property
753
-     */
754
-    public function createNewDirectory(string $lib, string $path)
755
-    {
756
-        $lib = $this->verifyLib($lib);
757
-        $path = $this->normalizePath($path);
758
-        $pathEncoded = rawurlencode($path);
759
-
760
-        return $this->jsonDecode(
761
-            $this->post(
762
-                "$this->baseurl/api2/repos/$lib/dir/?p=$pathEncoded",
763
-                ['operation' => 'mkdir'],
764
-                [CURLOPT_HTTPHEADER => ["Authorization: Token $this->token"]],
765
-            ),
766
-            self::JSON_DECODE_ACCEPT_STRING | self::JSON_DECODE_ACCEPT_OBJECT,
767
-        );
768
-    }
769
-
770
-    /**
771
-     * delete a file
772
-     *
773
-     * @link https://download.seafile.com/published/web-api/v2.1/file.md#user-content-Delete%20File
774
-     *
775
-     * @param string $lib library id (guid)
776
-     * @param string $path of the fle to delete (e.g.: "/path/to/file-to-delete", leading and trailing slashes can be omitted)
777
-     * @throws InvalidArgumentException|Exception
778
-     * @return string|object the common "success" or the known object with error_msg property
779
-     */
780
-    public function deleteFile(string $lib, string $path)
781
-    {
782
-        $lib = $this->verifyLib($lib);
783
-        $path = $this->normalizePath($path);
784
-        $pathEncoded = rawurlencode($path);
785
-
786
-        return $this->jsonDecode(
787
-            $this->delete(
788
-                "$this->baseurl/api2/repos/$lib/file/?p=$pathEncoded",
789
-                [CURLOPT_HTTPHEADER => ["Authorization: Token $this->token"]],
790
-            ),
791
-            self::JSON_DECODE_ACCEPT_STRING | self::JSON_DECODE_ACCEPT_OBJECT,
792
-        );
793
-    }
794
-
795
-    /**
796
-     * get a file download URL
797
-     *
798
-     * @link https://download.seafile.com/published/web-api/v2.1/file.md#user-content-Download%20File
799
-     *
800
-     * @param string $lib
801
-     * @param string $path
802
-     * @return string download URL (http/s)
803
-     * @throws InvalidArgumentException|Exception
804
-     */
805
-    public function downloadFile(string $lib, string $path): string
806
-    {
807
-        $lib = $this->verifyLib($lib);
808
-        $path = $this->normalizePath($path);
809
-        $pathEncoded = rawurlencode($path);
810
-
811
-        return $this->jsonDecode(
812
-            $this->get(
813
-                "$this->baseurl/api2/repos/$lib/file/?p=$pathEncoded",
814
-                [CURLOPT_HTTPHEADER => ["Authorization: Token $this->token"]],
815
-            ),
816
-            self::JSON_DECODE_ACCEPT_STRING,
817
-        );
818
-    }
819
-
820
-    /**
821
-     * download a file
822
-     *
823
-     * get file contents of a file in a library
824
-     *
825
-     * @param string $lib
826
-     * @param string $path
827
-     * @throws InvalidArgumentException|Exception
828
-     * @return string|false on failure
829
-     */
830
-    public function downloadFileAsBuffer(string $lib, string $path)
831
-    {
832
-        $url = $this->downloadFile($lib, $path);
833
-
834
-        return $this->get($url);
835
-    }
836
-
837
-    /**
838
-     * download a file to a local file
839
-     *
840
-     * @param string $lib
841
-     * @param string $path
842
-     * @param string $localPath path to a file - existing or not - on the local file-system
843
-     * @throws InvalidArgumentException|Exception
844
-     * @return bool success/failure
845
-     */
846
-    public function downloadFileToFile(string $lib, string $path, string $localPath): bool
847
-    {
848
-        $handle = fopen($localPath, 'wb');
849
-        if (false === $handle) {
850
-            throw new Exception('failed to open local path for writing', self::ERROR_CODE_FILE_IO);
851
-        }
852
-
853
-        try {
854
-            $result = $this->downloadFileToStream($lib, $path, $handle);
855
-        } finally {
856
-            $close = fclose($handle);
857
-        }
858
-
859
-        if (!$close) {
860
-            throw new Exception('failed to close local path handle', self::ERROR_CODE_FILE_IO);
861
-        }
862
-
863
-        return $result;
864
-    }
865
-
866
-    /**
867
-     * download a file to a stream handle
868
-     *
869
-     * @param string $lib
870
-     * @param string $path
871
-     * @param resource $handle stream handle
872
-     * @throws InvalidArgumentException|Exception
873
-     * @return bool success/failure
874
-     */
875
-    public function downloadFileToStream(string $lib, string $path, $handle): bool
876
-    {
877
-        $url = $this->downloadFile($lib, $path);
878
-
879
-        return $this->get($url, [CURLOPT_RETURNTRANSFER => true, CURLOPT_FILE => $handle]);
880
-    }
881
-
882
-    /**
883
-     * list items in directory
884
-     *
885
-     * @link https://download.seafile.com/published/web-api/v2.1/directories.md#user-content-List%20Items%20in%20Directory
886
-     *
887
-     * @param string $lib
888
-     * @param string $path
889
-     * @return array
890
-     * @throws Exception
891
-     * @throws InvalidArgumentException
892
-     * @throws InvalidResponseException
893
-     */
894
-    public function listItemsInDirectory(string $lib, string $path): array
895
-    {
896
-        $lib = $this->verifyLib($lib);
897
-        $path = $this->normalizePath($path);
898
-        $pathEncoded = rawurlencode($path);
899
-
900
-        $result = $this->jsonDecode(
901
-            $this->get(
902
-                "$this->baseurl/api2/repos/$lib/dir/?p=$pathEncoded",
903
-                [CURLOPT_HTTPHEADER => ['Authorization: Token ' . $this->token]],
904
-            ),
905
-        );
906
-
907
-        if (is_object($result)) {
908
-            // likely a folder not found.
909
-            $result = [];
910
-        }
911
-
912
-        return $result;
913
-    }
914
-
915
-    /**
916
-     * move a file
917
-     *
918
-     * @link https://download.seafile.com/published/web-api/v2.1/file.md#user-content-Move%20File
919
-     *
920
-     * @param string $lib
921
-     * @param string $path
922
-     * @param string $dstLib
923
-     * @param string $dstDir
924
-     * @return object
925
-     * @throws InvalidArgumentException|Exception
926
-     */
927
-    public function moveFile(string $lib, string $path, string $dstLib, string $dstDir): object
928
-    {
929
-        $lib = $this->verifyLib($lib);
930
-        $path = $this->normalizePath($path);
931
-        $pathEncoded = rawurlencode($path);
932
-        $dstLib = $this->verifyLib($dstLib);
933
-        $dstDir = $this->normalizePath($dstDir);
934
-
935
-        return $this->jsonDecode(
936
-            $this->post(
937
-                "$this->baseurl/api2/repos/$lib/file/?p=$pathEncoded",
938
-                ['operation' => 'move', 'dst_repo' => $dstLib, 'dst_dir' => $dstDir],
939
-                [CURLOPT_HTTPHEADER => ["Authorization: Token $this->token"]],
940
-            ),
941
-            self::JSON_DECODE_ACCEPT_OBJECT,
942
-        );
943
-    }
944
-
945
-    /**
946
-     * rename a file
947
-     *
948
-     * @link https://download.seafile.com/published/web-api/v2.1/file.md#user-content-Move%20File
949
-     *
950
-     * @param string $lib library id (guid)
951
-     * @param string $path of the file to rename (e.g. "/path/to/file-to-rename")
952
-     * @param string $newName new basename for the basename of $path (e.g. "new-file-name")
953
-     * @return string|object the common "success" or the known object with error_msg property
954
-     * @throws Exception
955
-     */
956
-    public function renameFile(string $lib, string $path, string $newName)
957
-    {
958
-        $lib = $this->verifyLib($lib);
959
-        $path = $this->normalizePath($path);
960
-        $pathEncoded = rawurlencode($path);
961
-
962
-        return $this->jsonDecode(
963
-            $this->post(
964
-                "$this->baseurl/api2/repos/$lib/file/?p=$pathEncoded",
965
-                ['operation' => 'rename', 'newname' => $newName],
966
-                [CURLOPT_HTTPHEADER => ["Authorization: Token $this->token"]],
967
-            ),
968
-            self::JSON_DECODE_ACCEPT_STRING | self::JSON_DECODE_ACCEPT_OBJECT,
969
-        );
970
-    }
971
-
972
-    /**
973
-     * simplified file upload routine for string buffer
974
-     *
975
-     * @param string $lib
976
-     * @param string $path
977
-     * @param string $buffer
978
-     * @throws InvalidArgumentException
979
-     * @throws Exception
980
-     * @return object
981
-     */
982
-    public function uploadBuffer(string $lib, string $path, string $buffer): object
983
-    {
984
-        $lib = $this->verifyLib($lib);
985
-        $path = $this->normalizePath($path);
986
-
987
-        $parentDir = dirname($path);
988
-        $uploadLink = $this->uploadGetLink($lib, $parentDir);
989
-        $fileName = basename($path);
990
-
991
-        return $this->uploadFileBuffer($uploadLink, $parentDir, '', $buffer, $fileName);
992
-    }
993
-
994
-    /**
995
-     * simplified file upload routine for standard file
996
-     *
997
-     * @param string $lib
998
-     * @param string $path path in seafile to upload the file as
999
-     * @param string $file path of file to upload
1000
-     * @throws InvalidArgumentException
1001
-     * @throws Exception
1002
-     * @return object
1003
-     */
1004
-    public function uploadFile(string $lib, string $path, string $file): object
1005
-    {
1006
-        $lib = $this->verifyLib($lib);
1007
-        $path = $this->normalizePath($path);
1008
-        if (!is_file($file) && !is_readable($file)) {
1009
-            throw new InvalidArgumentException(sprintf('Not a readable file: %s', $file));
1010
-        }
1011
-
1012
-        $parentDir = dirname($path);
1013
-        $uploadLink = $this->uploadGetLink($lib, $parentDir);
1014
-        $fileName = basename($path);
1015
-
1016
-        return $this->uploadFilePath($uploadLink, $parentDir, '', $file, $fileName);
1017
-    }
1018
-
1019
-    /**
1020
-     * upload string buffer as a file
1021
-     *
1022
-     * same as {@see SeafileApi::uploadFile()} with the option to upload without a
1023
-     * concrete file on the system. the temporary file to upload is created
1024
-     * from the string buffer.
1025
-     *
1026
-     * @param string $uploadLink from {@see uploadGetLink}
1027
-     * @param string $parentDir the parent directory to upload the file to
1028
-     * @param string $relativePath the name of the file, subdirectories possible (e.g. uploading a folder)
1029
-     * @param string $buffer file contents to upload as string (not the file-name)
1030
-     * @param string $fileName to use as basename for the data
1031
-     * @param bool $replace
1032
-     * @throws Exception
1033
-     * @return object
1034
-     */
1035
-    public function uploadFileBuffer(
1036
-        string $uploadLink, string $parentDir, string $relativePath,
1037
-        string $buffer, string $fileName = 'upload.dat', bool $replace = false
1038
-    ): object
1039
-    {
1040
-        $tmpHandle = tmpfile();
1041
-        if (false === $tmpHandle) {
1042
-            throw new Exception('Upload data rejected: Unable to open temporary stream.');
1043
-        }
1044
-
1045
-        $meta = stream_get_meta_data($tmpHandle);
1046
-        $tmpFile = $meta['uri'];
1047
-        if (!is_file($tmpFile)) {
1048
-            fclose($tmpHandle);
1049
-            throw new Exception('Upload data rejected: No file with temporary stream.');
1050
-        }
1051
-
1052
-        $bytes = fwrite($tmpHandle, $buffer);
1053
-        if (false === $bytes) {
1054
-            fclose($tmpHandle);
1055
-            throw new Exception('Upload data rejected: Failed to write to temporary stream.');
1056
-        }
1057
-
1058
-        $diff = strlen($buffer) - $bytes;
1059
-        if ($diff !== 0) {
1060
-            fclose($tmpHandle);
1061
-            throw new Exception(sprintf("Upload data rejected: Unexpected difference writing to temporary stream: %d bytes", $diff));
1062
-        }
1063
-
1064
-        $result = rewind($tmpHandle);
1065
-        if (false === $result) {
1066
-            fclose($tmpHandle);
1067
-            throw new Exception('Upload data rejected: Failed to rewind temporary stream.');
1068
-        }
1069
-
1070
-        $result = $this->uploadFilePath($uploadLink, $parentDir, $relativePath, $tmpFile, $fileName, $replace);
1071
-        fclose($tmpHandle);
1072
-
1073
-        return $result;
1074
-    }
1075
-
1076
-    /**
1077
-     * upload file
1078
-     *
1079
-     * @link https://download.seafile.com/published/web-api/v2.1/file-upload.md#user-content-Upload%20File
1080
-     *
1081
-     * @param string $uploadLink from {@see uploadGetLink}
1082
-     * @param string $parentDir the parent directory to upload a file to
1083
-     * @param string $relativePath to place the file in under $uploadPath (can include subdirectories)
1084
-     * @param string $path path of the file to upload
1085
-     * @param ?string $fileName to use as basename for the file (the name used in Seafile)
1086
-     * @param bool $replace
1087
-     * @return object
1088
-     * @throws Exception
1089
-     */
1090
-    public function uploadFilePath(
1091
-        string $uploadLink, string $parentDir, string $relativePath,
1092
-        string $path, string $fileName = null, bool $replace = false
1093
-    ): object
1094
-    {
1095
-        $parentDir = $this->normalizePath($parentDir);
1096
-        $relativePath = ltrim('/', $this->normalizePath($relativePath));
1097
-        $fileName = $fileName ?? basename($path);
1098
-
1099
-        $fields = [
1100
-            'file' => new CURLFile($path, 'application/octet-stream', $fileName),
1101
-            'parent_dir' => $parentDir,
1102
-            'relative_path' => $relativePath,
1103
-            'replace' => $replace ? '1' : '0',
1104
-        ];
1105
-
1106
-        return $this->jsonDecode(
1107
-            $this->post(
1108
-                "$uploadLink?ret-json=1",
1109
-                $fields,
1110
-                [CURLOPT_HTTPHEADER => ["Authorization: Token $this->token"]],
1111
-            ),
1112
-            self::JSON_DECODE_ACCEPT_ARRAY_SINGLE_OBJECT,
1113
-        );
1114
-    }
1115
-
1116
-    /**
1117
-     * get file upload link
1118
-     *
1119
-     * @link https://download.seafile.com/published/web-api/v2.1/file-upload.md#user-content-Get%20Upload%20Link
1120
-     *
1121
-     * @param string $lib
1122
-     * @param string $uploadDir the directory to upload file(s) to
1123
-     * @return string upload link (https?://...)
1124
-     * @throws InvalidArgumentException|Exception
1125
-     */
1126
-    public function uploadGetLink(string $lib, string $uploadDir): string
1127
-    {
1128
-        $lib = $this->verifyLib($lib);
1129
-        $uploadDir = $this->normalizePath($uploadDir);
1130
-        $pathEncoded = rawurlencode($uploadDir);
1131
-
1132
-        return $this->jsonDecode(
1133
-            $this->get(
1134
-                "$this->baseurl/api2/repos/$lib/upload-link/?p=$pathEncoded",
1135
-                [CURLOPT_HTTPHEADER => ["Authorization: Token $this->token"]],
1136
-            ),
1137
-            self::JSON_DECODE_ACCEPT_STRING,
1138
-        );
1139
-    }
1140
-
1141
-    public function generateUserAuthToken(string $email): object
1142
-    {
1143
-        return $this->jsonDecode(
1144
-            $this->post(
1145
-                "$this->baseurl/api/v2.1/admin/generate-user-auth-token/",
1146
-                ['email' => $email],
1147
-                [CURLOPT_HTTPHEADER => ["Authorization: Token $this->token"]],
1148
-            ),
1149
-            self::JSON_DECODE_ACCEPT_OBJECT,
1150
-        );
1151
-    }
1152
-
1153
-    public function getUserActivity(string $email, int $page = 1, int $perPage = 25): object
1154
-    {
1155
-        return $this->jsonDecode(
1156
-            $this->get(
1157
-                "$this->baseurl/api/v2.1/admin/user-activities/?user=$email&page=$page&per_page=$perPage",
1158
-                [CURLOPT_HTTPHEADER => ["Authorization: Token $this->token"]],
1159
-            ),
1160
-            self::JSON_DECODE_ACCEPT_OBJECT,
1161
-        );
1162
-    }
1163
-
1164
-    /**
1165
-     * set authorization token
1166
-     *
1167
-     * @param string $token
1168
-     * @return void
1169
-     */
1170
-    public function setToken(string $token): void
1171
-    {
1172
-        $this->token = $token;
1173
-    }
1174
-
1175
-    public function listDevices(): array
1176
-    {
1177
-        return $this->jsonDecode(
1178
-            $this->get(
1179
-                "$this->baseurl/api2/devices/",
1180
-                [CURLOPT_HTTPHEADER => ["Authorization: Token $this->token"]],
1181
-            ),
1182
-        );
1183
-    }
1184
-
1185
-    public function listStarredItems(): object
1186
-    {
1187
-        return $this->jsonDecode(
1188
-            $this->get(
1189
-                "$this->baseurl/api/v2.1/starred-items/",
1190
-                [CURLOPT_HTTPHEADER => ["Authorization: Token $this->token"]],
1191
-            ),
1192
-        );
1193
-    }
1194
-
1195
-    public function listGroups(): object
1196
-    {
1197
-        return $this->jsonDecode(
1198
-            $this->get(
1199
-                "$this->baseurl/api2/groups/",
1200
-                [CURLOPT_HTTPHEADER => ["Authorization: Token $this->token"]],
1201
-            ),
1202
-        );
1203
-    }
1204
-
1205
-    public function listInvitations(): array
1206
-    {
1207
-        return $this->jsonDecode(
1208
-            $this->get(
1209
-                "$this->baseurl/api/v2.1/invitations/",
1210
-                [CURLOPT_HTTPHEADER => ["Authorization: Token $this->token"]],
1211
-            ),
1212
-        );
1213
-    }
1214
-
1215
-    /**
1216
-     * @param string $start e.g. date('Y-m-d', time() - 7776000 ); // 3 months
1217
-     * @param string $end e.g. date('Y-m-d', time());
1218
-     *
1219
-     * @return array
1220
-     */
1221
-    public function getLoginLog(string $start, string $end): array
1222
-    {
1223
-        $start = rawurlencode($start);
1224
-        $end = rawurlencode($end);
1225
-
1226
-        return $this->jsonDecode(
1227
-            $this->get(
1228
-                "$this->baseurl/api/v2.1/admin/logs/login/?start=$start&end=$end",
1229
-                [CURLOPT_HTTPHEADER => ["Authorization: Token $this->token"]],
1230
-            ),
1231
-        );
1232
-    }
1233
-
1234
-    public function listUploadLinks(): array
1235
-    {
1236
-        return $this->jsonDecode(
1237
-            $this->get(
1238
-                "$this->baseurl/api/v2.1/upload-links/",
1239
-                [CURLOPT_HTTPHEADER => ["Authorization: Token $this->token"]],
1240
-            ),
1241
-        );
1242
-    }
1243
-
1244
-    public function listRepoApiTokens(string $lib): object
1245
-    {
1246
-        $lib = $this->verifyLib($lib);
1247
-        return $this->jsonDecode(
1248
-            $this->get(
1249
-                "$this->baseurl/api/v2.1/repos/$lib/repo-api-tokens/",
1250
-                [CURLOPT_HTTPHEADER => ["Authorization: Token $this->token"]],
1251
-            ),
1252
-        );
1253
-    }
1254
-
1255
-    /**
1256
-     * internal api implementation to get library by name
1257
-     *
1258
-     * @param string $name
1259
-     * @throws Exception
1260
-     * @return ?string id (guid) of the library, null if library does not exist
1261
-     */
1262
-    private function getLibraryIdByLibraryName(string $name): ?string
1263
-    {
1264
-        $name = explode('/', $this->normalizePath($name), 2)[1];
1265
-
1266
-        $libraries = $this->listLibrariesCached();
1267
-        $libraries = array_column($libraries, null, 'name');
1268
-        return $libraries[$name]->id ?? null;
1269
-    }
1270
-
1271
-    /**
1272
-     * List libraries a user can access.
1273
-     *
1274
-     * internal api implementation for {@see listLibrariesCached()} and {@see listLibraries()}
1275
-     *
1276
-     * @link https://download.seafile.com/published/web-api/v2.1/libraries.md#user-content-List%20Libraries
1277
-     *
1278
-     * @throws Exception
1279
-     * @return object[]
1280
-     */
1281
-    private function listLibrariesDo(): array
1282
-    {
1283
-        return $this->jsonDecode(
1284
-            $this->get(
1285
-                "$this->baseurl/api2/repos/",
1286
-                [CURLOPT_HTTPHEADER => ["Authorization: Token $this->token"]],
1287
-            ),
1288
-            self::JSON_DECODE_ACCEPT_ARRAY_OF_OBJECTS,
1289
-        );
1290
-    }
1291
-
1292
-    /**
1293
-     * like {@see listLibraries()} but cached.
1294
-     *
1295
-     * @param bool $invalidate
1296
-     * @throws Exception
1297
-     * @throws InvalidResponseException
1298
-     * @return void
1299
-     */
1300
-    private function listLibrariesCached(bool $invalidate = false): array
1301
-    {
1302
-        static $librariesCache;
1303
-
1304
-        return $librariesCache = ($invalidate ? null : $librariesCache) ?? $this->listLibrariesDo();
1305
-    }
1306
-
1307
-    /**
1308
-     * normalize path
1309
-     *
1310
-     * normalizes the path component separator <slash> "/" <U002F> U+002F SOLIDUS
1311
-     * with first character being the slash, no consecutive slashes within the
1312
-     * path and no terminating slash.
1313
-     *
1314
-     * @param string $path
1315
-     * @return string
1316
-     */
1317
-    private function normalizePath(string $path): string
1318
-    {
1319
-        $buffer = rtrim($path, '/');
1320
-        $buffer = preg_replace('~/{2,}~', '/', $buffer);
1321
-        '' === $buffer && $buffer = '/';
1322
-        '/' === $buffer[0] || $buffer = "/$buffer";
1323
-
1324
-        return $buffer;
1325
-    }
1326
-
1327
-    /**
1328
-     * verify library id
1329
-     *
1330
-     * verifies the format of a library id. can be used in URLs
1331
-     * afterwards. case normalization to lowercase.
1332
-     *
1333
-     * example library id strings:
1334
-     *  - 21b941c2-5411-4372-a514-00b62ab99ef2 (from the docs)
1335
-     *  - 79144b25-f772-42b6-a1c0-60e6359f5884 (from a server)
1336
-     *
1337
-     * @param string $lib
1338
-     * @param bool $allowEmpty
1339
-     * @return string library id
1340
-     */
1341
-    private function verifyLib(string $lib, bool $allowEmpty = false): string
1342
-    {
1343
-        if ($allowEmpty && ('' === $lib)) {
1344
-            return $lib;
1345
-        }
1346
-
1347
-        $buffer = strtr($lib, self::HEX_ALPHA_UPPER, self::HEX_ALPHA_LOWER);
1348
-        $format = '%04x%04x-%04x-%04x-%04x-%04x%04x%04x';
1349
-        $values = sscanf($buffer, $format);
1350
-        $result = vsprintf($format, $values);
1351
-
1352
-        if ($buffer !== $result) {
1353
-            throw new InvalidArgumentException(sprintf('Not a library id: "%s"', $lib));
1354
-        }
1355
-
1356
-        return $result;
1357
-    }
1358
-
1359
-    /**
1360
-     * verify share link token
1361
-     *
1362
-     * verifies the format of a share link token. can be used in URLs
1363
-     * afterwards. case normalization to lowercase.
1364
-     *
1365
-     * @param string $token e.g. "0a29ff44dc0b4b56be74"
1366
-     * @return string
1367
-     */
1368
-    private function verifyToken(string $token): string
1369
-    {
1370
-        $buffer = strtr($token, self::HEX_ALPHA_UPPER, self::HEX_ALPHA_LOWER);
1371
-        $format = '%04x%04x%04x%04x%04x';
1372
-        $values = sscanf($buffer, $format);
1373
-        $result = vsprintf($format, $values);
1374
-
1375
-        if ($buffer !== $result) {
1376
-            throw new InvalidArgumentException(sprintf('Not a token: "%s"', $token));
1377
-        }
1378
-
1379
-        return $result;
1380
-    }
1381
-
1382
-    /**
1383
-     * authenticate class against seafile api
1384
-     *
1385
-     * @link https://download.seafile.com/published/web-api/home.md#user-content-Quick%20Start
1386
-     *
1387
-     * @param string|null $otp (optional) Seafile OTP (if user uses OTP access)
1388
-     * @return void
1389
-     */
1390
-    private function setTokenByUsernameAndPassword(string $otp = null): void
1391
-    {
1392
-        // @auth:token:<email> : password is auth token
1393
-        if (0 === strpos($this->user, $needle = self::USER_PREFIX_AUTH_TOKEN)) {
1394
-            $this->user = \substr($this->user, strlen($needle)) ?: '';
1395
-            $this->token = $this->pass;
1396
-            if ('pong' !== $this->ping()) {
1397
-                throw new ConnectionException('token authentication failure');
1398
-            }
1399
-            return;
1400
-        }
1401
-
1402
-        $data = $this->jsonDecode(
1403
-            $this->post(
1404
-                "$this->baseurl/api2/auth-token/",
1405
-                ['username' => $this->user, 'password' => $this->pass],
1406
-                $otp ? [CURLOPT_HTTPHEADER => ["X-SEAFILE-OTP: $otp"]] : [],
1407
-            ),
1408
-            self::JSON_DECODE_ACCEPT_OBJECT,
1409
-        );
1410
-        $this->token = (string)$data->token;
1411
-    }
1412
-
1413
-    /**
1414
-     * http request with get method
1415
-     *
1416
-     * @param string $url
1417
-     * @param array $curlOptions
1418
-     * @return bool|string
1419
-     */
1420
-    private function get(string $url, array $curlOptions = [])
1421
-    {
1422
-        $curlOptions += $this->curlSharedOptions;
1423
-
1424
-        return $this->curlExec($url, $curlOptions);
1425
-    }
1426
-
1427
-    /**
1428
-     * http request with post method
1429
-     *
1430
-     * @param string $url
1431
-     * @param array $fields
1432
-     * @param array $curlOptions
1433
-     * @return bool|string
1434
-     */
1435
-    private function post(string $url, array $fields = [], array $curlOptions = [])
1436
-    {
1437
-        $curlOptions += $this->curlSharedOptions;
1438
-        $curlOptions[CURLOPT_POST] = true;
1439
-        $curlOptions[CURLOPT_POSTFIELDS] = $fields;
1440
-
1441
-        return $this->curlExec($url, $curlOptions);
1442
-    }
1443
-
1444
-    /**
1445
-     * http request with put method
1446
-     *
1447
-     * @param string $url
1448
-     * @param array $fields
1449
-     * @param array $curlOptions
1450
-     * @return bool|string
1451
-     */
1452
-    public function put(string $url, array $fields = [], array $curlOptions = [])
1453
-    {
1454
-        $curlOptions += $this->curlSharedOptions;
1455
-        $curlOptions[CURLOPT_CUSTOMREQUEST] = 'PUT';
1456
-        $curlOptions[CURLOPT_POSTFIELDS] = $fields;
1457
-
1458
-        return $this->curlExec($url, $curlOptions);
1459
-    }
1460
-
1461
-    /**
1462
-     * http request with delete method
1463
-     *
1464
-     * @param string $url
1465
-     * @param array $curlOptions
1466
-     * @return bool|string
1467
-     */
1468
-    public function delete(string $url, array $curlOptions = [])
1469
-    {
1470
-
1471
-        $curlOptions += $this->curlSharedOptions;
1472
-        $curlOptions[CURLOPT_CUSTOMREQUEST] = 'DELETE';
1473
-
1474
-        return $this->curlExec($url, $curlOptions);
1475
-    }
1476
-
1477
-    /**
1478
-     * json decode handler
1479
-     *
1480
-     * decode json with structural acceptance
1481
-     *
1482
-     * @param string $jsonText
1483
-     * @param int $flags decode accept flag
1484
-     * @throws InvalidResponseException
1485
-     * @return string|object|array
1486
-     */
1487
-    private function jsonDecode(string $jsonText, int $flags = self::JSON_DECODE_ACCEPT_DEFAULT)
1488
-    {
1489
-        $accept = $flags & self::JSON_DECODE_ACCEPT_MASK;
1490
-        if (0 === $accept) {
1491
-            return $jsonText;
1492
-        }
1493
-
1494
-        try {
1495
-            $result = json_decode($jsonText, false, 512, JSON_THROW_ON_ERROR);
1496
-        } /** @noinspection PhpMultipleClassDeclarationsInspection */ catch (JsonException $e) {
1497
-            throw JsonDecodeException::create(sprintf('json decode error of %s', JsonDecodeException::shorten($jsonText)), $jsonText, $e);
1498
-        }
1499
-
1500
-        if (self::JSON_DECODE_ACCEPT_JSON === $accept) {
1501
-            return $result;
1502
-        }
1503
-
1504
-        if (self::JSON_DECODE_ACCEPT_ARRAY_OF_OBJECTS === $accept) {
1505
-            if (is_array($result) && $result === array_filter($result, 'is_object')) {
1506
-                return $result;
1507
-            }
1508
-            throw JsonDecodeException::create(sprintf('json decode accept %5d error [%s] of %s', decbin($accept), gettype($result), JsonDecodeException::shorten($jsonText)), $jsonText);
1509
-        }
1510
-
1511
-        if (self::JSON_DECODE_ACCEPT_ARRAY_SINGLE_OBJECT_NULLABLE === $accept) {
1512
-            if (is_array($result)
1513
-                && (
1514
-                    (count($result) === 1 && is_object($result[0] ?? null))
1515
-                    || (count($result) === 0)
1516
-                )
1517
-            ) {
1518
-                return $result[0] ?? null;
1519
-            }
1520
-            throw JsonDecodeException::create(sprintf('json decode accept %5d error [%s] of %s', decbin($accept), gettype($result), JsonDecodeException::shorten($jsonText)), $jsonText);
1521
-        }
1522
-
1523
-        if (self::JSON_DECODE_ACCEPT_ARRAY_SINGLE_OBJECT === $accept) {
1524
-            if (is_array($result) && is_object($result[0] ?? null) && count($result) === 1) {
1525
-                return $result[0];
1526
-            }
1527
-            throw JsonDecodeException::create(sprintf('json decode accept %5d error [%s] of %s', decbin($accept), gettype($result), JsonDecodeException::shorten($jsonText)), $jsonText);
1528
-        }
1529
-
1530
-        if (self::JSON_DECODE_ACCEPT_SUCCESS_OBJECT === $accept) {
1531
-            if (is_object($result) && (array)$result === ['success' => true]) {
1532
-                return $result;
1533
-            }
1534
-            throw JsonDecodeException::create(sprintf('json decode accept %5d error [%s] of %s', decbin($accept), gettype($result), JsonDecodeException::shorten($jsonText)), $jsonText);
1535
-        }
1536
-
1537
-        if (self::JSON_DECODE_ACCEPT_SUCCESS_STRING === $accept) {
1538
-            if (self::STRING_SUCCESS === $result) {
1539
-                return $result;
1540
-            }
1541
-            throw JsonDecodeException::create(sprintf('json decode accept %5d error [%s] of %s', decbin($accept), gettype($result), JsonDecodeException::shorten($jsonText)), $jsonText);
1542
-        }
1543
-
1544
-        if (is_string($result) && (self::JSON_DECODE_ACCEPT_STRING !== ($accept & self::JSON_DECODE_ACCEPT_STRING))) {
1545
-            throw JsonDecodeException::create(sprintf('json decode type %s not accepted; of %s', gettype($result), JsonDecodeException::shorten($jsonText)), $jsonText);
1546
-        }
1547
-
1548
-        if (is_array($result) && (self::JSON_DECODE_ACCEPT_ARRAY !== ($accept & self::JSON_DECODE_ACCEPT_ARRAY))) {
1549
-            throw JsonDecodeException::create(sprintf('json decode type %s not accepted; of %s', gettype($result), JsonDecodeException::shorten($jsonText)), $jsonText);
1550
-        }
1551
-
1552
-        if (is_object($result) && (self::JSON_DECODE_ACCEPT_OBJECT !== ($accept & self::JSON_DECODE_ACCEPT_OBJECT))) {
1553
-            throw JsonDecodeException::create(sprintf('json decode type %s not accepted; of %s', gettype($result), JsonDecodeException::shorten($jsonText)), $jsonText);
1554
-        }
1555
-
1556
-        return $result;
1557
-    }
1558
-
1559
-
1560
-    /**
1561
-     * execute curl with url and options
1562
-     *
1563
-     * @param string $url
1564
-     * @param array $options
1565
-     * @return bool|string
1566
-     */
1567
-    private function curlExec(string $url, array $options)
1568
-    {
1569
-        $handle = curl_init($url);
1570
-        if (!($handle instanceof CurlHandle)) {
1571
-            throw new ConnectionException('Unable to initialise cURL session.', self::ERROR_CODE_NO_CURL);
1572
-        }
1573
-
1574
-        $this->handle = $handle;
1575
-
1576
-        if (!curl_setopt_array($this->handle, $options)) {
1577
-            throw new Exception("Error setting cURL request options.");
1578
-        }
1579
-        $result = curl_exec($this->handle);
1580
-        $this->curlExecHandleResult($result);
1581
-        curl_close($this->handle);
1582
-
1583
-        return $result;
1584
-    }
1585
-
1586
-    /**
1587
-     * internal handling of curl_exec() return
1588
-     *
1589
-     * {@see curlExec()}
1590
-     *
1591
-     * @param bool|string $curlResult return value from curl_exec();
1592
-     * @throws ConnectionException
1593
-     * @return void
1594
-     */
1595
-    private function curlExecHandleResult($curlResult): void
1596
-    {
1597
-        if (empty($curlResult)) {
1598
-            throw new ConnectionException(curl_error($this->handle), -1);
1599
-        }
1600
-
1601
-        $code = (int)curl_getinfo($this->handle)['http_code'];
1602
-
1603
-        $codeIsInErrorRange = $code >= 400 && $code <= 600;
1604
-        $codeIsNotInNonErrorCodes = !in_array($code, [200, 201, 202, 203, 204, 205, 206, 207, 301], true);
1605
-
1606
-        if ($codeIsInErrorRange || $codeIsNotInNonErrorCodes) {
1607
-            ConnectionException::throwCurlResult($code, $curlResult);
1608
-        }
1609
-    }
27
+	public const USER_PREFIX_AUTH_TOKEN = '@auth:token:';
28
+
29
+	public const TYPE_DIR = 'dir';
30
+	public const TYPE_FILE = 'file';
31
+	public const TYPE_REPO = 'repo';
32
+	public const TYPE_SREPO = 'srepo';
33
+	public const TYPE_GREPO = 'grepo';
34
+
35
+	public const TYPES = self::TYPES_FILE + self::TYPES_DIR_LIKE;
36
+	public const TYPES_DIR_LIKE = self::TYPES_DIR + self::TYPES_REPO;
37
+	public const TYPES_DIR = [self::TYPE_DIR => self::TYPE_DIR];
38
+	public const TYPES_FILE = [self::TYPE_FILE => self::TYPE_FILE];
39
+	public const TYPES_REPO = [self::TYPE_REPO => self::TYPE_REPO, self::TYPE_SREPO => self::TYPE_SREPO, self::TYPE_GREPO => self::TYPE_GREPO];
40
+
41
+	/**
42
+	 * @const string
43
+	 */
44
+	public const STRING_SUCCESS = 'success';
45
+
46
+	/**
47
+	 * Error codes
48
+	 */
49
+	public const ERROR_CODE_FEATURES = 802;
50
+	public const ERROR_CODE_NO_CURL = 803;
51
+	public const ERROR_CODE_FILE_IO = 808;
52
+
53
+	/**
54
+	 * default curl options
55
+	 */
56
+	private const CURL_OPTION_DEFAULTS = [
57
+		CURLOPT_AUTOREFERER => true,
58
+		CURLOPT_TIMEOUT => 10,
59
+		CURLOPT_RETURNTRANSFER => true,
60
+		CURLOPT_FOLLOWLOCATION => false,
61
+	];
62
+
63
+	/**
64
+	 * @var array shared curl options between all requests
65
+	 */
66
+	private array $curlSharedOptions = self::CURL_OPTION_DEFAULTS;
67
+
68
+	/**
69
+	 * jsonDecode accept flags
70
+	 *
71
+	 * @see jsonDecode
72
+	 */
73
+	private const JSON_DECODE_ACCEPT_MASK = 31;                         # 1 1111 accept bitmask (five bits with the msb flags)
74
+	private const JSON_DECODE_ACCEPT_JSON = 16;                         # 1 0000 JSON text
75
+	private const JSON_DECODE_ACCEPT_DEFAULT = 23;                      # 1 0111 default: string, array or object
76
+	private const JSON_DECODE_ACCEPT_OBJECT = 17;                       # 1 0001 object
77
+	private const JSON_DECODE_ACCEPT_ARRAY = 18;                        # 1 0010 array
78
+	private const JSON_DECODE_ACCEPT_STRING = 20;                       # 1 0100 string
79
+	private const JSON_DECODE_ACCEPT_ARRAY_OF_OBJECTS = 24;             # 1 1000 array with only objects (incl. none)
80
+	private const JSON_DECODE_ACCEPT_ARRAY_SINGLE_OBJECT = 25;          # 1 1001 array with one single object, return that item
81
+	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
82
+	private const JSON_DECODE_ACCEPT_SUCCESS_STRING = 28;               # 1 1100 string "success"
83
+	private const JSON_DECODE_ACCEPT_SUCCESS_OBJECT = 29;               # 1 1101 object with single "success" property and value true
84
+
85
+	/**
86
+	 * @const string ASCII upper-case characters part of a hexit
87
+	 */
88
+	private const HEX_ALPHA_UPPER = 'ABCDEF';
89
+
90
+	/**
91
+	 * @const string ASCII lower-case characters part of a hexit
92
+	 */
93
+	private const HEX_ALPHA_LOWER = 'abcdef';
94
+
95
+	/**
96
+	 * @var string Server base URL
97
+	 */
98
+	private string $baseurl;
99
+
100
+	/**
101
+	 * @var string Username
102
+	 */
103
+	private string $user;
104
+
105
+	/**
106
+	 * @var string Password
107
+	 */
108
+	private string $pass;
109
+
110
+	private ?CurlHandle $handle = null;
111
+
112
+	/**
113
+	 * @var string
114
+	 */
115
+	private string $token = '';
116
+
117
+	/**
118
+	 * constructor
119
+	 *
120
+	 * @param string $baseurl
121
+	 * @param string $user
122
+	 * @param string $pass
123
+	 * @param string|null $otp
124
+	 */
125
+	public function __construct(string $baseurl, string $user, string $pass, string $otp = null)
126
+	{
127
+		if (!(function_exists('curl_version'))) {
128
+			throw new ConnectionException('PHP-CURL not installed', self::ERROR_CODE_NO_CURL);
129
+		}
130
+
131
+		$this->baseurl = $baseurl;
132
+		$this->user = $user;
133
+		$this->pass = $pass;
134
+		// trigger_error(sprintf("ctor: %s:%s", $user, $pass), E_USER_NOTICE);
135
+
136
+		$this->setTokenByUsernameAndPassword($otp);
137
+	}
138
+
139
+	/**
140
+	 * ping (with authentication)
141
+	 *
142
+	 * @link https://download.seafile.com/published/web-api/home.md#user-content-Quick%20Start
143
+	 *
144
+	 * @return string "pong"
145
+	 */
146
+	public function ping(): string
147
+	{
148
+		return $this->jsonDecode(
149
+			$this->get(
150
+				"$this->baseurl/api2/ping/",
151
+				[CURLOPT_HTTPHEADER => ["Authorization: Token $this->token"]],
152
+			),
153
+			self::JSON_DECODE_ACCEPT_STRING,
154
+		);
155
+	}
156
+
157
+	/**
158
+	 * get server version
159
+	 *
160
+	 * @throws Exception
161
+	 * @return string
162
+	 */
163
+	public function getServerVersion(): string
164
+	{
165
+		$serverInfo = $this->getServerInformation();
166
+		if (
167
+			!is_string($serverInfo->version ?? null)
168
+			|| !is_array($serverInfo->features ?? null)
169
+		) {
170
+			throw new InvalidResponseException('We could not retrieve list of server features.', self::ERROR_CODE_FEATURES);
171
+		}
172
+
173
+		$isSeafilePro = in_array('seafile-pro', $serverInfo->features, true);
174
+		$edition = $isSeafilePro ? 'Professional' : 'Community';
175
+
176
+		return "$serverInfo->version ($edition)";
177
+	}
178
+
179
+	/**
180
+	 * get server information
181
+	 *
182
+	 * @link https://download.seafile.com/published/web-api/v2.1/server-info.md#user-content-Get%20Server%20Information
183
+	 *
184
+	 * @throws Exception
185
+	 * @return object
186
+	 */
187
+	public function getServerInformation(): object
188
+	{
189
+		return $this->jsonDecode(
190
+			$this->get(
191
+				"$this->baseurl/api2/server-info/",
192
+				[CURLOPT_HTTPHEADER => ["Authorization: Token $this->token"]],
193
+			),
194
+			self::JSON_DECODE_ACCEPT_OBJECT,
195
+		);
196
+	}
197
+
198
+	/**
199
+	 * check account info
200
+	 *
201
+	 * @link https://download.seafile.com/published/web-api/v2.1/account.md#user-content-Check%20Account%20Info
202
+	 * @return object
203
+	 */
204
+	public function checkAccountInfo(): object
205
+	{
206
+		return $this->jsonDecode(
207
+			$this->get(
208
+				"$this->baseurl/api2/account/info/",
209
+				[CURLOPT_HTTPHEADER => ["Authorization: Token $this->token"]]
210
+			),
211
+			self::JSON_DECODE_ACCEPT_OBJECT,
212
+		);
213
+	}
214
+
215
+	/**
216
+	 * list seafile server libraries
217
+	 *
218
+	 * that are all libraries a user can access.
219
+	 *
220
+	 * @link https://download.seafile.com/published/web-api/v2.1/libraries.md#user-content-List%20Libraries
221
+	 *
222
+	 * @throws Exception
223
+	 * @return object[]
224
+	 */
225
+	public function listLibraries(): array
226
+	{
227
+		return $this->listLibrariesCached(true);
228
+	}
229
+
230
+	/**
231
+	 * get default library
232
+	 *
233
+	 * @link https://download.seafile.com/published/web-api/v2.1/libraries.md#user-content-Get%20Default%20Library
234
+	 *
235
+	 * @return object{exists: bool, repo_id: string}
236
+	 */
237
+	public function getDefaultLibrary(): object
238
+	{
239
+		return $this->jsonDecode(
240
+			$this->get(
241
+				"$this->baseurl/api2/default-repo/",
242
+				[CURLOPT_HTTPHEADER => ["Authorization: Token $this->token"]],
243
+			),
244
+			self::JSON_DECODE_ACCEPT_OBJECT,
245
+		);
246
+	}
247
+
248
+	/**
249
+	 * create a library
250
+	 *
251
+	 * @link https://download.seafile.com/published/web-api/v2.1/libraries.md#user-content-Create%20Library
252
+	 *
253
+	 * @param string $name of library
254
+	 * @return object
255
+	 */
256
+	public function createLibrary(string $name): object
257
+	{
258
+		$name = trim($name, "/");
259
+
260
+		return $this->jsonDecode(
261
+			$this->post(
262
+				"$this->baseurl/api2/repos/",
263
+				['name' => $name],
264
+				[CURLOPT_HTTPHEADER => ["Authorization: Token $this->token"]],
265
+			),
266
+			self::JSON_DECODE_ACCEPT_OBJECT,
267
+		);
268
+	}
269
+
270
+	/**
271
+	 * name a library
272
+	 *
273
+	 * @link https://download.seafile.com/published/web-api/v2.1/libraries.md#user-content-Rename%20Library
274
+	 */
275
+	public function nameLibrary(string $lib, string $name): void
276
+	{
277
+		$lib = $this->verifyLib($lib);
278
+		$fields = ['repo_name' => $name];
279
+		$_ = $this->jsonDecode(
280
+			$this->post(
281
+				"$this->baseurl/api2/repos/$lib/?op=rename",
282
+				$fields,
283
+				[CURLOPT_HTTPHEADER => ["Authorization: Token $this->token"]],
284
+			),
285
+			self::JSON_DECODE_ACCEPT_SUCCESS_STRING,
286
+		);
287
+		unset($_);
288
+	}
289
+
290
+	/**
291
+	 * rename a library
292
+	 *
293
+	 * do nothing if the library can not be found
294
+	 *
295
+	 * @link https://download.seafile.com/published/web-api/v2.1/libraries.md#user-content-Rename%20Library
296
+	 *
297
+	 * @param string $oldName
298
+	 * @param string $newName
299
+	 * @return void
300
+	 */
301
+	public function renameLibrary(string $oldName, string $newName): void
302
+	{
303
+		$lib = $this->getLibraryIdByLibraryName($oldName);
304
+		if (null === $lib) {
305
+			return; // no library to rename
306
+		}
307
+
308
+		$this->nameLibrary($lib, $newName);
309
+	}
310
+
311
+	/**
312
+	 * delete a library by name
313
+	 *
314
+	 * @link https://download.seafile.com/published/web-api/v2.1/libraries.md#user-content-Delete%20Library
315
+	 *
316
+	 * @param string $name
317
+	 * @return void
318
+	 */
319
+	public function deleteLibraryByName(string $name): void
320
+	{
321
+		$id = $this->getLibraryIdByLibraryName($name);
322
+		if (null === $id) {
323
+			return; // library already gone
324
+		}
325
+
326
+		$this->deleteLibraryById($id);
327
+	}
328
+
329
+	/**
330
+	 * delete a library by id
331
+	 *
332
+	 * @link https://download.seafile.com/published/web-api/v2.1/libraries.md#user-content-Delete%20Library
333
+	 *
334
+	 * @param string $id
335
+	 * @return void
336
+	 */
337
+	public function deleteLibraryById(string $id): void
338
+	{
339
+		$lib = $this->verifyLib($id);
340
+
341
+		$_ = $this->jsonDecode(
342
+			$this->delete(
343
+				"$this->baseurl/api2/repos/$lib/",
344
+				[CURLOPT_HTTPHEADER => ["Authorization: Token $this->token"]],
345
+			),
346
+			self::JSON_DECODE_ACCEPT_SUCCESS_STRING,
347
+		);
348
+		unset($_);
349
+	}
350
+
351
+	/**
352
+	 * get library info array from path
353
+	 *
354
+	 * @param string $libNamedPath with the library name as first component
355
+	 * @throws Exception
356
+	 * @throws InvalidResponseException
357
+	 * @return object with 'id' and 'name' of library, both NULL if not found
358
+	 */
359
+	public function getLibraryFromPath(string $libNamedPath): object
360
+	{
361
+		$libraries = $this->listLibrariesCached();
362
+		$libraries = array_column($libraries, null, 'name');
363
+
364
+		$name = explode('/', ltrim($this->normalizePath($libNamedPath), '/'), 2)[0];
365
+
366
+		return (object)[
367
+			'id' => $libraries[$name]->id ?? null,
368
+			'name' => $libraries[$name]->name ?? null,
369
+		];
370
+	}
371
+
372
+	/**
373
+	 * list all share links
374
+	 *
375
+	 * all folder/file download share links in all libraries created by user.
376
+	 *
377
+	 * @link https://download.seafile.com/published/web-api/v2.1/share-links.md#user-content-List%20all%20Share%20Links
378
+	 *
379
+	 * @return object[]
380
+	 * @throws Exception
381
+	 * @throws InvalidResponseException
382
+	 * @noinspection PhpUnused
383
+	 */
384
+	public function listAllShareLinks(): array
385
+	{
386
+		return $this->jsonDecode(
387
+			$this->get(
388
+				"$this->baseurl/api/v2.1/share-links/",
389
+				[CURLOPT_HTTPHEADER => ["Authorization: Token $this->token"]],
390
+			),
391
+			self::JSON_DECODE_ACCEPT_ARRAY_OF_OBJECTS,
392
+		);
393
+	}
394
+
395
+	/**
396
+	 * Create Share Link
397
+	 *
398
+	 * @link https://download.seafile.com/published/web-api/v2.1/share-links.md#user-content-Create%20Share%20Link
399
+	 *
400
+	 * @param string $lib
401
+	 * @param string $path
402
+	 * @param ?string $password [optional]
403
+	 * @param ?int|DateTimeInterface $expire [optional] number of days to expire (int) or DateTime to expire
404
+	 * @param ?array $permissions [optional] see seafile api docs
405
+	 * @return object
406
+	 * @throws Exception
407
+	 * @throws InvalidArgumentException
408
+	 * @throws InvalidResponseException
409
+	 */
410
+	public function createShareLink(string $lib, string $path, string $password = null, $expire = null, array $permissions = null): object
411
+	{
412
+		$lib = $this->verifyLib($lib);
413
+		$path = $this->normalizePath($path);
414
+
415
+		$fields = [
416
+			'repo_id' => $lib,
417
+			'path' => $path,
418
+		];
419
+		if (null !== $password) {
420
+			$fields['password'] = $password;
421
+		}
422
+		if (null !== $expire) {
423
+			$expireTime = $expire;
424
+			if (is_int($expire)) {
425
+				$expireDays = max(1, min(365, (int)$expire));
426
+				$expireTime = (new DateTimeImmutable())->add(
427
+					new \DateInterval("P{$expireDays}D")
428
+				);
429
+			}
430
+			if (!$expireTime instanceof DateTimeInterface) {
431
+				throw new InvalidArgumentException('Expire type mismatch: ' . gettype($expireTime));
432
+			}
433
+			$fields['expiration_time'] = $expireTime->format(\DATE_ATOM);
434
+		}
435
+		if (null !== $permissions) {
436
+			try {
437
+				$fields['permissions'] = json_encode($permissions, JSON_THROW_ON_ERROR);
438
+			} /** @noinspection PhpMultipleClassDeclarationsInspection */ catch (JsonException $ex) {
439
+				throw new InvalidArgumentException('permissions');
440
+			}
441
+		}
442
+
443
+		return $this->jsonDecode(
444
+			$this->post(
445
+				"$this->baseurl/api/v2.1/share-links/",
446
+				$fields,
447
+				[CURLOPT_HTTPHEADER => ["Authorization: Token $this->token"]],
448
+			),
449
+			self::JSON_DECODE_ACCEPT_OBJECT,
450
+		);
451
+	}
452
+
453
+	/**
454
+	 * List Share Links of a Library
455
+	 *
456
+	 * @link https://download.seafile.com/published/web-api/v2.1/share-links.md#user-content-List%20all%20Share%20Links
457
+	 *
458
+	 * @param ?string $lib [optional] library id (guid), default/null for all libraries
459
+	 * @throws Exception
460
+	 * @return object[]
461
+	 */
462
+	public function listShareLinksOfALibrary(string $lib = null): array
463
+	{
464
+		$lib = $this->verifyLib($lib ?? '', true);
465
+
466
+		return $this->jsonDecode(
467
+			$this->get(
468
+				"$this->baseurl/api/v2.1/share-links/?repo_id=$lib",
469
+				[CURLOPT_HTTPHEADER => ["Authorization: Token $this->token"]],
470
+			),
471
+			self::JSON_DECODE_ACCEPT_ARRAY_OF_OBJECTS,
472
+		);
473
+	}
474
+
475
+	/**
476
+	 * check password (of a share link by token)
477
+	 *
478
+	 * @link https://download.seafile.com/published/web-api/v2.1-admin/share-links.md#user-content-Check%20Password
479
+	 *
480
+	 * @param string $token of share link
481
+	 * @param string $password in plain
482
+	 * @return object
483
+	 * @throws Exception
484
+	 * @throws InvalidResponseException
485
+	 * @noinspection PhpUnused
486
+	 */
487
+	public function checkShareLinkPassword(string $token, string $password): object
488
+	{
489
+		$tokenEncoded = rawurlencode($token);
490
+
491
+		return $this->jsonDecode(
492
+			$this->post(
493
+				"$this->baseurl/api/v2.1/admin/share-links/$tokenEncoded/check-password/",
494
+				['password' => $password],
495
+				[CURLOPT_HTTPHEADER => ["Authorization: Token $this->token"]],
496
+			),
497
+			self::JSON_DECODE_ACCEPT_SUCCESS_OBJECT,
498
+		);
499
+	}
500
+
501
+	/**
502
+	 * share Link of a folder (or file)
503
+	 *
504
+	 * @link https://download.seafile.com/published/web-api/v2.1/share-links.md#user-content-List%20Share%20Link%20of%20a%20Folder%20(File)
505
+	 *
506
+	 * @param string $lib
507
+	 * @param string $path
508
+	 * @return object the share link
509
+	 * @throws Exception
510
+	 * @throws InvalidArgumentException
511
+	 * @throws InvalidResponseException
512
+	 * @noinspection PhpUnused
513
+	 */
514
+	public function listShareLinksOfAFolder(string $lib, string $path): ?object
515
+	{
516
+		$lib = $this->verifyLib($lib);
517
+		$path = $this->normalizePath($path);
518
+		$pathEncoded = rawurlencode($path);
519
+
520
+		return $this->jsonDecode(
521
+			$this->get(
522
+				"$this->baseurl/api/v2.1/share-links/?repo_id=$lib&path=$pathEncoded",
523
+				[CURLOPT_HTTPHEADER => ["Authorization: Token $this->token"]],
524
+			),
525
+			self::JSON_DECODE_ACCEPT_ARRAY_SINGLE_OBJECT_NULLABLE,
526
+		);
527
+	}
528
+
529
+	/**
530
+	 * Delete Share Link
531
+	 *
532
+	 * @link https://download.seafile.com/published/web-api/v2.1/share-links.md#user-content-Delete%20Share%20Link
533
+	 *
534
+	 * @param string $token
535
+	 * @throws Exception
536
+	 * @return object success {"success": true}
537
+	 */
538
+	public function deleteShareLink(string $token): object
539
+	{
540
+		$token = $this->verifyToken($token);
541
+
542
+		return $this->jsonDecode(
543
+			$this->delete(
544
+				"$this->baseurl/api/v2.1/share-links/$token/",
545
+				[CURLOPT_HTTPHEADER => ["Authorization: Token $this->token"]],
546
+			),
547
+			self::JSON_DECODE_ACCEPT_SUCCESS_OBJECT,
548
+		);
549
+	}
550
+
551
+	/**
552
+	 * search user
553
+	 *
554
+	 * @link https://download.seafile.com/published/web-api/v2.1/user-search.md#user-content-Search%20User
555
+	 *
556
+	 * @param string $search
557
+	 * @throws Exception
558
+	 * @return object
559
+	 */
560
+	public function searchUser(string $search): object
561
+	{
562
+		$searchEncoded = rawurlencode($search);
563
+
564
+		return $this->jsonDecode(
565
+			$this->get(
566
+				"$this->baseurl/api2/search-user/?q=$searchEncoded",
567
+				[CURLOPT_HTTPHEADER => ['Authorization: Token ' . $this->token]],
568
+			),
569
+			self::JSON_DECODE_ACCEPT_OBJECT,
570
+		);
571
+	}
572
+
573
+	/**
574
+	 * list groups for user sharing
575
+	 *
576
+	 * @throws Exception
577
+	 * @throws InvalidResponseException
578
+	 * @return array|object|string
579
+	 * @link (undocumented)
580
+	 *
581
+	 */
582
+	public function shareableGroups(): array
583
+	{
584
+		return $this->jsonDecode(
585
+			$this->get(
586
+				"$this->baseurl/api/v2.1/shareable-groups/",
587
+				[CURLOPT_HTTPHEADER => ["Authorization: Token $this->token"]],
588
+			),
589
+			self::JSON_DECODE_ACCEPT_ARRAY_OF_OBJECTS,
590
+		);
591
+	}
592
+
593
+	/**
594
+	 * Share a Library to Group
595
+	 *
596
+	 * @link https://download.seafile.com/published/web-api/v2.1/share.md#user-content-Share%20a%20Library%20to%20Group
597
+	 *
598
+	 * @param string $lib
599
+	 * @param string $path
600
+	 * @param int|int[] $group
601
+	 * @param string|null $permission [optional] r, rw, admin (default: r)
602
+	 * @throws Exception
603
+	 * @throws InvalidArgumentException
604
+	 * @return array
605
+	 */
606
+	public function shareLibraryPathToGroup(string $lib, string $path, $group, string $permission = null)
607
+	{
608
+		$lib = $this->verifyLib($lib);
609
+		$path = $this->normalizePath($path);
610
+		$pathEncoded = rawurlencode($path);
611
+
612
+		$fields = [
613
+			'share_type' => 'group',
614
+			'group_id' => $group,
615
+			'permission' => $permission ?? 'r',
616
+		];
617
+		return $this->jsonDecode(
618
+			$this->put(
619
+				"$this->baseurl/api2/repos/$lib/dir/shared_items/?p=$pathEncoded",
620
+				$fields,
621
+				[CURLOPT_HTTPHEADER => ["Authorization: Token $this->token"]],
622
+			),
623
+			self::JSON_DECODE_ACCEPT_OBJECT,
624
+		);
625
+	}
626
+
627
+	/**
628
+	 * Share a Library to User
629
+	 *
630
+	 * @link https://download.seafile.com/published/web-api/v2.1/share.md#user-content-Share%20a%20Library%20to%20User
631
+	 *
632
+	 * @param string $lib
633
+	 * @param string $path
634
+	 * @param string $user
635
+	 * @param string|null $permission [optional] r, rw, admin (default: r)
636
+	 * @throws Exception
637
+	 * @throws InvalidArgumentException
638
+	 * @return array
639
+	 */
640
+	public function shareLibraryPathToUser(string $lib, string $path, string $user, string $permission = null)
641
+	{
642
+		$lib = $this->verifyLib($lib);
643
+		$path = $this->normalizePath($path);
644
+		$pathEncoded = rawurlencode($path);
645
+
646
+		$fields = [
647
+			'share_type' => 'user',
648
+			'username' => $user,
649
+			'permission' => $permission ?? 'r',
650
+		];
651
+		return $this->jsonDecode(
652
+			$this->put(
653
+				"$this->baseurl/api2/repos/$lib/dir/shared_items/?p=$pathEncoded",
654
+				$fields,
655
+				[CURLOPT_HTTPHEADER => ["Authorization: Token $this->token"]],
656
+			),
657
+			// either array of objects -or- failure object
658
+			self::JSON_DECODE_ACCEPT_ARRAY | self::JSON_DECODE_ACCEPT_OBJECT,
659
+		);
660
+	}
661
+
662
+	/**
663
+	 * Unshare a Library to Group
664
+	 *
665
+	 * @link https://download.seafile.com/published/web-api/v2.1/share.md#user-content-Unshare%20a%20Library%20from%20Group
666
+	 *
667
+	 * @param string $lib
668
+	 * @param string $path
669
+	 * @param int $group
670
+	 * @throws Exception
671
+	 * @throws InvalidArgumentException
672
+	 * @return object
673
+	 */
674
+	public function unshareLibraryPathToGroup(string $lib, string $path, int $group): object
675
+	{
676
+		$lib = $this->verifyLib($lib);
677
+		$path = $this->normalizePath($path);
678
+		$pathEncoded = rawurlencode($path);
679
+
680
+		return $this->jsonDecode(
681
+			$this->delete(
682
+				"$this->baseurl/api2/repos/$lib/dir/shared_items/?p=$pathEncoded&share_type=group&group_id=$group",
683
+				[CURLOPT_HTTPHEADER => ["Authorization: Token $this->token"]],
684
+			),
685
+			self::JSON_DECODE_ACCEPT_SUCCESS_OBJECT,
686
+		);
687
+	}
688
+
689
+	/**
690
+	 * Unshare a Library to User
691
+	 *
692
+	 * @link https://download.seafile.com/published/web-api/v2.1/share.md#user-content-Unshare%20a%20Library%20from%20User
693
+	 *
694
+	 * @param string $lib
695
+	 * @param string $path
696
+	 * @param string|string[] $user
697
+	 * @throws Exception
698
+	 * @throws InvalidArgumentException
699
+	 * @return object
700
+	 */
701
+	public function unshareLibraryPathToUser(string $lib, string $path, $user): object
702
+	{
703
+		$lib = $this->verifyLib($lib);
704
+		$path = $this->normalizePath($path);
705
+		$pathEncoded = rawurlencode($path);
706
+		$userEncoded = rawurlencode($user);
707
+
708
+		return $this->jsonDecode(
709
+			$this->delete(
710
+				"$this->baseurl/api2/repos/$lib/dir/shared_items/?p=$pathEncoded&share_type=user&username=$userEncoded",
711
+				[CURLOPT_HTTPHEADER => ["Authorization: Token $this->token"]],
712
+			),
713
+			self::JSON_DECODE_ACCEPT_SUCCESS_OBJECT,
714
+		);
715
+	}
716
+
717
+	/**
718
+	 * list user shares for a library path
719
+	 *
720
+	 * @link https://download.seafile.com/published/web-api/v2.1/share.md#user-content-List%20Shared%20Users%20of%20a%20Library
721
+	 *
722
+	 * @param string $lib
723
+	 * @param string $path
724
+	 * @throws Exception
725
+	 * @throws InvalidArgumentException
726
+	 * @throws InvalidResponseException
727
+	 * @return array<int, object>
728
+	 */
729
+	public function listSharesOfLibraryPath(string $lib, string $path): array
730
+	{
731
+		$lib = $this->verifyLib($lib);
732
+		$path = $this->normalizePath($path);
733
+		$pathEncoded = rawurlencode($path);
734
+
735
+		return $this->jsonDecode(
736
+			$this->get(
737
+				"$this->baseurl/api2/repos/$lib/dir/shared_items/?p=$pathEncoded",
738
+				[CURLOPT_HTTPHEADER => ["Authorization: Token $this->token"]],
739
+			),
740
+			self::JSON_DECODE_ACCEPT_ARRAY_OF_OBJECTS,
741
+		);
742
+	}
743
+
744
+	/**
745
+	 * create a new directory
746
+	 *
747
+	 * @link https://download.seafile.com/published/web-api/v2.1/directories.md#user-content-Create%20New%20Directory
748
+	 *
749
+	 * @param string $lib library id (guid)
750
+	 * @param string $path of the directory to create (e.g.: "/path/to/new-directory", leading and trailing slashes can be omitted)
751
+	 * @throws InvalidArgumentException|Exception
752
+	 * @return string|object the common "success" or the object with error_msg property
753
+	 */
754
+	public function createNewDirectory(string $lib, string $path)
755
+	{
756
+		$lib = $this->verifyLib($lib);
757
+		$path = $this->normalizePath($path);
758
+		$pathEncoded = rawurlencode($path);
759
+
760
+		return $this->jsonDecode(
761
+			$this->post(
762
+				"$this->baseurl/api2/repos/$lib/dir/?p=$pathEncoded",
763
+				['operation' => 'mkdir'],
764
+				[CURLOPT_HTTPHEADER => ["Authorization: Token $this->token"]],
765
+			),
766
+			self::JSON_DECODE_ACCEPT_STRING | self::JSON_DECODE_ACCEPT_OBJECT,
767
+		);
768
+	}
769
+
770
+	/**
771
+	 * delete a file
772
+	 *
773
+	 * @link https://download.seafile.com/published/web-api/v2.1/file.md#user-content-Delete%20File
774
+	 *
775
+	 * @param string $lib library id (guid)
776
+	 * @param string $path of the fle to delete (e.g.: "/path/to/file-to-delete", leading and trailing slashes can be omitted)
777
+	 * @throws InvalidArgumentException|Exception
778
+	 * @return string|object the common "success" or the known object with error_msg property
779
+	 */
780
+	public function deleteFile(string $lib, string $path)
781
+	{
782
+		$lib = $this->verifyLib($lib);
783
+		$path = $this->normalizePath($path);
784
+		$pathEncoded = rawurlencode($path);
785
+
786
+		return $this->jsonDecode(
787
+			$this->delete(
788
+				"$this->baseurl/api2/repos/$lib/file/?p=$pathEncoded",
789
+				[CURLOPT_HTTPHEADER => ["Authorization: Token $this->token"]],
790
+			),
791
+			self::JSON_DECODE_ACCEPT_STRING | self::JSON_DECODE_ACCEPT_OBJECT,
792
+		);
793
+	}
794
+
795
+	/**
796
+	 * get a file download URL
797
+	 *
798
+	 * @link https://download.seafile.com/published/web-api/v2.1/file.md#user-content-Download%20File
799
+	 *
800
+	 * @param string $lib
801
+	 * @param string $path
802
+	 * @return string download URL (http/s)
803
+	 * @throws InvalidArgumentException|Exception
804
+	 */
805
+	public function downloadFile(string $lib, string $path): string
806
+	{
807
+		$lib = $this->verifyLib($lib);
808
+		$path = $this->normalizePath($path);
809
+		$pathEncoded = rawurlencode($path);
810
+
811
+		return $this->jsonDecode(
812
+			$this->get(
813
+				"$this->baseurl/api2/repos/$lib/file/?p=$pathEncoded",
814
+				[CURLOPT_HTTPHEADER => ["Authorization: Token $this->token"]],
815
+			),
816
+			self::JSON_DECODE_ACCEPT_STRING,
817
+		);
818
+	}
819
+
820
+	/**
821
+	 * download a file
822
+	 *
823
+	 * get file contents of a file in a library
824
+	 *
825
+	 * @param string $lib
826
+	 * @param string $path
827
+	 * @throws InvalidArgumentException|Exception
828
+	 * @return string|false on failure
829
+	 */
830
+	public function downloadFileAsBuffer(string $lib, string $path)
831
+	{
832
+		$url = $this->downloadFile($lib, $path);
833
+
834
+		return $this->get($url);
835
+	}
836
+
837
+	/**
838
+	 * download a file to a local file
839
+	 *
840
+	 * @param string $lib
841
+	 * @param string $path
842
+	 * @param string $localPath path to a file - existing or not - on the local file-system
843
+	 * @throws InvalidArgumentException|Exception
844
+	 * @return bool success/failure
845
+	 */
846
+	public function downloadFileToFile(string $lib, string $path, string $localPath): bool
847
+	{
848
+		$handle = fopen($localPath, 'wb');
849
+		if (false === $handle) {
850
+			throw new Exception('failed to open local path for writing', self::ERROR_CODE_FILE_IO);
851
+		}
852
+
853
+		try {
854
+			$result = $this->downloadFileToStream($lib, $path, $handle);
855
+		} finally {
856
+			$close = fclose($handle);
857
+		}
858
+
859
+		if (!$close) {
860
+			throw new Exception('failed to close local path handle', self::ERROR_CODE_FILE_IO);
861
+		}
862
+
863
+		return $result;
864
+	}
865
+
866
+	/**
867
+	 * download a file to a stream handle
868
+	 *
869
+	 * @param string $lib
870
+	 * @param string $path
871
+	 * @param resource $handle stream handle
872
+	 * @throws InvalidArgumentException|Exception
873
+	 * @return bool success/failure
874
+	 */
875
+	public function downloadFileToStream(string $lib, string $path, $handle): bool
876
+	{
877
+		$url = $this->downloadFile($lib, $path);
878
+
879
+		return $this->get($url, [CURLOPT_RETURNTRANSFER => true, CURLOPT_FILE => $handle]);
880
+	}
881
+
882
+	/**
883
+	 * list items in directory
884
+	 *
885
+	 * @link https://download.seafile.com/published/web-api/v2.1/directories.md#user-content-List%20Items%20in%20Directory
886
+	 *
887
+	 * @param string $lib
888
+	 * @param string $path
889
+	 * @return array
890
+	 * @throws Exception
891
+	 * @throws InvalidArgumentException
892
+	 * @throws InvalidResponseException
893
+	 */
894
+	public function listItemsInDirectory(string $lib, string $path): array
895
+	{
896
+		$lib = $this->verifyLib($lib);
897
+		$path = $this->normalizePath($path);
898
+		$pathEncoded = rawurlencode($path);
899
+
900
+		$result = $this->jsonDecode(
901
+			$this->get(
902
+				"$this->baseurl/api2/repos/$lib/dir/?p=$pathEncoded",
903
+				[CURLOPT_HTTPHEADER => ['Authorization: Token ' . $this->token]],
904
+			),
905
+		);
906
+
907
+		if (is_object($result)) {
908
+			// likely a folder not found.
909
+			$result = [];
910
+		}
911
+
912
+		return $result;
913
+	}
914
+
915
+	/**
916
+	 * move a file
917
+	 *
918
+	 * @link https://download.seafile.com/published/web-api/v2.1/file.md#user-content-Move%20File
919
+	 *
920
+	 * @param string $lib
921
+	 * @param string $path
922
+	 * @param string $dstLib
923
+	 * @param string $dstDir
924
+	 * @return object
925
+	 * @throws InvalidArgumentException|Exception
926
+	 */
927
+	public function moveFile(string $lib, string $path, string $dstLib, string $dstDir): object
928
+	{
929
+		$lib = $this->verifyLib($lib);
930
+		$path = $this->normalizePath($path);
931
+		$pathEncoded = rawurlencode($path);
932
+		$dstLib = $this->verifyLib($dstLib);
933
+		$dstDir = $this->normalizePath($dstDir);
934
+
935
+		return $this->jsonDecode(
936
+			$this->post(
937
+				"$this->baseurl/api2/repos/$lib/file/?p=$pathEncoded",
938
+				['operation' => 'move', 'dst_repo' => $dstLib, 'dst_dir' => $dstDir],
939
+				[CURLOPT_HTTPHEADER => ["Authorization: Token $this->token"]],
940
+			),
941
+			self::JSON_DECODE_ACCEPT_OBJECT,
942
+		);
943
+	}
944
+
945
+	/**
946
+	 * rename a file
947
+	 *
948
+	 * @link https://download.seafile.com/published/web-api/v2.1/file.md#user-content-Move%20File
949
+	 *
950
+	 * @param string $lib library id (guid)
951
+	 * @param string $path of the file to rename (e.g. "/path/to/file-to-rename")
952
+	 * @param string $newName new basename for the basename of $path (e.g. "new-file-name")
953
+	 * @return string|object the common "success" or the known object with error_msg property
954
+	 * @throws Exception
955
+	 */
956
+	public function renameFile(string $lib, string $path, string $newName)
957
+	{
958
+		$lib = $this->verifyLib($lib);
959
+		$path = $this->normalizePath($path);
960
+		$pathEncoded = rawurlencode($path);
961
+
962
+		return $this->jsonDecode(
963
+			$this->post(
964
+				"$this->baseurl/api2/repos/$lib/file/?p=$pathEncoded",
965
+				['operation' => 'rename', 'newname' => $newName],
966
+				[CURLOPT_HTTPHEADER => ["Authorization: Token $this->token"]],
967
+			),
968
+			self::JSON_DECODE_ACCEPT_STRING | self::JSON_DECODE_ACCEPT_OBJECT,
969
+		);
970
+	}
971
+
972
+	/**
973
+	 * simplified file upload routine for string buffer
974
+	 *
975
+	 * @param string $lib
976
+	 * @param string $path
977
+	 * @param string $buffer
978
+	 * @throws InvalidArgumentException
979
+	 * @throws Exception
980
+	 * @return object
981
+	 */
982
+	public function uploadBuffer(string $lib, string $path, string $buffer): object
983
+	{
984
+		$lib = $this->verifyLib($lib);
985
+		$path = $this->normalizePath($path);
986
+
987
+		$parentDir = dirname($path);
988
+		$uploadLink = $this->uploadGetLink($lib, $parentDir);
989
+		$fileName = basename($path);
990
+
991
+		return $this->uploadFileBuffer($uploadLink, $parentDir, '', $buffer, $fileName);
992
+	}
993
+
994
+	/**
995
+	 * simplified file upload routine for standard file
996
+	 *
997
+	 * @param string $lib
998
+	 * @param string $path path in seafile to upload the file as
999
+	 * @param string $file path of file to upload
1000
+	 * @throws InvalidArgumentException
1001
+	 * @throws Exception
1002
+	 * @return object
1003
+	 */
1004
+	public function uploadFile(string $lib, string $path, string $file): object
1005
+	{
1006
+		$lib = $this->verifyLib($lib);
1007
+		$path = $this->normalizePath($path);
1008
+		if (!is_file($file) && !is_readable($file)) {
1009
+			throw new InvalidArgumentException(sprintf('Not a readable file: %s', $file));
1010
+		}
1011
+
1012
+		$parentDir = dirname($path);
1013
+		$uploadLink = $this->uploadGetLink($lib, $parentDir);
1014
+		$fileName = basename($path);
1015
+
1016
+		return $this->uploadFilePath($uploadLink, $parentDir, '', $file, $fileName);
1017
+	}
1018
+
1019
+	/**
1020
+	 * upload string buffer as a file
1021
+	 *
1022
+	 * same as {@see SeafileApi::uploadFile()} with the option to upload without a
1023
+	 * concrete file on the system. the temporary file to upload is created
1024
+	 * from the string buffer.
1025
+	 *
1026
+	 * @param string $uploadLink from {@see uploadGetLink}
1027
+	 * @param string $parentDir the parent directory to upload the file to
1028
+	 * @param string $relativePath the name of the file, subdirectories possible (e.g. uploading a folder)
1029
+	 * @param string $buffer file contents to upload as string (not the file-name)
1030
+	 * @param string $fileName to use as basename for the data
1031
+	 * @param bool $replace
1032
+	 * @throws Exception
1033
+	 * @return object
1034
+	 */
1035
+	public function uploadFileBuffer(
1036
+		string $uploadLink, string $parentDir, string $relativePath,
1037
+		string $buffer, string $fileName = 'upload.dat', bool $replace = false
1038
+	): object
1039
+	{
1040
+		$tmpHandle = tmpfile();
1041
+		if (false === $tmpHandle) {
1042
+			throw new Exception('Upload data rejected: Unable to open temporary stream.');
1043
+		}
1044
+
1045
+		$meta = stream_get_meta_data($tmpHandle);
1046
+		$tmpFile = $meta['uri'];
1047
+		if (!is_file($tmpFile)) {
1048
+			fclose($tmpHandle);
1049
+			throw new Exception('Upload data rejected: No file with temporary stream.');
1050
+		}
1051
+
1052
+		$bytes = fwrite($tmpHandle, $buffer);
1053
+		if (false === $bytes) {
1054
+			fclose($tmpHandle);
1055
+			throw new Exception('Upload data rejected: Failed to write to temporary stream.');
1056
+		}
1057
+
1058
+		$diff = strlen($buffer) - $bytes;
1059
+		if ($diff !== 0) {
1060
+			fclose($tmpHandle);
1061
+			throw new Exception(sprintf("Upload data rejected: Unexpected difference writing to temporary stream: %d bytes", $diff));
1062
+		}
1063
+
1064
+		$result = rewind($tmpHandle);
1065
+		if (false === $result) {
1066
+			fclose($tmpHandle);
1067
+			throw new Exception('Upload data rejected: Failed to rewind temporary stream.');
1068
+		}
1069
+
1070
+		$result = $this->uploadFilePath($uploadLink, $parentDir, $relativePath, $tmpFile, $fileName, $replace);
1071
+		fclose($tmpHandle);
1072
+
1073
+		return $result;
1074
+	}
1075
+
1076
+	/**
1077
+	 * upload file
1078
+	 *
1079
+	 * @link https://download.seafile.com/published/web-api/v2.1/file-upload.md#user-content-Upload%20File
1080
+	 *
1081
+	 * @param string $uploadLink from {@see uploadGetLink}
1082
+	 * @param string $parentDir the parent directory to upload a file to
1083
+	 * @param string $relativePath to place the file in under $uploadPath (can include subdirectories)
1084
+	 * @param string $path path of the file to upload
1085
+	 * @param ?string $fileName to use as basename for the file (the name used in Seafile)
1086
+	 * @param bool $replace
1087
+	 * @return object
1088
+	 * @throws Exception
1089
+	 */
1090
+	public function uploadFilePath(
1091
+		string $uploadLink, string $parentDir, string $relativePath,
1092
+		string $path, string $fileName = null, bool $replace = false
1093
+	): object
1094
+	{
1095
+		$parentDir = $this->normalizePath($parentDir);
1096
+		$relativePath = ltrim('/', $this->normalizePath($relativePath));
1097
+		$fileName = $fileName ?? basename($path);
1098
+
1099
+		$fields = [
1100
+			'file' => new CURLFile($path, 'application/octet-stream', $fileName),
1101
+			'parent_dir' => $parentDir,
1102
+			'relative_path' => $relativePath,
1103
+			'replace' => $replace ? '1' : '0',
1104
+		];
1105
+
1106
+		return $this->jsonDecode(
1107
+			$this->post(
1108
+				"$uploadLink?ret-json=1",
1109
+				$fields,
1110
+				[CURLOPT_HTTPHEADER => ["Authorization: Token $this->token"]],
1111
+			),
1112
+			self::JSON_DECODE_ACCEPT_ARRAY_SINGLE_OBJECT,
1113
+		);
1114
+	}
1115
+
1116
+	/**
1117
+	 * get file upload link
1118
+	 *
1119
+	 * @link https://download.seafile.com/published/web-api/v2.1/file-upload.md#user-content-Get%20Upload%20Link
1120
+	 *
1121
+	 * @param string $lib
1122
+	 * @param string $uploadDir the directory to upload file(s) to
1123
+	 * @return string upload link (https?://...)
1124
+	 * @throws InvalidArgumentException|Exception
1125
+	 */
1126
+	public function uploadGetLink(string $lib, string $uploadDir): string
1127
+	{
1128
+		$lib = $this->verifyLib($lib);
1129
+		$uploadDir = $this->normalizePath($uploadDir);
1130
+		$pathEncoded = rawurlencode($uploadDir);
1131
+
1132
+		return $this->jsonDecode(
1133
+			$this->get(
1134
+				"$this->baseurl/api2/repos/$lib/upload-link/?p=$pathEncoded",
1135
+				[CURLOPT_HTTPHEADER => ["Authorization: Token $this->token"]],
1136
+			),
1137
+			self::JSON_DECODE_ACCEPT_STRING,
1138
+		);
1139
+	}
1140
+
1141
+	public function generateUserAuthToken(string $email): object
1142
+	{
1143
+		return $this->jsonDecode(
1144
+			$this->post(
1145
+				"$this->baseurl/api/v2.1/admin/generate-user-auth-token/",
1146
+				['email' => $email],
1147
+				[CURLOPT_HTTPHEADER => ["Authorization: Token $this->token"]],
1148
+			),
1149
+			self::JSON_DECODE_ACCEPT_OBJECT,
1150
+		);
1151
+	}
1152
+
1153
+	public function getUserActivity(string $email, int $page = 1, int $perPage = 25): object
1154
+	{
1155
+		return $this->jsonDecode(
1156
+			$this->get(
1157
+				"$this->baseurl/api/v2.1/admin/user-activities/?user=$email&page=$page&per_page=$perPage",
1158
+				[CURLOPT_HTTPHEADER => ["Authorization: Token $this->token"]],
1159
+			),
1160
+			self::JSON_DECODE_ACCEPT_OBJECT,
1161
+		);
1162
+	}
1163
+
1164
+	/**
1165
+	 * set authorization token
1166
+	 *
1167
+	 * @param string $token
1168
+	 * @return void
1169
+	 */
1170
+	public function setToken(string $token): void
1171
+	{
1172
+		$this->token = $token;
1173
+	}
1174
+
1175
+	public function listDevices(): array
1176
+	{
1177
+		return $this->jsonDecode(
1178
+			$this->get(
1179
+				"$this->baseurl/api2/devices/",
1180
+				[CURLOPT_HTTPHEADER => ["Authorization: Token $this->token"]],
1181
+			),
1182
+		);
1183
+	}
1184
+
1185
+	public function listStarredItems(): object
1186
+	{
1187
+		return $this->jsonDecode(
1188
+			$this->get(
1189
+				"$this->baseurl/api/v2.1/starred-items/",
1190
+				[CURLOPT_HTTPHEADER => ["Authorization: Token $this->token"]],
1191
+			),
1192
+		);
1193
+	}
1194
+
1195
+	public function listGroups(): object
1196
+	{
1197
+		return $this->jsonDecode(
1198
+			$this->get(
1199
+				"$this->baseurl/api2/groups/",
1200
+				[CURLOPT_HTTPHEADER => ["Authorization: Token $this->token"]],
1201
+			),
1202
+		);
1203
+	}
1204
+
1205
+	public function listInvitations(): array
1206
+	{
1207
+		return $this->jsonDecode(
1208
+			$this->get(
1209
+				"$this->baseurl/api/v2.1/invitations/",
1210
+				[CURLOPT_HTTPHEADER => ["Authorization: Token $this->token"]],
1211
+			),
1212
+		);
1213
+	}
1214
+
1215
+	/**
1216
+	 * @param string $start e.g. date('Y-m-d', time() - 7776000 ); // 3 months
1217
+	 * @param string $end e.g. date('Y-m-d', time());
1218
+	 *
1219
+	 * @return array
1220
+	 */
1221
+	public function getLoginLog(string $start, string $end): array
1222
+	{
1223
+		$start = rawurlencode($start);
1224
+		$end = rawurlencode($end);
1225
+
1226
+		return $this->jsonDecode(
1227
+			$this->get(
1228
+				"$this->baseurl/api/v2.1/admin/logs/login/?start=$start&end=$end",
1229
+				[CURLOPT_HTTPHEADER => ["Authorization: Token $this->token"]],
1230
+			),
1231
+		);
1232
+	}
1233
+
1234
+	public function listUploadLinks(): array
1235
+	{
1236
+		return $this->jsonDecode(
1237
+			$this->get(
1238
+				"$this->baseurl/api/v2.1/upload-links/",
1239
+				[CURLOPT_HTTPHEADER => ["Authorization: Token $this->token"]],
1240
+			),
1241
+		);
1242
+	}
1243
+
1244
+	public function listRepoApiTokens(string $lib): object
1245
+	{
1246
+		$lib = $this->verifyLib($lib);
1247
+		return $this->jsonDecode(
1248
+			$this->get(
1249
+				"$this->baseurl/api/v2.1/repos/$lib/repo-api-tokens/",
1250
+				[CURLOPT_HTTPHEADER => ["Authorization: Token $this->token"]],
1251
+			),
1252
+		);
1253
+	}
1254
+
1255
+	/**
1256
+	 * internal api implementation to get library by name
1257
+	 *
1258
+	 * @param string $name
1259
+	 * @throws Exception
1260
+	 * @return ?string id (guid) of the library, null if library does not exist
1261
+	 */
1262
+	private function getLibraryIdByLibraryName(string $name): ?string
1263
+	{
1264
+		$name = explode('/', $this->normalizePath($name), 2)[1];
1265
+
1266
+		$libraries = $this->listLibrariesCached();
1267
+		$libraries = array_column($libraries, null, 'name');
1268
+		return $libraries[$name]->id ?? null;
1269
+	}
1270
+
1271
+	/**
1272
+	 * List libraries a user can access.
1273
+	 *
1274
+	 * internal api implementation for {@see listLibrariesCached()} and {@see listLibraries()}
1275
+	 *
1276
+	 * @link https://download.seafile.com/published/web-api/v2.1/libraries.md#user-content-List%20Libraries
1277
+	 *
1278
+	 * @throws Exception
1279
+	 * @return object[]
1280
+	 */
1281
+	private function listLibrariesDo(): array
1282
+	{
1283
+		return $this->jsonDecode(
1284
+			$this->get(
1285
+				"$this->baseurl/api2/repos/",
1286
+				[CURLOPT_HTTPHEADER => ["Authorization: Token $this->token"]],
1287
+			),
1288
+			self::JSON_DECODE_ACCEPT_ARRAY_OF_OBJECTS,
1289
+		);
1290
+	}
1291
+
1292
+	/**
1293
+	 * like {@see listLibraries()} but cached.
1294
+	 *
1295
+	 * @param bool $invalidate
1296
+	 * @throws Exception
1297
+	 * @throws InvalidResponseException
1298
+	 * @return void
1299
+	 */
1300
+	private function listLibrariesCached(bool $invalidate = false): array
1301
+	{
1302
+		static $librariesCache;
1303
+
1304
+		return $librariesCache = ($invalidate ? null : $librariesCache) ?? $this->listLibrariesDo();
1305
+	}
1306
+
1307
+	/**
1308
+	 * normalize path
1309
+	 *
1310
+	 * normalizes the path component separator <slash> "/" <U002F> U+002F SOLIDUS
1311
+	 * with first character being the slash, no consecutive slashes within the
1312
+	 * path and no terminating slash.
1313
+	 *
1314
+	 * @param string $path
1315
+	 * @return string
1316
+	 */
1317
+	private function normalizePath(string $path): string
1318
+	{
1319
+		$buffer = rtrim($path, '/');
1320
+		$buffer = preg_replace('~/{2,}~', '/', $buffer);
1321
+		'' === $buffer && $buffer = '/';
1322
+		'/' === $buffer[0] || $buffer = "/$buffer";
1323
+
1324
+		return $buffer;
1325
+	}
1326
+
1327
+	/**
1328
+	 * verify library id
1329
+	 *
1330
+	 * verifies the format of a library id. can be used in URLs
1331
+	 * afterwards. case normalization to lowercase.
1332
+	 *
1333
+	 * example library id strings:
1334
+	 *  - 21b941c2-5411-4372-a514-00b62ab99ef2 (from the docs)
1335
+	 *  - 79144b25-f772-42b6-a1c0-60e6359f5884 (from a server)
1336
+	 *
1337
+	 * @param string $lib
1338
+	 * @param bool $allowEmpty
1339
+	 * @return string library id
1340
+	 */
1341
+	private function verifyLib(string $lib, bool $allowEmpty = false): string
1342
+	{
1343
+		if ($allowEmpty && ('' === $lib)) {
1344
+			return $lib;
1345
+		}
1346
+
1347
+		$buffer = strtr($lib, self::HEX_ALPHA_UPPER, self::HEX_ALPHA_LOWER);
1348
+		$format = '%04x%04x-%04x-%04x-%04x-%04x%04x%04x';
1349
+		$values = sscanf($buffer, $format);
1350
+		$result = vsprintf($format, $values);
1351
+
1352
+		if ($buffer !== $result) {
1353
+			throw new InvalidArgumentException(sprintf('Not a library id: "%s"', $lib));
1354
+		}
1355
+
1356
+		return $result;
1357
+	}
1358
+
1359
+	/**
1360
+	 * verify share link token
1361
+	 *
1362
+	 * verifies the format of a share link token. can be used in URLs
1363
+	 * afterwards. case normalization to lowercase.
1364
+	 *
1365
+	 * @param string $token e.g. "0a29ff44dc0b4b56be74"
1366
+	 * @return string
1367
+	 */
1368
+	private function verifyToken(string $token): string
1369
+	{
1370
+		$buffer = strtr($token, self::HEX_ALPHA_UPPER, self::HEX_ALPHA_LOWER);
1371
+		$format = '%04x%04x%04x%04x%04x';
1372
+		$values = sscanf($buffer, $format);
1373
+		$result = vsprintf($format, $values);
1374
+
1375
+		if ($buffer !== $result) {
1376
+			throw new InvalidArgumentException(sprintf('Not a token: "%s"', $token));
1377
+		}
1378
+
1379
+		return $result;
1380
+	}
1381
+
1382
+	/**
1383
+	 * authenticate class against seafile api
1384
+	 *
1385
+	 * @link https://download.seafile.com/published/web-api/home.md#user-content-Quick%20Start
1386
+	 *
1387
+	 * @param string|null $otp (optional) Seafile OTP (if user uses OTP access)
1388
+	 * @return void
1389
+	 */
1390
+	private function setTokenByUsernameAndPassword(string $otp = null): void
1391
+	{
1392
+		// @auth:token:<email> : password is auth token
1393
+		if (0 === strpos($this->user, $needle = self::USER_PREFIX_AUTH_TOKEN)) {
1394
+			$this->user = \substr($this->user, strlen($needle)) ?: '';
1395
+			$this->token = $this->pass;
1396
+			if ('pong' !== $this->ping()) {
1397
+				throw new ConnectionException('token authentication failure');
1398
+			}
1399
+			return;
1400
+		}
1401
+
1402
+		$data = $this->jsonDecode(
1403
+			$this->post(
1404
+				"$this->baseurl/api2/auth-token/",
1405
+				['username' => $this->user, 'password' => $this->pass],
1406
+				$otp ? [CURLOPT_HTTPHEADER => ["X-SEAFILE-OTP: $otp"]] : [],
1407
+			),
1408
+			self::JSON_DECODE_ACCEPT_OBJECT,
1409
+		);
1410
+		$this->token = (string)$data->token;
1411
+	}
1412
+
1413
+	/**
1414
+	 * http request with get method
1415
+	 *
1416
+	 * @param string $url
1417
+	 * @param array $curlOptions
1418
+	 * @return bool|string
1419
+	 */
1420
+	private function get(string $url, array $curlOptions = [])
1421
+	{
1422
+		$curlOptions += $this->curlSharedOptions;
1423
+
1424
+		return $this->curlExec($url, $curlOptions);
1425
+	}
1426
+
1427
+	/**
1428
+	 * http request with post method
1429
+	 *
1430
+	 * @param string $url
1431
+	 * @param array $fields
1432
+	 * @param array $curlOptions
1433
+	 * @return bool|string
1434
+	 */
1435
+	private function post(string $url, array $fields = [], array $curlOptions = [])
1436
+	{
1437
+		$curlOptions += $this->curlSharedOptions;
1438
+		$curlOptions[CURLOPT_POST] = true;
1439
+		$curlOptions[CURLOPT_POSTFIELDS] = $fields;
1440
+
1441
+		return $this->curlExec($url, $curlOptions);
1442
+	}
1443
+
1444
+	/**
1445
+	 * http request with put method
1446
+	 *
1447
+	 * @param string $url
1448
+	 * @param array $fields
1449
+	 * @param array $curlOptions
1450
+	 * @return bool|string
1451
+	 */
1452
+	public function put(string $url, array $fields = [], array $curlOptions = [])
1453
+	{
1454
+		$curlOptions += $this->curlSharedOptions;
1455
+		$curlOptions[CURLOPT_CUSTOMREQUEST] = 'PUT';
1456
+		$curlOptions[CURLOPT_POSTFIELDS] = $fields;
1457
+
1458
+		return $this->curlExec($url, $curlOptions);
1459
+	}
1460
+
1461
+	/**
1462
+	 * http request with delete method
1463
+	 *
1464
+	 * @param string $url
1465
+	 * @param array $curlOptions
1466
+	 * @return bool|string
1467
+	 */
1468
+	public function delete(string $url, array $curlOptions = [])
1469
+	{
1470
+
1471
+		$curlOptions += $this->curlSharedOptions;
1472
+		$curlOptions[CURLOPT_CUSTOMREQUEST] = 'DELETE';
1473
+
1474
+		return $this->curlExec($url, $curlOptions);
1475
+	}
1476
+
1477
+	/**
1478
+	 * json decode handler
1479
+	 *
1480
+	 * decode json with structural acceptance
1481
+	 *
1482
+	 * @param string $jsonText
1483
+	 * @param int $flags decode accept flag
1484
+	 * @throws InvalidResponseException
1485
+	 * @return string|object|array
1486
+	 */
1487
+	private function jsonDecode(string $jsonText, int $flags = self::JSON_DECODE_ACCEPT_DEFAULT)
1488
+	{
1489
+		$accept = $flags & self::JSON_DECODE_ACCEPT_MASK;
1490
+		if (0 === $accept) {
1491
+			return $jsonText;
1492
+		}
1493
+
1494
+		try {
1495
+			$result = json_decode($jsonText, false, 512, JSON_THROW_ON_ERROR);
1496
+		} /** @noinspection PhpMultipleClassDeclarationsInspection */ catch (JsonException $e) {
1497
+			throw JsonDecodeException::create(sprintf('json decode error of %s', JsonDecodeException::shorten($jsonText)), $jsonText, $e);
1498
+		}
1499
+
1500
+		if (self::JSON_DECODE_ACCEPT_JSON === $accept) {
1501
+			return $result;
1502
+		}
1503
+
1504
+		if (self::JSON_DECODE_ACCEPT_ARRAY_OF_OBJECTS === $accept) {
1505
+			if (is_array($result) && $result === array_filter($result, 'is_object')) {
1506
+				return $result;
1507
+			}
1508
+			throw JsonDecodeException::create(sprintf('json decode accept %5d error [%s] of %s', decbin($accept), gettype($result), JsonDecodeException::shorten($jsonText)), $jsonText);
1509
+		}
1510
+
1511
+		if (self::JSON_DECODE_ACCEPT_ARRAY_SINGLE_OBJECT_NULLABLE === $accept) {
1512
+			if (is_array($result)
1513
+				&& (
1514
+					(count($result) === 1 && is_object($result[0] ?? null))
1515
+					|| (count($result) === 0)
1516
+				)
1517
+			) {
1518
+				return $result[0] ?? null;
1519
+			}
1520
+			throw JsonDecodeException::create(sprintf('json decode accept %5d error [%s] of %s', decbin($accept), gettype($result), JsonDecodeException::shorten($jsonText)), $jsonText);
1521
+		}
1522
+
1523
+		if (self::JSON_DECODE_ACCEPT_ARRAY_SINGLE_OBJECT === $accept) {
1524
+			if (is_array($result) && is_object($result[0] ?? null) && count($result) === 1) {
1525
+				return $result[0];
1526
+			}
1527
+			throw JsonDecodeException::create(sprintf('json decode accept %5d error [%s] of %s', decbin($accept), gettype($result), JsonDecodeException::shorten($jsonText)), $jsonText);
1528
+		}
1529
+
1530
+		if (self::JSON_DECODE_ACCEPT_SUCCESS_OBJECT === $accept) {
1531
+			if (is_object($result) && (array)$result === ['success' => true]) {
1532
+				return $result;
1533
+			}
1534
+			throw JsonDecodeException::create(sprintf('json decode accept %5d error [%s] of %s', decbin($accept), gettype($result), JsonDecodeException::shorten($jsonText)), $jsonText);
1535
+		}
1536
+
1537
+		if (self::JSON_DECODE_ACCEPT_SUCCESS_STRING === $accept) {
1538
+			if (self::STRING_SUCCESS === $result) {
1539
+				return $result;
1540
+			}
1541
+			throw JsonDecodeException::create(sprintf('json decode accept %5d error [%s] of %s', decbin($accept), gettype($result), JsonDecodeException::shorten($jsonText)), $jsonText);
1542
+		}
1543
+
1544
+		if (is_string($result) && (self::JSON_DECODE_ACCEPT_STRING !== ($accept & self::JSON_DECODE_ACCEPT_STRING))) {
1545
+			throw JsonDecodeException::create(sprintf('json decode type %s not accepted; of %s', gettype($result), JsonDecodeException::shorten($jsonText)), $jsonText);
1546
+		}
1547
+
1548
+		if (is_array($result) && (self::JSON_DECODE_ACCEPT_ARRAY !== ($accept & self::JSON_DECODE_ACCEPT_ARRAY))) {
1549
+			throw JsonDecodeException::create(sprintf('json decode type %s not accepted; of %s', gettype($result), JsonDecodeException::shorten($jsonText)), $jsonText);
1550
+		}
1551
+
1552
+		if (is_object($result) && (self::JSON_DECODE_ACCEPT_OBJECT !== ($accept & self::JSON_DECODE_ACCEPT_OBJECT))) {
1553
+			throw JsonDecodeException::create(sprintf('json decode type %s not accepted; of %s', gettype($result), JsonDecodeException::shorten($jsonText)), $jsonText);
1554
+		}
1555
+
1556
+		return $result;
1557
+	}
1558
+
1559
+
1560
+	/**
1561
+	 * execute curl with url and options
1562
+	 *
1563
+	 * @param string $url
1564
+	 * @param array $options
1565
+	 * @return bool|string
1566
+	 */
1567
+	private function curlExec(string $url, array $options)
1568
+	{
1569
+		$handle = curl_init($url);
1570
+		if (!($handle instanceof CurlHandle)) {
1571
+			throw new ConnectionException('Unable to initialise cURL session.', self::ERROR_CODE_NO_CURL);
1572
+		}
1573
+
1574
+		$this->handle = $handle;
1575
+
1576
+		if (!curl_setopt_array($this->handle, $options)) {
1577
+			throw new Exception("Error setting cURL request options.");
1578
+		}
1579
+		$result = curl_exec($this->handle);
1580
+		$this->curlExecHandleResult($result);
1581
+		curl_close($this->handle);
1582
+
1583
+		return $result;
1584
+	}
1585
+
1586
+	/**
1587
+	 * internal handling of curl_exec() return
1588
+	 *
1589
+	 * {@see curlExec()}
1590
+	 *
1591
+	 * @param bool|string $curlResult return value from curl_exec();
1592
+	 * @throws ConnectionException
1593
+	 * @return void
1594
+	 */
1595
+	private function curlExecHandleResult($curlResult): void
1596
+	{
1597
+		if (empty($curlResult)) {
1598
+			throw new ConnectionException(curl_error($this->handle), -1);
1599
+		}
1600
+
1601
+		$code = (int)curl_getinfo($this->handle)['http_code'];
1602
+
1603
+		$codeIsInErrorRange = $code >= 400 && $code <= 600;
1604
+		$codeIsNotInNonErrorCodes = !in_array($code, [200, 201, 202, 203, 204, 205, 206, 207, 301], true);
1605
+
1606
+		if ($codeIsInErrorRange || $codeIsNotInNonErrorCodes) {
1607
+			ConnectionException::throwCurlResult($code, $curlResult);
1608
+		}
1609
+	}
1610 1610
 }
Please login to merge, or discard this patch.
plugins/filesbackendSeafile/php/lib/seafapi/autoload.php 1 patch
Indentation   +15 added lines, -15 removed lines patch added patch discarded remove patch
@@ -5,23 +5,23 @@
 block discarded – undo
5 5
 namespace Datamate\SeafileApi;
6 6
 
7 7
 if (!is_callable('spl_autoload_register')) {
8
-    throw new \RuntimeException('spl_autoload_register: n/a');
8
+	throw new \RuntimeException('spl_autoload_register: n/a');
9 9
 }
10 10
 
11 11
 \spl_autoload_register(
12
-    static function ($className) {
13
-        $namespacePrefix = __NAMESPACE__ . '\\';
14
-        $basePath = __DIR__ . '/';
15
-        if (0 !== strpos($className, $namespacePrefix)) {
16
-            return;
17
-        }
12
+	static function ($className) {
13
+		$namespacePrefix = __NAMESPACE__ . '\\';
14
+		$basePath = __DIR__ . '/';
15
+		if (0 !== strpos($className, $namespacePrefix)) {
16
+			return;
17
+		}
18 18
 
19
-        $classNamespaceSuffix = substr($className, strlen($namespacePrefix));
20
-        $classFileName = $basePath . str_replace('\\', '/', $classNamespaceSuffix) . '.php';
21
-        if (is_file($classFileName)) {
22
-            require($classFileName);
23
-        } else {
24
-            throw new \RuntimeException(sprintf('class: %s: no such file: %s', $className, $classFileName));
25
-        }
26
-    }
19
+		$classNamespaceSuffix = substr($className, strlen($namespacePrefix));
20
+		$classFileName = $basePath . str_replace('\\', '/', $classNamespaceSuffix) . '.php';
21
+		if (is_file($classFileName)) {
22
+			require($classFileName);
23
+		} else {
24
+			throw new \RuntimeException(sprintf('class: %s: no such file: %s', $className, $classFileName));
25
+		}
26
+	}
27 27
 );
Please login to merge, or discard this patch.
php/lib/seafapi/Exception/UnexpectedJsonTextResponseException.php 1 patch
Indentation   +37 added lines, -37 removed lines patch added patch discarded remove patch
@@ -4,41 +4,41 @@
 block discarded – undo
4 4
 
5 5
 final class UnexpectedJsonTextResponseException extends InvalidResponseException
6 6
 {
7
-    /**
8
-     * @var string the unexpected JSON Text of the API response
9
-     */
10
-    private $jsonText;
11
-
12
-    /**
13
-     * utility method to shorten a string (e.g. to create shorter messages)
14
-     *
15
-     * @param string $buffer
16
-     * @param int $size
17
-     * @return string ASCII, printable with other characters escaped (C-slashes)
18
-     */
19
-    public static function shorten(string $buffer, int $size = 32): string
20
-    {
21
-        if ($size >= $len = strlen($buffer)) {
22
-            $buffer = addcslashes($buffer, "\0..\37\42\134\177..\377");
23
-            return "\"$buffer\"";
24
-        }
25
-
26
-        $buffer = substr($buffer, 0, max(0, $size - 8));
27
-        $buffer = addcslashes($buffer, "\0..\37\42\134\177..\377");
28
-
29
-        return "($len) \"$buffer ...";
30
-    }
31
-
32
-    public static function create(string $message, string $jsonText, \Throwable $previous = null): UnexpectedJsonTextResponseException
33
-    {
34
-        $exception = new self($message, 500, $previous);
35
-        $exception->jsonText = $jsonText;
36
-
37
-        return $exception;
38
-    }
39
-
40
-    public function getJsonText(): string
41
-    {
42
-        return $this->jsonText;
43
-    }
7
+	/**
8
+	 * @var string the unexpected JSON Text of the API response
9
+	 */
10
+	private $jsonText;
11
+
12
+	/**
13
+	 * utility method to shorten a string (e.g. to create shorter messages)
14
+	 *
15
+	 * @param string $buffer
16
+	 * @param int $size
17
+	 * @return string ASCII, printable with other characters escaped (C-slashes)
18
+	 */
19
+	public static function shorten(string $buffer, int $size = 32): string
20
+	{
21
+		if ($size >= $len = strlen($buffer)) {
22
+			$buffer = addcslashes($buffer, "\0..\37\42\134\177..\377");
23
+			return "\"$buffer\"";
24
+		}
25
+
26
+		$buffer = substr($buffer, 0, max(0, $size - 8));
27
+		$buffer = addcslashes($buffer, "\0..\37\42\134\177..\377");
28
+
29
+		return "($len) \"$buffer ...";
30
+	}
31
+
32
+	public static function create(string $message, string $jsonText, \Throwable $previous = null): UnexpectedJsonTextResponseException
33
+	{
34
+		$exception = new self($message, 500, $previous);
35
+		$exception->jsonText = $jsonText;
36
+
37
+		return $exception;
38
+	}
39
+
40
+	public function getJsonText(): string
41
+	{
42
+		return $this->jsonText;
43
+	}
44 44
 }
Please login to merge, or discard this patch.
filesbackendSeafile/php/lib/seafapi/Exception/ConnectionException.php 1 patch
Indentation   +150 added lines, -150 removed lines patch added patch discarded remove patch
@@ -12,154 +12,154 @@
 block discarded – undo
12 12
  */
13 13
 final class ConnectionException extends Exception
14 14
 {
15
-    /**
16
-     * HTTP status code of the response
17
-     */
18
-    private ?int $responseCode = null;
19
-
20
-    /**
21
-     * HTTP raw response (if any)
22
-     */
23
-    private ?string $responseBodyRaw = null;
24
-
25
-    private const HTTP_STATUS = [
26
-        200 => 'OK',
27
-        201 => 'Created',
28
-        202 => 'Accepted',
29
-        301 => 'Moved Permanently',
30
-        400 => 'Bad Request',
31
-        401 => 'Unauthorized',
32
-        403 => 'Forbidden',
33
-        404 => 'Not Found',
34
-        409 => 'Conflict',
35
-        429 => 'Too Many Requests',
36
-        440 => 'REPO_PASSWD_REQUIRED',
37
-        441 => 'REPO_PASSWD_MAGIC_REQUIRED',
38
-        500 => 'Internal Server Error',
39
-        520 => 'OPERATION_FAILED',
40
-    ];
41
-
42
-    /**
43
-     * @param int $code HTTP status code, e.g. curl_getinfo($curl)['http_code']
44
-     * @param bool|string $curlResult return value from curl_exec();
45
-     * @throws ConnectionException
46
-     * @return no-return
47
-     */
48
-    public static function throwCurlResult(int $code, string|bool $curlResult): never
49
-    {
50
-        $exception = new self(self::reasonPhrase($code), $code);
51
-        $exception->responseCode = $code;
52
-        $exception->responseBodyRaw = is_string($curlResult) ? $curlResult : null;
53
-
54
-        throw $exception;
55
-    }
56
-
57
-    private static function reasonPhrase(int $code): string
58
-    {
59
-        return sprintf('%s %s', $code, self::HTTP_STATUS[$code] ?? "UNKNOWN_PHRASE");
60
-    }
61
-
62
-    public function __construct(string $message = "", int $code = 0, ?Throwable $previous = null)
63
-    {
64
-        // trigger E_USER_NOTICE if code is not known
65
-        $isHttpCode = 100 <= $code && $code < 600;
66
-        $isKnownHttpCode = $isHttpCode && isset(self::HTTP_STATUS[$code]);
67
-        $isKnownCode = $code === -1 || $isKnownHttpCode;
68
-        $this->responseCode = $isHttpCode ? $code : null;
69
-        $isKnownCode || trigger_error(sprintf("%s: Unknown code: %s (%s)", __CLASS__, $code, gettype($code)), E_USER_NOTICE);
70
-
71
-        parent::__construct($message, $code, $previous);
72
-    }
73
-
74
-    public function getStatusCode(): ?int
75
-    {
76
-        return $this->responseCode;
77
-    }
78
-
79
-    /**
80
-     * @param int $code
81
-     * @throws ConnectionException
82
-     * @return void
83
-     */
84
-    public function assertStatusCode(int $code): void
85
-    {
86
-        if ($this->responseCode !== $code) {
87
-            throw $this;
88
-        }
89
-    }
90
-
91
-    /**
92
-     * The raw response body
93
-     *
94
-     * @return string|null
95
-     */
96
-    public function getRawResponse(): ?string
97
-    {
98
-        return $this->responseBodyRaw;
99
-    }
100
-
101
-    /**
102
-     * Response body parsed as JSON Object
103
-     *
104
-     * @return null|object - null either if the parsed response is NULL or if it can't be parsed as object
105
-     */
106
-    public function tryParsedResponse(): ?object
107
-    {
108
-        /**
109
-         * @noinspection JsonEncodingApiUsageInspection
110
-         * @noinspection RedundantSuppression
111
-         */
112
-        $result = json_decode((string)$this->responseBodyRaw, false);
113
-        return is_object($result) ? $result : null;
114
-    }
115
-
116
-    public function getReasonPhrase(): ?string
117
-    {
118
-        $code = $this->responseCode;
119
-
120
-        if (!is_int($code)) {
121
-            return null;
122
-        }
123
-
124
-        return self::HTTP_STATUS[$code] ?? null;
125
-
126
-    }
127
-
128
-    /**
129
-     * A seafile JSON response may contain error information, try to get them.
130
-     *
131
-     * It is not fool-proof or overly complete but often more informative than just looking at JSON response dumps.
132
-     *
133
-     * @return null|array|string[] messages, null if n/a otherwise array of messages (which can not be empty)
134
-     */
135
-    public function tryApiErrorMessages(): ?array
136
-    {
137
-        $response = $this->tryParsedResponse();
138
-        if (null === $response) {
139
-            return null;
140
-        }
141
-
142
-        $buffer = [];
143
-
144
-        // {"error_msg": ... }
145
-        if (isset($response->error_msg) && is_string($response->error_msg)) {
146
-             $buffer[] = $response->error_msg;
147
-        }
148
-
149
-        // {"detail": "Invalid token header. No credentials provided."}
150
-        if (isset($response->detail) && is_string($response->detail)) {
151
-            $buffer[] = $response->detail;
152
-        }
153
-
154
-        // {"non_field_errors": [ "string...", ...]}
155
-        if (isset($response->non_field_errors) && is_array($response->non_field_errors)) {
156
-            foreach ($response->non_field_errors as $message) {
157
-                if (is_string($message)) {
158
-                    $buffer[] = $message;
159
-                }
160
-            }
161
-        }
162
-
163
-        return empty($buffer) ? null : $buffer;
164
-    }
15
+	/**
16
+	 * HTTP status code of the response
17
+	 */
18
+	private ?int $responseCode = null;
19
+
20
+	/**
21
+	 * HTTP raw response (if any)
22
+	 */
23
+	private ?string $responseBodyRaw = null;
24
+
25
+	private const HTTP_STATUS = [
26
+		200 => 'OK',
27
+		201 => 'Created',
28
+		202 => 'Accepted',
29
+		301 => 'Moved Permanently',
30
+		400 => 'Bad Request',
31
+		401 => 'Unauthorized',
32
+		403 => 'Forbidden',
33
+		404 => 'Not Found',
34
+		409 => 'Conflict',
35
+		429 => 'Too Many Requests',
36
+		440 => 'REPO_PASSWD_REQUIRED',
37
+		441 => 'REPO_PASSWD_MAGIC_REQUIRED',
38
+		500 => 'Internal Server Error',
39
+		520 => 'OPERATION_FAILED',
40
+	];
41
+
42
+	/**
43
+	 * @param int $code HTTP status code, e.g. curl_getinfo($curl)['http_code']
44
+	 * @param bool|string $curlResult return value from curl_exec();
45
+	 * @throws ConnectionException
46
+	 * @return no-return
47
+	 */
48
+	public static function throwCurlResult(int $code, string|bool $curlResult): never
49
+	{
50
+		$exception = new self(self::reasonPhrase($code), $code);
51
+		$exception->responseCode = $code;
52
+		$exception->responseBodyRaw = is_string($curlResult) ? $curlResult : null;
53
+
54
+		throw $exception;
55
+	}
56
+
57
+	private static function reasonPhrase(int $code): string
58
+	{
59
+		return sprintf('%s %s', $code, self::HTTP_STATUS[$code] ?? "UNKNOWN_PHRASE");
60
+	}
61
+
62
+	public function __construct(string $message = "", int $code = 0, ?Throwable $previous = null)
63
+	{
64
+		// trigger E_USER_NOTICE if code is not known
65
+		$isHttpCode = 100 <= $code && $code < 600;
66
+		$isKnownHttpCode = $isHttpCode && isset(self::HTTP_STATUS[$code]);
67
+		$isKnownCode = $code === -1 || $isKnownHttpCode;
68
+		$this->responseCode = $isHttpCode ? $code : null;
69
+		$isKnownCode || trigger_error(sprintf("%s: Unknown code: %s (%s)", __CLASS__, $code, gettype($code)), E_USER_NOTICE);
70
+
71
+		parent::__construct($message, $code, $previous);
72
+	}
73
+
74
+	public function getStatusCode(): ?int
75
+	{
76
+		return $this->responseCode;
77
+	}
78
+
79
+	/**
80
+	 * @param int $code
81
+	 * @throws ConnectionException
82
+	 * @return void
83
+	 */
84
+	public function assertStatusCode(int $code): void
85
+	{
86
+		if ($this->responseCode !== $code) {
87
+			throw $this;
88
+		}
89
+	}
90
+
91
+	/**
92
+	 * The raw response body
93
+	 *
94
+	 * @return string|null
95
+	 */
96
+	public function getRawResponse(): ?string
97
+	{
98
+		return $this->responseBodyRaw;
99
+	}
100
+
101
+	/**
102
+	 * Response body parsed as JSON Object
103
+	 *
104
+	 * @return null|object - null either if the parsed response is NULL or if it can't be parsed as object
105
+	 */
106
+	public function tryParsedResponse(): ?object
107
+	{
108
+		/**
109
+		 * @noinspection JsonEncodingApiUsageInspection
110
+		 * @noinspection RedundantSuppression
111
+		 */
112
+		$result = json_decode((string)$this->responseBodyRaw, false);
113
+		return is_object($result) ? $result : null;
114
+	}
115
+
116
+	public function getReasonPhrase(): ?string
117
+	{
118
+		$code = $this->responseCode;
119
+
120
+		if (!is_int($code)) {
121
+			return null;
122
+		}
123
+
124
+		return self::HTTP_STATUS[$code] ?? null;
125
+
126
+	}
127
+
128
+	/**
129
+	 * A seafile JSON response may contain error information, try to get them.
130
+	 *
131
+	 * It is not fool-proof or overly complete but often more informative than just looking at JSON response dumps.
132
+	 *
133
+	 * @return null|array|string[] messages, null if n/a otherwise array of messages (which can not be empty)
134
+	 */
135
+	public function tryApiErrorMessages(): ?array
136
+	{
137
+		$response = $this->tryParsedResponse();
138
+		if (null === $response) {
139
+			return null;
140
+		}
141
+
142
+		$buffer = [];
143
+
144
+		// {"error_msg": ... }
145
+		if (isset($response->error_msg) && is_string($response->error_msg)) {
146
+			 $buffer[] = $response->error_msg;
147
+		}
148
+
149
+		// {"detail": "Invalid token header. No credentials provided."}
150
+		if (isset($response->detail) && is_string($response->detail)) {
151
+			$buffer[] = $response->detail;
152
+		}
153
+
154
+		// {"non_field_errors": [ "string...", ...]}
155
+		if (isset($response->non_field_errors) && is_array($response->non_field_errors)) {
156
+			foreach ($response->non_field_errors as $message) {
157
+				if (is_string($message)) {
158
+					$buffer[] = $message;
159
+				}
160
+			}
161
+		}
162
+
163
+		return empty($buffer) ? null : $buffer;
164
+	}
165 165
 }
Please login to merge, or discard this patch.