Passed
Push — master ( 6745d5...76374b )
by
unknown
05:09
created

Backend   F

Complexity

Total Complexity 103

Size/Duplication

Total Lines 1045
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 376
c 1
b 0
f 0
dl 0
loc 1045
rs 2
wmc 103

33 Methods

Rating   Name   Duplication   Size   Complexity  
A put() 0 17 2
A mkcol() 0 32 4
A __construct() 0 19 1
B put_file() 0 37 8
B ls() 0 56 10
A delete() 0 29 4
A get_file() 0 16 2
A get() 0 22 3
A open() 0 18 3
C move() 0 82 14
A gpi() 0 10 2
A backendException() 0 19 3
A getFormConfigWithData() 0 2 1
A log() 0 4 2
A isLibrary() 0 2 1
A is_dir() 0 2 1
A copy_file() 0 2 1
A getQuotaBytesUsed() 0 4 1
A init_form() 0 75 1
A getServerVersion() 0 6 2
A is_file() 0 2 1
A set_debug() 0 2 1
A getFormConfig() 0 10 2
A exists() 0 2 1
A parseErrorCodeToMessage() 0 17 1
A backendError() 0 4 1
A splitGrommunioPath() 0 11 2
A backendErrorThrow() 0 6 1
C init_backend() 0 49 14
A debugLog() 0 29 6
A copy_coll() 0 2 1
A backendExceptionSeafapi() 0 18 4
A getQuotaBytesAvailable() 0 8 2

How to fix   Complexity   

Complex Class

Complex classes like Backend often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Backend, and based on these observations, apply Extract Interface, too.

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
	 * @var 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.69";
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 (!empty($config["use_grommunio_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
				// Prefer plugin-specific KEY/IV if defined, then legacy names; otherwise, fall back
569
				$key = null;
570
				$iv = null;
571
				if (\defined('FILES_PASSWORD_KEY')) {
572
					$key = \constant('FILES_PASSWORD_KEY');
573
				}
574
				elseif (\defined('PASSWORD_KEY')) {
575
					$key = \constant('PASSWORD_KEY');
576
				}
577
				if (\defined('FILES_PASSWORD_IV')) {
578
					$iv = \constant('FILES_PASSWORD_IV');
579
				}
580
				elseif (\defined('PASSWORD_IV')) {
581
					$iv = \constant('PASSWORD_IV');
582
				}
583
584
				if (\function_exists('openssl_decrypt') && is_string($key) && is_string($iv) && $key !== '' && $iv !== '') {
585
					$dec = \openssl_decrypt($password, 'des-ede3-cbc', $key, 0, $iv);
586
					$config['password'] = ($dec !== false) ? $dec : $password;
587
				}
588
				else {
589
					// If no KEY/IV configured, assume plaintext session password
590
					$config['password'] = $password;
591
				}
592
			}
593
		}
594
595
		$this->config->importConfigArray($config);
596
597
		SsoBackend::bind($this->sso)->initBackend($this->config);
598
599
		Logger::debug(self::LOG_CONTEXT, __FUNCTION__ . ' done.');
600
	}
601
602
	/**
603
	 * @return false|string
604
	 *
605
	 * @noinspection PhpMultipleClassDeclarationsInspection Grommunio has a \JsonException shim
606
	 */
607
	public function getFormConfig() {
608
		try {
609
			$json = json_encode($this->metaConfig, JSON_THROW_ON_ERROR);
610
		}
611
		catch (\JsonException $e) {
612
			$this->log(sprintf('[%s]: %s', $e::class, $e->getMessage()));
613
			$json = false;
614
		}
615
616
		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...
617
	}
618
619
	public function getFormConfigWithData() {
620
		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...
621
	}
622
623
	/**
624
	 * set debug on (1) or off (0).
625
	 * produces a lot of debug messages in webservers error log if set to on (1).
626
	 *
627
	 * @param bool $debug enable or disable debugging
628
	 */
629
	public function set_debug($debug) {
630
		$this->debug = (bool) $debug;
631
	}
632
633
	// //////////////////////////////////////////////////////////////////////////
634
	// / not_used_implemented()                                               ///
635
	// //////////////////////////////////////////////////////////////////////////
636
637
	/**
638
	 * Duplicates a folder on the backend server.
639
	 *
640
	 * @param string $src_path
641
	 * @param string $dst_path
642
	 * @param bool   $overwrite
643
	 *
644
	 * @return bool
645
	 *
646
	 * @throws BackendException
647
	 *
648
	 * @noinspection PhpReturnDocTypeMismatchInspection Upstream Interface Issue
649
	 */
650
	public function copy_coll($src_path, $dst_path, $overwrite = false) {
651
		$this->backendError(self::SFA_ERR_UNIMPLEMENTED, 'Not implemented');
652
	}
653
654
	/**
655
	 * Duplicates a file on the backend server.
656
	 *
657
	 * @param string $src_path
658
	 * @param string $dst_path
659
	 * @param bool   $overwrite
660
	 *
661
	 * @return bool
662
	 *
663
	 * @throws BackendException
664
	 *
665
	 * @noinspection PhpReturnDocTypeMismatchInspection Upstream Interface Issue
666
	 */
667
	public function copy_file($src_path, $dst_path, $overwrite = false) {
668
		$this->backendError(self::SFA_ERR_UNIMPLEMENTED, 'Not implemented');
669
	}
670
671
	/**
672
	 * Checks if the given $path exists on the remote server.
673
	 *
674
	 * @param string $path
675
	 *
676
	 * @return bool
677
	 *
678
	 * @throws BackendException
679
	 *
680
	 * @noinspection PhpReturnDocTypeMismatchInspection Upstream Interface Issue
681
	 */
682
	public function exists($path) {
683
		$this->backendError(self::SFA_ERR_UNIMPLEMENTED, 'Not implemented');
684
	}
685
686
	/**
687
	 * Gets path information from Seafile server.
688
	 *
689
	 * @param string $path
690
	 *
691
	 * @return array directory info
692
	 *
693
	 * @throws BackendException if request is not successful
694
	 */
695
	public function gpi($path) {
696
		$this->log("[GPI] '{$path}'");
697
		$list = $this->ls(dirname($path), false); // get contents of the parent dir
698
699
		if (isset($list[$path])) {
700
			return $list[$path];
701
		}
702
703
		$this->log('[GPI] wrong response from ls');
704
		$this->backendError(self::SFA_ERR_FAILED_DEPENDENCY, 'Connection failed');
705
	}
706
707
	/**
708
	 * Checks if the given $path is a folder.
709
	 *
710
	 * @param string $path
711
	 *
712
	 * @return bool
713
	 *
714
	 * @throws BackendException
715
	 *
716
	 * @noinspection PhpReturnDocTypeMismatchInspection Upstream Interface Issue
717
	 */
718
	public function is_dir($path) {
719
		$this->backendError(self::SFA_ERR_UNIMPLEMENTED, 'Not implemented');
720
	}
721
722
	/**
723
	 * Checks if the given $path is a file.
724
	 *
725
	 * @param string $path
726
	 *
727
	 * @return bool
728
	 *
729
	 * @throws BackendException
730
	 *
731
	 * @noinspection PhpReturnDocTypeMismatchInspection Upstream Interface Issue
732
	 */
733
	public function is_file($path) {
734
		$this->backendError(self::SFA_ERR_UNIMPLEMENTED, 'Not implemented');
735
	}
736
737
	// ///////////////////////////////////////////////////////////
738
	// @see iFeatureVersionInfo implementation                 //
739
	// ///////////////////////////////////////////////////////////
740
741
	/**
742
	 * Return the version string of the server backend.
743
	 *
744
	 * @return string
745
	 *
746
	 * @throws BackendException
747
	 */
748
	public function getServerVersion() {
749
		try {
750
			return $this->seafapi->getServerVersion();
751
		}
752
		catch (\Throwable $throwable) {
753
			$this->backendException($throwable);
754
		}
755
	}
756
757
	// ///////////////////////////////////////////////////////////
758
	// @see iFeatureQuota implementation                       //
759
	// ///////////////////////////////////////////////////////////
760
761
	/**
762
	 * @param string $dir
763
	 *
764
	 * @return float
765
	 *
766
	 * @noinspection PhpMissingParamTypeInspection
767
	 * @noinspection PhpUnusedParameterInspection
768
	 */
769
	public function getQuotaBytesUsed($dir) {
770
		$return = $this->seafapi->checkAccountInfo();
771
772
		return ($return->usage ?? 0) * self::QUOTA_MULTIPLIER_SEAFILE_TO_GROMMUNIO;
773
	}
774
775
	/**
776
	 * @param string $dir
777
	 *
778
	 * @return float|int
779
	 *
780
	 * @noinspection PhpUnusedParameterInspection
781
	 * @noinspection PhpMissingParamTypeInspection
782
	 */
783
	public function getQuotaBytesAvailable($dir) {
784
		$return = $this->seafapi->checkAccountInfo();
785
		$avail = $return->total - $return->usage;
786
		if ((int) $return->total === -2) {
787
			return -1;
788
		}
789
790
		return $avail * self::QUOTA_MULTIPLIER_SEAFILE_TO_GROMMUNIO;
791
	}
792
793
	// ///////////////////////////////////////////////////////////
794
	// @internal private helper methods                        //
795
	// ///////////////////////////////////////////////////////////
796
797
	/**
798
	 * Initialise form fields.
799
	 */
800
	private function init_form() {
801
		$this->metaConfig = [
802
			"success" => true,
803
			"metaData" => [
804
				"fields" => [
805
					[
806
						"name" => "server_address",
807
						"fieldLabel" => dgettext(self::GT_DOMAIN, 'Server address'),
808
						"editor" => [
809
							"allowBlank" => false,
810
						],
811
					],
812
					[
813
						"name" => "server_port",
814
						"fieldLabel" => dgettext(self::GT_DOMAIN, 'Server port'),
815
						"editor" => [
816
							"ref" => "../../portField",
817
							"allowBlank" => false,
818
						],
819
					],
820
					[
821
						"name" => "server_ssl",
822
						"fieldLabel" => dgettext(self::GT_DOMAIN, 'Use SSL'),
823
						"editor" => [
824
							"xtype" => "checkbox",
825
							"listeners" => [
826
								"check" => "Zarafa.plugins.files.data.Actions.onCheckSSL", // this javascript function will be called!
827
							],
828
						],
829
					],
830
					[
831
						"name" => "user",
832
						"fieldLabel" => dgettext(self::GT_DOMAIN, 'Username'),
833
						"editor" => [
834
							"ref" => "../../usernameField",
835
						],
836
					],
837
					[
838
						"name" => "password",
839
						"fieldLabel" => dgettext(self::GT_DOMAIN, 'Password'),
840
						"editor" => [
841
							"ref" => "../../passwordField",
842
							"inputType" => "password",
843
						],
844
					],
845
					[
846
						"name" => "use_grommunio_credentials",
847
						"fieldLabel" => dgettext(self::GT_DOMAIN, 'Use grommunio credentials'),
848
						"editor" => [
849
							"xtype" => "checkbox",
850
							"listeners" => [
851
								"check" => "Zarafa.plugins.files.data.Actions.onCheckCredentials", // this javascript function will be called!
852
							],
853
						],
854
					],
855
				],
856
				"formConfig" => [
857
					"labelAlign" => "left",
858
					"columnCount" => 1,
859
					"labelWidth" => 80,
860
					"defaults" => [
861
						"width" => 292,
862
					],
863
				],
864
			],
865
866
			// here we can specify the default values.
867
			"data" => [
868
				"server_address" => "seafile.example.com",
869
				"server_port" => "443",
870
				"server_ssl" => "1",
871
				"server_path" => "",
872
				"use_grommunio_credentials" => "0",
873
				"user" => "",
874
				"password" => "",
875
			],
876
		];
877
	}
878
879
	/**
880
	 * split grommunio path into library and library path.
881
	 *
882
	 * obtains the seafile library ID (if available, otherwise NULL)
883
	 *
884
	 * return protocol: object{
885
	 *   lib: ?string     # library ID e.g. "ccc60923-8cdf-4cc8-8f71-df86aba3a085"
886
	 *   path: ?string    # path inside library, always prefixed with "/" if set
887
	 *   libName: ?string # name of the library
888
	 * }
889
	 *
890
	 * @throws Exception
891
	 */
892
	private function splitGrommunioPath(string $grommunioPath): object {
893
		static $libraries;
894
		$libraries ??= array_column($this->seafapi->listLibraries(), null, 'name');
895
896
		[, $libName, $path] = explode('/', $grommunioPath, 3) + [null, null, null];
897
		if ($path !== null) {
898
			$path = "/{$path}";
899
		}
900
		$lib = $libraries[$libName]->id ?? null;
901
902
		return (object) ['lib' => $lib, 'path' => $path, 'libName' => $libName];
903
	}
904
905
	/**
906
	 * test if a grommunio path is a library only.
907
	 */
908
	private function isLibrary(string $grommunioPath): bool {
909
		return substr_count(trim($grommunioPath, '/'), '/') === 0;
910
	}
911
912
	/**
913
	 * Turn a Backend error code into a Backend exception.
914
	 *
915
	 * @param int     $errorCode one of the Backend::SFA_ERR_* codes, e.g. {@see Backend::SFA_ERR_INTERNAL}
916
	 * @param ?string $title     msg-id from the plugin_files domain, e.g. 'PHP-CURL not installed'
917
	 *
918
	 * @return never
919
	 *
920
	 * @throws BackendException
921
	 */
922
	private function backendError(int $errorCode, ?string $title = null) {
923
		$message = $this->parseErrorCodeToMessage($errorCode);
924
		$title = $this->backendTransName;
925
		$this->backendErrorThrow($title, $message, $errorCode);
926
	}
927
928
	/**
929
	 * Throw a BackendException w/ title, message and code.
930
	 *
931
	 * @throws BackendException
932
	 */
933
	private function backendErrorThrow(string $title, string $message, int $code = 0): never {
934
		/** {@see BackendException} */
935
		$exception = new BackendException($message, $code);
936
		$exception->setTitle($title);
937
938
		throw $exception;
939
	}
940
941
	/**
942
	 * Turn a throwable/exception with the Seafile API into a Backend exception.
943
	 *
944
	 * @return never
945
	 *
946
	 * @throws BackendException
947
	 */
948
	private function backendException(\Throwable $t) {
949
		// if it is already a backend exception, throw it.
950
		if ($t instanceof BackendException) {
951
			throw $t;
952
		}
953
954
		[$callSite, $inFunc] = debug_backtrace();
955
		$logLabel = "{$inFunc['function']}:{$callSite['line']}";
956
957
		$class = $t::class;
958
		$message = $t->getMessage();
959
		$this->log(sprintf('%s: [%s] #%s: %s', $logLabel, $class, $t->getCode(), $message));
960
961
		// All SeafileApi exceptions are handled by this
962
		if ($t instanceof Exception) {
963
			$this->backendExceptionSeafapi($t);
964
		}
965
966
		$this->backendErrorThrow('Error', "[SEAFILE {$logLabel}] {$class}: {$message}", 500);
967
	}
968
969
	/**
970
	 * Turn an Exception into a BackendException.
971
	 *
972
	 * Enriches message information for grommunio with API error messages
973
	 * if a Seafile ConnectionException.
974
	 *
975
	 * helper for {@see Backend::backendException()}
976
	 *
977
	 * @throws BackendException
978
	 */
979
	private function backendExceptionSeafapi(Exception $exception) {
980
		$code = $exception->getCode();
981
		$message = $exception->getMessage();
982
983
		$apiErrorMessagesHtml = null;
984
		if ($exception instanceof Exception\ConnectionException) {
985
			$messages = $exception->tryApiErrorMessages();
986
			$messages === null || $apiErrorMessagesHtml = implode(
987
				"<br/>\n",
988
				array_map(static fn (string $subject) => htmlspecialchars($subject, ENT_QUOTES | ENT_HTML5), $messages)
989
			) . "<br/>\n";
990
		}
991
992
		if ($apiErrorMessagesHtml !== null) {
993
			$message .= " - {$apiErrorMessagesHtml}";
994
		}
995
996
		$this->backendErrorThrow($this->backendDisplayName . ' Error', $message, $code);
997
	}
998
999
	/**
1000
	 * a simple php error_log wrapper.
1001
	 *
1002
	 * @param string $err_string error message
1003
	 */
1004
	private function log(string $err_string) {
1005
		if ($this->debug) {
1006
			Logger::debug(self::LOG_CONTEXT, $err_string);
1007
			$this->debugLog($err_string, 2);
1008
		}
1009
	}
1010
1011
	/**
1012
	 * This function will return a user-friendly error string.
1013
	 *
1014
	 * Error codes were migrated from WebDav backend.
1015
	 *
1016
	 * @param int $error_code An error code
1017
	 *
1018
	 * @return string user friendly error message
1019
	 */
1020
	private function parseErrorCodeToMessage(int $error_code) {
1021
		$error = $error_code;
1022
1023
		return match ($error) {
1024
			CURLE_BAD_PASSWORD_ENTERED, self::SFA_ERR_UNAUTHORIZED => dgettext(self::GT_DOMAIN, 'Unauthorized. Wrong username or password.'),
1025
			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?'),
1026
			self::SFA_ERR_FORBIDDEN => dgettext(self::GT_DOMAIN, 'You don\'t have enough permissions for this operation.'),
1027
			self::SFA_ERR_NOTFOUND => dgettext(self::GT_DOMAIN, 'File is not available any more.'),
1028
			self::SFA_ERR_TIMEOUT => dgettext(self::GT_DOMAIN, 'Connection to server timed out. Retry later.'),
1029
			self::SFA_ERR_LOCKED => dgettext(self::GT_DOMAIN, 'This file is locked by another user.'),
1030
			self::SFA_ERR_FAILED_DEPENDENCY => dgettext(self::GT_DOMAIN, 'The request failed due to failure of a previous request.'),
1031
			self::SFA_ERR_INTERNAL => dgettext(self::GT_DOMAIN, 'Seafile-server encountered a problem.'),
1032
			self::SFA_ERR_TMP => dgettext(self::GT_DOMAIN, 'Could not write to temporary directory. Contact the server administrator.'),
1033
			self::SFA_ERR_FEATURES => dgettext(self::GT_DOMAIN, 'Could not retrieve list of server features. Contact the server administrator.'),
1034
			self::SFA_ERR_NO_CURL => dgettext(self::GT_DOMAIN, 'PHP-Curl is not available. Contact your system administrator.'),
1035
			self::SFA_ERR_UNIMPLEMENTED => dgettext(self::GT_DOMAIN, 'This function is not yet implemented.'),
1036
			default => dgettext(self::GT_DOMAIN, 'Unknown error'),
1037
		};
1038
	}
1039
1040
	// ///////////////////////////////////////////////////////////
1041
	// @debug development helper method                        //
1042
	// ///////////////////////////////////////////////////////////
1043
1044
	/**
1045
	 * Log debug message while developing the plugin in dedicated DEBUG.log file.
1046
	 *
1047
	 * TODO(tk): remove debugLog, we shall not use it in production.
1048
	 *
1049
	 * @param mixed $message
1050
	 * @param int   $backSteps [optional] offset of call point in stacktrace
1051
	 *
1052
	 * @see \Files\Backend\Seafile\Backend::log()
1053
	 */
1054
	public function debugLog($message, int $backSteps = 0): void {
1055
		$baseDir = dirname(__DIR__);
1056
		$debugLogFile = $baseDir . '/DEBUG.log';
1057
		$backtrace = debug_backtrace();
1058
		$callPoint = $backtrace[$backSteps];
1059
		$path = $callPoint['file'];
1060
		$shortPath = $path;
1061
		if (str_starts_with($path, $baseDir)) {
1062
			$shortPath = substr($path, strlen($baseDir));
1063
		}
1064
		// TODO(tk): track if the parent function is log() or not, not only the number of back-steps (or check all call points)
1065
		$callInfoExtra = '';
1066
		if ($backSteps !== 1) { // this is not a log() call with debug switched on
1067
			$callInfoExtra = " ({$backSteps}) " . $backtrace[$backSteps + 1]['type'] . $backtrace[$backSteps + 1]['function'] . '()';
1068
		}
1069
		$callInfo = sprintf(' [ %s:%s ]%s', $shortPath, $callPoint['line'], $callInfoExtra);
1070
1071
		if (!is_string($message)) {
1072
			/** @noinspection JsonEncodingApiUsageInspection */
1073
			$type = gettype($message);
1074
			if ($type === 'object' && is_callable([$message, '__debugInfo'])) {
1075
				$message = $message->__debugInfo();
1076
			}
1077
			$message = $type . ': ' . json_encode($message, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
1078
		}
1079
1080
		$message = substr(sprintf('%.3f', $_SERVER['REQUEST_TIME_FLOAT']), -7) . " {$message}";
1081
1082
		error_log(str_pad($message, 48) . $callInfo . "\n", 3, $debugLogFile);
1083
	}
1084
}
1085