Backend::delete()   A
last analyzed

Complexity

Conditions 3
Paths 7

Size

Total Lines 25
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 18
c 0
b 0
f 0
nc 7
nop 1
dl 0
loc 25
rs 9.6666
1
<?php
2
3
namespace Files\Backend\Webdav;
4
5
require_once __DIR__ . "/sabredav/FilesWebDavClient.php";
6
require_once __DIR__ . "/../class.abstract_backend.php";
7
require_once __DIR__ . "/../class.exception.php";
8
9
use Files\Backend\AbstractBackend;
10
use Files\Backend\Exception as BackendException;
11
use Files\Backend\iFeatureQuota;
12
use Files\Backend\iFeatureVersionInfo;
13
use Files\Backend\Webdav\sabredav\FilesWebDavClient;
0 ignored issues
show
Bug introduced by
The type Files\Backend\Webdav\sabredav\FilesWebDavClient was not found. Maybe you did not declare it correctly or list all dependencies?

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

filter:
    dependency_paths: ["lib/*"]

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

Loading history...
14
use Sabre\DAV\Client;
15
use Sabre\DAV\Exception;
16
use Sabre\HTTP\ClientException;
17
18
/**
19
 * This is a file backend for webdav servers.
20
 *
21
 * @class   Backend
22
 *
23
 * @extends AbstractBackend
24
 */
25
class Backend extends AbstractBackend implements iFeatureQuota, iFeatureVersionInfo {
26
	/**
27
	 * Error codes
28
	 * see @parseErrorCodeToMessage for description.
29
	 */
30
	public const WD_ERR_UNAUTHORIZED = 401;
31
	public const WD_ERR_FORBIDDEN = 403;
32
	public const WD_ERR_NOTFOUND = 404;
33
	public const WD_ERR_NOTALLOWED = 405;
34
	public const WD_ERR_TIMEOUT = 408;
35
	public const WD_ERR_LOCKED = 423;
36
	public const WD_ERR_FAILED_DEPENDENCY = 423;
37
	public const WD_ERR_INTERNAL = 500;
38
	public const WD_ERR_UNREACHABLE = 800;
39
	public const WD_ERR_TMP = 801;
40
	public const WD_ERR_FEATURES = 802;
41
	public const WD_ERR_NO_CURL = 803;
42
43
	/**
44
	 * Configuration data for the extjs metaform.
45
	 */
46
	protected $formConfig;
47
	protected $formFields;
48
	protected $metaConfig;
49
50
	/**
51
	 * @var bool debugging flag, if true, debugging is enabled
52
	 */
53
	public $debug = false;
54
55
	/**
56
	 * @var int webdav server port
57
	 */
58
	public $port = 80;
59
60
	/**
61
	 * @var string hostname or ip
62
	 */
63
	public $server = "localhost";
64
65
	/**
66
	 * @var string global path prefix for all requests
67
	 */
68
	public $path = "/webdav.php";
69
70
	/**
71
	 * @var bool if true, ssl is used
72
	 */
73
	public $ssl = false;
74
75
	/**
76
	 * @var bool allow self signed certificates
77
	 */
78
	public $allowselfsigned = true;
79
80
	/**
81
	 * @var string the username
82
	 */
83
	public $user = "";
84
85
	/**
86
	 * @var string the password
87
	 */
88
	public $pass = "";
89
90
	/**
91
	 * @var FilesWebDavClient the SabreDAV client object
92
	 */
93
	public $sabre_client;
94
95
	/**
96
	 * @var string Backend name used in translations
97
	 */
98
	public $backendTransName;
99
100
	/**
101
	 * @constructor
102
	 */
103
	public function __construct() {
104
		// initialization
105
		$this->debug = PLUGIN_FILESBROWSER_LOGLEVEL === "DEBUG" ? true : false;
0 ignored issues
show
introduced by
The condition Files\Backend\Webdav\PLU...ER_LOGLEVEL === 'DEBUG' is always false.
Loading history...
106
107
		$this->init_form();
108
109
		// set backend description
110
		$this->backendDescription = _("With this backend, you can connect to any WebDAV server.");
111
112
		// set backend display name
113
		$this->backendDisplayName = "Webdav";
114
115
		// set backend version
116
		// TODO: this should be changed on every release
117
		$this->backendVersion = "3.0";
118
119
		// Backend name used in translations
120
		$this->backendTransName = _('Files WebDAV Backend: ');
121
	}
122
123
	/**
124
	 * Initialise form fields.
125
	 */
126
	private function init_form() {
127
		$this->formConfig = [
128
			"labelAlign" => "left",
129
			"columnCount" => 1,
130
			"labelWidth" => 80,
131
			"defaults" => [
132
				"width" => 292,
133
			],
134
		];
135
136
		$this->formFields = [
137
			[
138
				"name" => "server_address",
139
				"fieldLabel" => _('Server address'),
140
				"editor" => [
141
					"allowBlank" => false,
142
				],
143
			],
144
			[
145
				"name" => "server_port",
146
				"fieldLabel" => _('Server port'),
147
				"editor" => [
148
					"ref" => "../../portField",
149
					"allowBlank" => false,
150
				],
151
			],
152
			[
153
				"name" => "server_ssl",
154
				"fieldLabel" => _('Use TLS'),
155
				"editor" => [
156
					"xtype" => "checkbox",
157
					"listeners" => [
158
						"check" => "Zarafa.plugins.files.data.Actions.onCheckSSL", // this javascript function will be called!
159
					],
160
				],
161
			],
162
			[
163
				"name" => "server_path",
164
				"fieldLabel" => _('Webdav base path'),
165
				"editor" => [
166
				],
167
			],
168
			[
169
				"name" => "user",
170
				"fieldLabel" => _('Username'),
171
				"editor" => [
172
					"ref" => "../../usernameField",
173
				],
174
			],
175
			[
176
				"name" => "password",
177
				"fieldLabel" => _('Password'),
178
				"editor" => [
179
					"ref" => "../../passwordField",
180
					"inputType" => "password",
181
				],
182
			],
183
			[
184
				"name" => "use_grommunio_credentials",
185
				"fieldLabel" => _('Use grommunio credentials'),
186
				"editor" => [
187
					"xtype" => "checkbox",
188
					"listeners" => [
189
						"check" => "Zarafa.plugins.files.data.Actions.onCheckCredentials", // this javascript function will be called!
190
					],
191
				],
192
			],
193
		];
194
195
		$this->metaConfig = [
196
			"success" => true,
197
			"metaData" => [
198
				"fields" => $this->formFields,
199
				"formConfig" => $this->formConfig,
200
			],
201
			"data" => [
202
				"server_address" => "files.demo.com",
203
				"server_port" => "80",
204
				"server_path" => "/remote.php/webdav",
205
			],
206
		];
207
	}
208
209
	/**
210
	 * Initialize backend from $backend_config array.
211
	 */
212
	public function init_backend($backend_config) {
213
		$this->set_server($backend_config["server_address"]);
214
		$this->set_port($backend_config["server_port"]);
215
		$this->set_base($backend_config["server_path"]);
216
		$this->set_ssl($backend_config["server_ssl"]);
217
218
		// set user and password
219
		if ($backend_config["use_grommunio_credentials"] === false) {
220
			$this->set_user($backend_config["user"]);
221
			$this->set_pass($backend_config["password"]);
222
		}
223
		else {
224
			// For backward compatibility we will check if the Encryption store exists. If not,
225
			// we will fall back to the old way of retrieving the password from the session.
226
			if (class_exists('EncryptionStore')) {
227
				// Get the username and password from the Encryption store
228
				$encryptionStore = \EncryptionStore::getInstance();
229
				$this->set_user($encryptionStore->get('username'));
230
				$this->set_pass($encryptionStore->get('password'));
231
			}
232
			else {
233
				$this->set_user($GLOBALS['mapisession']->getUserName());
234
				$password = $_SESSION['password'];
235
				if (function_exists('openssl_decrypt')) {
236
					$this->set_pass(openssl_decrypt($password, "des-ede3-cbc", PASSWORD_KEY, 0, PASSWORD_IV));
237
				}
238
			}
239
		}
240
	}
241
242
	/**
243
	 * Set webdav server. FQN or IP address.
244
	 *
245
	 * @param string $server hostname or ip of the ftp server
246
	 */
247
	public function set_server($server) {
248
		$this->server = $server;
249
	}
250
251
	/**
252
	 * Set base path.
253
	 *
254
	 * @param string $pp the global path prefix
255
	 */
256
	public function set_base($pp) {
257
		$this->path = $pp;
258
		$this->log('Base path set to ' . $this->path);
259
	}
260
261
	/**
262
	 * Set ssl.
263
	 *
264
	 * @param mixed $ssl (1 = true, 0 = false)
265
	 */
266
	public function set_ssl($ssl) {
267
		$this->ssl = $ssl ? true : false;
268
		$this->log('SSL extension was set to ' . $this->ssl);
269
	}
270
271
	/**
272
	 * Allow self signed certificates - unimplemented.
273
	 *
274
	 * @param bool $allowselfsigned Allow self signed certificates. Not yet implemented.
275
	 */
276
	public function set_selfsigned($allowselfsigned) {
277
		$this->allowselfsigned = $allowselfsigned;
278
	}
279
280
	/**
281
	 * Set tcp port of webdav server. Default is 80.
282
	 *
283
	 * @param int $port the port of the ftp server
284
	 */
285
	public function set_port($port) {
286
		$this->port = $port;
287
	}
288
289
	/**
290
	 * set user name for authentication.
291
	 *
292
	 * @param string $user username
293
	 */
294
	public function set_user($user) {
295
		$this->user = $user;
296
	}
297
298
	/**
299
	 * Set password for authentication.
300
	 *
301
	 * @param string $pass password
302
	 */
303
	public function set_pass($pass) {
304
		$this->pass = $pass;
305
	}
306
307
	/**
308
	 * set debug on (1) or off (0).
309
	 * produces a lot of debug messages in webservers error log if set to on (1).
310
	 *
311
	 * @param bool $debug enable or disable debugging
312
	 */
313
	public function set_debug($debug) {
314
		$this->debug = $debug;
315
	}
316
317
	/**
318
	 * Opens the connection to the webdav server.
319
	 *
320
	 * @return bool true if action succeeded
321
	 *
322
	 * @throws BackendException if connection is not successful
323
	 */
324
	public function open() {
325
		// check if curl is available
326
		$serverHasCurl = function_exists('curl_version');
327
		if (!$serverHasCurl) {
328
			$e = new BackendException($this->parseErrorCodeToMessage(self::WD_ERR_NO_CURL), 500);
329
			$e->setTitle($this->backendTransName . _('PHP-CURL is not installed'));
330
331
			throw $e;
332
		}
333
334
		$davsettings = [
335
			'baseUri' => $this->webdavUrl(),
336
			'userName' => $this->user,
337
			'password' => $this->pass,
338
			'authType' => Client::AUTH_BASIC,
339
		];
340
341
		try {
342
			$this->sabre_client = new FilesWebDavClient($davsettings);
343
			$this->sabre_client->addCurlSetting(CURLOPT_SSL_VERIFYPEER, !$this->allowselfsigned);
344
345
			return true;
346
		}
347
		catch (Exception $e) {
348
			$this->log('Failed to open: ' . $e->getMessage());
349
			if (intval($e->getHTTPCode()) == 401) {
350
				$e = new BackendException($this->parseErrorCodeToMessage(self::WD_ERR_UNAUTHORIZED), $e->getHTTPCode());
351
				$e->setTitle($this->backendTransName . _('Access denied'));
352
353
				throw $e;
354
			}
355
			$e = new BackendException($this->parseErrorCodeToMessage(self::WD_ERR_UNREACHABLE), $e->getHTTPCode());
356
			$e->setTitle($this->backendTransName . _('Connection failed'));
357
358
			throw $e;
359
		}
360
	}
361
362
	/**
363
	 * show content of a directory.
364
	 *
365
	 * @param bool  $hidefirst Optional parameter to hide the root entry. Default true
366
	 * @param mixed $dir
367
	 *
368
	 * @return mixed array with directory content
369
	 *
370
	 * @throws BackendException if request is not successful
371
	 */
372
	public function ls($dir, $hidefirst = true) {
373
		$time_start = microtime(true);
374
		$dir = $this->removeSlash($dir);
375
		$lsdata = [];
376
		$this->log("[LS] start for dir: {$dir}");
377
378
		try {
379
			$response = $this->sabre_client->propFind($dir, [
380
				'{http://owncloud.org/ns}fileid',
381
				'{DAV:}resourcetype',
382
				'{DAV:}getcontentlength',
383
				'{DAV:}getlastmodified',
384
				'{DAV:}getcontenttype',
385
				'{DAV:}quota-used-bytes',
386
				'{DAV:}quota-available-bytes',
387
			], 1);
388
			$this->log("[LS] backend fetched in: " . (microtime(true) - $time_start) . " seconds.");
389
390
			foreach ($response as $name => $fields) {
391
				if ($hidefirst) {
392
					$hidefirst = false; // skip the first line - its the requested dir itself
393
394
					continue;
395
				}
396
397
				$name = substr($name, strlen($this->path)); // skip the webdav path
398
				$name = urldecode($name);
399
400
				// Always fallback to a file resourceType
401
				$type = "file";
402
				if (isset($fields['{DAV:}resourcetype'])) {
403
					$value = $fields['{DAV:}resourcetype']->getValue();
404
					if (!empty($value) && $value[0] === "{DAV:}collection") {
405
						$type = "collection";
406
					}
407
				}
408
409
				$lsdata[$name] = [
410
					"fileid" => $fields["{http://owncloud.org/ns}fileid"] ?? '-1',
411
					"resourcetype" => $type,
412
					"getcontentlength" => $fields["{DAV:}getcontentlength"] ?? null,
413
					"getlastmodified" => $fields["{DAV:}getlastmodified"] ?? null,
414
					"getcontenttype" => $fields["{DAV:}getcontenttype"] ?? null,
415
					"quota-used-bytes" => $fields["{DAV:}quota-used-bytes"] ?? null,
416
					"quota-available-bytes" => $fields["{DAV:}quota-available-bytes"] ?? null,
417
				];
418
			}
419
			$time_end = microtime(true);
420
			$time = $time_end - $time_start;
421
			$this->log("[LS] done in {$time} seconds");
422
423
			return $lsdata;
424
		}
425
		catch (ClientException $e) {
426
			$this->log('ls sabre ClientException: ' . $e->getMessage());
427
			$e = new BackendException($this->parseErrorCodeToMessage($e->getCode()), $e->getCode());
428
			$e->setTitle($this->backendTransName . _('Sabre error'));
429
430
			throw $e;
431
		}
432
		catch (Exception $e) {
433
			$this->log('ls general exception: ' . $e->getMessage() . " [" . $e->getHTTPCode() . "]");
434
			// THIS IS A FIX FOR OWNCLOUD - It does return 500 instead of 401...
435
			$err_code = $e->getHTTPCode();
436
			// check if code is 500 - then we should try to parse the error message
437
			if ($err_code === 500) {
438
				// message example: HTTP-Code: 401
439
				$regx = '/[0-9]{3}/';
440
				if (preg_match($regx, $e->getMessage(), $found)) {
441
					$err_code = $found[0];
442
				}
443
			}
444
			$e = new BackendException($this->parseErrorCodeToMessage($err_code), $err_code);
445
			$e->setTitle($this->backendTransName . _('Connection failed'));
446
447
			throw $e;
448
		}
449
	}
450
451
	/**
452
	 * create a new directory.
453
	 *
454
	 * @param string $dir directory path
455
	 *
456
	 * @return bool true if action succeeded
457
	 *
458
	 * @throws BackendException if request is not successful
459
	 */
460
	public function mkcol($dir) {
461
		$time_start = microtime(true);
462
		$dir = $this->removeSlash($dir);
463
		$this->log("[MKCOL] start for dir: {$dir}");
464
465
		try {
466
			$response = $this->sabre_client->request("MKCOL", $dir, null);
467
			$time_end = microtime(true);
468
			$time = $time_end - $time_start;
469
			$this->log("[MKCOL] done in {$time} seconds: " . $response['statusCode']);
470
471
			return true;
472
		}
473
		catch (ClientException $e) {
474
			$e = new BackendException($this->parseErrorCodeToMessage($e->getCode()), $e->getCode());
475
			$e->setTitle($this->backendTransName . _('Sabre error'));
476
477
			throw $e;
478
		}
479
		catch (Exception $e) {
480
			$this->log('[MKCOL] fatal: ' . $e->getMessage());
481
			$e = new BackendException($this->parseErrorCodeToMessage($e->getHTTPCode()), $e->getHTTPCode());
482
			$e->setTitle($this->backendTransName . _('Directory creation failed'));
483
484
			throw $e;
485
		}
486
	}
487
488
	/**
489
	 * delete a file or directory.
490
	 *
491
	 * @param string $path file/directory path
492
	 *
493
	 * @return bool true if action succeeded
494
	 *
495
	 * @throws BackendException if request is not successful
496
	 */
497
	public function delete($path) {
498
		$time_start = microtime(true);
499
		$path = $this->removeSlash($path);
500
		$this->log("[DELETE] start for dir: {$path}");
501
502
		try {
503
			$response = $this->sabre_client->request("DELETE", $path, null);
504
			$time_end = microtime(true);
505
			$time = $time_end - $time_start;
506
			$this->log("[DELETE] done in {$time} seconds: " . $response['statusCode']);
507
508
			return true;
509
		}
510
		catch (ClientException $e) {
511
			$e = new BackendException($this->parseErrorCodeToMessage($e->getCode()), $e->getCode());
512
			$e->setTitle($this->backendTransName . _('Sabre error'));
513
514
			throw $e;
515
		}
516
		catch (Exception $e) {
517
			$this->log('[DELETE] fatal: ' . $e->getMessage());
518
			$e = new BackendException($this->parseErrorCodeToMessage($e->getHTTPCode()), $e->getHTTPCode());
519
			$e->setTitle($this->backendTransName . _('Deletion failed'));
520
521
			throw $e;
522
		}
523
	}
524
525
	/**
526
	 * Move a file or collection on webdav server (serverside)
527
	 * If you set param overwrite as true, the target will be overwritten.
528
	 *
529
	 * @param string $src_path  Source path
530
	 * @param bool   $overwrite Overwrite file if exists in $dest_path
531
	 * @param mixed  $dst_path
532
	 *
533
	 * @return bool true if action succeeded
534
	 *
535
	 * @throws BackendException if request is not successful
536
	 */
537
	public function move($src_path, $dst_path, $overwrite = false) {
538
		$time_start = microtime(true);
539
		$src_path = $this->removeSlash($src_path);
540
		$dst_path = $this->webdavUrl() . $this->removeSlash($dst_path);
541
		$this->log("[MOVE] start for dir: {$src_path} -> {$dst_path}");
542
		if ($overwrite) {
543
			$overwrite = 'T';
544
		}
545
		else {
546
			$overwrite = 'F';
547
		}
548
549
		try {
550
			$response = $this->sabre_client->request("MOVE", $src_path, null, ["Destination" => $dst_path, 'Overwrite' => $overwrite]);
551
			$time_end = microtime(true);
552
			$time = $time_end - $time_start;
553
			$this->log("[MOVE] done in {$time} seconds: " . $response['statusCode']);
554
555
			return true;
556
		}
557
		catch (ClientException $e) {
558
			$e = new BackendException($this->parseErrorCodeToMessage($e->getCode()), $e->getCode());
559
			$e->setTitle($this->backendTransName . _('Sabre error'));
560
561
			throw $e;
562
		}
563
		catch (Exception $e) {
564
			$this->log('[MOVE] fatal: ' . $e->getMessage());
565
			$e = new BackendException($this->parseErrorCodeToMessage($e->getHTTPCode()), $e->getHTTPCode());
566
			$e->setTitle($this->backendTransName . _('Moving failed'));
567
568
			throw $e;
569
		}
570
	}
571
572
	/**
573
	 * Puts a file into a collection.
574
	 *
575
	 * @param string $path Destination path
576
	 * @param mixed  $data
577
	 *
578
	 * @string mixed $data Any kind of data
579
	 *
580
	 * @return bool true if action succeeded
581
	 *
582
	 * @throws BackendException if request is not successful
583
	 */
584
	public function put($path, $data) {
585
		$time_start = microtime(true);
586
		$path = $this->removeSlash($path);
587
		$this->log("[PUT] start for dir: {$path} strlen: " . strlen((string) $data));
588
589
		try {
590
			$response = $this->sabre_client->request("PUT", $path, $data);
591
			$time_end = microtime(true);
592
			$time = $time_end - $time_start;
593
			$this->log("[PUT] done in {$time} seconds: " . $response['statusCode']);
594
595
			return true;
596
		}
597
		catch (ClientException $e) {
598
			$e = new BackendException($this->parseErrorCodeToMessage($e->getCode()), $e->getCode());
599
			$e->setTitle($this->backendTransName . _('Sabre error'));
600
601
			throw $e;
602
		}
603
		catch (Exception $e) {
604
			$this->log('[PUT] fatal: ' . $e->getMessage());
605
			$e = new BackendException($this->parseErrorCodeToMessage($e->getHTTPCode()), $e->getHTTPCode());
606
			$e->setTitle($this->backendTransName . _('Connection failed'));
607
608
			throw $e;
609
		}
610
	}
611
612
	/**
613
	 * Upload a local file.
614
	 *
615
	 * @param string $path     Destination path on the server
616
	 * @param string $filename Local filename for the file that should be uploaded
617
	 *
618
	 * @return bool true if action succeeded
619
	 *
620
	 * @throws BackendException if request is not successful
621
	 */
622
	public function put_file($path, $filename) {
623
		$buffer = file_get_contents($filename);
624
625
		if ($buffer !== false) {
626
			return $this->put($path, $buffer);
627
		}
628
		$e = new BackendException($this->parseErrorCodeToMessage(self::WD_ERR_TMP), self::WD_ERR_TMP);
629
		$e->setTitle($this->backendTransName . _('Temporary directory problems'));
630
631
		throw $e;
632
	}
633
634
	/**
635
	 * Gets a file from a webdav collection.
636
	 *
637
	 * @param string $path   The source path on the server
638
	 * @param mixed  $buffer Buffer for the received data
639
	 *
640
	 * @return bool true if action succeeded
641
	 *
642
	 * @throws BackendException if request is not successful
643
	 */
644
	public function get($path, &$buffer) {
645
		$tmpfile = tempnam(TMP_PATH, stripslashes(base64_encode($path)));
646
647
		$this->log("[GET] buffer path: {$tmpfile}");
648
		$this->get_file($path, $tmpfile);
649
650
		$buffer = file_get_contents($tmpfile);
651
		unlink($tmpfile);
652
	}
653
654
	/**
655
	 * Gets a file from a collection into local filesystem.
656
	 *
657
	 * @param string $srcpath   Source path on server
658
	 * @param string $localpath Destination path on local filesystem
659
	 *
660
	 * @return bool true if action succeeded
661
	 *
662
	 * @throws BackendException if request is not successful
663
	 */
664
	public function get_file($srcpath, $localpath) {
665
		$time_start = microtime(true);
666
		$path = $this->removeSlash($srcpath);
667
		$this->log("[GET_FILE] start for dir: {$path}");
668
		$this->log("[GET_FILE] local path (" . $localpath . ") writeable: " . is_writable($localpath));
669
670
		try {
671
			$response = $this->sabre_client->getFile($path, $localpath);
672
			$time_end = microtime(true);
673
			$time = $time_end - $time_start;
674
			$this->log("[GET_FILE] done in {$time} seconds: " . $response['statusCode']);
675
		}
676
		catch (ClientException $e) {
677
			$e = new BackendException($this->parseErrorCodeToMessage($e->getCode()), $e->getCode());
678
			$e->setTitle($this->backendTransName . _('Sabre error'));
679
680
			throw $e;
681
		}
682
		catch (Exception $e) {
683
			$this->log('[GET_FILE] - FATAL -' . $e->getMessage());
684
			$e = new BackendException($this->parseErrorCodeToMessage($e->getHTTPCode()), $e->getHTTPCode());
685
			$e->setTitle($this->backendTransName . _('File or folder not found'));
686
687
			throw $e;
688
		}
689
	}
690
691
	/**
692
	 * Public method copy_file.
693
	 *
694
	 * Copy a file on webdav server
695
	 * Duplicates a file on the webdav server (serverside).
696
	 * All work is done on the webdav server. If you set param overwrite as true,
697
	 * the target will be overwritten.
698
	 *
699
	 * @param string $src_path  Source path
700
	 * @param bool   $overwrite Overwrite if file exists in $dst_path
701
	 * @param mixed  $dst_path
702
	 *
703
	 * @return bool true if action succeeded
704
	 *
705
	 * @throws BackendException if request is not successful
706
	 */
707
	public function copy_file($src_path, $dst_path, $overwrite = false) {
708
		return $this->copy($src_path, $dst_path, $overwrite, false);
709
	}
710
711
	/**
712
	 * Public method copy_coll.
713
	 *
714
	 * Copy a collection on webdav server
715
	 * Duplicates a collection on the webdav server (serverside).
716
	 * All work is done on the webdav server. If you set param overwrite as true,
717
	 * the target will be overwritten.
718
	 *
719
	 * @param string $src_path  Source path
720
	 * @param bool   $overwrite Overwrite if collection exists in $dst_path
721
	 * @param mixed  $dst_path
722
	 *
723
	 * @return bool true if action succeeded
724
	 *
725
	 * @throws BackendException if request is not successful
726
	 */
727
	public function copy_coll($src_path, $dst_path, $overwrite = false) {
728
		return $this->copy($src_path, $dst_path, $overwrite, true);
729
	}
730
731
	/**
732
	 * Gets path information from webdav server for one element.
733
	 *
734
	 * @param string $path Path to file or folder
735
	 *
736
	 * @return array directory info
737
	 *
738
	 * @throws BackendException if request is not successful
739
	 */
740
	public function gpi($path) {
741
		$path = $this->removeSlash($path);
742
		$response = $this->sabre_client->propFind($path, [
743
			'{http://owncloud.org/ns}fileid',
744
			'{DAV:}resourcetype',
745
			'{DAV:}getcontentlength',
746
			'{DAV:}getlastmodified',
747
			'{DAV:}getcontenttype',
748
			'{DAV:}quota-used-bytes',
749
			'{DAV:}quota-available-bytes',
750
		]);
751
752
		$type = $response["{DAV:}resourcetype"]->resourceType;
753
		if (is_array($type) && !empty($type)) {
754
			$type = $type[0] == "{DAV:}collection" ? "collection" : "file";
755
		}
756
		else {
757
			$type = "file"; // fall back to file if detection fails... less harmful
758
		}
759
760
		return [
761
			"fileid" => $response["{http://owncloud.org/ns}fileid"] ?? '-1',
762
			"resourcetype" => $type,
763
			"getcontentlength" => $response["{DAV:}getcontentlength"] ?? null,
764
			"getlastmodified" => $response["{DAV:}getlastmodified"] ?? null,
765
			"getcontenttype" => $response["{DAV:}getcontenttype"] ?? null,
766
			"quota-used-bytes" => $response["{DAV:}quota-used-bytes"] ?? null,
767
			"quota-available-bytes" => $response["{DAV:}quota-available-bytes"] ?? null,
768
		];
769
	}
770
771
	/**
772
	 * Gets server information.
773
	 *
774
	 * @return array with all header fields returned from webdav server
775
	 *
776
	 * @throws BackendException if request is not successful
777
	 */
778
	public function options() {
779
		$features = $this->sabre_client->options();
780
781
		// be sure it is an array
782
		if (is_array($features)) {
783
			return $features;
784
		}
785
786
		$this->log('[OPTIONS] - ERROR - Error getting server features');
787
		$e = new BackendException($this->parseErrorCodeToMessage(self::WD_ERR_FEATURES), self::WD_ERR_FEATURES);
788
		$e->setTitle($this->backendTransName . _('Not implemented'));
789
790
		throw $e;
791
	}
792
793
	/**
794
	 * Gather whether a path points to a file or not.
795
	 *
796
	 * @param string $path Path to file or folder
797
	 *
798
	 * @return bool true if path points to a file, false otherwise
799
	 */
800
	public function is_file($path) {
801
		$item = $this->gpi($path);
802
803
		return $item === false ? false : ($item['resourcetype'] != 'collection');
0 ignored issues
show
introduced by
The condition $item === false is always false.
Loading history...
804
	}
805
806
	/**
807
	 * Gather whether a path points to a directory.
808
	 *
809
	 * @param string $path Path to file or folder
810
	 *
811
	 * @return bool true if path points to a directory, false otherwise
812
	 */
813
	public function is_dir($path) {
814
		$item = $this->gpi($path);
815
816
		return $item === false ? false : ($item['resourcetype'] == 'collection');
0 ignored issues
show
introduced by
The condition $item === false is always false.
Loading history...
817
	}
818
819
	/**
820
	 * check if file/directory exists.
821
	 *
822
	 * @param string $path Path to file or folder
823
	 *
824
	 * @return bool true if path exists, false otherwise
825
	 */
826
	public function exists($path) {
827
		return $this->is_dir($path) || $this->is_file($path);
828
	}
829
830
	/**
831
	 * Copy a collection on webdav server
832
	 * Duplicates a collection on the webdav server (serverside).
833
	 * All work is done on the webdav server. If you set param overwrite as true,
834
	 * the target will be overwritten.
835
	 *
836
	 * @param string $src_path  Source path
837
	 * @param bool   $overwrite Overwrite if collection exists in $dst_path
838
	 * @param bool   $coll      set this to true if you want to copy a folder
839
	 * @param mixed  $dst_path
840
	 *
841
	 * @return bool true if action succeeded
842
	 *
843
	 * @throws BackendException if request is not successful
844
	 */
845
	private function copy($src_path, $dst_path, $overwrite, $coll) {
846
		$time_start = microtime(true);
847
		$src_path = $this->removeSlash($src_path);
848
		$dst_path = $this->webdavUrl() . $this->removeSlash($dst_path);
849
		$this->log("[COPY] start for dir: {$src_path} -> {$dst_path}");
850
		if ($overwrite) {
851
			$overwrite = 'T';
852
		}
853
		else {
854
			$overwrite = 'F';
855
		}["Destination" => $dst_path, 'Overwrite' => $overwrite];
856
		if ($coll) {
857
			$settings = ["Destination" => $dst_path, 'Depth' => 'Infinity'];
858
		}
859
860
		try {
861
			$response = $this->sabre_client->request("COPY", $src_path, null, $settings);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $settings does not seem to be defined for all execution paths leading up to this point.
Loading history...
862
			$time_end = microtime(true);
863
			$time = $time_end - $time_start;
864
			$this->log("[COPY] done in {$time} seconds: " . $response['statusCode']);
865
866
			return true;
867
		}
868
		catch (ClientException $e) {
869
			$e = new BackendException($this->parseErrorCodeToMessage($e->getCode()), $e->getCode());
870
			$e->setTitle($this->backendTransName . _('Sabre error'));
871
872
			throw $e;
873
		}
874
		catch (Exception $e) {
875
			$this->log('[COPY] - FATAL - ' . $e->getMessage());
876
			$e = new BackendException($this->parseErrorCodeToMessage($e->getHTTPCode()), $e->getHTTPCode());
877
			$e->setTitle($this->backendTransName . _('Copying failed'));
878
879
			throw $e;
880
		}
881
	}
882
883
	/**
884
	 * Create the base webdav url.
885
	 *
886
	 * @return string baseURL
887
	 */
888
	protected function webdavUrl() {
889
		if ($this->ssl) {
890
			$url = "https://";
891
		}
892
		else {
893
			$url = "http://";
894
		}
895
896
		// make sure that we do not have any trailing / in our url
897
		$server = rtrim($this->server, '/');
898
		$path = rtrim($this->path, '/');
899
900
		$url .= $server . ":" . $this->port . $path . "/";
901
902
		return $url;
903
	}
904
905
	/**
906
	 * Removes the leading slash from the folder path.
907
	 *
908
	 * @param string $dir directory path
909
	 *
910
	 * @return string trimmed directory path
911
	 */
912
	public function removeSlash($dir) {
913
		if (str_starts_with($dir, '/')) {
914
			$dir = substr($dir, 1);
915
		}
916
917
		// remove all html entities and urlencode the path...
918
		$nohtml = html_entity_decode($dir);
919
920
		return implode("/", array_map("rawurlencode", explode("/", $nohtml)));
921
	}
922
923
	/**
924
	 * This function will return a user friendly error string.
925
	 *
926
	 * @param number $error_code A error code
927
	 *
928
	 * @return string userfriendly error message
929
	 */
930
	private function parseErrorCodeToMessage($error_code) {
931
		$error = intval($error_code);
932
933
		$msg = _('Unknown error');
934
		$contactAdmin = _('Please contact your system administrator');
935
936
		return match ($error) {
937
			CURLE_BAD_PASSWORD_ENTERED, self::WD_ERR_UNAUTHORIZED => _('Unauthorized. Wrong username or password.'),
938
			CURLE_SSL_CONNECT_ERROR, CURLE_COULDNT_RESOLVE_HOST, CURLE_COULDNT_CONNECT, CURLE_OPERATION_TIMEOUTED, self::WD_ERR_UNREACHABLE => _('File server is not reachable. Please verify the connection.'),
939
			self::WD_ERR_NOTALLOWED => _('File server is not reachable. Please verify the file server URL.'),
940
			self::WD_ERR_FORBIDDEN => _('You don\'t have enough permissions to view this file or folder.'),
941
			self::WD_ERR_NOTFOUND => _('The file or folder is not available anymore.'),
942
			self::WD_ERR_TIMEOUT => _('Connection to the file server timed out. Please check again later.'),
943
			self::WD_ERR_LOCKED => _('This file is locked by another user. Please try again later.'),
944
			self::WD_ERR_FAILED_DEPENDENCY => _('The request failed.') . ' ' . $contactAdmin,
945
			// This is a general error, might be thrown due to a wrong IP, but we don't know.
946
			self::WD_ERR_INTERNAL => _('The file server encountered an internal problem.') . ' ' . $contactAdmin,
947
			self::WD_ERR_TMP => _('We could not write to temporary directory.') . ' ' . $contactAdmin,
948
			self::WD_ERR_FEATURES => _('We could not retrieve list of server features.') . ' ' . $contactAdmin,
949
			self::WD_ERR_NO_CURL => _('PHP-Curl is not available.') . ' ' . $contactAdmin,
950
			default => $msg,
951
		};
952
	}
953
954
	public function getFormConfig() {
955
		$json = json_encode($this->metaConfig);
956
957
		if ($json === false) {
958
			error_log(json_last_error());
959
		}
960
961
		return $json;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $json returns the type 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...
962
	}
963
964
	public function getFormConfigWithData() {
965
		return json_encode($this->metaConfig);
0 ignored issues
show
Bug Best Practice introduced by
The expression return json_encode($this->metaConfig) returns the type 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...
966
	}
967
968
	/**
969
	 * a simple php error_log wrapper.
970
	 *
971
	 * @param string $err_string error message
972
	 */
973
	private function log($err_string) {
974
		if ($this->debug) {
975
			error_log("[BACKEND_WEBDAV]: " . $err_string);
976
		}
977
	}
978
979
	/**
980
	 * ============================ FEATURE FUNCTIONS ========================.
981
	 *
982
	 * @param mixed $dir
983
	 */
984
985
	/**
986
	 * Returns the bytes that are currently used.
987
	 *
988
	 * @param string $dir directory to check
989
	 *
990
	 * @return int bytes that are used or -1 on error
991
	 */
992
	public function getQuotaBytesUsed($dir) {
993
		$lsdata = $this->ls($dir, false);
994
995
		if (isset($lsdata) && is_array($lsdata)) {
996
			return $lsdata[$dir]["quota-used-bytes"];
997
		}
998
999
		return -1;
1000
	}
1001
1002
	/**
1003
	 * Returns the bytes that are currently available.
1004
	 *
1005
	 * @param string $dir directory to check
1006
	 *
1007
	 * @return int bytes that are available or -1 on error
1008
	 */
1009
	public function getQuotaBytesAvailable($dir) {
1010
		$lsdata = $this->ls($dir, false);
1011
1012
		if (isset($lsdata) && is_array($lsdata)) {
1013
			return $lsdata[$dir]["quota-available-bytes"];
1014
		}
1015
1016
		return -1;
1017
	}
1018
1019
	/**
1020
	 * Return the version string of the server backend.
1021
	 *
1022
	 * @return string
1023
	 *
1024
	 * @throws BackendException
1025
	 */
1026
	public function getServerVersion() {
1027
		// check if curl is available
1028
		$serverHasCurl = function_exists('curl_version');
1029
		if (!$serverHasCurl) {
1030
			$e = new BackendException($this->parseErrorCodeToMessage(self::WD_ERR_NO_CURL), 500);
1031
			$e->setTitle($this->backendTransName . _('PHP-CURL not installed'));
1032
1033
			throw $e;
1034
		}
1035
1036
		$webdavurl = $this->webdavUrl();
1037
1038
		$url = substr($webdavurl, 0, strlen($webdavurl) - strlen("remote.php/webdav/")) . "status.php";
1039
1040
		// try to get the contents of the owncloud status page
1041
		$ch = curl_init();
1042
		curl_setopt($ch, CURLOPT_AUTOREFERER, true);
1043
		curl_setopt($ch, CURLOPT_TIMEOUT, 3); // timeout of 3 seconds
1044
		curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
1045
		curl_setopt($ch, CURLOPT_URL, $url);
1046
		curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
1047
		if ($this->allowselfsigned) {
1048
			curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
1049
			curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
1050
		}
1051
		$versiondata = curl_exec($ch);
1052
		$httpcode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
1053
		curl_close($ch);
1054
1055
		if ($httpcode && $httpcode == "200" && $versiondata) {
1056
			$versions = json_decode($versiondata);
0 ignored issues
show
Bug introduced by
It seems like $versiondata can also be of type true; however, parameter $json of json_decode() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

1056
			$versions = json_decode(/** @scrutinizer ignore-type */ $versiondata);
Loading history...
1057
			$version = $versions->versionstring;
1058
		}
1059
		else {
1060
			$version = "Undetected (no Owncloud?)";
1061
		}
1062
1063
		return $version;
1064
	}
1065
}
1066