grommunio /
grommunio-web
| 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
|
|||||||
| 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
|
|||||||
| 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
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
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
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
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
|
|||||||
| 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
|
|||||||
| 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
|
|||||||
| 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
|
|||||||
| 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
|
|||||||
| 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
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
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 |
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:For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths