Passed
Push — master ( 37cafd...a8b392 )
by
unknown
07:18
created
plugins/filesbackendDefault/php/class.plugindefault.php 2 patches
Indentation   +51 added lines, -51 removed lines patch added patch discarded remove patch
@@ -7,58 +7,58 @@
 block discarded – undo
7 7
  */
8 8
 class PluginFilesbackendDefault extends Plugin
9 9
 {
10
-    /**
11
-     * Constructor.
12
-     */
13
-    public function __construct()
14
-    {
15
-    }
10
+	/**
11
+	 * Constructor.
12
+	 */
13
+	public function __construct()
14
+	{
15
+	}
16 16
 
17
-    /**
18
-     * Called to initialize the plugin and register for hooks.
19
-     */
20
-    public function init()
21
-    {
22
-        $this->registerHook('server.core.settings.init.before');
23
-    }
17
+	/**
18
+	 * Called to initialize the plugin and register for hooks.
19
+	 */
20
+	public function init()
21
+	{
22
+		$this->registerHook('server.core.settings.init.before');
23
+	}
24 24
 
25
-    /**
26
-     * Function is executed when a hook is triggered by the PluginManager.
27
-     *
28
-     * @param string $eventID Identifier of the hook
29
-     * @param array  $data    Reference to the data of the triggered hook
30
-     */
31
-    public function execute($eventID, &$data)
32
-    {
33
-        switch ($eventID) {
34
-            case 'server.core.settings.init.before':
35
-                $this->onBeforeSettingsInit($data);
36
-                break;
37
-        }
38
-    }
25
+	/**
26
+	 * Function is executed when a hook is triggered by the PluginManager.
27
+	 *
28
+	 * @param string $eventID Identifier of the hook
29
+	 * @param array  $data    Reference to the data of the triggered hook
30
+	 */
31
+	public function execute($eventID, &$data)
32
+	{
33
+		switch ($eventID) {
34
+			case 'server.core.settings.init.before':
35
+				$this->onBeforeSettingsInit($data);
36
+				break;
37
+		}
38
+	}
39 39
 
40
-    /**
41
-     * Called when the core Settings class is initialized and ready to accept sysadmin default
42
-     * settings. Registers the sysadmin defaults for the files backend plugin.
43
-     *
44
-     * @param array $data Reference to the data of the triggered hook
45
-     */
46
-    public function onBeforeSettingsInit(&$data)
47
-    {
48
-        $data['settingsObj']->addSysAdminDefaults([
49
-            'zarafa' => [
50
-                'v1' => [
51
-                    'plugins' => [
52
-                        'filesbackendDefault' => [
53
-                            'enable' => true,
54
-                        ],
55
-                        // Compatibility alias for pre-existing configurations
56
-                        'filesbackendOwncloud' => [
57
-                            'enable' => true,
58
-                        ],
59
-                    ],
60
-                ],
61
-            ],
62
-        ]);
63
-    }
40
+	/**
41
+	 * Called when the core Settings class is initialized and ready to accept sysadmin default
42
+	 * settings. Registers the sysadmin defaults for the files backend plugin.
43
+	 *
44
+	 * @param array $data Reference to the data of the triggered hook
45
+	 */
46
+	public function onBeforeSettingsInit(&$data)
47
+	{
48
+		$data['settingsObj']->addSysAdminDefaults([
49
+			'zarafa' => [
50
+				'v1' => [
51
+					'plugins' => [
52
+						'filesbackendDefault' => [
53
+							'enable' => true,
54
+						],
55
+						// Compatibility alias for pre-existing configurations
56
+						'filesbackendOwncloud' => [
57
+							'enable' => true,
58
+						],
59
+					],
60
+				],
61
+			],
62
+		]);
63
+	}
64 64
 }
Please login to merge, or discard this patch.
Braces   +4 added lines, -8 removed lines patch added patch discarded remove patch
@@ -10,15 +10,13 @@  discard block
 block discarded – undo
10 10
     /**
11 11
      * Constructor.
12 12
      */
13
-    public function __construct()
14
-    {
13
+    public function __construct() {
15 14
     }
16 15
 
17 16
     /**
18 17
      * Called to initialize the plugin and register for hooks.
19 18
      */
20
-    public function init()
21
-    {
19
+    public function init() {
22 20
         $this->registerHook('server.core.settings.init.before');
23 21
     }
24 22
 
@@ -28,8 +26,7 @@  discard block
 block discarded – undo
28 26
      * @param string $eventID Identifier of the hook
29 27
      * @param array  $data    Reference to the data of the triggered hook
30 28
      */
31
-    public function execute($eventID, &$data)
32
-    {
29
+    public function execute($eventID, &$data) {
33 30
         switch ($eventID) {
34 31
             case 'server.core.settings.init.before':
35 32
                 $this->onBeforeSettingsInit($data);
@@ -43,8 +40,7 @@  discard block
 block discarded – undo
43 40
      *
44 41
      * @param array $data Reference to the data of the triggered hook
45 42
      */
46
-    public function onBeforeSettingsInit(&$data)
47
-    {
43
+    public function onBeforeSettingsInit(&$data) {
48 44
         $data['settingsObj']->addSysAdminDefaults([
49 45
             'zarafa' => [
50 46
                 'v1' => [
Please login to merge, or discard this patch.
plugins/filesbackendSeafile/php/class.backend.php 3 patches
Indentation   +1034 added lines, -1034 removed lines patch added patch discarded remove patch
@@ -37,1038 +37,1038 @@
 block discarded – undo
37 37
  */
38 38
 final class Backend extends AbstractBackend implements iFeatureVersionInfo
39 39
 {
40
-    public const LOG_CONTEXT = "SeafileBackend"; // Context for the Logger
41
-
42
-    /**
43
-     * @const string gettext domain
44
-     */
45
-    private const GT_DOMAIN = 'plugin_filesbackendSeafile';
46
-
47
-    /**
48
-     * Seafile "usage" number ("bytes") to Grommunio usage display number ("bytes") multiplier.
49
-     *
50
-     * 1 megabyte in bytes within Seafile represents 1 mebibyte in bytes for Grommunio
51
-     *
52
-     * (Seafile Usage "Bytes" U) / 1000 / 1000 * 1024 * 1024 (1.048576)
53
-     */
54
-    private const QUOTA_MULTIPLIER_SEAFILE_TO_GROMMUNIO = 1.048576;
55
-
56
-    /**
57
-     * Error codes
58
-     *
59
-     * @see parseErrorCodeToMessage for description
60
-     * @see Backend::backendException() for Seafile API handling
61
-     */
62
-    private const SFA_ERR_UNAUTHORIZED = 401;
63
-    private const SFA_ERR_FORBIDDEN = 403;
64
-    private const SFA_ERR_NOTFOUND = 404;
65
-    private const SFA_ERR_NOTALLOWED = 405;
66
-    private const SFA_ERR_TIMEOUT = 408;
67
-    private const SFA_ERR_LOCKED = 423;
68
-    private const SFA_ERR_FAILED_DEPENDENCY = 423;
69
-    private const SFA_ERR_INTERNAL = 500;
70
-    private const SFA_ERR_UNREACHABLE = 800;
71
-    private const SFA_ERR_TMP = 801;
72
-    private const SFA_ERR_FEATURES = 802;
73
-    private const SFA_ERR_NO_CURL = 803;
74
-    private const SFA_ERR_UNIMPLEMENTED = 804;
75
-
76
-    /**
77
-     * @var ?SeafileApi The Seafile API client.
78
-     */
79
-    private ?SeafileApi $seafapi = null;
80
-
81
-    /**
82
-     * Configuration data for the Ext JS metaform.
83
-     */
84
-    private array $metaConfig = [];
85
-
86
-    /**
87
-     * Debug flag that mirrors `PLUGIN_FILESBROWSER_LOGLEVEL`.
88
-     */
89
-    private bool $debug = false;
90
-
91
-    private Config $config;
92
-
93
-    private ?SsoBackend $sso = null;
94
-
95
-    /**
96
-     * Backend name used in translations.
97
-     */
98
-    private string $backendTransName = '';
99
-
100
-    /**
101
-     * Seafile backend constructor
102
-     */
103
-    public function __construct()
104
-    {
105
-        // initialization
106
-        $this->debug = PLUGIN_FILESBROWSER_LOGLEVEL === 'DEBUG';
107
-
108
-        $this->config = new Config();
109
-
110
-        $this->init_form();
111
-
112
-        // set backend description
113
-        $this->backendDescription = dgettext(self::GT_DOMAIN, "With this backend, you can connect to any Seafile server.");
114
-
115
-        // set backend display name
116
-        $this->backendDisplayName = "Seafile";
117
-
118
-        // set backend version
119
-        $this->backendVersion = "2.0.68";
120
-
121
-        // set backend name used in translations
122
-        $this->backendTransName = dgettext(self::GT_DOMAIN, 'Files ' . $this->backendDisplayName . ' Backend: ');
123
-    }
124
-
125
-    ////////////////////////////////////////////////////////////////////////////
126
-    /// seafapi backend methods                                              ///
127
-    ////////////////////////////////////////////////////////////////////////////
128
-
129
-    /**
130
-     * Opens the connection to the Seafile server.
131
-     *
132
-     * @throws BackendException if connection is not successful
133
-     * @return boolean true if action succeeded
134
-     */
135
-    public function open()
136
-    {
137
-        $url = $this->config->getApiUrl();
138
-
139
-        try {
140
-            $this->sso->open();
141
-        } catch (Throwable $throwable) {
142
-            $this->backendException($throwable);
143
-        }
144
-
145
-        try {
146
-            $this->seafapi = new SeafileApi($url, $this->config->user, $this->config->pass);
147
-        } catch (Throwable $throwable) {
148
-            $this->backendException($throwable);
149
-        }
150
-
151
-        return true;
152
-    }
153
-
154
-    /**
155
-     * This function will read a list of files and folders from the server.
156
-     *
157
-     * @param string $dir to get listing from
158
-     * @param bool $hidefirst skip the first entry (we ignore this for the Seafile backend)
159
-     * @throws BackendException
160
-     *
161
-     * @return array
162
-     */
163
-    public function ls($dir, $hidefirst = true)
164
-    {
165
-        $timer = new Timer();
166
-        $this->log("[LS] '$dir'");
167
-
168
-        if ('' === trim($dir, '/')) {
169
-            try {
170
-                $listing = $this->seafapi->listLibraries();
171
-            } catch (Throwable $throwable) {
172
-                $this->backendException($throwable);
173
-            }
174
-            goto result;
175
-        }
176
-
177
-        $lsDir = $this->splitGrommunioPath($dir);
178
-        if (null === $lsDir->lib) {
179
-            // the library does not exist, the listing is short.
180
-            $listing = [];
181
-            goto result;
182
-        }
183
-
184
-        try {
185
-            $listing = $this->seafapi->listItemsInDirectory($lsDir->lib, $lsDir->path ?? '');
186
-        } catch (Throwable $throwable) {
187
-            $this->backendException($throwable);
188
-        }
189
-
190
-        result:
191
-
192
-        $result = [];
193
-        $baseDir = rtrim($dir, '/') . '/';
194
-        foreach ($listing as $node) {
195
-            if (!isset($this->seafapi::TYPES[$node->type])) {
196
-                $this->backendException(
197
-                    new \UnexpectedValueException(sprintf('Unhandled Seafile node-type "%s" (for "%s")', $node->type, $node->name))
198
-                );
199
-            }
200
-            $isDir = isset($this->seafapi::TYPES_DIR_LIKE[$node->type]);
201
-            $name = rtrim($baseDir . $node->name, '/') . '/';
202
-            $isDir || $name = rtrim($name, '/');
203
-            $result[$name] = [
204
-                'resourcetype' => $isDir ? 'collection' : 'file',
205
-                'getcontentlength' => $isDir ? null : $node->size,
206
-                'getlastmodified' => date('r', $node->mtime),
207
-                'getcontenttype' => null,
208
-                'quota-used-bytes' => null,
209
-                'quota-available-bytes' => null,
210
-            ];
211
-        }
212
-
213
-        $this->log("[LS] done in $timer seconds.");
214
-        return $result;
215
-    }
216
-
217
-    /**
218
-     * Creates a new directory on the server.
219
-     *
220
-     * @param string $dir
221
-     * @throws BackendException
222
-     * @return bool
223
-     */
224
-    public function mkcol($dir)
225
-    {
226
-        $timer = new Timer();
227
-        $this->log("[MKCOL] '$dir'");
228
-
229
-        if ($this->isLibrary($dir)) {
230
-            // create library
231
-            try {
232
-                $result = $this->seafapi->createLibrary($dir);
233
-                unset($result);
234
-            } catch (Throwable $throwable) {
235
-                $this->backendException($throwable);
236
-            }
237
-            $success = true;
238
-        } else {
239
-            // create directory within library
240
-            $lib = $this->seafapi->getLibraryFromPath($dir)->id;
241
-            [, $path] = explode('/', trim($dir, '/'), 2);
242
-            try {
243
-                $result = $this->seafapi->createNewDirectory($lib, $path);
244
-            } catch (Throwable $throwable) {
245
-                $this->backendException($throwable);
246
-            }
247
-            $success = 'success' === $result;
248
-        }
249
-
250
-        $this->log("[MKCOL] done in $timer seconds.");
251
-        return $success;
252
-    }
253
-
254
-    /**
255
-     * Deletes a files or folder from the backend.
256
-     *
257
-     * @param string $path
258
-     * @throws BackendException
259
-     * @return bool
260
-     */
261
-    public function delete($path)
262
-    {
263
-        $timer = new Timer();
264
-        $this->log("[DELETE] '$path'");
265
-
266
-        if ($this->isLibrary($path)) {
267
-            // delete library
268
-            try {
269
-                $this->seafapi->deleteLibraryByName($path);
270
-                $result = 'success';
271
-            } catch (Throwable $throwable) {
272
-                $this->backendException($throwable);
273
-            }
274
-        } else {
275
-            // delete file or directory within library
276
-            $deletePath = $this->splitGrommunioPath($path);
277
-            try {
278
-                $result = $this->seafapi->deleteFile($deletePath->lib, $deletePath->path);
279
-            } catch (Throwable $throwable) {
280
-                $this->backendException($throwable);
281
-            }
282
-        }
283
-
284
-        $this->log("[DELETE] done in $timer seconds.");
285
-        return 'success' === $result;
286
-    }
287
-
288
-    /**
289
-     * Move a file or collection on the backend server (serverside).
290
-     *
291
-     * @param string $src_path Source path
292
-     * @param string $dst_path Destination path
293
-     * @param bool $overwrite Overwrite file if exists in $dest_path
294
-     * @throws BackendException
295
-     * @return bool
296
-     */
297
-    public function move($src_path, $dst_path, $overwrite = false)
298
-    {
299
-        $timer = new Timer();
300
-        $this->log("[MOVE] '$src_path' -> '$dst_path'");
301
-
302
-        // check if the move operation would move src into itself - error condition
303
-        if (0 === strpos($dst_path, $src_path . '/')) {
304
-            $this->backendError(self::SFA_ERR_FORBIDDEN, 'Moving failed');
305
-        }
306
-
307
-        // move library/file/directory is one of in the following order:
308
-        // 1/5: rename library
309
-        // 2/5: noop - source and destination are the same
310
-        // 3/5: rename file/directory
311
-        // 4/5: move file/directory
312
-        // 5/5: every other operation (e.g. move library into another library) is not implemented
313
-
314
-        $src = $this->splitGrommunioPath($src_path);
315
-        $dst = $this->splitGrommunioPath($dst_path);
316
-
317
-        // 1/5: rename library
318
-        if ($src->path === null && $dst->path === null) {
319
-            if ($dst->lib !== null) {
320
-                // rename to an existing library name (not allowed as not supported)
321
-                $this->backendError(self::SFA_ERR_NOTALLOWED, 'Moving failed');
322
-            }
323
-            try {
324
-                $this->seafapi->renameLibrary($src->libName, $dst->libName);
325
-                $result = true;
326
-            } catch (Throwable $throwable) {
327
-                $this->backendException($throwable);
328
-            }
329
-            goto done;
330
-        }
331
-
332
-        $isIntraLibTransaction = $src->libName === $dst->libName;
333
-
334
-        // 2/5: noop - src and dst are the same
335
-        if ($isIntraLibTransaction && $src->path === $dst->path) {
336
-            // source and destination are the same path, nothing to do
337
-            $result = 'success';
338
-            goto done;
339
-        }
340
-
341
-        $dirNames = array_map('dirname', [$src->path, $dst->path]);
342
-        $pathsHaveSameDirNames = $dirNames[0] === $dirNames[1];
343
-
344
-        // 3/5: rename file/directory
345
-        if ($isIntraLibTransaction && $pathsHaveSameDirNames) {
346
-            try {
347
-                $result = $this->seafapi->renameFile($src->lib, $src->path, basename($dst->path));
348
-            } catch (Throwable $throwable) {
349
-                $this->backendException($throwable);
350
-            }
351
-            goto done;
352
-        }
353
-
354
-        // 4/5: move file/directory
355
-        if (isset($src->path, $dst->lib)) {
356
-            try {
357
-                $result = $this->seafapi->moveFile($src->lib, $src->path, $dst->lib, $dirNames[1]);
358
-            } catch (Throwable $throwable) {
359
-                $this->backendException($throwable);
360
-            }
361
-        }
362
-
363
-        done:
364
-
365
-        // 5/5: every other operation (move library into another library, not implemented)
366
-        if (!isset($result)) {
367
-            $this->backendError(self::SFA_ERR_UNIMPLEMENTED, 'Not implemented.');
368
-        }
369
-
370
-        $this->log("[MOVE] done in $timer seconds.");
371
-        return 'success' === $result;
372
-    }
373
-
374
-    /**
375
-     * Download a remote file to a buffer variable.
376
-     *
377
-     * @param string $path The source path on the server
378
-     * @param mixed $buffer Buffer for the received data
379
-     *
380
-     * @throws BackendException if request is not successful
381
-     *
382
-     * @return boolean true if action succeeded
383
-     */
384
-    public function get($path, &$buffer)
385
-    {
386
-        $timer = new Timer();
387
-        $this->log("[GET] '$path'");
388
-
389
-        $src = $this->splitGrommunioPath($path);
390
-
391
-        try {
392
-            $result = $this->seafapi->downloadFileAsBuffer($src->lib, $src->path);
393
-        } catch (Throwable $throwable) {
394
-            $this->backendException($throwable);
395
-        }
396
-
397
-        $success = false !== $result;
398
-
399
-        if ($success) {
400
-            $buffer = $result;
401
-        }
402
-
403
-        $this->log("[GET] done in $timer seconds.");
404
-        return $success;
405
-    }
406
-
407
-    /**
408
-     * Download a remote file to a local file.
409
-     *
410
-     * @param string $srcpath Source path on server
411
-     * @param string $localpath Destination path on local filesystem
412
-     *
413
-     * @throws BackendException if request is not successful
414
-     *
415
-     * @return boolean true if action succeeded
416
-     */
417
-    public function get_file($srcpath, $localpath)
418
-    {
419
-        $timer = new Timer();
420
-        $this->log("[GET_FILE] '$srcpath' -> '$localpath'");
421
-
422
-        $src = $this->splitGrommunioPath($srcpath);
423
-
424
-        try {
425
-            $result = $this->seafapi->downloadFileToFile($src->lib, $src->path, $localpath);
426
-        } catch (Throwable $throwable) {
427
-            $this->backendException($throwable);
428
-        }
429
-
430
-        $this->log("[GET_FILE] done in $timer seconds.");
431
-        return $result;
432
-    }
433
-
434
-    /**
435
-     * Puts a file into a collection.
436
-     *
437
-     * @param string $path Destination path
438
-     *
439
-     * @string mixed $data Any kind of data
440
-     * @throws BackendException if request is not successful
441
-     *
442
-     * @return boolean true if action succeeded
443
-     */
444
-    public function put($path, $data)
445
-    {
446
-        $timer = new Timer();
447
-        $this->log(sprintf("[PUT] start: path: %s (%d)", $path, strlen($data)));
448
-
449
-        $target = $this->splitGrommunioPath($path);
450
-
451
-        try {
452
-            /** @noinspection PhpUnusedLocalVariableInspection */
453
-            $result = $this->seafapi->uploadBuffer($target->lib, $target->path, $data);
454
-        } catch (Throwable $throwable) {
455
-            $this->backendException($throwable);
456
-        }
457
-
458
-        $this->log("[PUT] done in $timer seconds.");
459
-        return true;
460
-    }
461
-
462
-    /**
463
-     * Upload a local file
464
-     *
465
-     * @param string $path Destination path on the server
466
-     * @param string $filename Local filename for the file that should be uploaded
467
-     *
468
-     * @throws BackendException if request is not successful
469
-     *
470
-     * @return boolean true if action succeeded
471
-     */
472
-    public function put_file($path, $filename)
473
-    {
474
-        $timer = new Timer();
475
-        $this->log(sprintf("[PUT_FILE] %s -> %s", $filename, $path));
476
-
477
-        // filename can be null if an attachment of draft-email that has not been saved
478
-        if (empty($filename)) {
479
-            return false;
480
-        }
481
-
482
-        $target = $this->splitGrommunioPath($path);
483
-
484
-        // put file into users default library if no library given
485
-        if ($target->path === null && $target->libName !== null) {
486
-            try {
487
-                $defaultLibrary = $this->seafapi->getDefaultLibrary();
488
-            } catch (Throwable $throwable) {
489
-                $this->backendException($throwable);
490
-            }
491
-            if (isset($defaultLibrary->repo_id, $defaultLibrary->exists) && true === $defaultLibrary->exists) {
492
-                $target->path = $target->libName;
493
-                $target->libName = null;
494
-                $target->lib = $defaultLibrary->repo_id;
495
-            }
496
-        }
497
-
498
-        try {
499
-            /** @noinspection PhpUnusedLocalVariableInspection */
500
-            $result = $this->seafapi->uploadFile($target->lib, $target->path, $filename);
501
-        } catch (Throwable $throwable) {
502
-            $this->backendException($throwable);
503
-        }
504
-
505
-        $this->log("[PUT_FILE] done in $timer seconds.");
506
-        return true;
507
-    }
508
-
509
-    ////////////////////////////////////////////////////////////////////////////
510
-    /// non-seafapi backend implementation                                   ///
511
-    ////////////////////////////////////////////////////////////////////////////
512
-
513
-    /**
514
-     * Initialize backend from $backend_config array
515
-     *
516
-     * @param $backend_config
517
-     * @return void
518
-     */
519
-    public function init_backend($backend_config)
520
-    {
521
-        $config = $backend_config;
522
-
523
-        if ($backend_config["use_zarafa_credentials"]) {
524
-            // For backward compatibility we will check if the Encryption store exists. If not,
525
-            // we will fall back to the old way of retrieving the password from the session.
526
-            if (class_exists('EncryptionStore')) {
527
-                // Get the username and password from the Encryption store
528
-                $encryptionStore = EncryptionStore::getInstance();
529
-                if ($encryptionStore instanceof EncryptionStore) {
530
-                    $config['user'] = $encryptionStore->get('username');
531
-                    $config['password'] = $encryptionStore->get('password');
532
-                }
533
-            } else {
534
-                $config['user'] = ConfigUtil::loadSmtpAddress();
535
-                $password = $_SESSION['password'];
536
-                if (function_exists('openssl_decrypt')) {
537
-                    /** @noinspection PhpUndefinedConstantInspection */
538
-                    $config['password'] = openssl_decrypt($password, "des-ede3-cbc", PASSWORD_KEY, 0, PASSWORD_IV);
539
-                }
540
-            }
541
-        }
542
-
543
-        $this->config->importConfigArray($config);
544
-
545
-        SsoBackend::bind($this->sso)->initBackend($this->config);
546
-
547
-        Logger::debug(self::LOG_CONTEXT, __FUNCTION__ . ' done.');
548
-    }
549
-
550
-    /**
551
-     * @return false|string
552
-     * @noinspection PhpMultipleClassDeclarationsInspection Grommunio has a \JsonException shim
553
-     */
554
-    public function getFormConfig()
555
-    {
556
-        try {
557
-            $json = json_encode($this->metaConfig, JSON_THROW_ON_ERROR);
558
-        } catch (JsonException $e) {
559
-            $this->log(sprintf('[%s]: %s', get_class($e), $e->getMessage()));
560
-            $json = false;
561
-        }
562
-
563
-        return $json;
564
-    }
565
-
566
-    public function getFormConfigWithData()
567
-    {
568
-        return $this->getFormConfig();
569
-    }
570
-
571
-    /**
572
-     * set debug on (1) or off (0).
573
-     * produces a lot of debug messages in webservers error log if set to on (1).
574
-     *
575
-     * @param boolean $debug enable or disable debugging
576
-     *
577
-     * @return void
578
-     */
579
-    public function set_debug($debug)
580
-    {
581
-        $this->debug = (bool)$debug;
582
-    }
583
-
584
-    ////////////////////////////////////////////////////////////////////////////
585
-    /// not_used_implemented()                                               ///
586
-    ////////////////////////////////////////////////////////////////////////////
587
-
588
-    /**
589
-     * Duplicates a folder on the backend server.
590
-     *
591
-     * @param string $src_path
592
-     * @param string $dst_path
593
-     * @param bool $overwrite
594
-     * @throws BackendException
595
-     * @return bool
596
-     * @noinspection PhpReturnDocTypeMismatchInspection Upstream Interface Issue
597
-     */
598
-    public function copy_coll($src_path, $dst_path, $overwrite = false)
599
-    {
600
-        $this->backendError(self::SFA_ERR_UNIMPLEMENTED, 'Not implemented');
601
-    }
602
-
603
-    /**
604
-     * Duplicates a file on the backend server.
605
-     *
606
-     * @param string $src_path
607
-     * @param string $dst_path
608
-     * @param bool $overwrite
609
-     * @throws BackendException
610
-     * @return bool
611
-     * @noinspection PhpReturnDocTypeMismatchInspection Upstream Interface Issue
612
-     */
613
-    public function copy_file($src_path, $dst_path, $overwrite = false)
614
-    {
615
-        $this->backendError(self::SFA_ERR_UNIMPLEMENTED, 'Not implemented');
616
-    }
617
-
618
-    /**
619
-     * Checks if the given $path exists on the remote server.
620
-     *
621
-     * @param string $path
622
-     * @throws BackendException
623
-     * @return bool
624
-     * @noinspection PhpReturnDocTypeMismatchInspection Upstream Interface Issue
625
-     */
626
-    public function exists($path)
627
-    {
628
-        $this->backendError(self::SFA_ERR_UNIMPLEMENTED, 'Not implemented');
629
-    }
630
-
631
-    /**
632
-     * Gets path information from Seafile server.
633
-     *
634
-     * @param string $path
635
-     * @throws BackendException if request is not successful
636
-     * @return array directory info
637
-     */
638
-    public function gpi($path)
639
-    {
640
-        $this->log("[GPI] '$path'");
641
-        $list = $this->ls(dirname($path), false); // get contents of the parent dir
642
-
643
-        if (isset($list[$path])) {
644
-            return $list[$path];
645
-        }
646
-
647
-        $this->log('[GPI] wrong response from ls');
648
-        $this->backendError(self::SFA_ERR_FAILED_DEPENDENCY, 'Connection failed');
649
-    }
650
-
651
-    /**
652
-     * Checks if the given $path is a folder.
653
-     *
654
-     * @param string $path
655
-     * @throws BackendException
656
-     * @return bool
657
-     * @noinspection PhpReturnDocTypeMismatchInspection Upstream Interface Issue
658
-     */
659
-    public function is_dir($path)
660
-    {
661
-        $this->backendError(self::SFA_ERR_UNIMPLEMENTED, 'Not implemented');
662
-    }
663
-
664
-    /**
665
-     * Checks if the given $path is a file.
666
-     *
667
-     * @param string $path
668
-     * @throws BackendException
669
-     * @return bool
670
-     * @noinspection PhpReturnDocTypeMismatchInspection Upstream Interface Issue
671
-     */
672
-    public function is_file($path)
673
-    {
674
-        $this->backendError(self::SFA_ERR_UNIMPLEMENTED, 'Not implemented');
675
-    }
676
-
677
-    /////////////////////////////////////////////////////////////
678
-    // @see iFeatureVersionInfo implementation                 //
679
-    /////////////////////////////////////////////////////////////
680
-
681
-    /**
682
-     * Return the version string of the server backend.
683
-     *
684
-     * @throws BackendException
685
-     * @return String
686
-     */
687
-    public function getServerVersion()
688
-    {
689
-        try {
690
-            return $this->seafapi->getServerVersion();
691
-        } catch (Throwable $throwable) {
692
-            $this->backendException($throwable);
693
-        }
694
-    }
695
-
696
-    /////////////////////////////////////////////////////////////
697
-    // @see iFeatureQuota implementation                       //
698
-    /////////////////////////////////////////////////////////////
699
-
700
-    /**
701
-     * @param string $dir
702
-     * @return float
703
-     * @noinspection PhpMissingParamTypeInspection
704
-     * @noinspection PhpUnusedParameterInspection
705
-     */
706
-    public function getQuotaBytesUsed($dir)
707
-    {
708
-        $return = $this->seafapi->checkAccountInfo();
709
-
710
-        return ($return->usage ?? 0) * self::QUOTA_MULTIPLIER_SEAFILE_TO_GROMMUNIO;
711
-    }
712
-
713
-    /**
714
-     * @param string $dir
715
-     * @return float|int
716
-     * @noinspection PhpUnusedParameterInspection
717
-     * @noinspection PhpMissingParamTypeInspection
718
-     */
719
-    public function getQuotaBytesAvailable($dir)
720
-    {
721
-        $return = $this->seafapi->checkAccountInfo();
722
-        $avail = $return->total - $return->usage;
723
-        if (-2 === (int)$return->total) {
724
-            return -1;
725
-        }
726
-
727
-        return $avail * self::QUOTA_MULTIPLIER_SEAFILE_TO_GROMMUNIO;
728
-    }
729
-
730
-    /////////////////////////////////////////////////////////////
731
-    // @internal private helper methods                        //
732
-    /////////////////////////////////////////////////////////////
733
-
734
-    /**
735
-     * Initialise form fields
736
-     */
737
-    private function init_form()
738
-    {
739
-        $this->metaConfig = [
740
-            "success" => true,
741
-            "metaData" => [
742
-                "fields" => [
743
-                    [
744
-                        "name" => "server_address",
745
-                        "fieldLabel" => dgettext(self::GT_DOMAIN, 'Server address'),
746
-                        "editor" => [
747
-                            "allowBlank" => false,
748
-                        ],
749
-                    ],
750
-                    [
751
-                        "name" => "server_port",
752
-                        "fieldLabel" => dgettext(self::GT_DOMAIN, 'Server port'),
753
-                        "editor" => [
754
-                            "ref" => "../../portField",
755
-                            "allowBlank" => false,
756
-                        ],
757
-                    ],
758
-                    [
759
-                        "name" => "server_ssl",
760
-                        "fieldLabel" => dgettext(self::GT_DOMAIN, 'Use SSL'),
761
-                        "editor" => [
762
-                            "xtype" => "checkbox",
763
-                            "listeners" => [
764
-                                "check" => "Zarafa.plugins.files.data.Actions.onCheckSSL" // this javascript function will be called!
765
-                            ],
766
-                        ],
767
-                    ],
768
-                    [
769
-                        "name" => "user",
770
-                        "fieldLabel" => dgettext(self::GT_DOMAIN, 'Username'),
771
-                        "editor" => [
772
-                            "ref" => "../../usernameField",
773
-                        ],
774
-                    ],
775
-                    [
776
-                        "name" => "password",
777
-                        "fieldLabel" => dgettext(self::GT_DOMAIN, 'Password'),
778
-                        "editor" => [
779
-                            "ref" => "../../passwordField",
780
-                            "inputType" => "password",
781
-                        ],
782
-                    ],
783
-                    [
784
-                        "name" => "use_zarafa_credentials",
785
-                        "fieldLabel" => dgettext(self::GT_DOMAIN, 'Use grommunio credentials'),
786
-                        "editor" => [
787
-                            "xtype" => "checkbox",
788
-                            "listeners" => [
789
-                                "check" => "Zarafa.plugins.files.data.Actions.onCheckCredentials" // this javascript function will be called!
790
-                            ],
791
-                        ],
792
-                    ],
793
-                ],
794
-                "formConfig" => [
795
-                    "labelAlign" => "left",
796
-                    "columnCount" => 1,
797
-                    "labelWidth" => 80,
798
-                    "defaults" => [
799
-                        "width" => 292,
800
-                    ],
801
-                ],
802
-            ],
803
-
804
-            // here we can specify the default values.
805
-            "data" => [
806
-                "server_address" => "seafile.example.com",
807
-                "server_port" => "443",
808
-                "server_ssl" => "1",
809
-                "server_path" => "",
810
-                "use_zarafa_credentials" => "0",
811
-                "user" => "",
812
-                "password" => "",
813
-            ],
814
-        ];
815
-    }
816
-
817
-    /**
818
-     * split grommunio path into library and library path
819
-     *
820
-     * obtains the seafile library ID (if available, otherwise NULL)
821
-     *
822
-     * return protocol: object{
823
-     *   lib: ?string     # library ID e.g. "ccc60923-8cdf-4cc8-8f71-df86aba3a085"
824
-     *   path: ?string    # path inside library, always prefixed with "/" if set
825
-     *   libName: ?string # name of the library
826
-     * }
827
-     *
828
-     * @param string $grommunioPath
829
-     * @throws Exception
830
-     * @return object
831
-     */
832
-    private function splitGrommunioPath(string $grommunioPath): object
833
-    {
834
-        static $libraries;
835
-        $libraries = $libraries ?? array_column($this->seafapi->listLibraries(), null, 'name');
836
-
837
-        [, $libName, $path] = explode('/', $grommunioPath, 3) + [null, null, null];
838
-        if (null !== $path) {
839
-            $path = "/$path";
840
-        }
841
-        $lib = $libraries[$libName]->id ?? null;
842
-        return (object)['lib' => $lib, 'path' => $path, 'libName' => $libName];
843
-    }
844
-
845
-    /**
846
-     * test if a grommunio path is a library only
847
-     *
848
-     * @param string $grommunioPath
849
-     * @return bool
850
-     */
851
-    private function isLibrary(string $grommunioPath): bool
852
-    {
853
-        return 0 === substr_count(trim($grommunioPath, '/'), '/');
854
-    }
855
-
856
-    /**
857
-     * Turn a Backend error code into a Backend exception
858
-     *
859
-     * @param int $errorCode one of the Backend::SFA_ERR_* codes, e.g. {@see Backend::SFA_ERR_INTERNAL}
860
-     * @param ?string $title msg-id from the plugin_files domain, e.g. 'PHP-CURL not installed'
861
-     * @throws BackendException
862
-     * @return no-return
863
-     */
864
-    private function backendError(int $errorCode, string $title = null)
865
-    {
866
-        $message = $this->parseErrorCodeToMessage($errorCode);
867
-        $title = $this->backendTransName;
868
-        $this->backendErrorThrow($title, $message, $errorCode);
869
-    }
870
-
871
-    /**
872
-     * Throw a BackendException w/ title, message and code
873
-     *
874
-     * @param string $title
875
-     * @param string $message
876
-     * @param int $code
877
-     * @throws BackendException
878
-     * @return no-return
879
-     */
880
-    private function backendErrorThrow(string $title, string $message, int $code = 0)
881
-    {
882
-        /** {@see \Files\Backend\Exception} */
883
-        $exception = new BackendException($message, $code);
884
-        $exception->setTitle($title);
885
-        throw $exception;
886
-    }
887
-
888
-    /**
889
-     * Turn a throwable/exception with the Seafile API into a Backend exception
890
-     *
891
-     * @param Throwable $t
892
-     * @throws BackendException
893
-     * @return no-return
894
-     */
895
-    private function backendException(Throwable $t)
896
-    {
897
-        // if it is already a backend exception, throw it.
898
-        if ($t instanceof BackendException) {
899
-            throw $t;
900
-        }
901
-
902
-        [$callSite, $inFunc] = debug_backtrace();
903
-        $logLabel = "$inFunc[function]:$callSite[line]";
904
-
905
-        $class = get_class($t);
906
-        $message = $t->getMessage();
907
-        $this->log(sprintf('%s: [%s] #%s: %s', $logLabel, $class, $t->getCode(), $message));
908
-
909
-        // All SeafileApi exceptions are handled by this
910
-        if ($t instanceof Exception) {
911
-            $this->backendExceptionSeafapi($t);
912
-        }
913
-
914
-        $this->backendErrorThrow('Error', "[SEAFILE $logLabel] $class: $message", 500);
915
-    }
916
-
917
-    /**
918
-     * Turn an Exception into a BackendException
919
-     *
920
-     * Enriches message information for grommunio with API error messages
921
-     * if a Seafile ConnectionException.
922
-     *
923
-     * helper for {@see Backend::backendException()}
924
-     *
925
-     * @param Exception $exception
926
-     * @throws BackendException
927
-     * @return void
928
-     */
929
-    private function backendExceptionSeafapi(Exception $exception)
930
-    {
931
-        $code = $exception->getCode();
932
-        $message = $exception->getMessage();
933
-
934
-        $apiErrorMessagesHtml = null;
935
-        if ($exception instanceof Exception\ConnectionException) {
936
-            $messages = $exception->tryApiErrorMessages();
937
-            null === $messages || $apiErrorMessagesHtml = implode(
938
-                    "<br/>\n",
939
-                    array_map(static function (string $subject) {
940
-                        return htmlspecialchars($subject, ENT_QUOTES | ENT_HTML5);
941
-                    }, $messages)
942
-                ) . "<br/>\n";
943
-        }
944
-
945
-        if (null !== $apiErrorMessagesHtml) {
946
-            $message .= " - $apiErrorMessagesHtml";
947
-        }
948
-
949
-        $this->backendErrorThrow($this->backendDisplayName . ' Error', $message, $code);
950
-    }
951
-
952
-    /**
953
-     * a simple php error_log wrapper.
954
-     *
955
-     * @param string $err_string error message
956
-     *
957
-     * @return void
958
-     */
959
-    private function log(string $err_string)
960
-    {
961
-        if ($this->debug) {
962
-            Logger::debug(self::LOG_CONTEXT, $err_string);
963
-            $this->debugLog($err_string, 2);
964
-        }
965
-    }
966
-
967
-    /**
968
-     * This function will return a user-friendly error string.
969
-     *
970
-     * Error codes were migrated from WebDav backend.
971
-     *
972
-     * @param int $error_code An error code
973
-     *
974
-     * @return string user friendly error message
975
-     */
976
-    private function parseErrorCodeToMessage(int $error_code)
977
-    {
978
-        $error = $error_code;
979
-
980
-        switch ($error) {
981
-            case CURLE_BAD_PASSWORD_ENTERED:
982
-            case self::SFA_ERR_UNAUTHORIZED:
983
-                $msg = dgettext(self::GT_DOMAIN, 'Unauthorized. Wrong username or password.');
984
-                break;
985
-            case CURLE_SSL_CONNECT_ERROR:
986
-            case CURLE_COULDNT_RESOLVE_HOST:
987
-            case CURLE_COULDNT_CONNECT:
988
-            case CURLE_OPERATION_TIMEOUTED:
989
-            case self::SFA_ERR_UNREACHABLE:
990
-                $msg = dgettext(self::GT_DOMAIN, 'Seafile is not reachable. Correct backend address entered?');
991
-                break;
992
-            case self::SFA_ERR_FORBIDDEN:
993
-                $msg = dgettext(self::GT_DOMAIN, 'You don\'t have enough permissions for this operation.');
994
-                break;
995
-            case self::SFA_ERR_NOTFOUND:
996
-                $msg = dgettext(self::GT_DOMAIN, 'File is not available any more.');
997
-                break;
998
-            case self::SFA_ERR_TIMEOUT:
999
-                $msg = dgettext(self::GT_DOMAIN, 'Connection to server timed out. Retry later.');
1000
-                break;
1001
-            case self::SFA_ERR_LOCKED:
1002
-                $msg = dgettext(self::GT_DOMAIN, 'This file is locked by another user.');
1003
-                break;
1004
-            case self::SFA_ERR_FAILED_DEPENDENCY:
1005
-                $msg = dgettext(self::GT_DOMAIN, 'The request failed due to failure of a previous request.');
1006
-                break;
1007
-            case self::SFA_ERR_INTERNAL:
1008
-                $msg = dgettext(self::GT_DOMAIN, 'Seafile-server encountered a problem.');
1009
-                break;
1010
-            case self::SFA_ERR_TMP:
1011
-                $msg = dgettext(self::GT_DOMAIN, 'Could not write to temporary directory. Contact the server administrator.');
1012
-                break;
1013
-            case self::SFA_ERR_FEATURES:
1014
-                $msg = dgettext(self::GT_DOMAIN, 'Could not retrieve list of server features. Contact the server administrator.');
1015
-                break;
1016
-            case self::SFA_ERR_NO_CURL:
1017
-                $msg = dgettext(self::GT_DOMAIN, 'PHP-Curl is not available. Contact your system administrator.');
1018
-                break;
1019
-            case self::SFA_ERR_UNIMPLEMENTED:
1020
-                $msg = dgettext(self::GT_DOMAIN, 'This function is not yet implemented.');
1021
-                break;
1022
-            default:
1023
-                $msg = dgettext(self::GT_DOMAIN, 'Unknown error');
1024
-        }
1025
-
1026
-        return $msg;
1027
-    }
1028
-
1029
-    /////////////////////////////////////////////////////////////
1030
-    // @debug development helper method                        //
1031
-    /////////////////////////////////////////////////////////////
1032
-
1033
-    /**
1034
-     * Log debug message while developing the plugin in dedicated DEBUG.log file
1035
-     *
1036
-     * TODO(tk): remove debugLog, we shall not use it in production.
1037
-     *
1038
-     * @param mixed $message
1039
-     * @param int $backSteps [optional] offset of call point in stacktrace
1040
-     * @return void
1041
-     * @see \Files\Backend\Seafile\Backend::log()
1042
-     */
1043
-    public function debugLog($message, int $backSteps = 0): void
1044
-    {
1045
-        $baseDir = dirname(__DIR__);
1046
-        $debugLogFile = $baseDir . '/DEBUG.log';
1047
-        $backtrace = debug_backtrace();
1048
-        $callPoint = $backtrace[$backSteps];
1049
-        $path = $callPoint['file'];
1050
-        $shortPath = $path;
1051
-        if (0 === strpos($path, $baseDir)) {
1052
-            $shortPath = substr($path, strlen($baseDir));
1053
-        }
1054
-        // TODO(tk): track if the parent function is log() or not, not only the number of back-steps (or check all call points)
1055
-        $callInfoExtra = '';
1056
-        if (1 !== $backSteps) { // this is not a log() call with debug switched on
1057
-            $callInfoExtra = " ($backSteps) " . $backtrace[$backSteps + 1]['type'] . $backtrace[$backSteps + 1]['function'] . '()';
1058
-        }
1059
-        $callInfo = sprintf(' [ %s:%s ]%s', $shortPath, $callPoint['line'], $callInfoExtra);
1060
-
1061
-        if (!is_string($message)) {
1062
-            /** @noinspection JsonEncodingApiUsageInspection */
1063
-            $type = gettype($message);
1064
-            if ('object' === $type && is_callable([$message, '__debugInfo'])) {
1065
-                $message = $message->__debugInfo();
1066
-            }
1067
-            $message = $type . ': ' . json_encode($message, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
1068
-        }
1069
-
1070
-        $message = substr(sprintf('%.3f', $_SERVER['REQUEST_TIME_FLOAT']), -7) . " $message";
1071
-
1072
-        error_log(str_pad($message, 48) . $callInfo . "\n", 3, $debugLogFile);
1073
-    }
40
+	public const LOG_CONTEXT = "SeafileBackend"; // Context for the Logger
41
+
42
+	/**
43
+	 * @const string gettext domain
44
+	 */
45
+	private const GT_DOMAIN = 'plugin_filesbackendSeafile';
46
+
47
+	/**
48
+	 * Seafile "usage" number ("bytes") to Grommunio usage display number ("bytes") multiplier.
49
+	 *
50
+	 * 1 megabyte in bytes within Seafile represents 1 mebibyte in bytes for Grommunio
51
+	 *
52
+	 * (Seafile Usage "Bytes" U) / 1000 / 1000 * 1024 * 1024 (1.048576)
53
+	 */
54
+	private const QUOTA_MULTIPLIER_SEAFILE_TO_GROMMUNIO = 1.048576;
55
+
56
+	/**
57
+	 * Error codes
58
+	 *
59
+	 * @see parseErrorCodeToMessage for description
60
+	 * @see Backend::backendException() for Seafile API handling
61
+	 */
62
+	private const SFA_ERR_UNAUTHORIZED = 401;
63
+	private const SFA_ERR_FORBIDDEN = 403;
64
+	private const SFA_ERR_NOTFOUND = 404;
65
+	private const SFA_ERR_NOTALLOWED = 405;
66
+	private const SFA_ERR_TIMEOUT = 408;
67
+	private const SFA_ERR_LOCKED = 423;
68
+	private const SFA_ERR_FAILED_DEPENDENCY = 423;
69
+	private const SFA_ERR_INTERNAL = 500;
70
+	private const SFA_ERR_UNREACHABLE = 800;
71
+	private const SFA_ERR_TMP = 801;
72
+	private const SFA_ERR_FEATURES = 802;
73
+	private const SFA_ERR_NO_CURL = 803;
74
+	private const SFA_ERR_UNIMPLEMENTED = 804;
75
+
76
+	/**
77
+	 * @var ?SeafileApi The Seafile API client.
78
+	 */
79
+	private ?SeafileApi $seafapi = null;
80
+
81
+	/**
82
+	 * Configuration data for the Ext JS metaform.
83
+	 */
84
+	private array $metaConfig = [];
85
+
86
+	/**
87
+	 * Debug flag that mirrors `PLUGIN_FILESBROWSER_LOGLEVEL`.
88
+	 */
89
+	private bool $debug = false;
90
+
91
+	private Config $config;
92
+
93
+	private ?SsoBackend $sso = null;
94
+
95
+	/**
96
+	 * Backend name used in translations.
97
+	 */
98
+	private string $backendTransName = '';
99
+
100
+	/**
101
+	 * Seafile backend constructor
102
+	 */
103
+	public function __construct()
104
+	{
105
+		// initialization
106
+		$this->debug = PLUGIN_FILESBROWSER_LOGLEVEL === 'DEBUG';
107
+
108
+		$this->config = new Config();
109
+
110
+		$this->init_form();
111
+
112
+		// set backend description
113
+		$this->backendDescription = dgettext(self::GT_DOMAIN, "With this backend, you can connect to any Seafile server.");
114
+
115
+		// set backend display name
116
+		$this->backendDisplayName = "Seafile";
117
+
118
+		// set backend version
119
+		$this->backendVersion = "2.0.68";
120
+
121
+		// set backend name used in translations
122
+		$this->backendTransName = dgettext(self::GT_DOMAIN, 'Files ' . $this->backendDisplayName . ' Backend: ');
123
+	}
124
+
125
+	////////////////////////////////////////////////////////////////////////////
126
+	/// seafapi backend methods                                              ///
127
+	////////////////////////////////////////////////////////////////////////////
128
+
129
+	/**
130
+	 * Opens the connection to the Seafile server.
131
+	 *
132
+	 * @throws BackendException if connection is not successful
133
+	 * @return boolean true if action succeeded
134
+	 */
135
+	public function open()
136
+	{
137
+		$url = $this->config->getApiUrl();
138
+
139
+		try {
140
+			$this->sso->open();
141
+		} catch (Throwable $throwable) {
142
+			$this->backendException($throwable);
143
+		}
144
+
145
+		try {
146
+			$this->seafapi = new SeafileApi($url, $this->config->user, $this->config->pass);
147
+		} catch (Throwable $throwable) {
148
+			$this->backendException($throwable);
149
+		}
150
+
151
+		return true;
152
+	}
153
+
154
+	/**
155
+	 * This function will read a list of files and folders from the server.
156
+	 *
157
+	 * @param string $dir to get listing from
158
+	 * @param bool $hidefirst skip the first entry (we ignore this for the Seafile backend)
159
+	 * @throws BackendException
160
+	 *
161
+	 * @return array
162
+	 */
163
+	public function ls($dir, $hidefirst = true)
164
+	{
165
+		$timer = new Timer();
166
+		$this->log("[LS] '$dir'");
167
+
168
+		if ('' === trim($dir, '/')) {
169
+			try {
170
+				$listing = $this->seafapi->listLibraries();
171
+			} catch (Throwable $throwable) {
172
+				$this->backendException($throwable);
173
+			}
174
+			goto result;
175
+		}
176
+
177
+		$lsDir = $this->splitGrommunioPath($dir);
178
+		if (null === $lsDir->lib) {
179
+			// the library does not exist, the listing is short.
180
+			$listing = [];
181
+			goto result;
182
+		}
183
+
184
+		try {
185
+			$listing = $this->seafapi->listItemsInDirectory($lsDir->lib, $lsDir->path ?? '');
186
+		} catch (Throwable $throwable) {
187
+			$this->backendException($throwable);
188
+		}
189
+
190
+		result:
191
+
192
+		$result = [];
193
+		$baseDir = rtrim($dir, '/') . '/';
194
+		foreach ($listing as $node) {
195
+			if (!isset($this->seafapi::TYPES[$node->type])) {
196
+				$this->backendException(
197
+					new \UnexpectedValueException(sprintf('Unhandled Seafile node-type "%s" (for "%s")', $node->type, $node->name))
198
+				);
199
+			}
200
+			$isDir = isset($this->seafapi::TYPES_DIR_LIKE[$node->type]);
201
+			$name = rtrim($baseDir . $node->name, '/') . '/';
202
+			$isDir || $name = rtrim($name, '/');
203
+			$result[$name] = [
204
+				'resourcetype' => $isDir ? 'collection' : 'file',
205
+				'getcontentlength' => $isDir ? null : $node->size,
206
+				'getlastmodified' => date('r', $node->mtime),
207
+				'getcontenttype' => null,
208
+				'quota-used-bytes' => null,
209
+				'quota-available-bytes' => null,
210
+			];
211
+		}
212
+
213
+		$this->log("[LS] done in $timer seconds.");
214
+		return $result;
215
+	}
216
+
217
+	/**
218
+	 * Creates a new directory on the server.
219
+	 *
220
+	 * @param string $dir
221
+	 * @throws BackendException
222
+	 * @return bool
223
+	 */
224
+	public function mkcol($dir)
225
+	{
226
+		$timer = new Timer();
227
+		$this->log("[MKCOL] '$dir'");
228
+
229
+		if ($this->isLibrary($dir)) {
230
+			// create library
231
+			try {
232
+				$result = $this->seafapi->createLibrary($dir);
233
+				unset($result);
234
+			} catch (Throwable $throwable) {
235
+				$this->backendException($throwable);
236
+			}
237
+			$success = true;
238
+		} else {
239
+			// create directory within library
240
+			$lib = $this->seafapi->getLibraryFromPath($dir)->id;
241
+			[, $path] = explode('/', trim($dir, '/'), 2);
242
+			try {
243
+				$result = $this->seafapi->createNewDirectory($lib, $path);
244
+			} catch (Throwable $throwable) {
245
+				$this->backendException($throwable);
246
+			}
247
+			$success = 'success' === $result;
248
+		}
249
+
250
+		$this->log("[MKCOL] done in $timer seconds.");
251
+		return $success;
252
+	}
253
+
254
+	/**
255
+	 * Deletes a files or folder from the backend.
256
+	 *
257
+	 * @param string $path
258
+	 * @throws BackendException
259
+	 * @return bool
260
+	 */
261
+	public function delete($path)
262
+	{
263
+		$timer = new Timer();
264
+		$this->log("[DELETE] '$path'");
265
+
266
+		if ($this->isLibrary($path)) {
267
+			// delete library
268
+			try {
269
+				$this->seafapi->deleteLibraryByName($path);
270
+				$result = 'success';
271
+			} catch (Throwable $throwable) {
272
+				$this->backendException($throwable);
273
+			}
274
+		} else {
275
+			// delete file or directory within library
276
+			$deletePath = $this->splitGrommunioPath($path);
277
+			try {
278
+				$result = $this->seafapi->deleteFile($deletePath->lib, $deletePath->path);
279
+			} catch (Throwable $throwable) {
280
+				$this->backendException($throwable);
281
+			}
282
+		}
283
+
284
+		$this->log("[DELETE] done in $timer seconds.");
285
+		return 'success' === $result;
286
+	}
287
+
288
+	/**
289
+	 * Move a file or collection on the backend server (serverside).
290
+	 *
291
+	 * @param string $src_path Source path
292
+	 * @param string $dst_path Destination path
293
+	 * @param bool $overwrite Overwrite file if exists in $dest_path
294
+	 * @throws BackendException
295
+	 * @return bool
296
+	 */
297
+	public function move($src_path, $dst_path, $overwrite = false)
298
+	{
299
+		$timer = new Timer();
300
+		$this->log("[MOVE] '$src_path' -> '$dst_path'");
301
+
302
+		// check if the move operation would move src into itself - error condition
303
+		if (0 === strpos($dst_path, $src_path . '/')) {
304
+			$this->backendError(self::SFA_ERR_FORBIDDEN, 'Moving failed');
305
+		}
306
+
307
+		// move library/file/directory is one of in the following order:
308
+		// 1/5: rename library
309
+		// 2/5: noop - source and destination are the same
310
+		// 3/5: rename file/directory
311
+		// 4/5: move file/directory
312
+		// 5/5: every other operation (e.g. move library into another library) is not implemented
313
+
314
+		$src = $this->splitGrommunioPath($src_path);
315
+		$dst = $this->splitGrommunioPath($dst_path);
316
+
317
+		// 1/5: rename library
318
+		if ($src->path === null && $dst->path === null) {
319
+			if ($dst->lib !== null) {
320
+				// rename to an existing library name (not allowed as not supported)
321
+				$this->backendError(self::SFA_ERR_NOTALLOWED, 'Moving failed');
322
+			}
323
+			try {
324
+				$this->seafapi->renameLibrary($src->libName, $dst->libName);
325
+				$result = true;
326
+			} catch (Throwable $throwable) {
327
+				$this->backendException($throwable);
328
+			}
329
+			goto done;
330
+		}
331
+
332
+		$isIntraLibTransaction = $src->libName === $dst->libName;
333
+
334
+		// 2/5: noop - src and dst are the same
335
+		if ($isIntraLibTransaction && $src->path === $dst->path) {
336
+			// source and destination are the same path, nothing to do
337
+			$result = 'success';
338
+			goto done;
339
+		}
340
+
341
+		$dirNames = array_map('dirname', [$src->path, $dst->path]);
342
+		$pathsHaveSameDirNames = $dirNames[0] === $dirNames[1];
343
+
344
+		// 3/5: rename file/directory
345
+		if ($isIntraLibTransaction && $pathsHaveSameDirNames) {
346
+			try {
347
+				$result = $this->seafapi->renameFile($src->lib, $src->path, basename($dst->path));
348
+			} catch (Throwable $throwable) {
349
+				$this->backendException($throwable);
350
+			}
351
+			goto done;
352
+		}
353
+
354
+		// 4/5: move file/directory
355
+		if (isset($src->path, $dst->lib)) {
356
+			try {
357
+				$result = $this->seafapi->moveFile($src->lib, $src->path, $dst->lib, $dirNames[1]);
358
+			} catch (Throwable $throwable) {
359
+				$this->backendException($throwable);
360
+			}
361
+		}
362
+
363
+		done:
364
+
365
+		// 5/5: every other operation (move library into another library, not implemented)
366
+		if (!isset($result)) {
367
+			$this->backendError(self::SFA_ERR_UNIMPLEMENTED, 'Not implemented.');
368
+		}
369
+
370
+		$this->log("[MOVE] done in $timer seconds.");
371
+		return 'success' === $result;
372
+	}
373
+
374
+	/**
375
+	 * Download a remote file to a buffer variable.
376
+	 *
377
+	 * @param string $path The source path on the server
378
+	 * @param mixed $buffer Buffer for the received data
379
+	 *
380
+	 * @throws BackendException if request is not successful
381
+	 *
382
+	 * @return boolean true if action succeeded
383
+	 */
384
+	public function get($path, &$buffer)
385
+	{
386
+		$timer = new Timer();
387
+		$this->log("[GET] '$path'");
388
+
389
+		$src = $this->splitGrommunioPath($path);
390
+
391
+		try {
392
+			$result = $this->seafapi->downloadFileAsBuffer($src->lib, $src->path);
393
+		} catch (Throwable $throwable) {
394
+			$this->backendException($throwable);
395
+		}
396
+
397
+		$success = false !== $result;
398
+
399
+		if ($success) {
400
+			$buffer = $result;
401
+		}
402
+
403
+		$this->log("[GET] done in $timer seconds.");
404
+		return $success;
405
+	}
406
+
407
+	/**
408
+	 * Download a remote file to a local file.
409
+	 *
410
+	 * @param string $srcpath Source path on server
411
+	 * @param string $localpath Destination path on local filesystem
412
+	 *
413
+	 * @throws BackendException if request is not successful
414
+	 *
415
+	 * @return boolean true if action succeeded
416
+	 */
417
+	public function get_file($srcpath, $localpath)
418
+	{
419
+		$timer = new Timer();
420
+		$this->log("[GET_FILE] '$srcpath' -> '$localpath'");
421
+
422
+		$src = $this->splitGrommunioPath($srcpath);
423
+
424
+		try {
425
+			$result = $this->seafapi->downloadFileToFile($src->lib, $src->path, $localpath);
426
+		} catch (Throwable $throwable) {
427
+			$this->backendException($throwable);
428
+		}
429
+
430
+		$this->log("[GET_FILE] done in $timer seconds.");
431
+		return $result;
432
+	}
433
+
434
+	/**
435
+	 * Puts a file into a collection.
436
+	 *
437
+	 * @param string $path Destination path
438
+	 *
439
+	 * @string mixed $data Any kind of data
440
+	 * @throws BackendException if request is not successful
441
+	 *
442
+	 * @return boolean true if action succeeded
443
+	 */
444
+	public function put($path, $data)
445
+	{
446
+		$timer = new Timer();
447
+		$this->log(sprintf("[PUT] start: path: %s (%d)", $path, strlen($data)));
448
+
449
+		$target = $this->splitGrommunioPath($path);
450
+
451
+		try {
452
+			/** @noinspection PhpUnusedLocalVariableInspection */
453
+			$result = $this->seafapi->uploadBuffer($target->lib, $target->path, $data);
454
+		} catch (Throwable $throwable) {
455
+			$this->backendException($throwable);
456
+		}
457
+
458
+		$this->log("[PUT] done in $timer seconds.");
459
+		return true;
460
+	}
461
+
462
+	/**
463
+	 * Upload a local file
464
+	 *
465
+	 * @param string $path Destination path on the server
466
+	 * @param string $filename Local filename for the file that should be uploaded
467
+	 *
468
+	 * @throws BackendException if request is not successful
469
+	 *
470
+	 * @return boolean true if action succeeded
471
+	 */
472
+	public function put_file($path, $filename)
473
+	{
474
+		$timer = new Timer();
475
+		$this->log(sprintf("[PUT_FILE] %s -> %s", $filename, $path));
476
+
477
+		// filename can be null if an attachment of draft-email that has not been saved
478
+		if (empty($filename)) {
479
+			return false;
480
+		}
481
+
482
+		$target = $this->splitGrommunioPath($path);
483
+
484
+		// put file into users default library if no library given
485
+		if ($target->path === null && $target->libName !== null) {
486
+			try {
487
+				$defaultLibrary = $this->seafapi->getDefaultLibrary();
488
+			} catch (Throwable $throwable) {
489
+				$this->backendException($throwable);
490
+			}
491
+			if (isset($defaultLibrary->repo_id, $defaultLibrary->exists) && true === $defaultLibrary->exists) {
492
+				$target->path = $target->libName;
493
+				$target->libName = null;
494
+				$target->lib = $defaultLibrary->repo_id;
495
+			}
496
+		}
497
+
498
+		try {
499
+			/** @noinspection PhpUnusedLocalVariableInspection */
500
+			$result = $this->seafapi->uploadFile($target->lib, $target->path, $filename);
501
+		} catch (Throwable $throwable) {
502
+			$this->backendException($throwable);
503
+		}
504
+
505
+		$this->log("[PUT_FILE] done in $timer seconds.");
506
+		return true;
507
+	}
508
+
509
+	////////////////////////////////////////////////////////////////////////////
510
+	/// non-seafapi backend implementation                                   ///
511
+	////////////////////////////////////////////////////////////////////////////
512
+
513
+	/**
514
+	 * Initialize backend from $backend_config array
515
+	 *
516
+	 * @param $backend_config
517
+	 * @return void
518
+	 */
519
+	public function init_backend($backend_config)
520
+	{
521
+		$config = $backend_config;
522
+
523
+		if ($backend_config["use_zarafa_credentials"]) {
524
+			// For backward compatibility we will check if the Encryption store exists. If not,
525
+			// we will fall back to the old way of retrieving the password from the session.
526
+			if (class_exists('EncryptionStore')) {
527
+				// Get the username and password from the Encryption store
528
+				$encryptionStore = EncryptionStore::getInstance();
529
+				if ($encryptionStore instanceof EncryptionStore) {
530
+					$config['user'] = $encryptionStore->get('username');
531
+					$config['password'] = $encryptionStore->get('password');
532
+				}
533
+			} else {
534
+				$config['user'] = ConfigUtil::loadSmtpAddress();
535
+				$password = $_SESSION['password'];
536
+				if (function_exists('openssl_decrypt')) {
537
+					/** @noinspection PhpUndefinedConstantInspection */
538
+					$config['password'] = openssl_decrypt($password, "des-ede3-cbc", PASSWORD_KEY, 0, PASSWORD_IV);
539
+				}
540
+			}
541
+		}
542
+
543
+		$this->config->importConfigArray($config);
544
+
545
+		SsoBackend::bind($this->sso)->initBackend($this->config);
546
+
547
+		Logger::debug(self::LOG_CONTEXT, __FUNCTION__ . ' done.');
548
+	}
549
+
550
+	/**
551
+	 * @return false|string
552
+	 * @noinspection PhpMultipleClassDeclarationsInspection Grommunio has a \JsonException shim
553
+	 */
554
+	public function getFormConfig()
555
+	{
556
+		try {
557
+			$json = json_encode($this->metaConfig, JSON_THROW_ON_ERROR);
558
+		} catch (JsonException $e) {
559
+			$this->log(sprintf('[%s]: %s', get_class($e), $e->getMessage()));
560
+			$json = false;
561
+		}
562
+
563
+		return $json;
564
+	}
565
+
566
+	public function getFormConfigWithData()
567
+	{
568
+		return $this->getFormConfig();
569
+	}
570
+
571
+	/**
572
+	 * set debug on (1) or off (0).
573
+	 * produces a lot of debug messages in webservers error log if set to on (1).
574
+	 *
575
+	 * @param boolean $debug enable or disable debugging
576
+	 *
577
+	 * @return void
578
+	 */
579
+	public function set_debug($debug)
580
+	{
581
+		$this->debug = (bool)$debug;
582
+	}
583
+
584
+	////////////////////////////////////////////////////////////////////////////
585
+	/// not_used_implemented()                                               ///
586
+	////////////////////////////////////////////////////////////////////////////
587
+
588
+	/**
589
+	 * Duplicates a folder on the backend server.
590
+	 *
591
+	 * @param string $src_path
592
+	 * @param string $dst_path
593
+	 * @param bool $overwrite
594
+	 * @throws BackendException
595
+	 * @return bool
596
+	 * @noinspection PhpReturnDocTypeMismatchInspection Upstream Interface Issue
597
+	 */
598
+	public function copy_coll($src_path, $dst_path, $overwrite = false)
599
+	{
600
+		$this->backendError(self::SFA_ERR_UNIMPLEMENTED, 'Not implemented');
601
+	}
602
+
603
+	/**
604
+	 * Duplicates a file on the backend server.
605
+	 *
606
+	 * @param string $src_path
607
+	 * @param string $dst_path
608
+	 * @param bool $overwrite
609
+	 * @throws BackendException
610
+	 * @return bool
611
+	 * @noinspection PhpReturnDocTypeMismatchInspection Upstream Interface Issue
612
+	 */
613
+	public function copy_file($src_path, $dst_path, $overwrite = false)
614
+	{
615
+		$this->backendError(self::SFA_ERR_UNIMPLEMENTED, 'Not implemented');
616
+	}
617
+
618
+	/**
619
+	 * Checks if the given $path exists on the remote server.
620
+	 *
621
+	 * @param string $path
622
+	 * @throws BackendException
623
+	 * @return bool
624
+	 * @noinspection PhpReturnDocTypeMismatchInspection Upstream Interface Issue
625
+	 */
626
+	public function exists($path)
627
+	{
628
+		$this->backendError(self::SFA_ERR_UNIMPLEMENTED, 'Not implemented');
629
+	}
630
+
631
+	/**
632
+	 * Gets path information from Seafile server.
633
+	 *
634
+	 * @param string $path
635
+	 * @throws BackendException if request is not successful
636
+	 * @return array directory info
637
+	 */
638
+	public function gpi($path)
639
+	{
640
+		$this->log("[GPI] '$path'");
641
+		$list = $this->ls(dirname($path), false); // get contents of the parent dir
642
+
643
+		if (isset($list[$path])) {
644
+			return $list[$path];
645
+		}
646
+
647
+		$this->log('[GPI] wrong response from ls');
648
+		$this->backendError(self::SFA_ERR_FAILED_DEPENDENCY, 'Connection failed');
649
+	}
650
+
651
+	/**
652
+	 * Checks if the given $path is a folder.
653
+	 *
654
+	 * @param string $path
655
+	 * @throws BackendException
656
+	 * @return bool
657
+	 * @noinspection PhpReturnDocTypeMismatchInspection Upstream Interface Issue
658
+	 */
659
+	public function is_dir($path)
660
+	{
661
+		$this->backendError(self::SFA_ERR_UNIMPLEMENTED, 'Not implemented');
662
+	}
663
+
664
+	/**
665
+	 * Checks if the given $path is a file.
666
+	 *
667
+	 * @param string $path
668
+	 * @throws BackendException
669
+	 * @return bool
670
+	 * @noinspection PhpReturnDocTypeMismatchInspection Upstream Interface Issue
671
+	 */
672
+	public function is_file($path)
673
+	{
674
+		$this->backendError(self::SFA_ERR_UNIMPLEMENTED, 'Not implemented');
675
+	}
676
+
677
+	/////////////////////////////////////////////////////////////
678
+	// @see iFeatureVersionInfo implementation                 //
679
+	/////////////////////////////////////////////////////////////
680
+
681
+	/**
682
+	 * Return the version string of the server backend.
683
+	 *
684
+	 * @throws BackendException
685
+	 * @return String
686
+	 */
687
+	public function getServerVersion()
688
+	{
689
+		try {
690
+			return $this->seafapi->getServerVersion();
691
+		} catch (Throwable $throwable) {
692
+			$this->backendException($throwable);
693
+		}
694
+	}
695
+
696
+	/////////////////////////////////////////////////////////////
697
+	// @see iFeatureQuota implementation                       //
698
+	/////////////////////////////////////////////////////////////
699
+
700
+	/**
701
+	 * @param string $dir
702
+	 * @return float
703
+	 * @noinspection PhpMissingParamTypeInspection
704
+	 * @noinspection PhpUnusedParameterInspection
705
+	 */
706
+	public function getQuotaBytesUsed($dir)
707
+	{
708
+		$return = $this->seafapi->checkAccountInfo();
709
+
710
+		return ($return->usage ?? 0) * self::QUOTA_MULTIPLIER_SEAFILE_TO_GROMMUNIO;
711
+	}
712
+
713
+	/**
714
+	 * @param string $dir
715
+	 * @return float|int
716
+	 * @noinspection PhpUnusedParameterInspection
717
+	 * @noinspection PhpMissingParamTypeInspection
718
+	 */
719
+	public function getQuotaBytesAvailable($dir)
720
+	{
721
+		$return = $this->seafapi->checkAccountInfo();
722
+		$avail = $return->total - $return->usage;
723
+		if (-2 === (int)$return->total) {
724
+			return -1;
725
+		}
726
+
727
+		return $avail * self::QUOTA_MULTIPLIER_SEAFILE_TO_GROMMUNIO;
728
+	}
729
+
730
+	/////////////////////////////////////////////////////////////
731
+	// @internal private helper methods                        //
732
+	/////////////////////////////////////////////////////////////
733
+
734
+	/**
735
+	 * Initialise form fields
736
+	 */
737
+	private function init_form()
738
+	{
739
+		$this->metaConfig = [
740
+			"success" => true,
741
+			"metaData" => [
742
+				"fields" => [
743
+					[
744
+						"name" => "server_address",
745
+						"fieldLabel" => dgettext(self::GT_DOMAIN, 'Server address'),
746
+						"editor" => [
747
+							"allowBlank" => false,
748
+						],
749
+					],
750
+					[
751
+						"name" => "server_port",
752
+						"fieldLabel" => dgettext(self::GT_DOMAIN, 'Server port'),
753
+						"editor" => [
754
+							"ref" => "../../portField",
755
+							"allowBlank" => false,
756
+						],
757
+					],
758
+					[
759
+						"name" => "server_ssl",
760
+						"fieldLabel" => dgettext(self::GT_DOMAIN, 'Use SSL'),
761
+						"editor" => [
762
+							"xtype" => "checkbox",
763
+							"listeners" => [
764
+								"check" => "Zarafa.plugins.files.data.Actions.onCheckSSL" // this javascript function will be called!
765
+							],
766
+						],
767
+					],
768
+					[
769
+						"name" => "user",
770
+						"fieldLabel" => dgettext(self::GT_DOMAIN, 'Username'),
771
+						"editor" => [
772
+							"ref" => "../../usernameField",
773
+						],
774
+					],
775
+					[
776
+						"name" => "password",
777
+						"fieldLabel" => dgettext(self::GT_DOMAIN, 'Password'),
778
+						"editor" => [
779
+							"ref" => "../../passwordField",
780
+							"inputType" => "password",
781
+						],
782
+					],
783
+					[
784
+						"name" => "use_zarafa_credentials",
785
+						"fieldLabel" => dgettext(self::GT_DOMAIN, 'Use grommunio credentials'),
786
+						"editor" => [
787
+							"xtype" => "checkbox",
788
+							"listeners" => [
789
+								"check" => "Zarafa.plugins.files.data.Actions.onCheckCredentials" // this javascript function will be called!
790
+							],
791
+						],
792
+					],
793
+				],
794
+				"formConfig" => [
795
+					"labelAlign" => "left",
796
+					"columnCount" => 1,
797
+					"labelWidth" => 80,
798
+					"defaults" => [
799
+						"width" => 292,
800
+					],
801
+				],
802
+			],
803
+
804
+			// here we can specify the default values.
805
+			"data" => [
806
+				"server_address" => "seafile.example.com",
807
+				"server_port" => "443",
808
+				"server_ssl" => "1",
809
+				"server_path" => "",
810
+				"use_zarafa_credentials" => "0",
811
+				"user" => "",
812
+				"password" => "",
813
+			],
814
+		];
815
+	}
816
+
817
+	/**
818
+	 * split grommunio path into library and library path
819
+	 *
820
+	 * obtains the seafile library ID (if available, otherwise NULL)
821
+	 *
822
+	 * return protocol: object{
823
+	 *   lib: ?string     # library ID e.g. "ccc60923-8cdf-4cc8-8f71-df86aba3a085"
824
+	 *   path: ?string    # path inside library, always prefixed with "/" if set
825
+	 *   libName: ?string # name of the library
826
+	 * }
827
+	 *
828
+	 * @param string $grommunioPath
829
+	 * @throws Exception
830
+	 * @return object
831
+	 */
832
+	private function splitGrommunioPath(string $grommunioPath): object
833
+	{
834
+		static $libraries;
835
+		$libraries = $libraries ?? array_column($this->seafapi->listLibraries(), null, 'name');
836
+
837
+		[, $libName, $path] = explode('/', $grommunioPath, 3) + [null, null, null];
838
+		if (null !== $path) {
839
+			$path = "/$path";
840
+		}
841
+		$lib = $libraries[$libName]->id ?? null;
842
+		return (object)['lib' => $lib, 'path' => $path, 'libName' => $libName];
843
+	}
844
+
845
+	/**
846
+	 * test if a grommunio path is a library only
847
+	 *
848
+	 * @param string $grommunioPath
849
+	 * @return bool
850
+	 */
851
+	private function isLibrary(string $grommunioPath): bool
852
+	{
853
+		return 0 === substr_count(trim($grommunioPath, '/'), '/');
854
+	}
855
+
856
+	/**
857
+	 * Turn a Backend error code into a Backend exception
858
+	 *
859
+	 * @param int $errorCode one of the Backend::SFA_ERR_* codes, e.g. {@see Backend::SFA_ERR_INTERNAL}
860
+	 * @param ?string $title msg-id from the plugin_files domain, e.g. 'PHP-CURL not installed'
861
+	 * @throws BackendException
862
+	 * @return no-return
863
+	 */
864
+	private function backendError(int $errorCode, string $title = null)
865
+	{
866
+		$message = $this->parseErrorCodeToMessage($errorCode);
867
+		$title = $this->backendTransName;
868
+		$this->backendErrorThrow($title, $message, $errorCode);
869
+	}
870
+
871
+	/**
872
+	 * Throw a BackendException w/ title, message and code
873
+	 *
874
+	 * @param string $title
875
+	 * @param string $message
876
+	 * @param int $code
877
+	 * @throws BackendException
878
+	 * @return no-return
879
+	 */
880
+	private function backendErrorThrow(string $title, string $message, int $code = 0)
881
+	{
882
+		/** {@see \Files\Backend\Exception} */
883
+		$exception = new BackendException($message, $code);
884
+		$exception->setTitle($title);
885
+		throw $exception;
886
+	}
887
+
888
+	/**
889
+	 * Turn a throwable/exception with the Seafile API into a Backend exception
890
+	 *
891
+	 * @param Throwable $t
892
+	 * @throws BackendException
893
+	 * @return no-return
894
+	 */
895
+	private function backendException(Throwable $t)
896
+	{
897
+		// if it is already a backend exception, throw it.
898
+		if ($t instanceof BackendException) {
899
+			throw $t;
900
+		}
901
+
902
+		[$callSite, $inFunc] = debug_backtrace();
903
+		$logLabel = "$inFunc[function]:$callSite[line]";
904
+
905
+		$class = get_class($t);
906
+		$message = $t->getMessage();
907
+		$this->log(sprintf('%s: [%s] #%s: %s', $logLabel, $class, $t->getCode(), $message));
908
+
909
+		// All SeafileApi exceptions are handled by this
910
+		if ($t instanceof Exception) {
911
+			$this->backendExceptionSeafapi($t);
912
+		}
913
+
914
+		$this->backendErrorThrow('Error', "[SEAFILE $logLabel] $class: $message", 500);
915
+	}
916
+
917
+	/**
918
+	 * Turn an Exception into a BackendException
919
+	 *
920
+	 * Enriches message information for grommunio with API error messages
921
+	 * if a Seafile ConnectionException.
922
+	 *
923
+	 * helper for {@see Backend::backendException()}
924
+	 *
925
+	 * @param Exception $exception
926
+	 * @throws BackendException
927
+	 * @return void
928
+	 */
929
+	private function backendExceptionSeafapi(Exception $exception)
930
+	{
931
+		$code = $exception->getCode();
932
+		$message = $exception->getMessage();
933
+
934
+		$apiErrorMessagesHtml = null;
935
+		if ($exception instanceof Exception\ConnectionException) {
936
+			$messages = $exception->tryApiErrorMessages();
937
+			null === $messages || $apiErrorMessagesHtml = implode(
938
+					"<br/>\n",
939
+					array_map(static function (string $subject) {
940
+						return htmlspecialchars($subject, ENT_QUOTES | ENT_HTML5);
941
+					}, $messages)
942
+				) . "<br/>\n";
943
+		}
944
+
945
+		if (null !== $apiErrorMessagesHtml) {
946
+			$message .= " - $apiErrorMessagesHtml";
947
+		}
948
+
949
+		$this->backendErrorThrow($this->backendDisplayName . ' Error', $message, $code);
950
+	}
951
+
952
+	/**
953
+	 * a simple php error_log wrapper.
954
+	 *
955
+	 * @param string $err_string error message
956
+	 *
957
+	 * @return void
958
+	 */
959
+	private function log(string $err_string)
960
+	{
961
+		if ($this->debug) {
962
+			Logger::debug(self::LOG_CONTEXT, $err_string);
963
+			$this->debugLog($err_string, 2);
964
+		}
965
+	}
966
+
967
+	/**
968
+	 * This function will return a user-friendly error string.
969
+	 *
970
+	 * Error codes were migrated from WebDav backend.
971
+	 *
972
+	 * @param int $error_code An error code
973
+	 *
974
+	 * @return string user friendly error message
975
+	 */
976
+	private function parseErrorCodeToMessage(int $error_code)
977
+	{
978
+		$error = $error_code;
979
+
980
+		switch ($error) {
981
+			case CURLE_BAD_PASSWORD_ENTERED:
982
+			case self::SFA_ERR_UNAUTHORIZED:
983
+				$msg = dgettext(self::GT_DOMAIN, 'Unauthorized. Wrong username or password.');
984
+				break;
985
+			case CURLE_SSL_CONNECT_ERROR:
986
+			case CURLE_COULDNT_RESOLVE_HOST:
987
+			case CURLE_COULDNT_CONNECT:
988
+			case CURLE_OPERATION_TIMEOUTED:
989
+			case self::SFA_ERR_UNREACHABLE:
990
+				$msg = dgettext(self::GT_DOMAIN, 'Seafile is not reachable. Correct backend address entered?');
991
+				break;
992
+			case self::SFA_ERR_FORBIDDEN:
993
+				$msg = dgettext(self::GT_DOMAIN, 'You don\'t have enough permissions for this operation.');
994
+				break;
995
+			case self::SFA_ERR_NOTFOUND:
996
+				$msg = dgettext(self::GT_DOMAIN, 'File is not available any more.');
997
+				break;
998
+			case self::SFA_ERR_TIMEOUT:
999
+				$msg = dgettext(self::GT_DOMAIN, 'Connection to server timed out. Retry later.');
1000
+				break;
1001
+			case self::SFA_ERR_LOCKED:
1002
+				$msg = dgettext(self::GT_DOMAIN, 'This file is locked by another user.');
1003
+				break;
1004
+			case self::SFA_ERR_FAILED_DEPENDENCY:
1005
+				$msg = dgettext(self::GT_DOMAIN, 'The request failed due to failure of a previous request.');
1006
+				break;
1007
+			case self::SFA_ERR_INTERNAL:
1008
+				$msg = dgettext(self::GT_DOMAIN, 'Seafile-server encountered a problem.');
1009
+				break;
1010
+			case self::SFA_ERR_TMP:
1011
+				$msg = dgettext(self::GT_DOMAIN, 'Could not write to temporary directory. Contact the server administrator.');
1012
+				break;
1013
+			case self::SFA_ERR_FEATURES:
1014
+				$msg = dgettext(self::GT_DOMAIN, 'Could not retrieve list of server features. Contact the server administrator.');
1015
+				break;
1016
+			case self::SFA_ERR_NO_CURL:
1017
+				$msg = dgettext(self::GT_DOMAIN, 'PHP-Curl is not available. Contact your system administrator.');
1018
+				break;
1019
+			case self::SFA_ERR_UNIMPLEMENTED:
1020
+				$msg = dgettext(self::GT_DOMAIN, 'This function is not yet implemented.');
1021
+				break;
1022
+			default:
1023
+				$msg = dgettext(self::GT_DOMAIN, 'Unknown error');
1024
+		}
1025
+
1026
+		return $msg;
1027
+	}
1028
+
1029
+	/////////////////////////////////////////////////////////////
1030
+	// @debug development helper method                        //
1031
+	/////////////////////////////////////////////////////////////
1032
+
1033
+	/**
1034
+	 * Log debug message while developing the plugin in dedicated DEBUG.log file
1035
+	 *
1036
+	 * TODO(tk): remove debugLog, we shall not use it in production.
1037
+	 *
1038
+	 * @param mixed $message
1039
+	 * @param int $backSteps [optional] offset of call point in stacktrace
1040
+	 * @return void
1041
+	 * @see \Files\Backend\Seafile\Backend::log()
1042
+	 */
1043
+	public function debugLog($message, int $backSteps = 0): void
1044
+	{
1045
+		$baseDir = dirname(__DIR__);
1046
+		$debugLogFile = $baseDir . '/DEBUG.log';
1047
+		$backtrace = debug_backtrace();
1048
+		$callPoint = $backtrace[$backSteps];
1049
+		$path = $callPoint['file'];
1050
+		$shortPath = $path;
1051
+		if (0 === strpos($path, $baseDir)) {
1052
+			$shortPath = substr($path, strlen($baseDir));
1053
+		}
1054
+		// TODO(tk): track if the parent function is log() or not, not only the number of back-steps (or check all call points)
1055
+		$callInfoExtra = '';
1056
+		if (1 !== $backSteps) { // this is not a log() call with debug switched on
1057
+			$callInfoExtra = " ($backSteps) " . $backtrace[$backSteps + 1]['type'] . $backtrace[$backSteps + 1]['function'] . '()';
1058
+		}
1059
+		$callInfo = sprintf(' [ %s:%s ]%s', $shortPath, $callPoint['line'], $callInfoExtra);
1060
+
1061
+		if (!is_string($message)) {
1062
+			/** @noinspection JsonEncodingApiUsageInspection */
1063
+			$type = gettype($message);
1064
+			if ('object' === $type && is_callable([$message, '__debugInfo'])) {
1065
+				$message = $message->__debugInfo();
1066
+			}
1067
+			$message = $type . ': ' . json_encode($message, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
1068
+		}
1069
+
1070
+		$message = substr(sprintf('%.3f', $_SERVER['REQUEST_TIME_FLOAT']), -7) . " $message";
1071
+
1072
+		error_log(str_pad($message, 48) . $callInfo . "\n", 3, $debugLogFile);
1073
+	}
1074 1074
 }
Please login to merge, or discard this patch.
Spacing   +4 added lines, -4 removed lines patch added patch discarded remove patch
@@ -578,7 +578,7 @@  discard block
 block discarded – undo
578 578
      */
579 579
     public function set_debug($debug)
580 580
     {
581
-        $this->debug = (bool)$debug;
581
+        $this->debug = (bool) $debug;
582 582
     }
583 583
 
584 584
     ////////////////////////////////////////////////////////////////////////////
@@ -720,7 +720,7 @@  discard block
 block discarded – undo
720 720
     {
721 721
         $return = $this->seafapi->checkAccountInfo();
722 722
         $avail = $return->total - $return->usage;
723
-        if (-2 === (int)$return->total) {
723
+        if (-2 === (int) $return->total) {
724 724
             return -1;
725 725
         }
726 726
 
@@ -839,7 +839,7 @@  discard block
 block discarded – undo
839 839
             $path = "/$path";
840 840
         }
841 841
         $lib = $libraries[$libName]->id ?? null;
842
-        return (object)['lib' => $lib, 'path' => $path, 'libName' => $libName];
842
+        return (object) ['lib' => $lib, 'path' => $path, 'libName' => $libName];
843 843
     }
844 844
 
845 845
     /**
@@ -936,7 +936,7 @@  discard block
 block discarded – undo
936 936
             $messages = $exception->tryApiErrorMessages();
937 937
             null === $messages || $apiErrorMessagesHtml = implode(
938 938
                     "<br/>\n",
939
-                    array_map(static function (string $subject) {
939
+                    array_map(static function(string $subject) {
940 940
                         return htmlspecialchars($subject, ENT_QUOTES | ENT_HTML5);
941 941
                     }, $messages)
942 942
                 ) . "<br/>\n";
Please login to merge, or discard this patch.
Braces   +72 added lines, -81 removed lines patch added patch discarded remove patch
@@ -100,8 +100,7 @@  discard block
 block discarded – undo
100 100
     /**
101 101
      * Seafile backend constructor
102 102
      */
103
-    public function __construct()
104
-    {
103
+    public function __construct() {
105 104
         // initialization
106 105
         $this->debug = PLUGIN_FILESBROWSER_LOGLEVEL === 'DEBUG';
107 106
 
@@ -132,19 +131,20 @@  discard block
 block discarded – undo
132 131
      * @throws BackendException if connection is not successful
133 132
      * @return boolean true if action succeeded
134 133
      */
135
-    public function open()
136
-    {
134
+    public function open() {
137 135
         $url = $this->config->getApiUrl();
138 136
 
139 137
         try {
140 138
             $this->sso->open();
141
-        } catch (Throwable $throwable) {
139
+        }
140
+        catch (Throwable $throwable) {
142 141
             $this->backendException($throwable);
143 142
         }
144 143
 
145 144
         try {
146 145
             $this->seafapi = new SeafileApi($url, $this->config->user, $this->config->pass);
147
-        } catch (Throwable $throwable) {
146
+        }
147
+        catch (Throwable $throwable) {
148 148
             $this->backendException($throwable);
149 149
         }
150 150
 
@@ -160,15 +160,15 @@  discard block
 block discarded – undo
160 160
      *
161 161
      * @return array
162 162
      */
163
-    public function ls($dir, $hidefirst = true)
164
-    {
163
+    public function ls($dir, $hidefirst = true) {
165 164
         $timer = new Timer();
166 165
         $this->log("[LS] '$dir'");
167 166
 
168 167
         if ('' === trim($dir, '/')) {
169 168
             try {
170 169
                 $listing = $this->seafapi->listLibraries();
171
-            } catch (Throwable $throwable) {
170
+            }
171
+            catch (Throwable $throwable) {
172 172
                 $this->backendException($throwable);
173 173
             }
174 174
             goto result;
@@ -183,7 +183,8 @@  discard block
 block discarded – undo
183 183
 
184 184
         try {
185 185
             $listing = $this->seafapi->listItemsInDirectory($lsDir->lib, $lsDir->path ?? '');
186
-        } catch (Throwable $throwable) {
186
+        }
187
+        catch (Throwable $throwable) {
187 188
             $this->backendException($throwable);
188 189
         }
189 190
 
@@ -221,8 +222,7 @@  discard block
 block discarded – undo
221 222
      * @throws BackendException
222 223
      * @return bool
223 224
      */
224
-    public function mkcol($dir)
225
-    {
225
+    public function mkcol($dir) {
226 226
         $timer = new Timer();
227 227
         $this->log("[MKCOL] '$dir'");
228 228
 
@@ -231,17 +231,20 @@  discard block
 block discarded – undo
231 231
             try {
232 232
                 $result = $this->seafapi->createLibrary($dir);
233 233
                 unset($result);
234
-            } catch (Throwable $throwable) {
234
+            }
235
+            catch (Throwable $throwable) {
235 236
                 $this->backendException($throwable);
236 237
             }
237 238
             $success = true;
238
-        } else {
239
+        }
240
+        else {
239 241
             // create directory within library
240 242
             $lib = $this->seafapi->getLibraryFromPath($dir)->id;
241 243
             [, $path] = explode('/', trim($dir, '/'), 2);
242 244
             try {
243 245
                 $result = $this->seafapi->createNewDirectory($lib, $path);
244
-            } catch (Throwable $throwable) {
246
+            }
247
+            catch (Throwable $throwable) {
245 248
                 $this->backendException($throwable);
246 249
             }
247 250
             $success = 'success' === $result;
@@ -258,8 +261,7 @@  discard block
 block discarded – undo
258 261
      * @throws BackendException
259 262
      * @return bool
260 263
      */
261
-    public function delete($path)
262
-    {
264
+    public function delete($path) {
263 265
         $timer = new Timer();
264 266
         $this->log("[DELETE] '$path'");
265 267
 
@@ -268,15 +270,18 @@  discard block
 block discarded – undo
268 270
             try {
269 271
                 $this->seafapi->deleteLibraryByName($path);
270 272
                 $result = 'success';
271
-            } catch (Throwable $throwable) {
273
+            }
274
+            catch (Throwable $throwable) {
272 275
                 $this->backendException($throwable);
273 276
             }
274
-        } else {
277
+        }
278
+        else {
275 279
             // delete file or directory within library
276 280
             $deletePath = $this->splitGrommunioPath($path);
277 281
             try {
278 282
                 $result = $this->seafapi->deleteFile($deletePath->lib, $deletePath->path);
279
-            } catch (Throwable $throwable) {
283
+            }
284
+            catch (Throwable $throwable) {
280 285
                 $this->backendException($throwable);
281 286
             }
282 287
         }
@@ -294,8 +299,7 @@  discard block
 block discarded – undo
294 299
      * @throws BackendException
295 300
      * @return bool
296 301
      */
297
-    public function move($src_path, $dst_path, $overwrite = false)
298
-    {
302
+    public function move($src_path, $dst_path, $overwrite = false) {
299 303
         $timer = new Timer();
300 304
         $this->log("[MOVE] '$src_path' -> '$dst_path'");
301 305
 
@@ -323,7 +327,8 @@  discard block
 block discarded – undo
323 327
             try {
324 328
                 $this->seafapi->renameLibrary($src->libName, $dst->libName);
325 329
                 $result = true;
326
-            } catch (Throwable $throwable) {
330
+            }
331
+            catch (Throwable $throwable) {
327 332
                 $this->backendException($throwable);
328 333
             }
329 334
             goto done;
@@ -345,7 +350,8 @@  discard block
 block discarded – undo
345 350
         if ($isIntraLibTransaction && $pathsHaveSameDirNames) {
346 351
             try {
347 352
                 $result = $this->seafapi->renameFile($src->lib, $src->path, basename($dst->path));
348
-            } catch (Throwable $throwable) {
353
+            }
354
+            catch (Throwable $throwable) {
349 355
                 $this->backendException($throwable);
350 356
             }
351 357
             goto done;
@@ -355,7 +361,8 @@  discard block
 block discarded – undo
355 361
         if (isset($src->path, $dst->lib)) {
356 362
             try {
357 363
                 $result = $this->seafapi->moveFile($src->lib, $src->path, $dst->lib, $dirNames[1]);
358
-            } catch (Throwable $throwable) {
364
+            }
365
+            catch (Throwable $throwable) {
359 366
                 $this->backendException($throwable);
360 367
             }
361 368
         }
@@ -381,8 +388,7 @@  discard block
 block discarded – undo
381 388
      *
382 389
      * @return boolean true if action succeeded
383 390
      */
384
-    public function get($path, &$buffer)
385
-    {
391
+    public function get($path, &$buffer) {
386 392
         $timer = new Timer();
387 393
         $this->log("[GET] '$path'");
388 394
 
@@ -390,7 +396,8 @@  discard block
 block discarded – undo
390 396
 
391 397
         try {
392 398
             $result = $this->seafapi->downloadFileAsBuffer($src->lib, $src->path);
393
-        } catch (Throwable $throwable) {
399
+        }
400
+        catch (Throwable $throwable) {
394 401
             $this->backendException($throwable);
395 402
         }
396 403
 
@@ -414,8 +421,7 @@  discard block
 block discarded – undo
414 421
      *
415 422
      * @return boolean true if action succeeded
416 423
      */
417
-    public function get_file($srcpath, $localpath)
418
-    {
424
+    public function get_file($srcpath, $localpath) {
419 425
         $timer = new Timer();
420 426
         $this->log("[GET_FILE] '$srcpath' -> '$localpath'");
421 427
 
@@ -423,7 +429,8 @@  discard block
 block discarded – undo
423 429
 
424 430
         try {
425 431
             $result = $this->seafapi->downloadFileToFile($src->lib, $src->path, $localpath);
426
-        } catch (Throwable $throwable) {
432
+        }
433
+        catch (Throwable $throwable) {
427 434
             $this->backendException($throwable);
428 435
         }
429 436
 
@@ -441,8 +448,7 @@  discard block
 block discarded – undo
441 448
      *
442 449
      * @return boolean true if action succeeded
443 450
      */
444
-    public function put($path, $data)
445
-    {
451
+    public function put($path, $data) {
446 452
         $timer = new Timer();
447 453
         $this->log(sprintf("[PUT] start: path: %s (%d)", $path, strlen($data)));
448 454
 
@@ -451,7 +457,8 @@  discard block
 block discarded – undo
451 457
         try {
452 458
             /** @noinspection PhpUnusedLocalVariableInspection */
453 459
             $result = $this->seafapi->uploadBuffer($target->lib, $target->path, $data);
454
-        } catch (Throwable $throwable) {
460
+        }
461
+        catch (Throwable $throwable) {
455 462
             $this->backendException($throwable);
456 463
         }
457 464
 
@@ -469,8 +476,7 @@  discard block
 block discarded – undo
469 476
      *
470 477
      * @return boolean true if action succeeded
471 478
      */
472
-    public function put_file($path, $filename)
473
-    {
479
+    public function put_file($path, $filename) {
474 480
         $timer = new Timer();
475 481
         $this->log(sprintf("[PUT_FILE] %s -> %s", $filename, $path));
476 482
 
@@ -485,7 +491,8 @@  discard block
 block discarded – undo
485 491
         if ($target->path === null && $target->libName !== null) {
486 492
             try {
487 493
                 $defaultLibrary = $this->seafapi->getDefaultLibrary();
488
-            } catch (Throwable $throwable) {
494
+            }
495
+            catch (Throwable $throwable) {
489 496
                 $this->backendException($throwable);
490 497
             }
491 498
             if (isset($defaultLibrary->repo_id, $defaultLibrary->exists) && true === $defaultLibrary->exists) {
@@ -498,7 +505,8 @@  discard block
 block discarded – undo
498 505
         try {
499 506
             /** @noinspection PhpUnusedLocalVariableInspection */
500 507
             $result = $this->seafapi->uploadFile($target->lib, $target->path, $filename);
501
-        } catch (Throwable $throwable) {
508
+        }
509
+        catch (Throwable $throwable) {
502 510
             $this->backendException($throwable);
503 511
         }
504 512
 
@@ -516,8 +524,7 @@  discard block
 block discarded – undo
516 524
      * @param $backend_config
517 525
      * @return void
518 526
      */
519
-    public function init_backend($backend_config)
520
-    {
527
+    public function init_backend($backend_config) {
521 528
         $config = $backend_config;
522 529
 
523 530
         if ($backend_config["use_zarafa_credentials"]) {
@@ -530,7 +537,8 @@  discard block
 block discarded – undo
530 537
                     $config['user'] = $encryptionStore->get('username');
531 538
                     $config['password'] = $encryptionStore->get('password');
532 539
                 }
533
-            } else {
540
+            }
541
+            else {
534 542
                 $config['user'] = ConfigUtil::loadSmtpAddress();
535 543
                 $password = $_SESSION['password'];
536 544
                 if (function_exists('openssl_decrypt')) {
@@ -551,11 +559,11 @@  discard block
 block discarded – undo
551 559
      * @return false|string
552 560
      * @noinspection PhpMultipleClassDeclarationsInspection Grommunio has a \JsonException shim
553 561
      */
554
-    public function getFormConfig()
555
-    {
562
+    public function getFormConfig() {
556 563
         try {
557 564
             $json = json_encode($this->metaConfig, JSON_THROW_ON_ERROR);
558
-        } catch (JsonException $e) {
565
+        }
566
+        catch (JsonException $e) {
559 567
             $this->log(sprintf('[%s]: %s', get_class($e), $e->getMessage()));
560 568
             $json = false;
561 569
         }
@@ -563,8 +571,7 @@  discard block
 block discarded – undo
563 571
         return $json;
564 572
     }
565 573
 
566
-    public function getFormConfigWithData()
567
-    {
574
+    public function getFormConfigWithData() {
568 575
         return $this->getFormConfig();
569 576
     }
570 577
 
@@ -576,8 +583,7 @@  discard block
 block discarded – undo
576 583
      *
577 584
      * @return void
578 585
      */
579
-    public function set_debug($debug)
580
-    {
586
+    public function set_debug($debug) {
581 587
         $this->debug = (bool)$debug;
582 588
     }
583 589
 
@@ -595,8 +601,7 @@  discard block
 block discarded – undo
595 601
      * @return bool
596 602
      * @noinspection PhpReturnDocTypeMismatchInspection Upstream Interface Issue
597 603
      */
598
-    public function copy_coll($src_path, $dst_path, $overwrite = false)
599
-    {
604
+    public function copy_coll($src_path, $dst_path, $overwrite = false) {
600 605
         $this->backendError(self::SFA_ERR_UNIMPLEMENTED, 'Not implemented');
601 606
     }
602 607
 
@@ -610,8 +615,7 @@  discard block
 block discarded – undo
610 615
      * @return bool
611 616
      * @noinspection PhpReturnDocTypeMismatchInspection Upstream Interface Issue
612 617
      */
613
-    public function copy_file($src_path, $dst_path, $overwrite = false)
614
-    {
618
+    public function copy_file($src_path, $dst_path, $overwrite = false) {
615 619
         $this->backendError(self::SFA_ERR_UNIMPLEMENTED, 'Not implemented');
616 620
     }
617 621
 
@@ -623,8 +627,7 @@  discard block
 block discarded – undo
623 627
      * @return bool
624 628
      * @noinspection PhpReturnDocTypeMismatchInspection Upstream Interface Issue
625 629
      */
626
-    public function exists($path)
627
-    {
630
+    public function exists($path) {
628 631
         $this->backendError(self::SFA_ERR_UNIMPLEMENTED, 'Not implemented');
629 632
     }
630 633
 
@@ -635,8 +638,7 @@  discard block
 block discarded – undo
635 638
      * @throws BackendException if request is not successful
636 639
      * @return array directory info
637 640
      */
638
-    public function gpi($path)
639
-    {
641
+    public function gpi($path) {
640 642
         $this->log("[GPI] '$path'");
641 643
         $list = $this->ls(dirname($path), false); // get contents of the parent dir
642 644
 
@@ -656,8 +658,7 @@  discard block
 block discarded – undo
656 658
      * @return bool
657 659
      * @noinspection PhpReturnDocTypeMismatchInspection Upstream Interface Issue
658 660
      */
659
-    public function is_dir($path)
660
-    {
661
+    public function is_dir($path) {
661 662
         $this->backendError(self::SFA_ERR_UNIMPLEMENTED, 'Not implemented');
662 663
     }
663 664
 
@@ -669,8 +670,7 @@  discard block
 block discarded – undo
669 670
      * @return bool
670 671
      * @noinspection PhpReturnDocTypeMismatchInspection Upstream Interface Issue
671 672
      */
672
-    public function is_file($path)
673
-    {
673
+    public function is_file($path) {
674 674
         $this->backendError(self::SFA_ERR_UNIMPLEMENTED, 'Not implemented');
675 675
     }
676 676
 
@@ -684,11 +684,11 @@  discard block
 block discarded – undo
684 684
      * @throws BackendException
685 685
      * @return String
686 686
      */
687
-    public function getServerVersion()
688
-    {
687
+    public function getServerVersion() {
689 688
         try {
690 689
             return $this->seafapi->getServerVersion();
691
-        } catch (Throwable $throwable) {
690
+        }
691
+        catch (Throwable $throwable) {
692 692
             $this->backendException($throwable);
693 693
         }
694 694
     }
@@ -703,8 +703,7 @@  discard block
 block discarded – undo
703 703
      * @noinspection PhpMissingParamTypeInspection
704 704
      * @noinspection PhpUnusedParameterInspection
705 705
      */
706
-    public function getQuotaBytesUsed($dir)
707
-    {
706
+    public function getQuotaBytesUsed($dir) {
708 707
         $return = $this->seafapi->checkAccountInfo();
709 708
 
710 709
         return ($return->usage ?? 0) * self::QUOTA_MULTIPLIER_SEAFILE_TO_GROMMUNIO;
@@ -716,8 +715,7 @@  discard block
 block discarded – undo
716 715
      * @noinspection PhpUnusedParameterInspection
717 716
      * @noinspection PhpMissingParamTypeInspection
718 717
      */
719
-    public function getQuotaBytesAvailable($dir)
720
-    {
718
+    public function getQuotaBytesAvailable($dir) {
721 719
         $return = $this->seafapi->checkAccountInfo();
722 720
         $avail = $return->total - $return->usage;
723 721
         if (-2 === (int)$return->total) {
@@ -734,8 +732,7 @@  discard block
 block discarded – undo
734 732
     /**
735 733
      * Initialise form fields
736 734
      */
737
-    private function init_form()
738
-    {
735
+    private function init_form() {
739 736
         $this->metaConfig = [
740 737
             "success" => true,
741 738
             "metaData" => [
@@ -861,8 +858,7 @@  discard block
 block discarded – undo
861 858
      * @throws BackendException
862 859
      * @return no-return
863 860
      */
864
-    private function backendError(int $errorCode, string $title = null)
865
-    {
861
+    private function backendError(int $errorCode, string $title = null) {
866 862
         $message = $this->parseErrorCodeToMessage($errorCode);
867 863
         $title = $this->backendTransName;
868 864
         $this->backendErrorThrow($title, $message, $errorCode);
@@ -877,8 +873,7 @@  discard block
 block discarded – undo
877 873
      * @throws BackendException
878 874
      * @return no-return
879 875
      */
880
-    private function backendErrorThrow(string $title, string $message, int $code = 0)
881
-    {
876
+    private function backendErrorThrow(string $title, string $message, int $code = 0) {
882 877
         /** {@see \Files\Backend\Exception} */
883 878
         $exception = new BackendException($message, $code);
884 879
         $exception->setTitle($title);
@@ -892,8 +887,7 @@  discard block
 block discarded – undo
892 887
      * @throws BackendException
893 888
      * @return no-return
894 889
      */
895
-    private function backendException(Throwable $t)
896
-    {
890
+    private function backendException(Throwable $t) {
897 891
         // if it is already a backend exception, throw it.
898 892
         if ($t instanceof BackendException) {
899 893
             throw $t;
@@ -926,8 +920,7 @@  discard block
 block discarded – undo
926 920
      * @throws BackendException
927 921
      * @return void
928 922
      */
929
-    private function backendExceptionSeafapi(Exception $exception)
930
-    {
923
+    private function backendExceptionSeafapi(Exception $exception) {
931 924
         $code = $exception->getCode();
932 925
         $message = $exception->getMessage();
933 926
 
@@ -956,8 +949,7 @@  discard block
 block discarded – undo
956 949
      *
957 950
      * @return void
958 951
      */
959
-    private function log(string $err_string)
960
-    {
952
+    private function log(string $err_string) {
961 953
         if ($this->debug) {
962 954
             Logger::debug(self::LOG_CONTEXT, $err_string);
963 955
             $this->debugLog($err_string, 2);
@@ -973,8 +965,7 @@  discard block
 block discarded – undo
973 965
      *
974 966
      * @return string user friendly error message
975 967
      */
976
-    private function parseErrorCodeToMessage(int $error_code)
977
-    {
968
+    private function parseErrorCodeToMessage(int $error_code) {
978 969
         $error = $error_code;
979 970
 
980 971
         switch ($error) {
Please login to merge, or discard this patch.
plugins/filesbackendSeafile/php/Model/Config.php 3 patches
Indentation   +154 added lines, -154 removed lines patch added patch discarded remove patch
@@ -19,158 +19,158 @@
 block discarded – undo
19 19
  */
20 20
 class Config implements ArrayAccess
21 21
 {
22
-    private const DEF = [
23
-        'server' => 'server_address',
24
-        'port' => 'server_port',
25
-        'ssl' => 'server_ssl',
26
-        'path' => 'server_path',
27
-        'user' => 'user',
28
-        'pass' => 'password',
29
-        'sso_auth_user_token' => 'sso_auth_user_token',
30
-    ];
31
-
32
-    private const SCHEMA = [
33
-        self::DEF['server'] => [
34
-            'filter' => FILTER_VALIDATE_DOMAIN,
35
-            'flags' => FILTER_FLAG_HOSTNAME,
36
-            'options' => ['default' => 'seafile.example.com'],
37
-        ],
38
-        self::DEF['port'] => [
39
-            'filter' => FILTER_VALIDATE_INT,
40
-            'options' => ['min_range' => 0, 'max_range' => 65535, 'default' => 443],
41
-        ],
42
-        self::DEF['ssl'] => [
43
-            'filter' => FILTER_VALIDATE_BOOLEAN,
44
-            'options' => ['default' => true],
45
-        ],
46
-        self::DEF['path'] => [
47
-            'filter' => FILTER_UNSAFE_RAW,
48
-            'flags' => FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_HIGH,
49
-            'options' => ['default' => ''],
50
-        ],
51
-        self::DEF['user'] => [
52
-            'filter' => FILTER_UNSAFE_RAW,
53
-            'flags' => FILTER_FLAG_STRIP_LOW,
54
-            'options' => ['default' => ''],
55
-        ],
56
-        self::DEF['pass'] => [
57
-            'filter' => FILTER_DEFAULT,
58
-            'options' => ['default' => ''],
59
-        ],
60
-        self::DEF['sso_auth_user_token'] => [
61
-            'filter' => FILTER_DEFAULT,
62
-            'flags' => FILTER_FLAG_EMPTY_STRING_NULL,
63
-            'options' => ['default' => null],
64
-        ],
65
-    ];
66
-
67
-    private array $config = [];
68
-
69
-    public function __construct(array $config = [])
70
-    {
71
-        $this->importConfigArray($config);
72
-    }
73
-
74
-    /**
75
-     * Get the URL of the Seafile servers REST API
76
-     *
77
-     * @return string
78
-     */
79
-    public function getApiUrl(): string
80
-    {
81
-        $config = $this;
82
-
83
-        $ssl = (bool)$config->ssl;
84
-        $defaultPort = $ssl ? 443 : 80;
85
-        $port = max(0, min(65535, (int)$config->port)) ?: $defaultPort;
86
-        $host = rtrim($config->server, '/');
87
-        $path = ltrim($config->path, '/');
88
-
89
-        $url = sprintf('http%s://%s:%d/%s', $ssl ? 's' : '', $host, $port, $path);
90
-
91
-        return rtrim($url, '/');
92
-    }
93
-
94
-    /**
95
-     * read-only properties
96
-     *
97
-     * @param string $name
98
-     * @return mixed
99
-     *
100
-     * @noinspection MagicMethodsValidityInspection
101
-     * @noinspection RedundantSuppression
102
-     */
103
-    public function __get(string $name): mixed
104
-    {
105
-        if (!isset(self::DEF[$name])) {
106
-            throw new OutOfBoundsException("Not a property: \"$name\"");
107
-        }
108
-        return $this->config[self::DEF[$name]];
109
-    }
110
-
111
-    /**
112
-     * init configuration data
113
-     *
114
-     * set all named properties from associative array, overwriting self with defaults
115
-     * from the schema.
116
-     *
117
-     * @param array $config
118
-     * @return void
119
-     */
120
-    public function importConfigArray(array $config): void
121
-    {
122
-        $result = [];
123
-        foreach (self::SCHEMA as $name => $definition) {
124
-            $result[$name] =
125
-                array_key_exists($name, $config)
126
-                    ? $config[$name]
127
-                    : $this->config[$name] ?? $definition['options']['default'] ?? null;
128
-        }
129
-        $filtered = filter_var_array($result, self::SCHEMA);
130
-
131
-        if (!is_array($filtered)) {
132
-            throw new UnexpectedValueException('Failed to filter Seafile configuration values.');
133
-        }
134
-
135
-        $this->config = $filtered;
136
-    }
137
-
138
-    /**
139
-     * Hide its internal state from var_dump()
140
-     *
141
-     * Note: The xdebug extension may break this behavior.
142
-     * You should not rely on it if you have debugging extensions installed.
143
-     *
144
-     * @return array
145
-     */
146
-    public function __debugInfo(): array
147
-    {
148
-        $info = $this->config;
149
-        $info['password'] = is_string($info['password']) ? '*' : null;
150
-        $info['sso_auth_user_token'] = is_string($info['sso_auth_user_token']) ? '*' : null;
151
-
152
-        return $info;
153
-    }
154
-
155
-    /* ArrayAccess implementation */
156
-
157
-    public function offsetExists(mixed $offset): bool
158
-    {
159
-        return isset($this->config[$offset]);
160
-    }
161
-
162
-    public function offsetGet(mixed $offset): mixed
163
-    {
164
-        return $this->config[$offset];
165
-    }
166
-
167
-    public function offsetSet(mixed $offset, mixed $value): void
168
-    {
169
-        trigger_error('modification by write is undefined behaviour', E_USER_WARNING);
170
-    }
171
-
172
-    public function offsetUnset(mixed $offset): void
173
-    {
174
-        trigger_error('modification by delete is undefined behaviour', E_USER_WARNING);
175
-    }
22
+	private const DEF = [
23
+		'server' => 'server_address',
24
+		'port' => 'server_port',
25
+		'ssl' => 'server_ssl',
26
+		'path' => 'server_path',
27
+		'user' => 'user',
28
+		'pass' => 'password',
29
+		'sso_auth_user_token' => 'sso_auth_user_token',
30
+	];
31
+
32
+	private const SCHEMA = [
33
+		self::DEF['server'] => [
34
+			'filter' => FILTER_VALIDATE_DOMAIN,
35
+			'flags' => FILTER_FLAG_HOSTNAME,
36
+			'options' => ['default' => 'seafile.example.com'],
37
+		],
38
+		self::DEF['port'] => [
39
+			'filter' => FILTER_VALIDATE_INT,
40
+			'options' => ['min_range' => 0, 'max_range' => 65535, 'default' => 443],
41
+		],
42
+		self::DEF['ssl'] => [
43
+			'filter' => FILTER_VALIDATE_BOOLEAN,
44
+			'options' => ['default' => true],
45
+		],
46
+		self::DEF['path'] => [
47
+			'filter' => FILTER_UNSAFE_RAW,
48
+			'flags' => FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_HIGH,
49
+			'options' => ['default' => ''],
50
+		],
51
+		self::DEF['user'] => [
52
+			'filter' => FILTER_UNSAFE_RAW,
53
+			'flags' => FILTER_FLAG_STRIP_LOW,
54
+			'options' => ['default' => ''],
55
+		],
56
+		self::DEF['pass'] => [
57
+			'filter' => FILTER_DEFAULT,
58
+			'options' => ['default' => ''],
59
+		],
60
+		self::DEF['sso_auth_user_token'] => [
61
+			'filter' => FILTER_DEFAULT,
62
+			'flags' => FILTER_FLAG_EMPTY_STRING_NULL,
63
+			'options' => ['default' => null],
64
+		],
65
+	];
66
+
67
+	private array $config = [];
68
+
69
+	public function __construct(array $config = [])
70
+	{
71
+		$this->importConfigArray($config);
72
+	}
73
+
74
+	/**
75
+	 * Get the URL of the Seafile servers REST API
76
+	 *
77
+	 * @return string
78
+	 */
79
+	public function getApiUrl(): string
80
+	{
81
+		$config = $this;
82
+
83
+		$ssl = (bool)$config->ssl;
84
+		$defaultPort = $ssl ? 443 : 80;
85
+		$port = max(0, min(65535, (int)$config->port)) ?: $defaultPort;
86
+		$host = rtrim($config->server, '/');
87
+		$path = ltrim($config->path, '/');
88
+
89
+		$url = sprintf('http%s://%s:%d/%s', $ssl ? 's' : '', $host, $port, $path);
90
+
91
+		return rtrim($url, '/');
92
+	}
93
+
94
+	/**
95
+	 * read-only properties
96
+	 *
97
+	 * @param string $name
98
+	 * @return mixed
99
+	 *
100
+	 * @noinspection MagicMethodsValidityInspection
101
+	 * @noinspection RedundantSuppression
102
+	 */
103
+	public function __get(string $name): mixed
104
+	{
105
+		if (!isset(self::DEF[$name])) {
106
+			throw new OutOfBoundsException("Not a property: \"$name\"");
107
+		}
108
+		return $this->config[self::DEF[$name]];
109
+	}
110
+
111
+	/**
112
+	 * init configuration data
113
+	 *
114
+	 * set all named properties from associative array, overwriting self with defaults
115
+	 * from the schema.
116
+	 *
117
+	 * @param array $config
118
+	 * @return void
119
+	 */
120
+	public function importConfigArray(array $config): void
121
+	{
122
+		$result = [];
123
+		foreach (self::SCHEMA as $name => $definition) {
124
+			$result[$name] =
125
+				array_key_exists($name, $config)
126
+					? $config[$name]
127
+					: $this->config[$name] ?? $definition['options']['default'] ?? null;
128
+		}
129
+		$filtered = filter_var_array($result, self::SCHEMA);
130
+
131
+		if (!is_array($filtered)) {
132
+			throw new UnexpectedValueException('Failed to filter Seafile configuration values.');
133
+		}
134
+
135
+		$this->config = $filtered;
136
+	}
137
+
138
+	/**
139
+	 * Hide its internal state from var_dump()
140
+	 *
141
+	 * Note: The xdebug extension may break this behavior.
142
+	 * You should not rely on it if you have debugging extensions installed.
143
+	 *
144
+	 * @return array
145
+	 */
146
+	public function __debugInfo(): array
147
+	{
148
+		$info = $this->config;
149
+		$info['password'] = is_string($info['password']) ? '*' : null;
150
+		$info['sso_auth_user_token'] = is_string($info['sso_auth_user_token']) ? '*' : null;
151
+
152
+		return $info;
153
+	}
154
+
155
+	/* ArrayAccess implementation */
156
+
157
+	public function offsetExists(mixed $offset): bool
158
+	{
159
+		return isset($this->config[$offset]);
160
+	}
161
+
162
+	public function offsetGet(mixed $offset): mixed
163
+	{
164
+		return $this->config[$offset];
165
+	}
166
+
167
+	public function offsetSet(mixed $offset, mixed $value): void
168
+	{
169
+		trigger_error('modification by write is undefined behaviour', E_USER_WARNING);
170
+	}
171
+
172
+	public function offsetUnset(mixed $offset): void
173
+	{
174
+		trigger_error('modification by delete is undefined behaviour', E_USER_WARNING);
175
+	}
176 176
 }
Please login to merge, or discard this patch.
Spacing   +2 added lines, -2 removed lines patch added patch discarded remove patch
@@ -80,9 +80,9 @@
 block discarded – undo
80 80
     {
81 81
         $config = $this;
82 82
 
83
-        $ssl = (bool)$config->ssl;
83
+        $ssl = (bool) $config->ssl;
84 84
         $defaultPort = $ssl ? 443 : 80;
85
-        $port = max(0, min(65535, (int)$config->port)) ?: $defaultPort;
85
+        $port = max(0, min(65535, (int) $config->port)) ?: $defaultPort;
86 86
         $host = rtrim($config->server, '/');
87 87
         $path = ltrim($config->path, '/');
88 88
 
Please login to merge, or discard this patch.
Braces   +1 added lines, -2 removed lines patch added patch discarded remove patch
@@ -66,8 +66,7 @@
 block discarded – undo
66 66
 
67 67
     private array $config = [];
68 68
 
69
-    public function __construct(array $config = [])
70
-    {
69
+    public function __construct(array $config = []) {
71 70
         $this->importConfigArray($config);
72 71
     }
73 72
 
Please login to merge, or discard this patch.
plugins/filesbackendSeafile/php/Model/Timer.php 2 patches
Indentation   +10 added lines, -10 removed lines patch added patch discarded remove patch
@@ -9,17 +9,17 @@
 block discarded – undo
9 9
  */
10 10
 final class Timer
11 11
 {
12
-    private float $start;
12
+	private float $start;
13 13
 
14
-    public function __construct()
15
-    {
16
-        $this->start = microtime(true);
17
-    }
14
+	public function __construct()
15
+	{
16
+		$this->start = microtime(true);
17
+	}
18 18
 
19
-    public function __toString(): string
20
-    {
21
-        $time = microtime(true) - $this->start;
19
+	public function __toString(): string
20
+	{
21
+		$time = microtime(true) - $this->start;
22 22
 
23
-        return \sprintf('%.3F', $time);
24
-    }
23
+		return \sprintf('%.3F', $time);
24
+	}
25 25
 }
Please login to merge, or discard this patch.
Braces   +1 added lines, -2 removed lines patch added patch discarded remove patch
@@ -11,8 +11,7 @@
 block discarded – undo
11 11
 {
12 12
     private float $start;
13 13
 
14
-    public function __construct()
15
-    {
14
+    public function __construct() {
16 15
         $this->start = microtime(true);
17 16
     }
18 17
 
Please login to merge, or discard this patch.
plugins/filesbackendSeafile/php/Model/ConfigUtil.php 1 patch
Indentation   +9 added lines, -9 removed lines patch added patch discarded remove patch
@@ -4,13 +4,13 @@
 block discarded – undo
4 4
 
5 5
 class ConfigUtil
6 6
 {
7
-    /**
8
-     * Get the users email-address (in Grommunio MAPI-Session)
9
-     *
10
-     * @return string
11
-     */
12
-    public static function loadSmtpAddress(): string
13
-    {
14
-        return $GLOBALS['mapisession']->getSMTPAddress();
15
-    }
7
+	/**
8
+	 * Get the users email-address (in Grommunio MAPI-Session)
9
+	 *
10
+	 * @return string
11
+	 */
12
+	public static function loadSmtpAddress(): string
13
+	{
14
+		return $GLOBALS['mapisession']->getSMTPAddress();
15
+	}
16 16
 }
Please login to merge, or discard this patch.
plugins/filesbackendSeafile/php/Model/SsoBackend.php 2 patches
Indentation   +49 added lines, -49 removed lines patch added patch discarded remove patch
@@ -7,59 +7,59 @@
 block discarded – undo
7 7
 
8 8
 class SsoBackend
9 9
 {
10
-    /**
11
-     * @var Config of the Backends files account
12
-     */
13
-    private ?Config $backendConfig = null;
10
+	/**
11
+	 * @var Config of the Backends files account
12
+	 */
13
+	private ?Config $backendConfig = null;
14 14
 
15
-    /**
16
-     * Bind an SsoBackend
17
-     *
18
-     * @param ?SsoBackend $self
19
-     * @psalm-param-out SsoBackend $self
20
-     * @return SsoBackend
21
-     */
22
-    public static function bind(?SsoBackend &$self = null): SsoBackend
23
-    {
24
-        if ($self === null) {
25
-            $self = new self();
26
-        }
15
+	/**
16
+	 * Bind an SsoBackend
17
+	 *
18
+	 * @param ?SsoBackend $self
19
+	 * @psalm-param-out SsoBackend $self
20
+	 * @return SsoBackend
21
+	 */
22
+	public static function bind(?SsoBackend &$self = null): SsoBackend
23
+	{
24
+		if ($self === null) {
25
+			$self = new self();
26
+		}
27 27
 
28
-        return $self;
29
-    }
28
+		return $self;
29
+	}
30 30
 
31
-    /**
32
-     * hook for {@see Backend::init_backend()}
33
-     *
34
-     * @param Config $config
35
-     * @return void
36
-     */
37
-    public function initBackend(Config $config): void
38
-    {
39
-        if ($this->backendConfig instanceof Config) {
40
-            throw new \BadMethodCallException('backend is already configured');
41
-        }
31
+	/**
32
+	 * hook for {@see Backend::init_backend()}
33
+	 *
34
+	 * @param Config $config
35
+	 * @return void
36
+	 */
37
+	public function initBackend(Config $config): void
38
+	{
39
+		if ($this->backendConfig instanceof Config) {
40
+			throw new \BadMethodCallException('backend is already configured');
41
+		}
42 42
 
43
-        $this->backendConfig = $config;
44
-    }
43
+		$this->backendConfig = $config;
44
+	}
45 45
 
46
-    /**
47
-     * hook for {@see Backend::open()}
48
-     *
49
-     * @return void
50
-     */
51
-    public function open(): void
52
-    {
53
-        if (!($this->backendConfig instanceof Config)) {
54
-            // won't work w/o having the backend config for open()
55
-            return;
56
-        }
46
+	/**
47
+	 * hook for {@see Backend::open()}
48
+	 *
49
+	 * @return void
50
+	 */
51
+	public function open(): void
52
+	{
53
+		if (!($this->backendConfig instanceof Config)) {
54
+			// won't work w/o having the backend config for open()
55
+			return;
56
+		}
57 57
 
58
-        if (isset($this->backendConfig['sso_auth_user_token'])) {
59
-            $this->backendConfig->importConfigArray([
60
-                'user' => SeafileApi::USER_PREFIX_AUTH_TOKEN . ConfigUtil::loadSmtpAddress(),
61
-                'password' => $this->backendConfig['sso_auth_user_token'],
62
-            ]);
63
-        }
64
-    }
58
+		if (isset($this->backendConfig['sso_auth_user_token'])) {
59
+			$this->backendConfig->importConfigArray([
60
+				'user' => SeafileApi::USER_PREFIX_AUTH_TOKEN . ConfigUtil::loadSmtpAddress(),
61
+				'password' => $this->backendConfig['sso_auth_user_token'],
62
+			]);
63
+		}
64
+	}
65 65
 }
Please login to merge, or discard this patch.
Spacing   +1 added lines, -1 removed lines patch added patch discarded remove patch
@@ -19,7 +19,7 @@
 block discarded – undo
19 19
      * @psalm-param-out SsoBackend $self
20 20
      * @return SsoBackend
21 21
      */
22
-    public static function bind(?SsoBackend &$self = null): SsoBackend
22
+    public static function bind(?SsoBackend & $self = null): SsoBackend
23 23
     {
24 24
         if ($self === null) {
25 25
             $self = new self();
Please login to merge, or discard this patch.
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 2 patches
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.
Braces   +3 added lines, -6 removed lines patch added patch discarded remove patch
@@ -13,8 +13,7 @@  discard block
 block discarded – undo
13 13
 	 *
14 14
 	 * @return void
15 15
 	 */
16
-	public function init()
17
-	{
16
+	public function init() {
18 17
 		$this->registerHook('server.core.settings.init.before');
19 18
 	}
20 19
 
@@ -24,8 +23,7 @@  discard block
 block discarded – undo
24 23
 	 * @param String $eventID Identifier of the hook
25 24
 	 * @param Array $data Reference to the data of the triggered hook
26 25
 	 */
27
-	public function execute($eventID, &$data)
28
-	{
26
+	public function execute($eventID, &$data) {
29 27
 		switch ($eventID) {
30 28
 			case 'server.core.settings.init.before':
31 29
 				$this->onBeforeSettingsInit($data);
@@ -41,8 +39,7 @@  discard block
 block discarded – undo
41 39
 	 * @param Array $data Reference to the data of the triggered hook
42 40
 	 * @return void
43 41
 	 */
44
-	public function onBeforeSettingsInit(&$data)
45
-	{
42
+	public function onBeforeSettingsInit(&$data) {
46 43
 		$data['settingsObj']->addSysAdminDefaults(array(
47 44
 			'zarafa' => array(
48 45
 				'v1' => array(
Please login to merge, or discard this patch.
plugins/filesbackendSeafile/php/lib/seafapi/SeafileApi.php 3 patches
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.
Spacing   +15 added lines, -15 removed lines patch added patch discarded remove patch
@@ -70,17 +70,17 @@  discard block
 block discarded – undo
70 70
      *
71 71
      * @see jsonDecode
72 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
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 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
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 84
 
85 85
     /**
86 86
      * @const string ASCII upper-case characters part of a hexit
@@ -363,7 +363,7 @@  discard block
 block discarded – undo
363 363
 
364 364
         $name = explode('/', ltrim($this->normalizePath($libNamedPath), '/'), 2)[0];
365 365
 
366
-        return (object)[
366
+        return (object) [
367 367
             'id' => $libraries[$name]->id ?? null,
368 368
             'name' => $libraries[$name]->name ?? null,
369 369
         ];
@@ -422,7 +422,7 @@  discard block
 block discarded – undo
422 422
         if (null !== $expire) {
423 423
             $expireTime = $expire;
424 424
             if (is_int($expire)) {
425
-                $expireDays = max(1, min(365, (int)$expire));
425
+                $expireDays = max(1, min(365, (int) $expire));
426 426
                 $expireTime = (new DateTimeImmutable())->add(
427 427
                     new \DateInterval("P{$expireDays}D")
428 428
                 );
@@ -1407,7 +1407,7 @@  discard block
 block discarded – undo
1407 1407
             ),
1408 1408
             self::JSON_DECODE_ACCEPT_OBJECT,
1409 1409
         );
1410
-        $this->token = (string)$data->token;
1410
+        $this->token = (string) $data->token;
1411 1411
     }
1412 1412
 
1413 1413
     /**
@@ -1528,7 +1528,7 @@  discard block
 block discarded – undo
1528 1528
         }
1529 1529
 
1530 1530
         if (self::JSON_DECODE_ACCEPT_SUCCESS_OBJECT === $accept) {
1531
-            if (is_object($result) && (array)$result === ['success' => true]) {
1531
+            if (is_object($result) && (array) $result === ['success' => true]) {
1532 1532
                 return $result;
1533 1533
             }
1534 1534
             throw JsonDecodeException::create(sprintf('json decode accept %5d error [%s] of %s', decbin($accept), gettype($result), JsonDecodeException::shorten($jsonText)), $jsonText);
@@ -1598,7 +1598,7 @@  discard block
 block discarded – undo
1598 1598
             throw new ConnectionException(curl_error($this->handle), -1);
1599 1599
         }
1600 1600
 
1601
-        $code = (int)curl_getinfo($this->handle)['http_code'];
1601
+        $code = (int) curl_getinfo($this->handle)['http_code'];
1602 1602
 
1603 1603
         $codeIsInErrorRange = $code >= 400 && $code <= 600;
1604 1604
         $codeIsNotInNonErrorCodes = !in_array($code, [200, 201, 202, 203, 204, 205, 206, 207, 301], true);
Please login to merge, or discard this patch.
Braces   +15 added lines, -27 removed lines patch added patch discarded remove patch
@@ -122,8 +122,7 @@  discard block
 block discarded – undo
122 122
      * @param string $pass
123 123
      * @param string|null $otp
124 124
      */
125
-    public function __construct(string $baseurl, string $user, string $pass, string $otp = null)
126
-    {
125
+    public function __construct(string $baseurl, string $user, string $pass, string $otp = null) {
127 126
         if (!(function_exists('curl_version'))) {
128 127
             throw new ConnectionException('PHP-CURL not installed', self::ERROR_CODE_NO_CURL);
129 128
         }
@@ -603,8 +602,7 @@  discard block
 block discarded – undo
603 602
      * @throws InvalidArgumentException
604 603
      * @return array
605 604
      */
606
-    public function shareLibraryPathToGroup(string $lib, string $path, $group, string $permission = null)
607
-    {
605
+    public function shareLibraryPathToGroup(string $lib, string $path, $group, string $permission = null) {
608 606
         $lib = $this->verifyLib($lib);
609 607
         $path = $this->normalizePath($path);
610 608
         $pathEncoded = rawurlencode($path);
@@ -637,8 +635,7 @@  discard block
 block discarded – undo
637 635
      * @throws InvalidArgumentException
638 636
      * @return array
639 637
      */
640
-    public function shareLibraryPathToUser(string $lib, string $path, string $user, string $permission = null)
641
-    {
638
+    public function shareLibraryPathToUser(string $lib, string $path, string $user, string $permission = null) {
642 639
         $lib = $this->verifyLib($lib);
643 640
         $path = $this->normalizePath($path);
644 641
         $pathEncoded = rawurlencode($path);
@@ -751,8 +748,7 @@  discard block
 block discarded – undo
751 748
      * @throws InvalidArgumentException|Exception
752 749
      * @return string|object the common "success" or the object with error_msg property
753 750
      */
754
-    public function createNewDirectory(string $lib, string $path)
755
-    {
751
+    public function createNewDirectory(string $lib, string $path) {
756 752
         $lib = $this->verifyLib($lib);
757 753
         $path = $this->normalizePath($path);
758 754
         $pathEncoded = rawurlencode($path);
@@ -777,8 +773,7 @@  discard block
 block discarded – undo
777 773
      * @throws InvalidArgumentException|Exception
778 774
      * @return string|object the common "success" or the known object with error_msg property
779 775
      */
780
-    public function deleteFile(string $lib, string $path)
781
-    {
776
+    public function deleteFile(string $lib, string $path) {
782 777
         $lib = $this->verifyLib($lib);
783 778
         $path = $this->normalizePath($path);
784 779
         $pathEncoded = rawurlencode($path);
@@ -827,8 +822,7 @@  discard block
 block discarded – undo
827 822
      * @throws InvalidArgumentException|Exception
828 823
      * @return string|false on failure
829 824
      */
830
-    public function downloadFileAsBuffer(string $lib, string $path)
831
-    {
825
+    public function downloadFileAsBuffer(string $lib, string $path) {
832 826
         $url = $this->downloadFile($lib, $path);
833 827
 
834 828
         return $this->get($url);
@@ -852,7 +846,8 @@  discard block
 block discarded – undo
852 846
 
853 847
         try {
854 848
             $result = $this->downloadFileToStream($lib, $path, $handle);
855
-        } finally {
849
+        }
850
+        finally {
856 851
             $close = fclose($handle);
857 852
         }
858 853
 
@@ -953,8 +948,7 @@  discard block
 block discarded – undo
953 948
      * @return string|object the common "success" or the known object with error_msg property
954 949
      * @throws Exception
955 950
      */
956
-    public function renameFile(string $lib, string $path, string $newName)
957
-    {
951
+    public function renameFile(string $lib, string $path, string $newName) {
958 952
         $lib = $this->verifyLib($lib);
959 953
         $path = $this->normalizePath($path);
960 954
         $pathEncoded = rawurlencode($path);
@@ -1417,8 +1411,7 @@  discard block
 block discarded – undo
1417 1411
      * @param array $curlOptions
1418 1412
      * @return bool|string
1419 1413
      */
1420
-    private function get(string $url, array $curlOptions = [])
1421
-    {
1414
+    private function get(string $url, array $curlOptions = []) {
1422 1415
         $curlOptions += $this->curlSharedOptions;
1423 1416
 
1424 1417
         return $this->curlExec($url, $curlOptions);
@@ -1432,8 +1425,7 @@  discard block
 block discarded – undo
1432 1425
      * @param array $curlOptions
1433 1426
      * @return bool|string
1434 1427
      */
1435
-    private function post(string $url, array $fields = [], array $curlOptions = [])
1436
-    {
1428
+    private function post(string $url, array $fields = [], array $curlOptions = []) {
1437 1429
         $curlOptions += $this->curlSharedOptions;
1438 1430
         $curlOptions[CURLOPT_POST] = true;
1439 1431
         $curlOptions[CURLOPT_POSTFIELDS] = $fields;
@@ -1449,8 +1441,7 @@  discard block
 block discarded – undo
1449 1441
      * @param array $curlOptions
1450 1442
      * @return bool|string
1451 1443
      */
1452
-    public function put(string $url, array $fields = [], array $curlOptions = [])
1453
-    {
1444
+    public function put(string $url, array $fields = [], array $curlOptions = []) {
1454 1445
         $curlOptions += $this->curlSharedOptions;
1455 1446
         $curlOptions[CURLOPT_CUSTOMREQUEST] = 'PUT';
1456 1447
         $curlOptions[CURLOPT_POSTFIELDS] = $fields;
@@ -1465,8 +1456,7 @@  discard block
 block discarded – undo
1465 1456
      * @param array $curlOptions
1466 1457
      * @return bool|string
1467 1458
      */
1468
-    public function delete(string $url, array $curlOptions = [])
1469
-    {
1459
+    public function delete(string $url, array $curlOptions = []) {
1470 1460
 
1471 1461
         $curlOptions += $this->curlSharedOptions;
1472 1462
         $curlOptions[CURLOPT_CUSTOMREQUEST] = 'DELETE';
@@ -1484,8 +1474,7 @@  discard block
 block discarded – undo
1484 1474
      * @throws InvalidResponseException
1485 1475
      * @return string|object|array
1486 1476
      */
1487
-    private function jsonDecode(string $jsonText, int $flags = self::JSON_DECODE_ACCEPT_DEFAULT)
1488
-    {
1477
+    private function jsonDecode(string $jsonText, int $flags = self::JSON_DECODE_ACCEPT_DEFAULT) {
1489 1478
         $accept = $flags & self::JSON_DECODE_ACCEPT_MASK;
1490 1479
         if (0 === $accept) {
1491 1480
             return $jsonText;
@@ -1564,8 +1553,7 @@  discard block
 block discarded – undo
1564 1553
      * @param array $options
1565 1554
      * @return bool|string
1566 1555
      */
1567
-    private function curlExec(string $url, array $options)
1568
-    {
1556
+    private function curlExec(string $url, array $options) {
1569 1557
         $handle = curl_init($url);
1570 1558
         if (!($handle instanceof CurlHandle)) {
1571 1559
             throw new ConnectionException('Unable to initialise cURL session.', self::ERROR_CODE_NO_CURL);
Please login to merge, or discard this patch.