Passed
Push — master ( a4c20e...52e05d )
by
unknown
23:57 queued 17:16
created

Backend::debugLog()   A

Complexity

Conditions 6
Paths 12

Size

Total Lines 30
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

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