Passed
Push — master ( 37cafd...a8b392 )
by
unknown
07:18
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 /** @noinspection PhpMultipleClassDeclarationsInspection */
2
3
declare(strict_types=1);
4
5
namespace Files\Backend\Seafile;
6
7
require_once __DIR__ . '/../../files/php/Files/Backend/class.abstract_backend.php';
8
require_once __DIR__ . '/../../files/php/Files/Backend/class.exception.php';
9
require_once __DIR__ . '/../../files/php/Files/Backend/interface.quota.php';
10
require_once __DIR__ . '/../../files/php/Files/Backend/interface.version.php';
11
require_once __DIR__ . '/lib/seafapi/autoload.php';
12
13
require_once __DIR__ . '/Model/Timer.php';
14
require_once __DIR__ . '/Model/Config.php';
15
require_once __DIR__ . '/Model/ConfigUtil.php';
16
require_once __DIR__ . '/Model/SsoBackend.php';
17
18
use EncryptionStore;
19
use Files\Backend\AbstractBackend;
20
use Files\Backend\Exception as BackendException;
21
use Files\Backend\iFeatureVersionInfo;
22
use Files\Backend\Seafile\Model\Config;
23
use Files\Backend\Seafile\Model\ConfigUtil;
24
use Files\Backend\Seafile\Model\SsoBackend;
25
use Files\Backend\Seafile\Model\Timer;
26
use Files\Core\Util\Logger;
27
use JsonException;
28
use Datamate\SeafileApi\Exception;
29
use Datamate\SeafileApi\SeafileApi;
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
{
40
    public const LOG_CONTEXT = "SeafileBackend"; // Context for the Logger
41
42
    /**
43
     * @const string gettext domain
44
     */
45
    private const GT_DOMAIN = 'plugin_filesbackendSeafile';
46
47
    /**
48
     * Seafile "usage" number ("bytes") to Grommunio usage display number ("bytes") multiplier.
49
     *
50
     * 1 megabyte in bytes within Seafile represents 1 mebibyte in bytes for Grommunio
51
     *
52
     * (Seafile Usage "Bytes" U) / 1000 / 1000 * 1024 * 1024 (1.048576)
53
     */
54
    private const QUOTA_MULTIPLIER_SEAFILE_TO_GROMMUNIO = 1.048576;
55
56
    /**
57
     * Error codes
58
     *
59
     * @see parseErrorCodeToMessage for description
60
     * @see Backend::backendException() for Seafile API handling
61
     */
62
    private const SFA_ERR_UNAUTHORIZED = 401;
63
    private const SFA_ERR_FORBIDDEN = 403;
64
    private const SFA_ERR_NOTFOUND = 404;
65
    private const SFA_ERR_NOTALLOWED = 405;
66
    private const SFA_ERR_TIMEOUT = 408;
67
    private const SFA_ERR_LOCKED = 423;
68
    private const SFA_ERR_FAILED_DEPENDENCY = 423;
69
    private const SFA_ERR_INTERNAL = 500;
70
    private const SFA_ERR_UNREACHABLE = 800;
71
    private const SFA_ERR_TMP = 801;
72
    private const SFA_ERR_FEATURES = 802;
73
    private const SFA_ERR_NO_CURL = 803;
74
    private const SFA_ERR_UNIMPLEMENTED = 804;
75
76
    /**
77
     * @var ?SeafileApi The Seafile API client.
78
     */
79
    private ?SeafileApi $seafapi = null;
80
81
    /**
82
     * Configuration data for the Ext JS metaform.
83
     */
84
    private array $metaConfig = [];
85
86
    /**
87
     * Debug flag that mirrors `PLUGIN_FILESBROWSER_LOGLEVEL`.
88
     */
89
    private bool $debug = false;
90
91
    private Config $config;
92
93
    private ?SsoBackend $sso = null;
94
95
    /**
96
     * Backend name used in translations.
97
     */
98
    private string $backendTransName = '';
99
100
    /**
101
     * Seafile backend constructor
102
     */
103
    public function __construct()
104
    {
105
        // initialization
106
        $this->debug = PLUGIN_FILESBROWSER_LOGLEVEL === 'DEBUG';
107
108
        $this->config = new Config();
109
110
        $this->init_form();
111
112
        // set backend description
113
        $this->backendDescription = dgettext(self::GT_DOMAIN, "With this backend, you can connect to any Seafile server.");
114
115
        // set backend display name
116
        $this->backendDisplayName = "Seafile";
117
118
        // set backend version
119
        $this->backendVersion = "2.0.68";
120
121
        // set backend name used in translations
122
        $this->backendTransName = dgettext(self::GT_DOMAIN, 'Files ' . $this->backendDisplayName . ' Backend: ');
123
    }
124
125
    ////////////////////////////////////////////////////////////////////////////
126
    /// seafapi backend methods                                              ///
127
    ////////////////////////////////////////////////////////////////////////////
128
129
    /**
130
     * Opens the connection to the Seafile server.
131
     *
132
     * @throws BackendException if connection is not successful
133
     * @return boolean true if action succeeded
134
     */
135
    public function open()
136
    {
137
        $url = $this->config->getApiUrl();
138
139
        try {
140
            $this->sso->open();
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

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