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

Backend::getQuotaBytesAvailable()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 5
c 1
b 0
f 0
nc 2
nop 1
dl 0
loc 8
rs 10
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;
0 ignored issues
show
Bug introduced by
The type Files\Backend\Seafile\Model\Timer was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
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 LOG_CONTEXT = "SeafileBackend"; // Context for the Logger
40
41
	/**
42
	 * @const string gettext domain
43
	 */
44
	private const 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 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 SFA_ERR_UNAUTHORIZED = 401;
62
	private const SFA_ERR_FORBIDDEN = 403;
63
	private const SFA_ERR_NOTFOUND = 404;
64
	private const SFA_ERR_NOTALLOWED = 405;
65
	private const SFA_ERR_TIMEOUT = 408;
66
	private const SFA_ERR_LOCKED = 423;
67
	private const SFA_ERR_FAILED_DEPENDENCY = 423;
68
	private const SFA_ERR_INTERNAL = 500;
69
	private const SFA_ERR_UNREACHABLE = 800;
70
	private const SFA_ERR_TMP = 801;
71
	private const SFA_ERR_FEATURES = 802;
72
	private const SFA_ERR_NO_CURL = 803;
73
	private const 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();
0 ignored issues
show
Bug introduced by
The property config is declared read-only in Files\Backend\Seafile\Backend.
Loading history...
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();
0 ignored issues
show
Bug introduced by
The method open() does not exist on null. ( Ignorable by Annotation )

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

138
			$this->sso->/** @scrutinizer ignore-call */ 
139
               open();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
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();
0 ignored issues
show
Bug introduced by
The method listLibraries() does not exist on null. ( Ignorable by Annotation )

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

170
				/** @scrutinizer ignore-call */ 
171
    $listing = $this->seafapi->listLibraries();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
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';
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $result does not seem to be defined for all execution paths leading up to this point.
Loading history...
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';
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $result does not seem to be defined for all execution paths leading up to this point.
Loading history...
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) {
0 ignored issues
show
introduced by
The condition $success is always true.
Loading history...
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);
0 ignored issues
show
Unused Code introduced by
The assignment to $result is dead and can be removed.
Loading history...
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);
0 ignored issues
show
Unused Code introduced by
The assignment to $result is dead and can be removed.
Loading history...
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;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $json returns the type false|string which is incompatible with the return type mandated by Files\Backend\AbstractBackend::getFormConfig() of array.

In the issue above, the returned value is violating the contract defined by the mentioned interface.

Let's take a look at an example:

interface HasName {
    /** @return string */
    public function getName();
}

class Name {
    public $name;
}

class User implements HasName {
    /** @return string|Name */
    public function getName() {
        return new Name('foo'); // This is a violation of the ``HasName`` interface
                                // which only allows a string value to be returned.
    }
}
Loading history...
597
	}
598
599
	public function getFormConfigWithData() {
600
		return $this->getFormConfig();
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->getFormConfig() returns the type false|string which is incompatible with the return type mandated by Files\Backend\AbstractBa...getFormConfigWithData() of array.

In the issue above, the returned value is violating the contract defined by the mentioned interface.

Let's take a look at an example:

interface HasName {
    /** @return string */
    public function getName();
}

class Name {
    public $name;
}

class User implements HasName {
    /** @return string|Name */
    public function getName() {
        return new Name('foo'); // This is a violation of the ``HasName`` interface
                                // which only allows a string value to be returned.
    }
}
Loading history...
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
0 ignored issues
show
Documentation Bug introduced by
The doc comment no-return at position 0 could not be parsed: Unknown type name 'no-return' at position 0 in no-return.
Loading history...
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
0 ignored issues
show
Documentation Bug introduced by
The doc comment no-return at position 0 could not be parsed: Unknown type name 'no-return' at position 0 in no-return.
Loading history...
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
0 ignored issues
show
Documentation Bug introduced by
The doc comment no-return at position 0 could not be parsed: Unknown type name 'no-return' at position 0 in no-return.
Loading history...
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