Backend::removeSlash()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 4
c 0
b 0
f 0
nc 2
nop 1
dl 0
loc 9
rs 10
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
	 * @param mixed $backend_config
213
	 */
214
	public function init_backend($backend_config) {
215
		$this->set_server($backend_config["server_address"]);
216
		$this->set_port($backend_config["server_port"]);
217
		$this->set_base($backend_config["server_path"]);
218
		$this->set_ssl($backend_config["server_ssl"]);
219
220
		// set user and password
221
		if ($backend_config["use_grommunio_credentials"] === false) {
222
			$this->set_user($backend_config["user"]);
223
			$this->set_pass($backend_config["password"]);
224
		}
225
		else {
226
			// For backward compatibility we will check if the Encryption store exists. If not,
227
			// we will fall back to the old way of retrieving the password from the session.
228
			if (class_exists('EncryptionStore')) {
229
				// Get the username and password from the Encryption store
230
				$encryptionStore = \EncryptionStore::getInstance();
231
				$this->set_user($encryptionStore->get('username'));
232
				$this->set_pass($encryptionStore->get('password'));
233
			}
234
			else {
235
				$this->set_user($GLOBALS['mapisession']->getUserName());
236
				$password = $_SESSION['password'];
237
				if (function_exists('openssl_decrypt')) {
238
					$this->set_pass(openssl_decrypt($password, "des-ede3-cbc", PASSWORD_KEY, 0, PASSWORD_IV));
239
				}
240
			}
241
		}
242
	}
243
244
	/**
245
	 * Set webdav server. FQN or IP address.
246
	 *
247
	 * @param string $server hostname or ip of the ftp server
248
	 */
249
	public function set_server($server) {
250
		$this->server = $server;
251
	}
252
253
	/**
254
	 * Set base path.
255
	 *
256
	 * @param string $pp the global path prefix
257
	 */
258
	public function set_base($pp) {
259
		$this->path = $pp;
260
		$this->log('Base path set to ' . $this->path);
261
	}
262
263
	/**
264
	 * Set ssl.
265
	 *
266
	 * @param mixed $ssl (1 = true, 0 = false)
267
	 */
268
	public function set_ssl($ssl) {
269
		$this->ssl = $ssl ? true : false;
270
		$this->log('SSL extension was set to ' . $this->ssl);
271
	}
272
273
	/**
274
	 * Allow self signed certificates - unimplemented.
275
	 *
276
	 * @param bool $allowselfsigned Allow self signed certificates. Not yet implemented.
277
	 */
278
	public function set_selfsigned($allowselfsigned) {
279
		$this->allowselfsigned = $allowselfsigned;
280
	}
281
282
	/**
283
	 * Set tcp port of webdav server. Default is 80.
284
	 *
285
	 * @param int $port the port of the ftp server
286
	 */
287
	public function set_port($port) {
288
		$this->port = $port;
289
	}
290
291
	/**
292
	 * set user name for authentication.
293
	 *
294
	 * @param string $user username
295
	 */
296
	public function set_user($user) {
297
		$this->user = $user;
298
	}
299
300
	/**
301
	 * Set password for authentication.
302
	 *
303
	 * @param string $pass password
304
	 */
305
	public function set_pass($pass) {
306
		$this->pass = $pass;
307
	}
308
309
	/**
310
	 * set debug on (1) or off (0).
311
	 * produces a lot of debug messages in webservers error log if set to on (1).
312
	 *
313
	 * @param bool $debug enable or disable debugging
314
	 */
315
	public function set_debug($debug) {
316
		$this->debug = $debug;
317
	}
318
319
	/**
320
	 * Opens the connection to the webdav server.
321
	 *
322
	 * @return bool true if action succeeded
323
	 *
324
	 * @throws BackendException if connection is not successful
325
	 */
326
	public function open() {
327
		// check if curl is available
328
		$serverHasCurl = function_exists('curl_version');
329
		if (!$serverHasCurl) {
330
			$e = new BackendException($this->parseErrorCodeToMessage(self::WD_ERR_NO_CURL), 500);
331
			$e->setTitle($this->backendTransName . _('PHP-CURL is not installed'));
332
333
			throw $e;
334
		}
335
336
		$davsettings = [
337
			'baseUri' => $this->webdavUrl(),
338
			'userName' => $this->user,
339
			'password' => $this->pass,
340
			'authType' => Client::AUTH_BASIC,
341
		];
342
343
		try {
344
			$this->sabre_client = new FilesWebDavClient($davsettings);
345
			$this->sabre_client->addCurlSetting(CURLOPT_SSL_VERIFYPEER, !$this->allowselfsigned);
346
347
			return true;
348
		}
349
		catch (Exception $e) {
350
			$this->log('Failed to open: ' . $e->getMessage());
351
			if (intval($e->getHTTPCode()) == 401) {
352
				$e = new BackendException($this->parseErrorCodeToMessage(self::WD_ERR_UNAUTHORIZED), $e->getHTTPCode());
353
				$e->setTitle($this->backendTransName . _('Access denied'));
354
355
				throw $e;
356
			}
357
			$e = new BackendException($this->parseErrorCodeToMessage(self::WD_ERR_UNREACHABLE), $e->getHTTPCode());
358
			$e->setTitle($this->backendTransName . _('Connection failed'));
359
360
			throw $e;
361
		}
362
	}
363
364
	/**
365
	 * show content of a directory.
366
	 *
367
	 * @param bool  $hidefirst Optional parameter to hide the root entry. Default true
368
	 * @param mixed $dir
369
	 *
370
	 * @return mixed array with directory content
371
	 *
372
	 * @throws BackendException if request is not successful
373
	 */
374
	public function ls($dir, $hidefirst = true) {
375
		$time_start = microtime(true);
376
		$dir = $this->removeSlash($dir);
377
		$lsdata = [];
378
		$this->log("[LS] start for dir: {$dir}");
379
380
		try {
381
			$response = $this->sabre_client->propFind($dir, [
382
				'{http://owncloud.org/ns}fileid',
383
				'{DAV:}resourcetype',
384
				'{DAV:}getcontentlength',
385
				'{DAV:}getlastmodified',
386
				'{DAV:}getcontenttype',
387
				'{DAV:}quota-used-bytes',
388
				'{DAV:}quota-available-bytes',
389
			], 1);
390
			$this->log("[LS] backend fetched in: " . (microtime(true) - $time_start) . " seconds.");
391
392
			foreach ($response as $name => $fields) {
393
				if ($hidefirst) {
394
					$hidefirst = false; // skip the first line - its the requested dir itself
395
396
					continue;
397
				}
398
399
				$name = substr($name, strlen($this->path)); // skip the webdav path
400
				$name = urldecode($name);
401
402
				// Always fallback to a file resourceType
403
				$type = "file";
404
				if (isset($fields['{DAV:}resourcetype'])) {
405
					$value = $fields['{DAV:}resourcetype']->getValue();
406
					if (!empty($value) && $value[0] === "{DAV:}collection") {
407
						$type = "collection";
408
					}
409
				}
410
411
				$lsdata[$name] = [
412
					"fileid" => $fields["{http://owncloud.org/ns}fileid"] ?? '-1',
413
					"resourcetype" => $type,
414
					"getcontentlength" => $fields["{DAV:}getcontentlength"] ?? null,
415
					"getlastmodified" => $fields["{DAV:}getlastmodified"] ?? null,
416
					"getcontenttype" => $fields["{DAV:}getcontenttype"] ?? null,
417
					"quota-used-bytes" => $fields["{DAV:}quota-used-bytes"] ?? null,
418
					"quota-available-bytes" => $fields["{DAV:}quota-available-bytes"] ?? null,
419
				];
420
			}
421
			$time_end = microtime(true);
422
			$time = $time_end - $time_start;
423
			$this->log("[LS] done in {$time} seconds");
424
425
			return $lsdata;
426
		}
427
		catch (ClientException $e) {
428
			$this->log('ls sabre ClientException: ' . $e->getMessage());
429
			$e = new BackendException($this->parseErrorCodeToMessage($e->getCode()), $e->getCode());
430
			$e->setTitle($this->backendTransName . _('Sabre error'));
431
432
			throw $e;
433
		}
434
		catch (Exception $e) {
435
			$this->log('ls general exception: ' . $e->getMessage() . " [" . $e->getHTTPCode() . "]");
436
			// THIS IS A FIX FOR OWNCLOUD - It does return 500 instead of 401...
437
			$err_code = $e->getHTTPCode();
438
			// check if code is 500 - then we should try to parse the error message
439
			if ($err_code === 500) {
440
				// message example: HTTP-Code: 401
441
				$regx = '/[0-9]{3}/';
442
				if (preg_match($regx, $e->getMessage(), $found)) {
443
					$err_code = $found[0];
444
				}
445
			}
446
			$e = new BackendException($this->parseErrorCodeToMessage($err_code), $err_code);
447
			$e->setTitle($this->backendTransName . _('Connection failed'));
448
449
			throw $e;
450
		}
451
	}
452
453
	/**
454
	 * create a new directory.
455
	 *
456
	 * @param string $dir directory path
457
	 *
458
	 * @return bool true if action succeeded
459
	 *
460
	 * @throws BackendException if request is not successful
461
	 */
462
	public function mkcol($dir) {
463
		$time_start = microtime(true);
464
		$dir = $this->removeSlash($dir);
465
		$this->log("[MKCOL] start for dir: {$dir}");
466
467
		try {
468
			$response = $this->sabre_client->request("MKCOL", $dir, null);
469
			$time_end = microtime(true);
470
			$time = $time_end - $time_start;
471
			$this->log("[MKCOL] done in {$time} seconds: " . $response['statusCode']);
472
473
			return true;
474
		}
475
		catch (ClientException $e) {
476
			$e = new BackendException($this->parseErrorCodeToMessage($e->getCode()), $e->getCode());
477
			$e->setTitle($this->backendTransName . _('Sabre error'));
478
479
			throw $e;
480
		}
481
		catch (Exception $e) {
482
			$this->log('[MKCOL] fatal: ' . $e->getMessage());
483
			$e = new BackendException($this->parseErrorCodeToMessage($e->getHTTPCode()), $e->getHTTPCode());
484
			$e->setTitle($this->backendTransName . _('Directory creation failed'));
485
486
			throw $e;
487
		}
488
	}
489
490
	/**
491
	 * delete a file or directory.
492
	 *
493
	 * @param string $path file/directory path
494
	 *
495
	 * @return bool true if action succeeded
496
	 *
497
	 * @throws BackendException if request is not successful
498
	 */
499
	public function delete($path) {
500
		$time_start = microtime(true);
501
		$path = $this->removeSlash($path);
502
		$this->log("[DELETE] start for dir: {$path}");
503
504
		try {
505
			$response = $this->sabre_client->request("DELETE", $path, null);
506
			$time_end = microtime(true);
507
			$time = $time_end - $time_start;
508
			$this->log("[DELETE] done in {$time} seconds: " . $response['statusCode']);
509
510
			return true;
511
		}
512
		catch (ClientException $e) {
513
			$e = new BackendException($this->parseErrorCodeToMessage($e->getCode()), $e->getCode());
514
			$e->setTitle($this->backendTransName . _('Sabre error'));
515
516
			throw $e;
517
		}
518
		catch (Exception $e) {
519
			$this->log('[DELETE] fatal: ' . $e->getMessage());
520
			$e = new BackendException($this->parseErrorCodeToMessage($e->getHTTPCode()), $e->getHTTPCode());
521
			$e->setTitle($this->backendTransName . _('Deletion failed'));
522
523
			throw $e;
524
		}
525
	}
526
527
	/**
528
	 * Move a file or collection on webdav server (serverside)
529
	 * If you set param overwrite as true, the target will be overwritten.
530
	 *
531
	 * @param string $src_path  Source path
532
	 * @param bool   $overwrite Overwrite file if exists in $dest_path
533
	 * @param mixed  $dst_path
534
	 *
535
	 * @return bool true if action succeeded
536
	 *
537
	 * @throws BackendException if request is not successful
538
	 */
539
	public function move($src_path, $dst_path, $overwrite = false) {
540
		$time_start = microtime(true);
541
		$src_path = $this->removeSlash($src_path);
542
		$dst_path = $this->webdavUrl() . $this->removeSlash($dst_path);
543
		$this->log("[MOVE] start for dir: {$src_path} -> {$dst_path}");
544
		if ($overwrite) {
545
			$overwrite = 'T';
546
		}
547
		else {
548
			$overwrite = 'F';
549
		}
550
551
		try {
552
			$response = $this->sabre_client->request("MOVE", $src_path, null, ["Destination" => $dst_path, 'Overwrite' => $overwrite]);
553
			$time_end = microtime(true);
554
			$time = $time_end - $time_start;
555
			$this->log("[MOVE] done in {$time} seconds: " . $response['statusCode']);
556
557
			return true;
558
		}
559
		catch (ClientException $e) {
560
			$e = new BackendException($this->parseErrorCodeToMessage($e->getCode()), $e->getCode());
561
			$e->setTitle($this->backendTransName . _('Sabre error'));
562
563
			throw $e;
564
		}
565
		catch (Exception $e) {
566
			$this->log('[MOVE] fatal: ' . $e->getMessage());
567
			$e = new BackendException($this->parseErrorCodeToMessage($e->getHTTPCode()), $e->getHTTPCode());
568
			$e->setTitle($this->backendTransName . _('Moving failed'));
569
570
			throw $e;
571
		}
572
	}
573
574
	/**
575
	 * Puts a file into a collection.
576
	 *
577
	 * @param string $path Destination path
578
	 * @param mixed  $data
579
	 *
580
	 * @string mixed $data Any kind of data
581
	 *
582
	 * @return bool true if action succeeded
583
	 *
584
	 * @throws BackendException if request is not successful
585
	 */
586
	public function put($path, $data) {
587
		$time_start = microtime(true);
588
		$path = $this->removeSlash($path);
589
		$this->log("[PUT] start for dir: {$path} strlen: " . strlen((string) $data));
590
591
		try {
592
			$response = $this->sabre_client->request("PUT", $path, $data);
593
			$time_end = microtime(true);
594
			$time = $time_end - $time_start;
595
			$this->log("[PUT] done in {$time} seconds: " . $response['statusCode']);
596
597
			return true;
598
		}
599
		catch (ClientException $e) {
600
			$e = new BackendException($this->parseErrorCodeToMessage($e->getCode()), $e->getCode());
601
			$e->setTitle($this->backendTransName . _('Sabre error'));
602
603
			throw $e;
604
		}
605
		catch (Exception $e) {
606
			$this->log('[PUT] fatal: ' . $e->getMessage());
607
			$e = new BackendException($this->parseErrorCodeToMessage($e->getHTTPCode()), $e->getHTTPCode());
608
			$e->setTitle($this->backendTransName . _('Connection failed'));
609
610
			throw $e;
611
		}
612
	}
613
614
	/**
615
	 * Upload a local file.
616
	 *
617
	 * @param string $path     Destination path on the server
618
	 * @param string $filename Local filename for the file that should be uploaded
619
	 *
620
	 * @return bool true if action succeeded
621
	 *
622
	 * @throws BackendException if request is not successful
623
	 */
624
	public function put_file($path, $filename) {
625
		$buffer = file_get_contents($filename);
626
627
		if ($buffer !== false) {
628
			return $this->put($path, $buffer);
629
		}
630
		$e = new BackendException($this->parseErrorCodeToMessage(self::WD_ERR_TMP), self::WD_ERR_TMP);
631
		$e->setTitle($this->backendTransName . _('Temporary directory problems'));
632
633
		throw $e;
634
	}
635
636
	/**
637
	 * Gets a file from a webdav collection.
638
	 *
639
	 * @param string $path   The source path on the server
640
	 * @param mixed  $buffer Buffer for the received data
641
	 *
642
	 * @return bool true if action succeeded
643
	 *
644
	 * @throws BackendException if request is not successful
645
	 */
646
	public function get($path, &$buffer) {
647
		$tmpfile = tempnam(TMP_PATH, stripslashes(base64_encode($path)));
648
649
		$this->log("[GET] buffer path: {$tmpfile}");
650
		$this->get_file($path, $tmpfile);
651
652
		$buffer = file_get_contents($tmpfile);
653
		unlink($tmpfile);
654
	}
655
656
	/**
657
	 * Gets a file from a collection into local filesystem.
658
	 *
659
	 * @param string $srcpath   Source path on server
660
	 * @param string $localpath Destination path on local filesystem
661
	 *
662
	 * @return bool true if action succeeded
663
	 *
664
	 * @throws BackendException if request is not successful
665
	 */
666
	public function get_file($srcpath, $localpath) {
667
		$time_start = microtime(true);
668
		$path = $this->removeSlash($srcpath);
669
		$this->log("[GET_FILE] start for dir: {$path}");
670
		$this->log("[GET_FILE] local path (" . $localpath . ") writeable: " . is_writable($localpath));
671
672
		try {
673
			$response = $this->sabre_client->getFile($path, $localpath);
674
			$time_end = microtime(true);
675
			$time = $time_end - $time_start;
676
			$this->log("[GET_FILE] done in {$time} seconds: " . $response['statusCode']);
677
		}
678
		catch (ClientException $e) {
679
			$e = new BackendException($this->parseErrorCodeToMessage($e->getCode()), $e->getCode());
680
			$e->setTitle($this->backendTransName . _('Sabre error'));
681
682
			throw $e;
683
		}
684
		catch (Exception $e) {
685
			$this->log('[GET_FILE] - FATAL -' . $e->getMessage());
686
			$e = new BackendException($this->parseErrorCodeToMessage($e->getHTTPCode()), $e->getHTTPCode());
687
			$e->setTitle($this->backendTransName . _('File or folder not found'));
688
689
			throw $e;
690
		}
691
	}
692
693
	/**
694
	 * Public method copy_file.
695
	 *
696
	 * Copy a file on webdav server
697
	 * Duplicates a file on the webdav server (serverside).
698
	 * All work is done on the webdav server. If you set param overwrite as true,
699
	 * the target will be overwritten.
700
	 *
701
	 * @param string $src_path  Source path
702
	 * @param bool   $overwrite Overwrite if file exists in $dst_path
703
	 * @param mixed  $dst_path
704
	 *
705
	 * @return bool true if action succeeded
706
	 *
707
	 * @throws BackendException if request is not successful
708
	 */
709
	public function copy_file($src_path, $dst_path, $overwrite = false) {
710
		return $this->copy($src_path, $dst_path, $overwrite, false);
711
	}
712
713
	/**
714
	 * Public method copy_coll.
715
	 *
716
	 * Copy a collection on webdav server
717
	 * Duplicates a collection on the webdav server (serverside).
718
	 * All work is done on the webdav server. If you set param overwrite as true,
719
	 * the target will be overwritten.
720
	 *
721
	 * @param string $src_path  Source path
722
	 * @param bool   $overwrite Overwrite if collection exists in $dst_path
723
	 * @param mixed  $dst_path
724
	 *
725
	 * @return bool true if action succeeded
726
	 *
727
	 * @throws BackendException if request is not successful
728
	 */
729
	public function copy_coll($src_path, $dst_path, $overwrite = false) {
730
		return $this->copy($src_path, $dst_path, $overwrite, true);
731
	}
732
733
	/**
734
	 * Gets path information from webdav server for one element.
735
	 *
736
	 * @param string $path Path to file or folder
737
	 *
738
	 * @return array directory info
739
	 *
740
	 * @throws BackendException if request is not successful
741
	 */
742
	public function gpi($path) {
743
		$path = $this->removeSlash($path);
744
		$response = $this->sabre_client->propFind($path, [
745
			'{http://owncloud.org/ns}fileid',
746
			'{DAV:}resourcetype',
747
			'{DAV:}getcontentlength',
748
			'{DAV:}getlastmodified',
749
			'{DAV:}getcontenttype',
750
			'{DAV:}quota-used-bytes',
751
			'{DAV:}quota-available-bytes',
752
		]);
753
754
		$type = $response["{DAV:}resourcetype"]->resourceType;
755
		if (is_array($type) && !empty($type)) {
756
			$type = $type[0] == "{DAV:}collection" ? "collection" : "file";
757
		}
758
		else {
759
			$type = "file"; // fall back to file if detection fails... less harmful
760
		}
761
762
		return [
763
			"fileid" => $response["{http://owncloud.org/ns}fileid"] ?? '-1',
764
			"resourcetype" => $type,
765
			"getcontentlength" => $response["{DAV:}getcontentlength"] ?? null,
766
			"getlastmodified" => $response["{DAV:}getlastmodified"] ?? null,
767
			"getcontenttype" => $response["{DAV:}getcontenttype"] ?? null,
768
			"quota-used-bytes" => $response["{DAV:}quota-used-bytes"] ?? null,
769
			"quota-available-bytes" => $response["{DAV:}quota-available-bytes"] ?? null,
770
		];
771
	}
772
773
	/**
774
	 * Gets server information.
775
	 *
776
	 * @return array with all header fields returned from webdav server
777
	 *
778
	 * @throws BackendException if request is not successful
779
	 */
780
	public function options() {
781
		$features = $this->sabre_client->options();
782
783
		// be sure it is an array
784
		if (is_array($features)) {
785
			return $features;
786
		}
787
788
		$this->log('[OPTIONS] - ERROR - Error getting server features');
789
		$e = new BackendException($this->parseErrorCodeToMessage(self::WD_ERR_FEATURES), self::WD_ERR_FEATURES);
790
		$e->setTitle($this->backendTransName . _('Not implemented'));
791
792
		throw $e;
793
	}
794
795
	/**
796
	 * Gather whether a path points to a file or not.
797
	 *
798
	 * @param string $path Path to file or folder
799
	 *
800
	 * @return bool true if path points to a file, false otherwise
801
	 */
802
	public function is_file($path) {
803
		$item = $this->gpi($path);
804
805
		return $item === false ? false : ($item['resourcetype'] != 'collection');
0 ignored issues
show
introduced by
The condition $item === false is always false.
Loading history...
806
	}
807
808
	/**
809
	 * Gather whether a path points to a directory.
810
	 *
811
	 * @param string $path Path to file or folder
812
	 *
813
	 * @return bool true if path points to a directory, false otherwise
814
	 */
815
	public function is_dir($path) {
816
		$item = $this->gpi($path);
817
818
		return $item === false ? false : ($item['resourcetype'] == 'collection');
0 ignored issues
show
introduced by
The condition $item === false is always false.
Loading history...
819
	}
820
821
	/**
822
	 * check if file/directory exists.
823
	 *
824
	 * @param string $path Path to file or folder
825
	 *
826
	 * @return bool true if path exists, false otherwise
827
	 */
828
	public function exists($path) {
829
		return $this->is_dir($path) || $this->is_file($path);
830
	}
831
832
	/**
833
	 * Copy a collection on webdav server
834
	 * Duplicates a collection on the webdav server (serverside).
835
	 * All work is done on the webdav server. If you set param overwrite as true,
836
	 * the target will be overwritten.
837
	 *
838
	 * @param string $src_path  Source path
839
	 * @param bool   $overwrite Overwrite if collection exists in $dst_path
840
	 * @param bool   $coll      set this to true if you want to copy a folder
841
	 * @param mixed  $dst_path
842
	 *
843
	 * @return bool true if action succeeded
844
	 *
845
	 * @throws BackendException if request is not successful
846
	 */
847
	private function copy($src_path, $dst_path, $overwrite, $coll) {
848
		$time_start = microtime(true);
849
		$src_path = $this->removeSlash($src_path);
850
		$dst_path = $this->webdavUrl() . $this->removeSlash($dst_path);
851
		$this->log("[COPY] start for dir: {$src_path} -> {$dst_path}");
852
		if ($overwrite) {
853
			$overwrite = 'T';
854
		}
855
		else {
856
			$overwrite = 'F';
857
		}["Destination" => $dst_path, 'Overwrite' => $overwrite];
858
		if ($coll) {
859
			$settings = ["Destination" => $dst_path, 'Depth' => 'Infinity'];
860
		}
861
862
		try {
863
			$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...
864
			$time_end = microtime(true);
865
			$time = $time_end - $time_start;
866
			$this->log("[COPY] done in {$time} seconds: " . $response['statusCode']);
867
868
			return true;
869
		}
870
		catch (ClientException $e) {
871
			$e = new BackendException($this->parseErrorCodeToMessage($e->getCode()), $e->getCode());
872
			$e->setTitle($this->backendTransName . _('Sabre error'));
873
874
			throw $e;
875
		}
876
		catch (Exception $e) {
877
			$this->log('[COPY] - FATAL - ' . $e->getMessage());
878
			$e = new BackendException($this->parseErrorCodeToMessage($e->getHTTPCode()), $e->getHTTPCode());
879
			$e->setTitle($this->backendTransName . _('Copying failed'));
880
881
			throw $e;
882
		}
883
	}
884
885
	/**
886
	 * Create the base webdav url.
887
	 *
888
	 * @return string baseURL
889
	 */
890
	protected function webdavUrl() {
891
		if ($this->ssl) {
892
			$url = "https://";
893
		}
894
		else {
895
			$url = "http://";
896
		}
897
898
		// make sure that we do not have any trailing / in our url
899
		$server = rtrim($this->server, '/');
900
		$path = rtrim($this->path, '/');
901
902
		$url .= $server . ":" . $this->port . $path . "/";
903
904
		return $url;
905
	}
906
907
	/**
908
	 * Removes the leading slash from the folder path.
909
	 *
910
	 * @param string $dir directory path
911
	 *
912
	 * @return string trimmed directory path
913
	 */
914
	public function removeSlash($dir) {
915
		if (str_starts_with($dir, '/')) {
916
			$dir = substr($dir, 1);
917
		}
918
919
		// remove all html entities and urlencode the path...
920
		$nohtml = html_entity_decode($dir);
921
922
		return implode("/", array_map("rawurlencode", explode("/", $nohtml)));
923
	}
924
925
	/**
926
	 * This function will return a user friendly error string.
927
	 *
928
	 * @param number $error_code A error code
929
	 *
930
	 * @return string userfriendly error message
931
	 */
932
	private function parseErrorCodeToMessage($error_code) {
933
		$error = intval($error_code);
934
935
		$msg = _('Unknown error');
936
		$contactAdmin = _('Please contact your system administrator');
937
938
		// Avoid referencing CURLE_* constants if curl extension isn't loaded (PHP 8.1/8.2 fatal otherwise)
939
		if ((defined('CURLE_BAD_PASSWORD_ENTERED') && $error === CURLE_BAD_PASSWORD_ENTERED) || $error === self::WD_ERR_UNAUTHORIZED) {
940
			return _('Unauthorized. Wrong username or password.');
941
		}
942
		if ((defined('CURLE_SSL_CONNECT_ERROR') && $error === CURLE_SSL_CONNECT_ERROR) ||
943
			(defined('CURLE_COULDNT_RESOLVE_HOST') && $error === CURLE_COULDNT_RESOLVE_HOST) ||
944
			(defined('CURLE_COULDNT_CONNECT') && $error === CURLE_COULDNT_CONNECT) ||
945
			(defined('CURLE_OPERATION_TIMEOUTED') && $error === CURLE_OPERATION_TIMEOUTED) ||
946
			$error === self::WD_ERR_UNREACHABLE) {
947
			return _('File server is not reachable. Please verify the connection.');
948
		}
949
		if ($error === self::WD_ERR_NOTALLOWED) {
950
			return _('File server is not reachable. Please verify the file server URL.');
951
		}
952
		if ($error === self::WD_ERR_FORBIDDEN) {
953
			return _('You don\'t have enough permissions to view this file or folder.');
954
		}
955
		if ($error === self::WD_ERR_NOTFOUND) {
956
			return _('The file or folder is not available anymore.');
957
		}
958
		if ($error === self::WD_ERR_TIMEOUT) {
959
			return _('Connection to the file server timed out. Please check again later.');
960
		}
961
		if ($error === self::WD_ERR_LOCKED) {
962
			return _('This file is locked by another user. Please try again later.');
963
		}
964
		if ($error === self::WD_ERR_FAILED_DEPENDENCY) {
965
			return _('The request failed.') . ' ' . $contactAdmin;
966
		}
967
		if ($error === self::WD_ERR_INTERNAL) {
968
			return _('The file server encountered an internal problem.') . ' ' . $contactAdmin;
969
		}
970
		if ($error === self::WD_ERR_TMP) {
971
			return _('We could not write to temporary directory.') . ' ' . $contactAdmin;
972
		}
973
		if ($error === self::WD_ERR_FEATURES) {
974
			return _('We could not retrieve list of server features.') . ' ' . $contactAdmin;
975
		}
976
		if ($error === self::WD_ERR_NO_CURL) {
977
			return _('PHP-Curl is not available.') . ' ' . $contactAdmin;
978
		}
979
980
		return $msg;
981
	}
982
983
	public function getFormConfig() {
984
		$json = json_encode($this->metaConfig);
985
986
		if ($json === false) {
987
			error_log(json_last_error());
988
		}
989
990
		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...
991
	}
992
993
	public function getFormConfigWithData() {
994
		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...
995
	}
996
997
	/**
998
	 * a simple php error_log wrapper.
999
	 *
1000
	 * @param string $err_string error message
1001
	 */
1002
	private function log($err_string) {
1003
		if ($this->debug) {
1004
			error_log("[BACKEND_WEBDAV]: " . $err_string);
1005
		}
1006
	}
1007
1008
	/**
1009
	 * ============================ FEATURE FUNCTIONS ========================.
1010
	 *
1011
	 * @param mixed $dir
1012
	 */
1013
1014
	/**
1015
	 * Returns the bytes that are currently used.
1016
	 *
1017
	 * @param string $dir directory to check
1018
	 *
1019
	 * @return int bytes that are used or -1 on error
1020
	 */
1021
	public function getQuotaBytesUsed($dir) {
1022
		$lsdata = $this->ls($dir, false);
1023
1024
		if (isset($lsdata) && is_array($lsdata)) {
1025
			return $lsdata[$dir]["quota-used-bytes"];
1026
		}
1027
1028
		return -1;
1029
	}
1030
1031
	/**
1032
	 * Returns the bytes that are currently available.
1033
	 *
1034
	 * @param string $dir directory to check
1035
	 *
1036
	 * @return int bytes that are available or -1 on error
1037
	 */
1038
	public function getQuotaBytesAvailable($dir) {
1039
		$lsdata = $this->ls($dir, false);
1040
1041
		if (isset($lsdata) && is_array($lsdata)) {
1042
			return $lsdata[$dir]["quota-available-bytes"];
1043
		}
1044
1045
		return -1;
1046
	}
1047
1048
	/**
1049
	 * Return the version string of the server backend.
1050
	 *
1051
	 * @return string
1052
	 *
1053
	 * @throws BackendException
1054
	 */
1055
	public function getServerVersion() {
1056
		// check if curl is available
1057
		$serverHasCurl = function_exists('curl_version');
1058
		if (!$serverHasCurl) {
1059
			$e = new BackendException($this->parseErrorCodeToMessage(self::WD_ERR_NO_CURL), 500);
1060
			$e->setTitle($this->backendTransName . _('PHP-CURL not installed'));
1061
1062
			throw $e;
1063
		}
1064
1065
		$webdavurl = $this->webdavUrl();
1066
1067
		$url = substr($webdavurl, 0, strlen($webdavurl) - strlen("remote.php/webdav/")) . "status.php";
1068
1069
		// try to get the contents of the owncloud status page
1070
		$ch = curl_init();
1071
		curl_setopt($ch, CURLOPT_AUTOREFERER, true);
1072
		curl_setopt($ch, CURLOPT_TIMEOUT, 3); // timeout of 3 seconds
1073
		curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
1074
		curl_setopt($ch, CURLOPT_URL, $url);
1075
		curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
1076
		if ($this->allowselfsigned) {
1077
			curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
1078
			curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
1079
		}
1080
		$versiondata = curl_exec($ch);
1081
		$httpcode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
1082
		curl_close($ch);
1083
1084
		if ($httpcode && $httpcode == "200" && $versiondata) {
1085
			$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

1085
			$versions = json_decode(/** @scrutinizer ignore-type */ $versiondata);
Loading history...
1086
			$version = $versions->versionstring;
1087
		}
1088
		else {
1089
			$version = "Undetected (no Owncloud?)";
1090
		}
1091
1092
		return $version;
1093
	}
1094
}
1095